Make topo-generation more flexible 21/14221/1
authorMartin Skorupski <martin.skorupski@highstreet-technologies.com>
Fri, 7 Mar 2025 15:30:51 +0000 (16:30 +0100)
committerMartin Skorupski <martin.skorupski@highstreet-technologies.com>
Fri, 7 Mar 2025 15:31:25 +0000 (16:31 +0100)
- add operational-state parameters to network generation

IssueID: OAM-440
Change-Id: Ic604d1663bc273845fdf38949335565d9dd9d592
Signed-off-by: Martin Skorupski <martin.skorupski@highstreet-technologies.com>
code/network-generator/network_generation/model/python/o_ran_cloud_du.py
code/network-generator/network_generation/model/python/o_ran_cu.py
code/network-generator/network_generation/model/python/o_ran_near_rt_ric.py
code/network-generator/network_generation/model/python/o_ran_network.py
code/network-generator/network_generation/model/python/o_ran_node.py
code/network-generator/network_generation/model/python/o_ran_ru.py
code/network-generator/network_generation/model/python/o_ran_smo.py
code/network-generator/network_generation/model/python/o_ran_termination_point.py
code/network-generator/network_generation/model/python/tower.py

index 23edb48..7b22264 100644 (file)
@@ -93,6 +93,7 @@ class ORanCloudDu(ORanNode):
                         "layout": self.layout,
                         "parent": self,
                         "network": self.network,
+                        "operationalState": self.operationalState,
                     }
                 )
             )
index d214e0c..4823b45 100644 (file)
@@ -88,6 +88,7 @@ class ORanCu(ORanNode):
                         "layout": self.layout,
                         "parent": self,
                         "network": self.network,
+                        "operationalState": self.operationalState,
                     }
                 )
             )
index e2f2981..b7dc9ce 100644 (file)
@@ -94,6 +94,7 @@ class ORanNearRtRic(ORanNode):
                         "layout": self.layout,
                         "parent": self,
                         "network": self.network,
+                        "operationalState": self.operationalState,
                     }
                 )
             )
index 5011d1c..fc7fc4a 100644 (file)
@@ -1,4 +1,4 @@
-# Copyright 2024 highstreet technologies GmbH
+# Copyright 2024 highstreet technologies USA Corp.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
 """
 Module for a class representing a O-RAN Network
 """
+
 import os
 import uuid
 import xml.etree.ElementTree as ET
 from datetime import datetime, timezone
-from typing import Any, cast
+from typing import Any, cast, Dict, List, Union
 
 import network_generation.model.python.hexagon as Hexagon
-from network_generation.model.python.geo_location import (
-    GeoLocation,
-    IGeoLocation,
-)
+from network_generation.model.python.geo_location import GeoLocation, IGeoLocation
 from network_generation.model.python.hexagon import Layout
-from network_generation.model.python.o_ran_object import (
-    IORanObject,
-    ORanObject,
-)
+from network_generation.model.python.o_ran_object import IORanObject, ORanObject
 from network_generation.model.python.o_ran_smo import ORanSmo
-from network_generation.model.python.o_ran_spiral_radius_profile import (
-    SpiralRadiusProfile,
-)
+from network_generation.model.python.o_ran_spiral_radius_profile import SpiralRadiusProfile
 from network_generation.model.python.point import Point
 
 
@@ -50,9 +43,7 @@ class ORanNetwork(ORanObject):
     """
 
     __my_default_value: IORanNetwork = cast(IORanNetwork, ORanObject.default())
-
     __my_network_id = str(uuid.uuid5(uuid.NAMESPACE_DNS, "Operator A"))
-
     # Get the current date and time in UTC
     __current_time = datetime.now(timezone.utc)
     # Format the time string as required
@@ -73,51 +64,39 @@ class ORanNetwork(ORanObject):
         super().__init__(cast(dict[str, Any], o_ran_network_data), **kwargs)
         self.__configuration = configuration
 
-        self.name = str(configuration["name"])
+        self.name = str(configuration.get("name", "WhiteNetwork"))
+        self.operationalState = str(configuration.get("operationalState", "disabled"))
         self._center: IGeoLocation = cast(
             IGeoLocation, configuration["center"]
         )
 
-        size: int = int(
-            int(configuration["pattern"]["nrCellDu"]["maxReach"])
-            / (
-                1
-                + int(
-                    configuration["pattern"]["nrCellDu"][
-                        "cellScaleFactorForHandoverArea"
-                    ]
-                )
-                / 100
-            )
+        # Calculate layout size using the configuration values.
+        nr_cell_du = configuration["pattern"]["nrCellDu"]
+        size = int(
+            int(nr_cell_du["maxReach"])
+            / (1 + int(nr_cell_du["cellScaleFactorForHandoverArea"]) / 100)
         )
         layout = Layout(
             Hexagon.layout_flat, Point(size, size), Point(0, 0)
         )  # 1 pixel = 1 meter
-        self._spiral_radius_profile: SpiralRadiusProfile = SpiralRadiusProfile(
-            {
-                "oRanSmoSpiralRadiusOfNearRtRics": configuration["pattern"][
-                    "smo"
-                ]["nearRtRicSpiralRadius"],
-                "oRanNearRtRicSpiralRadiusOfOCus": configuration["pattern"][
-                    "nearRtRic"
-                ]["oRanCuSpiralRadius"],
-                "oRanCuSpiralRadiusOfODus": configuration["pattern"]["oRanCu"][
-                    "oRanDuSpiralRadius"
-                ],
-                "oRanDuSpiralRadiusOfTowers": configuration["pattern"][
-                    "oRanDu"
-                ]["towerSpiralRadius"],
-            }
-        )
-        self._o_ran_smo = ORanSmo(
-            {
-                "name": "O-RAN-SMO",
-                "geoLocation": self.center,
-                "layout": layout,
-                "parent": self,
-                "network": self,
-            }
-        )
+        self._spiral_radius_profile: SpiralRadiusProfile = SpiralRadiusProfile({
+            "oRanSmoSpiralRadiusOfNearRtRics": configuration["pattern"][
+                "smo"]["nearRtRicSpiralRadius"],
+            "oRanNearRtRicSpiralRadiusOfOCus": configuration["pattern"][
+                "nearRtRic"]["oRanCuSpiralRadius"],
+            "oRanCuSpiralRadiusOfODus": configuration["pattern"]["oRanCu"][
+                "oRanDuSpiralRadius"],
+            "oRanDuSpiralRadiusOfTowers": configuration["pattern"][
+                "oRanDu"]["towerSpiralRadius"],
+        })
+        self._o_ran_smo = ORanSmo({
+            "name": "O-RAN-SMO",
+            "geoLocation": self.center,
+            "layout": layout,
+            "parent": self,
+            "network": self,
+            "operationalState": self.operationalState
+        })
 
     def _to_o_ran_network_data(self, data: dict[str, Any]) -> IORanNetwork:
         result: IORanNetwork = self.__my_default_value
@@ -150,15 +129,87 @@ class ORanNetwork(ORanObject):
         """
         return self.__configuration
 
-    def to_topology(self) -> dict[str, Any]:
-        nodes: list[dict[str, Any]] = self._o_ran_smo.to_topology_nodes()
-        links: list[dict[str, Any]] = self._o_ran_smo.to_topology_links()
+    def __update_value_by_uuid(self, data: dict[str, Any], target_uuid: str, param_name: str, new_value: str) -> bool:
+        """
+        Recursively searches for an object with the target UUID and updates its parameter value.
+
+        :param data: The JSON dictionary (can be a list or dict).
+        :param target_uuid: The UUID to find.
+        :param param_name: The parameter key to modify.
+        :param new_value: The new value to set.
+        :return: True if updated, False otherwise.
+        """
+        if isinstance(data, dict):
+            # If 'uuid' matches, update the parameter
+            if data.get("o-ran-sc-network:uuid") == target_uuid:
+                if param_name in data:
+                    data[param_name] = new_value
+                    return True  # Stop recursion as we found and updated it
+
+            # Recursively search in nested dictionaries or lists
+            for _, value in data.items():
+                if isinstance(value, (dict, list)) and self.__update_value_by_uuid(value, target_uuid, param_name, new_value):
+                    return True  # Stop searching after finding the target
+
+        elif isinstance(data, list):
+            # Iterate over list items
+            for item in data:
+                if self.__update_value_by_uuid(item, target_uuid, param_name, new_value):
+                    return True  # Stop searching after finding the target
+
+        return False  # UUID not found
+
+    def to_topology(self) -> Dict[str, Any]:
+        """
+        Generate and return the network topology as a dictionary.
+        """
+        profile = self.configuration.get("disabledResourcesProfile", {})
+        resource_types = profile.keys()
+        nodes: List[Dict[str, Any]] = self._o_ran_smo.to_topology_nodes()
+
+        # Initialize identifier lists for each resource type.
+        identifier_lists: Dict[str, List[str]] = {identifier: [] for identifier in resource_types}
+
+        for node in nodes:
+            node_type = node.get("o-ran-sc-network:type")
+            node_uuid = node.get("o-ran-sc-network:uuid")
+            if node_type in resource_types and node_uuid:
+                identifier_lists[node_type].append(node_uuid)
+
+            termination_points = node.get("ietf-network-topology:termination-point", [])
+            for tp in termination_points:
+                if tp.get("o-ran-sc-network:type") == "o-ran-sc-network:cell":
+                    # Create key for cells if not already present.
+                    if "o-ran-sc-network:cell" not in identifier_lists:
+                        identifier_lists["o-ran-sc-network:cell"] = []
+                    cell_uuid = tp.get("o-ran-sc-network:uuid")
+                    if cell_uuid:
+                        identifier_lists["o-ran-sc-network:cell"].append(cell_uuid)
+
+        disabled_resources: List[int] = []
+        for resource_type in resource_types:
+            profile_percentage = profile[resource_type]
+            total_nodes = len(identifier_lists[resource_type])
+            max_disabled = round(profile_percentage * total_nodes / 100)
+            disabled_resources.append(max_disabled)
+            identifier_lists[resource_type].sort()
+            identifier_lists[resource_type] = identifier_lists[resource_type][:max_disabled]
+            for identifier in identifier_lists[resource_type]:
+                self.__update_value_by_uuid(
+                    nodes,
+                    identifier,
+                    "o-ran-sc-network:operational-state",
+                    "disabled"
+                )
+
+        links: List[Dict[str, Any]] = self._o_ran_smo.to_topology_links()
         return {
             "ietf-network:networks": {
                 "network": [
                     {
                         "network-id": self.id,
                         "o-ran-sc-network:name": self.name,
+                        "o-ran-sc-network:operational-state": self.operationalState,
                         "node": nodes,
                         "ietf-network-topology:link": links,
                     }
@@ -167,20 +218,26 @@ class ORanNetwork(ORanObject):
         }
 
     def to_directory(self, parent_dir: str) -> None:
+        """
+        Export the network topology to a specified directory.
+        """
         self._o_ran_smo.to_directory(os.path.join(parent_dir, self.id))
 
     def toKml(self) -> ET.Element:
-        root: ET.Element = ET.Element(
-            "kml", xmlns="http://www.opengis.net/kml/2.2"
-        )
+        """
+        Generate a KML representation of the network.
+
+        Returns:
+            An xml.etree.ElementTree.Element representing the KML.
+        """
+        root = ET.Element("kml", xmlns="http://www.opengis.net/kml/2.2")
         document = ET.SubElement(root, "Document")
-        open: ET.Element = ET.SubElement(document, "open")
-        open.text = "1"
-        name: ET.Element = ET.SubElement(document, "name")
-        name.text = self.name
+        open_elem = ET.SubElement(document, "open")
+        open_elem.text = "1"
+        name_elem = ET.SubElement(document, "name")
+        name_elem.text = self.name
 
         document.append(self._o_ran_smo.toKml())
-
         return root
 
     def toSvg(self) -> ET.Element:
@@ -211,14 +268,19 @@ class ORanNetwork(ORanObject):
         # root.append(self.__context.svg(x, y))
         return root
 
-    def json(self) -> dict[str, Any]:
+    def json(self) -> Dict[str, Any]:
+        """
+        Return a JSON representation of the network.
+        """
         return super().json()
 
-    def to_teiv_data(self) -> dict[str, Any]:
-        entities: dict[str, list[dict[str, Any]]] = (
-            self._o_ran_smo.add_teiv_data_entities()
-        )
-        relationships: dict[str, list[dict[str, Any]]] = (
-            self._o_ran_smo.add_teiv_data_relationships()
-        )
-        return {"entities": [entities], "relationships": [relationships]}
+    def to_teiv_data(self) -> Dict[str, Any]:
+        """
+        Return the network data formatted for TEIV.
+
+        Returns:
+            A dictionary containing TEIV entities and relationships.
+        """
+        entities = self._o_ran_smo.add_teiv_data_entities()
+        relationships = self._o_ran_smo.add_teiv_data_relationships()
+        return {"entities": [entities], "relationships": [relationships]}
\ No newline at end of file
index f7d83f0..fa9b305 100644 (file)
@@ -207,13 +207,15 @@ class ORanNode(ORanObject):
         result.append(ORanTerminationPoint({
             "name": phy_tp,
             "type": "o-ran-sc-network:phy",
-            "network": self.network
+            "operationalState": self.operationalState,
+            "network": self.network,
         }))
         for interface in logical_interfaces:
             id: str = "-".join([self.name, interface.upper()])
             result.append(ORanTerminationPoint({
                      "name": id,
                      "type": ":".join(["o-ran-sc-network", interface]),
+                     "operationalState": self.operationalState,
                      "supporter": {
                         "network-ref": self.network.id,
                         "node-ref": self.name,
@@ -246,6 +248,7 @@ class ORanNode(ORanObject):
                     )
                 ),
                 "o-ran-sc-network:type": self.type,
+                "o-ran-sc-network:operational-state": self.operationalState,
                 "ietf-network-topology:termination-point": tps,
             }
         )
index f85059d..fb45921 100644 (file)
@@ -81,6 +81,7 @@ class ORanRu(ORanNode):
             "layout": self.layout,
             "parent": self.parent.parent.parent,
             "network": self.network,
+            "operationalState": self.operationalState,
         }
         self._oRanDu: ORanDu = ORanDu(o_ran_du_data)
 
@@ -115,6 +116,7 @@ class ORanRu(ORanNode):
                         "layout": self.layout,
                         "parent": self,
                         "network": self.network,
+                        "operationalState": self.operationalState,
                         "cellAngle": cell_angle,
                         "cellScaleFactorForHandoverArea": cell_scale_factor,
                         "maxReach": maxReach,
index 02dc27f..b7946cf 100644 (file)
@@ -87,6 +87,7 @@ class ORanSmo(ORanNode):
                         "layout": self.layout,
                         "parent": self,
                         "network": self.network,
+                        "operationalState": self.operationalState
                     },
                 )
             )
index ae0265f..a036fa6 100644 (file)
@@ -57,6 +57,7 @@ class ORanTerminationPoint(ORanObject):
         super().__init__(cast(dict[str, Any], itp), **kwargs)
         self._supporter = cast(dict[str, str], itp["supporter"])
         self._network: Any = itp["network"]
+        self._operationalState = cast(dict[str, str], itp["operationalState"])
 
     def _to_itp_data(self, data: dict[str, Any]) -> IORanTerminationPoint:
         result: IORanTerminationPoint = default_value
@@ -88,6 +89,7 @@ class ORanTerminationPoint(ORanObject):
             "tp-id": self.name,
             "o-ran-sc-network:uuid": self.id,
             "o-ran-sc-network:type": self.type,
+            "o-ran-sc-network:operational-state": self.operationalState
         }
 
         if not (self.type == "o-ran-sc-network:phy"):
index 498b75b..0631744 100644 (file)
@@ -97,6 +97,7 @@ class Tower(ORanNode):
                         "layout": self.layout,
                         "parent": self,
                         "network": self.network,
+                        "operationalState": self.operationalState,
                         "cellCount": cell_count,
                         "ruAngle": ru_angle,
                         "ruAzimuth": ru_azimuth,