From 6423c8907cee3799547bb15a79ab1019961bd348 Mon Sep 17 00:00:00 2001 From: armingol Date: Thu, 11 Sep 2025 12:22:38 +0000 Subject: [PATCH 1/6] Add initial JSON templates for IPoWDM orchestrator, Optical slice, TAPI service, and network slice service - Created IPoWDM_orchestrator.json to define service configurations for optical and packet devices. - Added Optical_slice.json to outline the structure for optical slice contexts and topology. - Introduced TAPI_service.json for TAPI LSP service definitions, including service configurations and rules. - Implemented slice1.json to specify network slice services with SLO policies and connection groups. --- src/{Constants.py => constants.py} | 13 +- src/helpers.py | 93 +- src/network_slice_controller.py | 1046 ++++-- src/rules.json | 89 + src/slice_ddbb.json | 4119 +++++++++++++++++++++++- src/templates/IPoWDM_orchestrator.json | 31 + src/templates/Optical_slice.json | 28 + src/templates/TAPI_service.json | 43 + src/templates/slice1.json | 81 + 9 files changed, 5238 insertions(+), 305 deletions(-) rename src/{Constants.py => constants.py} (84%) create mode 100644 src/rules.json create mode 100644 src/templates/IPoWDM_orchestrator.json create mode 100644 src/templates/Optical_slice.json create mode 100644 src/templates/TAPI_service.json create mode 100644 src/templates/slice1.json diff --git a/src/Constants.py b/src/constants.py similarity index 84% rename from src/Constants.py rename to src/constants.py index 337be88..301cce9 100644 --- a/src/Constants.py +++ b/src/constants.py @@ -13,13 +13,15 @@ # limitations under the License. # This file is an original contribution from Telefonica Innovación Digital S.L. +""" This file contains constants used throughout the NSC application. """ -import logging, os +import logging +import os # Default logging level DEFAULT_LOGGING_LEVEL = logging.INFO # Default port for NSC deployment -NSC_PORT = 8081 +NSC_PORT = 8086 # Paths # Obtain the absolute path of the current file @@ -32,6 +34,7 @@ TEMPLATES_PATH = os.path.join(SRC_PATH, "templates") # Flag to determine if configurations should be uploaded to Teraflow TFS_UPLOAD = False # Teraflow IP -TFS_IP = "192.168.165.10" -# Flag to determine if additional L2VPN configuration support is required for deploying L2VPNs with path selection -TFS_L2VPN_SUPPORT = False \ No newline at end of file +TFS_IP = "10.95.86.58" +# Flag to determine if additional L2VPN configuration support is +# required for deploying L2VPNs with path selection +TFS_L2VPN_SUPPORT = False diff --git a/src/helpers.py b/src/helpers.py index 47adc16..f8457bb 100644 --- a/src/helpers.py +++ b/src/helpers.py @@ -11,10 +11,14 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +""" This file is an original contribution from Telefonica Innovación Digital S.L. """ -import logging, requests, json +import json +import logging +import os +import requests from netmiko import ConnectHandler -from src.Constants import DEFAULT_LOGGING_LEVEL +from src.constants import DEFAULT_LOGGING_LEVEL, SRC_PATH # Configure logging to provide clear and informative log messages logging.basicConfig( @@ -22,9 +26,16 @@ logging.basicConfig( format='%(levelname)s - %(message)s') #Teraflow -class tfs_connector(): - +class TFSConnector(): + """Connector class for interacting with Teraflow SDN. + This class provides methods to send requests to + Teraflow SDN for network service configuration.""" + def simple_post(self, tfs_ip, service): + """Send a simple POST request to Teraflow SDN. + This method sends a JSON payload to the Teraflow SDN + service endpoint to configure network services.""" + user="admin" password="admin" token="" @@ -33,28 +44,37 @@ class tfs_connector(): url=f'http://{tfs_ip}/webui' response=session.get(url=url) for item in response.iter_lines(): - if("csrf_token" in str(item)): + if "csrf_token" in str(item): string=str(item).split(' dict: + + url = 'http://192.168.1.143:9090/api/resource-allocation/transport-network-slice-l3' + headers = {'Content-Type': 'application/json'} + try: + if action == "delete": + data = { + "ietf-network-slice-service:network-slice-services": { + "slice-service": [ + { + "id": data + } + ] + } + } + response = requests.delete(url, headers=headers, json=data, timeout=15) + elif action == "create": + response = requests.post(url, headers=headers, json=data, timeout=15) + else: + raise ValueError("Invalid action. Use 'create' or 'delete'.") + except requests.exceptions.RequestException as e: + logging.error(f"HTTP request failed: {e}") + return {} + + + # Comprobar y devolver la respuesta + if response.ok: + return response.json() + else: + print(f"Request failed with status code {response.status_code}: {response.text}") + response.raise_for_status() diff --git a/src/network_slice_controller.py b/src/network_slice_controller.py index 7d14114..08d70d7 100644 --- a/src/network_slice_controller.py +++ b/src/network_slice_controller.py @@ -11,11 +11,19 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +""" This file is an original contribution from Telefonica Innovación Digital S.L. """ + +import json +import logging +import os +import time +import uuid -import json, time, os, logging, uuid from datetime import datetime -from src.helpers import tfs_connector, cisco_connector -from src.Constants import DEFAULT_LOGGING_LEVEL, TFS_UPLOAD, TFS_IP, TFS_L2VPN_SUPPORT, SRC_PATH, TEMPLATES_PATH + +import requests +from src.helpers import TFSConnector, CiscoConnector, send_network_slice_request +from src.constants import DEFAULT_LOGGING_LEVEL, TFS_UPLOAD, TFS_IP, TFS_L2VPN_SUPPORT, SRC_PATH, TEMPLATES_PATH # Configure logging to provide clear and informative log messages logging.basicConfig( @@ -24,11 +32,11 @@ logging.basicConfig( class NSController: """ - Network Slice Controller (NSC) - A class to manage network slice creation, + Network Slice Controller (NSC) - A class to manage network slice creation, modification, and deletion across different network domains. - This controller handles the translation, mapping, and realization of network - slice intents from different formats (3GPP and IETF) to network-specific + This controller handles the translation, mapping, and realization of network + slice intents from different formats (3GPP and IETF) to network-specific configurations. Key Functionalities: @@ -38,16 +46,17 @@ class NSController: - Slice Realization: Convert intents to specific network configurations (L2VPN, L3VPN) """ - def __init__(self, upload_to_tfs = TFS_UPLOAD, tfs_ip=TFS_IP, need_l2vpn_support=TFS_L2VPN_SUPPORT): + def __init__(self, upload_to_tfs = TFS_UPLOAD, tfs_ip=TFS_IP, + need_l2vpn_support=TFS_L2VPN_SUPPORT): """ Initialize the Network Slice Controller. Args: - upload_to_tfs (bool, optional): Flag to determine if configurations + upload_to_tfs (bool, optional): Flag to determine if configurations should be uploaded to Teraflow system. Defaults to False. need_l2vpn_support (bool, optional): Flag to determine if additional L2VPN configuration support is required. Defaults to False. - + Attributes: upload_to_tfs (bool): Flag for Teraflow upload answer (dict): Stores slice creation responses @@ -65,7 +74,6 @@ class NSController: self.setup_time = 0 self.need_l2vpn_support = need_l2vpn_support # Internal templates and views - self.__gpp_template = "" self.__ietf_template = "" self.__teraflow_template = "" self.__nrp_view = "" @@ -89,7 +97,7 @@ class NSController: ValueError: If no transport network slices are found Exception: For unexpected errors during slice creation process """ - return self.nsc(intent) + return self.nsc(intent, action="create") def get_flows(self,slice_id=None): """ @@ -100,11 +108,11 @@ class NSController: - A specific slice by its ID Args: - slice_id (str, optional): Unique identifier of a specific slice. + slice_id (str, optional): Unique identifier of a specific slice. Defaults to None. Returns: - dict or list: + dict or list: - If slice_id is provided: Returns the specific slice details - If slice_id is None: Returns a list of all slices - Returns an error response if no slices are found @@ -118,25 +126,25 @@ class NSController: """ try: # Read slice database from JSON file - with open(os.path.join(SRC_PATH, "slice_ddbb.json"), 'r') as file: + with open(os.path.join(SRC_PATH, "slice_ddbb.json"), 'r', encoding='utf-8') as file: content = json.load(file) # If specific slice ID is provided, find and return matching slice if slice_id: - for slice in content: - if slice["slice_id"] == slice_id: - return slice + for slice_item in content: + if slice_item["slice_id"] == slice_id: + return slice_item # If no slices exist, raise an error if len(content) == 0: raise ValueError("Transport network slices not found") - + # Return all slices if no specific ID is given return content - + except ValueError as e: # Handle case where no slices are found return self.__send_response(False, code=404, message=str(e)) - except Exception as e: - # Handle unexpected errors + except OSError as e: + # Handle file-related errors return self.__send_response(False, code=500, message=str(e)) def modify_flow(self,slice_id, intent): @@ -165,7 +173,7 @@ class NSController: - Optional cleanup of L2VPN configurations Args: - slice_id (str, optional): Unique identifier of slice to delete. + slice_id (str, optional): Unique identifier of slice to delete. Defaults to None. Returns: @@ -190,20 +198,8 @@ class NSController: # Delete specific slice if slice_id is provided if slice_id: - for i, slice in enumerate(content): - if slice["slice_id"] == slice_id: - del content[i] - id = i - break - # Raise error if slice not found - if id is None: - raise ValueError("Transport network slice not found") - # Update slice database - with open(os.path.join(SRC_PATH, "slice_ddbb.json"), 'w') as file: - json.dump(content, file, indent=4) - logging.info(f"Slice {slice_id} removed successfully") - return self.__send_response(False, code=200, status="success", message=f"Transpor network slice {slice_id} deleted successfully") - + return self.nsc(slice_id, action="delete") + # Delete all slices else: # Optional: Delete in Teraflow if configured @@ -216,21 +212,21 @@ class NSController: with open(os.path.join(SRC_PATH, "slice_ddbb.json"), 'r') as file: if len(json.load(file)) == 0: raise ValueError("Transport network slices not found") - + # Clear slice database with open(os.path.join(SRC_PATH, "slice_ddbb.json"), 'w') as file: json.dump([], file, indent=4) logging.info("All slices removed successfully") return self.__send_response(False, code=200, status="success", message="All transport network slices deleted successfully.") - + except ValueError as e: return self.__send_response(False, code=404, message=str(e)) except Exception as e: return self.__send_response(False, code=500, message=str(e)) - # Main NSC Functionalities - def nsc(self, intent_json, slice_id=None): + # Main NSC Functionalities + def nsc(self, intent_json, slice_id=None, action=None): """ Main Network Slice Controller method to process and realize network slice intents. @@ -249,57 +245,73 @@ class NSController: Returns: tuple: Response status and HTTP status code - + """ + service_uuid = None + try: - # Start performance tracking - self.start_time = time.perf_counter() - - # Reset requests and load IETF template - self.__load_template(1, os.path.join(TEMPLATES_PATH, "ietf_template_empty.json")) - tfs_requests = {"services":[]} - - # Process intent (translate if 3GPP) - ietf_intents = self.__nbi_processor(intent_json) - - if ietf_intents: - for intent in ietf_intents: - # Extract and store slice request details - self.__extract_data(intent) - self.__store_data(intent, slice_id) - # Mapper - self.__mapper(intent) - # Realizer - tfs_request = self.__realizer(intent) - tfs_requests["services"].append(tfs_request) + if action == 'delete': + logging.info("\n\n--------NEW REQUEST--------\n") + logging.info("DELETE REQUEST RECEIVED: %s", intent_json) + + rules = send_network_slice_request(intent_json, action) + logging.info("Rules loaded: %s", rules) + tfs_request = self.__realizer(rules=rules) else: - return self.__send_response(False, code=404, message="No intents found") + # Start performance tracking + self.start_time = time.perf_counter() + + # Reset requests and load IETF template + self.__load_template(1, os.path.join(TEMPLATES_PATH, "ietf_template_empty.json")) + tfs_requests = {"services":[]} + + # Process intent (translate if 3GPP) + ietf_intents = self.__nbi_processor(intent_json) + if ietf_intents: + for intent in ietf_intents: + + # Extract and store slice request details + self.__extract_data(intent) + # Mapper + self.__mapper(intent, action) + # Realizer + rules = send_network_slice_request(intent, action) + logging.info("Rules loaded: %s", rules) + service_uuid = rules.get("network-slice-uuid") + + self.__store_data(intent, slice_id, service_uuid = service_uuid) + tfs_request = self.__realizer(intent, rules=rules) + tfs_requests["services"].append(tfs_request) + else: + return self.__send_response(False, code=404, message="No intents found") + + # Generated service + logging.debug(json.dumps(tfs_requests, indent=2)) - # Generated service - logging.debug(json.dumps(tfs_requests, indent=2)) - - # Optional: Upload template to Teraflow - if self.upload_to_tfs == True: - response = tfs_connector().simple_post(self.tfs_ip, tfs_requests) + # Optional: Upload template to Teraflow + if self.upload_to_tfs is True: + response = TFSConnector().simple_post(self.tfs_ip, tfs_requests) - if not response.ok: - return self.__send_response(False, code=response.status_code, message=f"Teraflow upload failed. Response: {response.text}") - - # For deploying an L2VPN with path selection (not supported by Teraflow) - if self.need_l2vpn_support: - self.__tfs_l2vpn_support(tfs_requests["services"]) + if not response.ok: + return self.__send_response(False, + code=response.status_code, + message=f"Teraflow upload failed. Response: {response.text}") - logging.info("Request sent to Teraflow") + # For deploying an L2VPN with path selection (not supported by Teraflow) + if self.need_l2vpn_support: + self.__tfs_l2vpn_support(tfs_requests["services"]) + + logging.info("Request sent to Teraflow") # End performance tracking self.end_time = time.perf_counter() - return self.__send_response(True, code=200) + return self.__send_response(True, code=200, service_uuid = service_uuid) except ValueError as e: return self.__send_response(False, code=400, message=str(e)) - except Exception as e: + except OSError as e: return self.__send_response(False, code=500, message=str(e)) - + def __nbi_processor(self, intent_json): """ Process and translate network slice intents from different formats (3GPP or IETF). @@ -317,29 +329,33 @@ class NSController: ValueError: If the JSON request format is not recognized. """ # Detect the input JSON format (3GPP or IETF) - format = self.__detect_format(intent_json) + detected_format = self.__detect_format(intent_json) ietf_intents = [] - logging.info("--------NEW REQUEST--------") + logging.info("\n\n--------NEW REQUEST--------\n") - # TODO Needs to be generalized to support different names of slicesubnets + # Generalize to support different names of slice subnets # Process different input formats - if format == "3GPP": - # Translate each subnet in 3GPP format to IETF format - for subnet in intent_json["RANSliceSubnet1"]["networkSliceSubnetRef"]: - ietf_intents.append(self.__translator(intent_json, subnet)) - logging.info(f"3GPP requests translated to IETF template") - elif format == "IETF": + if detected_format == "3GPP": + # Find all keys in the intent that look like slice subnet containers + slice_subnet_keys = [k for k in intent_json.keys() + if k.lower().endswith("slicesubnet1")] + if not slice_subnet_keys: + raise ValueError("No slice subnet keys found in 3GPP intent") + for subnet_key in slice_subnet_keys: + for subnet in intent_json[subnet_key]["networkSliceSubnetRef"]: + ietf_intents.append(self.__translator(intent_json, subnet)) + logging.info("3GPP requests translated to IETF template") + elif detected_format == "IETF": # If already in IETF format, add directly - logging.info(f"IETF intent received") + logging.info("IETF intent received") ietf_intents.append(intent_json) else: # Handle unrecognized format - logging.error(f"JSON request format not recognized") + logging.error("JSON request format not recognized") raise ValueError("JSON request format not recognized") - return ietf_intents - def __mapper(self, ietf_intent): + def __mapper(self, ietf_intent, action): """ Map an IETF network slice intent to the most suitable Network Resource Partition (NRP). @@ -355,57 +371,86 @@ class NSController: Raises: Exception: If no suitable NRP is found and slice creation fails. - """ + """ # Retrieve NRP view self.__realizer(None, True, "READ") - # Extract Service Level Objectives (SLOs) from the intent - slos = ietf_intent["ietf-network-slice-service:network-slice-services"]["slo-sle-templates"]["slo-sle-template"][0]["slo-policy"]["metric-bound"] - - # Find candidate NRPs that can meet the SLO requirements - candidates = [ - (nrp, self.__slo_viability(slos, nrp)[1]) - for nrp in self.__nrp_view - if self.__slo_viability(slos, nrp)[0] and nrp["available"] - ] - logging.debug(f"Candidates: {candidates}") - - # Select the best NRP based on candidates - best_nrp = max(candidates, key=lambda x: x[1])[0] if candidates else None - logging.debug(f"Best NRP: {best_nrp}") - - if best_nrp: - best_nrp["slices"].append(ietf_intent["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["id"]) - # Update NRP view - self.__realizer(ietf_intent, True, "UPDATE") - # TODO Here we should put how the slice is attached to an already created nrp - else: - # Request the controller to create a new NRP that meets the SLOs - answer = self.__realizer(ietf_intent, True, "CREATE", best_nrp) - if not answer: - raise Exception("Slice rejected due to lack of NRPs") - # TODO Here we should put how the slice is attached to the new nrp - - def __realizer(self, ietf_intent, need_nrp=False, order=None, nrp=None): - """ - Manage the slice creation workflow. + if action == 'delete': + self.__realizer(ietf_intent, True, order ="DELETE") + else: + # Extract Service Level Objectives (SLOs) from the intent + slos = ietf_intent["ietf-network-slice-service:network-slice-services"][ + "slo-sle-templates"]["slo-sle-template"][0]["slo-policy"]["metric-bound"] + + # Find candidate NRPs that can meet the SLO requirements + candidates = [ + (nrp, self.__slo_viability(slos, nrp)[1]) + for nrp in self.__nrp_view + if self.__slo_viability(slos, nrp)[0] and nrp["available"] + ] + logging.debug("Candidates: %s", candidates) + + # Select the best NRP based on candidates + best_nrp = max(candidates, key=lambda x: x[1])[0] if candidates else None + logging.debug("Best NRP: %s", best_nrp) + + if best_nrp: + best_nrp["slices"].append(ietf_intent[ + "ietf-network-slice-service:network-slice-services"][ + "slice-service"][0]["id"]) + + # Update NRP view + self.__realizer(ietf_intent, True, "UPDATE") + # TODO Here we should put how the slice is attached to an already created nrp + else: + # Request the controller to create a new NRP that meets the SLOs + answer = self.__realizer(ietf_intent, True, "CREATE", best_nrp) + if not answer: + raise RuntimeError("Slice rejected due to lack of NRPs") + # TODO Here we should put how the slice is attached to the new nrp - This method handles two primary scenarios: - 1. Interact with network controllers for NRP (Network Resource Partition) operations when need_nrp is True - 2. Slice service selection when need_nrp is False + def __realizer(self, ietf_intent = None, need_nrp=False, order=None, nrp=None, rules=None, way=None): - Args: - ietf_intent (dict): IETF-formatted network slice intent. - need_nrp (bool, optional): Flag to indicate if NRP operations are needed. Defaults to False. - order (str, optional): Type of NRP operation (READ, UPDATE, CREATE). Defaults to None. - nrp (dict, optional): Specific Network Resource Partition to operate on. Defaults to None. - """ if need_nrp: # Perform NRP-related operations self.__nrp(order, nrp) else: - # Select slice service method - return self.__select_way("L2VPN", ietf_intent) + # Detect 'way' automatically if not specified + selected_way = way + if selected_way is None: + actions = rules.get("actions", []) + has_optical = any(act.get("type", "").startswith("CREATE_OPTICAL_SLICE") for act in actions) + has_l2 = any(act.get("type", "").startswith("CONFIG_VPNL2") for act in actions) + has_l3 = any(act.get("type", "").startswith("CONFIG_VPNL3") for act in actions) + del_l3 = any(act.get("type", "").startswith("REMOVE_VPNL3") for act in actions) + del_l2 = any(act.get("type", "").startswith("REMOVE_VPNL2") for act in actions) + del_optical = any(act.get("type", "").startswith("DEPROVISION_OPTICAL_RESOURCE") for act in actions) + + if has_l2 and not (has_l3 or has_optical): + selected_way = "L2VPN" + elif has_l3 and not (has_l2 or has_optical): + selected_way = "L3VPN" + elif has_optical and not (has_l2 or has_l3): + selected_way = "OPTIC" + elif has_optical and has_l2: + selected_way = "L2oWDM" + elif has_optical and has_l3: + selected_way = "L3oWDM" + elif del_l2 and not (del_l3 or del_optical): + selected_way = "DEL_L2VPN" + elif del_l3 and not (del_l2 or del_optical): + selected_way = "DEL_L3VPN" + elif del_optical and not (del_l2 or del_l3): + selected_way = "DEL_OPTIC" + elif del_optical and del_l2: + selected_way = "DEL_L2oWDM" + elif del_optical and del_l3: + selected_way = "DEL_L3oWDM" + else: + raise ValueError("Cannot determine the realization way from rules") + logging.info("Selected Realizer: %s", selected_way) + + return self.__select_way(selected_way, ietf_intent, rules=rules) ### Generic functionalities def __load_template(self, which, dir_t): @@ -418,23 +463,22 @@ class NSController: """ try: # Open and read the template file - with open(dir_t, 'r') as source: + with open(dir_t, 'r', encoding='utf-8') as source: # Clean up the JSON template template = source.read().replace('\t', '').replace('\n', '').replace("'", '"').strip() - + # Store template based on selector if which == 0: - self.__gpp_template = template - elif which == 1: + pass # No action for 3GPP template (or add logic if needed) + if which == 1: self.__ietf_template = template else: self.__teraflow_template = template - - except Exception as e: - logging.error(f"Template loading error: {e}") + except OSError as e: + logging.error("Template loading error: %s", e) return self.__send_response(False, code=500, message=f"Template loading error: {e}") - def __send_response(self, result, status="error", message=None, code=None): + def __send_response(self, result, status="error", message=None, code=None, service_uuid=None): """ Generate and send a response to the 3GPP client about the slice request. @@ -446,35 +490,36 @@ class NSController: Returns: tuple: A tuple containing the response dictionary and status code - """ + """ if result: # Successful slice creation logging.info("Your slice request was fulfilled sucessfully") self.setup_time = (self.end_time - self.start_time)*1000 - logging.info(f"Setup time: {self.setup_time:.2f}") + logging.info("Setup time: %.2f", self.setup_time) # Construct detailed successful response answer = { - "status": "success", - "code": code, - "slices": [], - "setup_time": self.setup_time - } - # Add slice details to the response - for subnet in self.answer: + "status": "success", + "code": code, + "slices": [], + "setup_time": self.setup_time + } + # Add slice details to the response + for subnet, subnet_info in self.answer.items(): slice_info = { - "id": subnet, - "source": self.answer[subnet]["Source"], - "destination": self.answer[subnet]["Destination"], - "vlan": self.answer[subnet]["VLAN"], - "bandwidth(Mbps)": self.answer[subnet]["QoS Requirements"][0], - "latency(ms)": self.answer[subnet]["QoS Requirements"][1] + "id": service_uuid if service_uuid else subnet, + "source": subnet_info["Source"], + "destination": subnet_info["Destination"], + "vlan": subnet_info["VLAN"], + "bandwidth(Mbps)": subnet_info["QoS Requirements"][0], + "latency(ms)": subnet_info["QoS Requirements"][1] } answer["slices"].append(slice_info) + self.cool_answer = answer else: # Failed slice creation - logging.info("Your request cannot be fulfilled. Reason: "+message) + logging.info("Your request cannot be fulfilled. Reason: %s", message) self.cool_answer = { "status" :status, "code": code, @@ -482,7 +527,7 @@ class NSController: } return self.cool_answer, code - def __extract_data(self, intent_json): + def __extract_data(self, intent_json, action = None): """ Extract source and destination IP addresses from the IETF intent. @@ -490,20 +535,26 @@ class NSController: intent_json (dict): IETF-formatted network slice intent """ # Extract source and destination IP addresses - source = intent_json["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["sdps"]["sdp"][0]["sdp-ip-address"] - destination = intent_json["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["sdps"]["sdp"][1]["sdp-ip-address"] + source = intent_json["ietf-network-slice-service:network-slice-services"][ + "slice-service"][0]["sdps"]["sdp"][0]["attachment-circuits"]['attachment-circuit'][0]['ac-ipv4-address'] - logging.info(f"Intent generated between {source} and {destination}") + destination = intent_json["ietf-network-slice-service:network-slice-services"][ + "slice-service"][0]["sdps"]["sdp"][1]["attachment-circuits"]['attachment-circuit'][0]['ac-ipv4-address'] + logging.info("Generating intent between %s and %s", source, destination) # Store slice and connection details - self.subnet = intent_json["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["id"] - self.subnet = intent_json["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["id"] + self.subnet = intent_json["ietf-network-slice-service:network-slice-services"][ + "slice-service"][0]["id"] + + self.subnet = intent_json["ietf-network-slice-service:network-slice-services"][ + "slice-service"][0]["id"] + self.answer[self.subnet] = { "Source": source, "Destination": destination } - - def __store_data(self, intent, slice_id): + + def __store_data(self, intent, slice_id, service_uuid =None): """ Store network slice intent information in a JSON database file. @@ -517,46 +568,59 @@ class NSController: slice_id (str, optional): Existing slice ID to update. Defaults to None. """ file_path = os.path.join(SRC_PATH, "slice_ddbb.json") - # Create initial JSON file if it doesn't exist + # Create initial file if does not exist if not os.path.exists(file_path): - with open(file_path, 'w') as file: + with open(file_path, 'w', encoding='utf-8') as file: json.dump([], file, indent=4) # Read existing content - with open(file_path, 'r') as file: + with open(file_path, 'r', encoding='utf-8') as file: content = json.load(file) - - # Update or add new slice intent + + # Determine the actual ID to use + slice_id = service_uuid if service_uuid is not None else slice_id + if slice_id is None: + # If neither given, fallback to intent's internal id + slice_id = intent["ietf-network-slice-service"]["slice_id"] if "slice_id" in intent["ietf-network-slice-service"] \ + else intent["ietf-network-slice-service"]["slice_id"] if "slice_id" in intent else intent["ietf-network-slice-service"]["slice_id"] + + # Update existing item if id found if slice_id: - # Update existing slice intent + updated = False for slice in content: if slice["slice_id"] == slice_id: slice["intent"] = intent - else: - # Add new slice intent - content.append( - { - "slice_id": intent["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["id"], + updated = True + break + if not updated: + content.append({ + "slice_id": slice_id, "intent": intent }) - - # # Write updated content back to file - with open(file_path, 'w') as file: + else: + # No slice id found, append with internal intent id + content.append({ + "slice_id": intent["ietf-network-slice-service:"]["slice_id"], + "intent": intent + }) + + # Write updated content + with open(file_path, 'w', encoding='utf-8') as file: json.dump(content, file, indent=4) ### NBI processor functionalities - def __detect_format(self,json_data): + def __detect_format(self,json_data): """ Detect the format of the input network slice intent. - This method identifies whether the input JSON is in 3GPP or IETF format + This method identifies whether the input JSON is in 3GPP or IETF format by checking for specific keys in the JSON structure. Args: json_data (dict): Input network slice intent JSON Returns: - str or None: + str or None: - "IETF" if IETF-specific keys are found - "3GPP" if 3GPP-specific keys are found - None if no recognizable format is detected @@ -565,16 +629,16 @@ class NSController: if "ietf-network-slice-service:network-slice-services" in json_data: return "IETF" # Check for 3GPP-specific keys - if any(key in json_data for key in ["NetworkSlice1", "TopSliceSubnet1", "CNSliceSubnet1", "RANSliceSubnet1"]): - return "3GPP" - + if any(key in json_data for key in ["NetworkSlice1", "TopSliceSubnet1", "CNSliceSubnet1", + "RANSliceSubnet1"]): return "3GPP" + return None - + def __translator(self, gpp_intent, subnet): """ Translate a 3GPP network slice intent to IETF format. - This method converts a 3GPP intent into a standardized IETF intent template, + This method converts a 3GPP intent into a standardized IETF intent template, mapping key parameters such as QoS profiles, service endpoints, and connection details. Args: @@ -583,7 +647,7 @@ class NSController: Returns: dict: Translated IETF-formatted network slice intent - + Notes: - Generates a unique slice service ID using UUID - Maps QoS requirements, source/destination endpoints @@ -596,46 +660,78 @@ class NSController: ep_transport_objects = gpp_intent[subnet]["EpTransport"] # Populate template with SLOs (currently supporting QoS profile, latency and bandwidth) - ietf_i["ietf-network-slice-service:network-slice-services"]["slo-sle-templates"]["slo-sle-template"][0]["id"] = gpp_intent[ep_transport_objects[0]]["qosProfile"] - ietf_i["ietf-network-slice-service:network-slice-services"]["slo-sle-templates"]["slo-sle-template"][0]["slo-policy"]["metric-bound"][0]["bound"] = gpp_intent[subnet]["SliceProfileList"][0]["RANSliceSubnetProfile"]["uLThptPerSliceSubnet"]["MaxThpt"] - ietf_i["ietf-network-slice-service:network-slice-services"]["slo-sle-templates"]["slo-sle-template"][0]["slo-policy"]["metric-bound"][1]["bound"] = gpp_intent[subnet]["SliceProfileList"][0]["RANSliceSubnetProfile"]["uLLatency"] + ietf_i["ietf-network-slice-service:network-slice-services"]["slo-sle-templates"][ + "slo-sle-template"][0]["id"] = gpp_intent[ep_transport_objects[0]]["qosProfile"] + ietf_i["ietf-network-slice-service:network-slice-services"]["slo-sle-templates"][ + "slo-sle-template"][0]["slo-policy"]["metric-bound"][0]["bound"] = gpp_intent[subnet][ + "SliceProfileList"][0]["RANSliceSubnetProfile"]["uLThptPerSliceSubnet"]["MaxThpt"] + + ietf_i["ietf-network-slice-service:network-slice-services"]["slo-sle-templates"][ + "slo-sle-template"][0]["slo-policy"]["metric-bound"][1]["bound"] = gpp_intent[subnet][ + "SliceProfileList"][0]["RANSliceSubnetProfile"]["uLLatency"] # Generate unique slice service ID and description - ietf_i["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["id"] = f"slice-service-{uuid.uuid4()}" - ietf_i["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["description"] = f"Transport network slice mapped with 3GPP slice {next(iter(gpp_intent))}" - ietf_i["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["slo-sle-policy"]["slo-sle-template"] = ietf_i["ietf-network-slice-service:network-slice-services"]["slo-sle-templates"]["slo-sle-template"][0]["id"] - + ietf_i["ietf-network-slice-service:network-slice-services"]["slice-service"][0][ + "id"] = f"slice-service-{uuid.uuid4()}" + + ietf_i["ietf-network-slice-service:network-slice-services"]["slice-service"][0][ + "description"] = f"Transport network slice mapped with 3GPP slice {next(iter(gpp_intent))}" + ietf_i["ietf-network-slice-service:network-slice-services"]["slice-service"][0][ + "slo-sle-policy"]["slo-sle-template"] = ietf_i[ + "ietf-network-slice-service:network-slice-services"]["slo-sle-templates"][ + "slo-sle-template"][0]["id"] + # Configure Source SDP - ietf_i["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["sdps"]["sdp"][0]["node-id"] = ep_transport_objects[0].split(" ", 1)[1] - ietf_i["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["sdps"]["sdp"][0]["sdp-ip-address"] = gpp_intent[gpp_intent[ep_transport_objects[0]]["EpApplicationRef"][0]]["localAddress"] - ietf_i["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["sdps"]["sdp"][0]["service-match-criteria"]["match-criterion"][0]["match-type"] = gpp_intent[ep_transport_objects[0]]["logicalInterfaceInfo"]["logicalInterfaceType"] - ietf_i["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["sdps"]["sdp"][0]["service-match-criteria"]["match-criterion"][0]["value"] = gpp_intent[ep_transport_objects[0]]["logicalInterfaceInfo"]["logicalInterfaceId"] - ietf_i["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["sdps"]["sdp"][0]["attachment-circuits"]["attachment-circuit"][0]["ac-ipv4-address"] = gpp_intent[ep_transport_objects[0]]["IpAddress"] - ietf_i["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["sdps"]["sdp"][0]["attachment-circuits"]["attachment-circuit"][0]["sdp-peering"]["peer-sap-id"] = gpp_intent[ep_transport_objects[0]]["NextHopInfo"] + ietf_i["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["sdps"][ + "sdp"][0]["node-id"] = ep_transport_objects[0].split(" ", 1)[1] + + ietf_i["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["sdps"][ + "sdp"][0]["sdp-ip-address"] = gpp_intent[gpp_intent[ep_transport_objects[0]][ + "EpApplicationRef"][0]]["localAddress"] + + + + ietf_i["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["sdps"][ + "sdp"][0]["attachment-circuits"]["attachment-circuit"][0][ + "ac-ipv4-address"] = gpp_intent[ep_transport_objects[0]]["IpAddress"] + + ietf_i["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["sdps"][ + "sdp"][0]["attachment-circuits"]["attachment-circuit"][0]["sdp-peering"][ + "peer-sap-id"] = gpp_intent[ep_transport_objects[0]]["NextHopInfo"] # Configure Destination SDP - ietf_i["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["sdps"]["sdp"][1]["node-id"] = ep_transport_objects[1].split(" ", 1)[1] - ietf_i["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["sdps"]["sdp"][1]["sdp-ip-address"] = gpp_intent[gpp_intent[ep_transport_objects[1]]["EpApplicationRef"][0]]["localAddress"] - ietf_i["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["sdps"]["sdp"][1]["service-match-criteria"]["match-criterion"][0]["match-type"] = gpp_intent[ep_transport_objects[1]]["logicalInterfaceInfo"]["logicalInterfaceType"] - ietf_i["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["sdps"]["sdp"][1]["service-match-criteria"]["match-criterion"][0]["value"] = gpp_intent[ep_transport_objects[1]]["logicalInterfaceInfo"]["logicalInterfaceId"] - ietf_i["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["sdps"]["sdp"][1]["attachment-circuits"]["attachment-circuit"][0]["ac-ipv4-address"] = gpp_intent[ep_transport_objects[1]]["IpAddress"] - ietf_i["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["sdps"]["sdp"][1]["attachment-circuits"]["attachment-circuit"][0]["sdp-peering"]["peer-sap-id"] = gpp_intent[ep_transport_objects[1]]["NextHopInfo"] + ietf_i["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["sdps"][ + "sdp"][1]["node-id"] = ep_transport_objects[1].split(" ", 1)[1] + + ietf_i["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["sdps"][ + "sdp"][1]["sdp-ip-address"] = gpp_intent[gpp_intent[ep_transport_objects[1]][ + "EpApplicationRef"][0]]["localAddress"] + + + + ietf_i["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["sdps"][ + "sdp"][1]["attachment-circuits"]["attachment-circuit"][0][ + "ac-ipv4-address"] = gpp_intent[ep_transport_objects[1]]["IpAddress"] + + ietf_i["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["sdps"][ + "sdp"][1]["attachment-circuits"]["attachment-circuit"][0]["sdp-peering"][ + "peer-sap-id"] = gpp_intent[ep_transport_objects[1]]["NextHopInfo"] # Configure Connection Group and match-criteria - ietf_i["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["connection-groups"]["connection-group"][0]["id"] = f"{ep_transport_objects[0].split(' ', 1)[1]}_{ep_transport_objects[1].split(' ', 1)[1]}" - ietf_i["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["sdps"]["sdp"][0]["service-match-criteria"]["match-criterion"][0]["target-connection-group-id"] = f"{ep_transport_objects[0].split(' ', 1)[1]}_{ep_transport_objects[1].split(' ', 1)[1]}" - ietf_i["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["sdps"]["sdp"][1]["service-match-criteria"]["match-criterion"][0]["target-connection-group-id"] = f"{ep_transport_objects[0].split(' ', 1)[1]}_{ep_transport_objects[1].split(' ', 1)[1]}" + ietf_i["ietf-network-slice-service:network-slice-services"]["slice-service"][0][ + "connection-groups"]["connection-group"][0]["id"] = f"{ep_transport_objects[0].split(' ', 1)[1]}_{ep_transport_objects[1].split(' ', 1)[1]}" # Log translated intent for debugging logging.debug(json.dumps(ietf_i,indent=2)) - with open(os.path.join(TEMPLATES_PATH, "ietf_template_example.json"), "w") as archivo: + with open(os.path.join(TEMPLATES_PATH, "ietf_template_example.json"), "w", encoding="utf-8") as archivo: archivo.write(json.dumps(ietf_i,indent=2)) return ietf_i - + ### Mapper functionalities def __slo_viability(self, slice_slos, nrp_slos): """ - Compare Service Level Objectives (SLOs) between a slice and a Network Resource Partition (NRP). + Compare Service Level Objectives (SLOs) between a slice and a Network Resource + Partition (NRP). This method assesses whether an NRP can satisfy the SLOs of a network slice. @@ -650,39 +746,43 @@ class NSController: """ # Define SLO types for maximum and minimum constraints slo_type = { - "max": ["one-way-delay-maximum", "two-way-delay-maximum", "one-way-delay-percentile", "two-way-delay-percentile", - "one-way-delay-variation-maximum", "two-way-delay-variation-maximum", - "one-way-delay-variation-percentile", "two-way-delay-variation-percentile", - "one-way-packet-loss", "two-way-packet-loss"], + "max": ["one-way-delay-maximum", "two-way-delay-maximum", "one-way-delay-percentile", + "two-way-delay-percentile","one-way-delay-variation-maximum", + "two-way-delay-variation-maximum", "one-way-delay-variation-percentile", + "two-way-delay-variation-percentile","one-way-packet-loss", + "two-way-packet-loss"], "min": ["one-way-bandwidth", "two-way-bandwidth", "shared-bandwidth"] } flexibility_scores = [] for slo in slice_slos: for nrp_slo in nrp_slos['slos']: if slo["metric-type"] == nrp_slo["metric-type"]: + flexibility = None # Handle maximum type SLOs if slo["metric-type"] in slo_type["max"]: flexibility = (nrp_slo["bound"] - slo["bound"]) / slo["bound"] if slo["bound"] > nrp_slo["bound"]: return False, 0 # Does not meet maximum constraint # Handle minimum type SLOs - if slo["metric-type"] in slo_type["min"]: + elif slo["metric-type"] in slo_type["min"]: flexibility = (slo["bound"] - nrp_slo["bound"]) / slo["bound"] if slo["bound"] < nrp_slo["bound"]: return False, 0 # Does not meet minimum constraint + else: + # If metric-type is not recognized, skip this SLO + continue flexibility_scores.append(flexibility) break # Exit inner loop after finding matching metric - # Calculate final viability score score = sum(flexibility_scores) / len(flexibility_scores) if flexibility_scores else 0 return True, score # Si pasó todas las verificaciones, la NRP es viable - + def __planner(self, intent, nrp_view): """ TODO Request slice viability from the Planner module. - This method prepares and sends a viability request for network slice creation, + This method prepares and sends a viability request for network slice creation, detaching the implementation from the main core thread. Args: @@ -693,22 +793,27 @@ class NSController: tuple: A tuple containing: - Viability status (str): "Good" or other status - Reason (str): Explanation of viability - + Notes: - Calculates source and destination service delivery points (SDP) - Extracts QoS requirements - Performs viability check through internal methods """ - + #Version 1 matriz = {} matriz["payloads"] = [] #for i in intent: # SI ya existe, suma, si no existe, lo crea - origen = self.__calculate_sdp(intent["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["sdps"]["sdp"][0]["sdp-ip-address"]) - destino = self.__calculate_sdp(intent["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["sdps"]["sdp"][1]["sdp-ip-address"]) - #qos_req = i["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["slo-sle-policy"]["slo-sle-template"] - qos_req = intent["ietf-network-slice-service:network-slice-services"]["slo-sle-templates"]["slo-sle-template"][0]["slo-policy"]["metric-bound"][0]["bound"] + origen = self.__calculate_sdp(intent["ietf-network-slice-service:network-slice-services"][ + "slice-service"][0]["sdps"]["sdp"][0]["sdp-ip-address"]) + + destino = self.__calculate_sdp(intent["ietf-network-slice-service:network-slice-services"][ + "slice-service"][0]["sdps"]["sdp"][1]["sdp-ip-address"]) + + qos_req = intent["ietf-network-slice-service:network-slice-services"]["slo-sle-templates"][ + "slo-sle-template"][0]["slo-policy"]["metric-bound"][0]["bound"] + payload = { "Source": origen, "Destination": destino, @@ -747,12 +852,13 @@ class NSController: TODO Imput: Output: - Work: Identifies the network entpoint from the IP-sdp received. + Work: Identifies the network entpoint from the IP-sdp received. Version 0 will be done directly with next hop. - Version 1 will translate directly from the public IP of each node to their Loopback address. + Version 1 will translate directly from the public IP of each + node to their Loopback address. ''' nid = 0 - with open(os.path.join(TEMPLATES_PATH, "ips.json"), 'r') as source: + with open(os.path.join(TEMPLATES_PATH, "ips.json"), 'r', encoding="utf-8") as source: jason = source.read() jason = jason.replace('\t', '').replace('\n', '').replace("'", '"').strip() nodos = json.loads(str(jason)) @@ -772,17 +878,17 @@ class NSController: nid = nodo["node-name"] return nid - ### Realizer functionalities. def __nrp(self, request, nrp): """ Manage Network Resource Partition (NRP) operations. This method handles CRUD operations for Network Resource Partitions, - interacting with Network Controllers (currently done statically via a JSON-based database file). + interacting with Network Controllers (currently done statically via a + JSON-based database file). Args: - request (str): The type of operation to perform. + request (str): The type of operation to perform. Supported values: - "CREATE": Add a new NRP to the database - "READ": Retrieve the current NRP view @@ -791,20 +897,21 @@ class NSController: nrp (dict): The Network Resource Partition details to create or update. Returns: - None or answer: + None or answer: - For "CREATE": Returns the response from the controller (currently using a static JSON) - For "READ": Gets the NRP view from the controller (currently using a static JSON) - For "UPDATE": Placeholder for update functionality Notes: - - Uses a local JSON file "nrp_ddbb.json" to store NRP information as controller operation is not yet defined + - Uses a local JSON file "nrp_ddbb.json" to store NRP information as controller + operation is not yet defined """ if request == "CREATE": # TODO: Implement actual request to Controller to create an NRP logging.debug("Creating NRP") # Load existing NRP database - with open(os.path.join(SRC_PATH, "nrp_ddbb.json"), "r") as archivo: + with open(os.path.join(SRC_PATH, "nrp_ddbb.json"), "r", encoding="utf-8") as archivo: self.__nrp_view = json.load(archivo) # Append new NRP to the view @@ -818,15 +925,15 @@ class NSController: logging.debug("Reading Topology") # Load NRP database - with open(os.path.join(SRC_PATH, "nrp_ddbb.json"), "r") as archivo: + with open(os.path.join(SRC_PATH, "nrp_ddbb.json"), "r", encoding="utf-8") as archivo: self.__nrp_view = json.load(archivo) - + elif request == "UPDATE": # TODO: Implement request to Controller to update NRP logging.debug("Updating NRP") answer = "" - - def __select_way(self,way, ietf_intent): + + def __select_way(self,way, ietf_intent, rules=None): """ Determine the method of slice realization. @@ -842,12 +949,359 @@ class NSController: dict: A realization request for the specified network slice type. """ + realizing_request = None # Ensure variable is always initialized if way == "L2VPN": realizing_request = self.__tfs_l2vpn(ietf_intent) elif way == "L3VPN": realizing_request = self.__tfs_l3vpn(ietf_intent) + elif way == "OPTIC": + self.__optic_slice(ietf_intent,rules=rules) + elif way == "L3oWDM": + self.__l3ipowdm_slice(ietf_intent,rules=rules) + elif way == "DEL_L3oWDM": + self.__del_l3ipowdm_slice(rules) return realizing_request - + + def __del_l3ipowdm_slice(self, rules): + """ + Delete a Optical service request for an optical slice. + + This method prepares a TeraFlow service request to delete an optical slice by: + 1. Loading a service template + 2. Setting the service UUID to be deleted + + Args: + rules (str): UUID of the service to be deleted. + + Returns: + dict: A TeraFlow service request for deleting the optical slice. + """ + + transceiver_params = [] + + for rule in rules["actions"]: + if rule["type"] == "DEACTIVATE_TRANSCEIVER": + params = { + "router_id": rule["content"]["node-uuid"], + "router_tp": rule["content"]["termination-point-uuid"], + } + transceiver_params.append(params) + + for rule in rules["actions"]: + if rule["type"] == "REMOVE_OPTICAL_SLICE": + logging.info("Sending DELETE Optical Slice request to Optical Controller") + url = f'http://11.1.1.101:4900/restconf/data/tapi-common:context={rule["content"]["tenant-uuid"]}' + response = requests.delete(url, timeout=10) + logging.info("Response from Optical Controller: %s", response) + + elif rule["type"] == "DEPROVISION_OPTICAL_RESOURCE": + url = f'http://10.95.86.58/restconf/E2E/v1/service/tapi_lsp={rule["tenant-uuid"]}:{rule["content"]["connectivity-service-uuid"]:}' + logging.info("Sending DELETE Media Channel request to Orchestrator") + requests.delete(url, timeout=10) + + elif rule["type"] == "REMOVE_VPNL3": + logging.info("Sending DELETE IPoWDM service request to Orchestrator") + data = { + 'src_router_id' : transceiver_params[0]["router_id"], + 'src_router_tp' : transceiver_params[0]["router_tp"], + 'dst_router_id' : transceiver_params[1]["router_id"], + 'dst_router_tp' : transceiver_params[1]["router_tp"], + } + url = f'http://10.95.86.58/restconf/E2E/v1/service/ipowdm={rule["content"]["tunnel-uuid"]}:{data}' + requests.delete(url, json=data, timeout=10) + + def __optic_slice(self, ietf_intent, rules=None): + """ + Prepare a Optical service request for an optical slice. + + This method prepares a TeraFlow service request for an optical slice by: + 1. Defining endpoint routers + 2. Loading a service template + 3. Generating a unique service UUID + 4. Configuring service endpoints + 5. Adding QoS constraints + + Args: + ietf_intent (dict): IETF-formatted network slice intent. + rules (dict, optional): Configuration rules for the optical slice. + + Returns: + dict: A TeraFlow service request for optical slice configuration. + """ + bandwidth = 0 + latency = 0 + vlan_value = 0 + + for rule in rules["actions"]: + logging.info("Processing rule: %s", rule["type"]) + if rule["type"] == "PROVISION_MEDIA_CHANNEL_OLS_PATH": + + origin_router_id = rule["content"]["src-sip-uuid"] + destination_router_id = rule["content"]["dest-sip-uuid"] + # direction = rule["content"]["direction"] + bandwidth = rule["content"]["bandwidth-ghz"] + # lower_frecuency = rule["content"]["lower-frequency-mhz"] + # upper_frecuency = rule["content"]["upper-frequency-mhz"] + service_uuid = str(uuid.uuid5(uuid.NAMESPACE_DNS, rule["content"]["ols-path-uuid"])) + + self.__load_template(2, os.path.join(TEMPLATES_PATH, "TAPI_service.json")) + tfs_request = json.loads(str(self.__teraflow_template)) + tfs_request["services"][0]["service_id"]["service_uuid"]["uuid"] = service_uuid + config_rules = tfs_request["services"][0]["service_config"]["config_rules"][0] + + config_rules["tapi_lsp"]["rule_set"]["src"] = origin_router_id + config_rules["tapi_lsp"]["rule_set"]["dst"] = destination_router_id + config_rules["tapi_lsp"]["rule_set"]["bw"] = str(bandwidth) + config_rules["tapi_lsp"]["rule_set"]["uuid"] = service_uuid + + logging.info("Service to send: %s",tfs_request) + + else: + logging.info("Unsupported rule type for optical slice: %s", rule["type"]) + + self.answer[self.subnet]["VLAN"] = vlan_value + self.answer[self.subnet]["QoS Requirements"] = [] + # Add service constraints + + self.answer[self.subnet]["QoS Requirements"].append(bandwidth) + self.answer[self.subnet]["QoS Requirements"].append(latency) + + def optical_slice_template(self, template, datos): + """ + Complete the optical slice template with the data provided. + Args: + template (dict): optical slice template. + data (dict): Data to complete the template. + Returns: + dict: Template completed. + """ + + for action in datos.get('actions', []): + content = action.get('content', {}) + nodes = content.get('node', []) + for node in nodes: + for onp in node.get('owned-node-edge-point', []): + if 'media-channel-node-edge-point-spec' in onp: + onp['tapi-photonic-media:media-channel-node-edge-point-spec'] = onp.pop('media-channel-node-edge-point-spec') + + for i, sip in enumerate(template['tapi-common:context']['service-interface-point']): + if i < len(datos['actions'][0]['content']['service-interface-point']): + sip['uuid'] = datos['actions'][0]['content']['service-interface-point'][i]['uuid'] + + nodes_template = template['tapi-common:context']['tapi-topology:topology-context']['topology'][0]['node'] + nodes_data = datos['actions'][0]['content']['node'] + for new_node in nodes_data: + nodes_template.append(new_node) + + links_template = template['tapi-common:context']['tapi-topology:topology-context']['topology'][0]['link'] + links_datos = datos['actions'][0]['content']['link'] + for link_t in links_datos: + links_template.append(link_t) + + template['tapi-common:context']['uuid'] = datos['actions'][0]['content']['tenant-uuid'] + template['tapi-common:context']['name'][0]['value'] = datos['network-slice-uuid'] + + return template + + def __l3ipowdm_slice(self, ietf_intent, rules=None): + """ + Prepare a Optical service request for an optical slice. + + This method prepares a TeraFlow service request for an optical slice by: + 1. Defining endpoint routers + 2. Loading a service template + 3. Generating a unique service UUID + 4. Configuring service endpoints + 5. Adding QoS constraints + + Args: + ietf_intent (dict): IETF-formatted network slice intent. + rules (dict, optional): Configuration rules for the optical slice. + + Returns: + dict: A TeraFlow service request for optical slice configuration. + """ + transceiver_params = [] + bandwidth = 0 + latency = 0 + vlan_value = 0 + + for rule in rules["actions"]: + if rule["type"] == "CREATE_OPTICAL_SLICE": + self.__load_template(2, os.path.join(TEMPLATES_PATH, "Optical_slice.json")) + tfs_request = json.loads(str(self.__teraflow_template)) + self.optical_slice_template(tfs_request, rules) + logging.info("Sending Optical Slice to Optical Controller") + logging.info(f"DATOS A ENVIAR: {tfs_request}") + + # self.optical_post(tfs_request) + + elif rule["type"] == "PROVISION_MEDIA_CHANNEL_OLS_PATH": + + origin_router_id = rule["content"]["src-sip-uuid"] + destination_router_id = rule["content"]["dest-sip-uuid"] + direction = rule["content"]["direction"] + bandwidth = rule["content"]["bandwidth-ghz"] + service_uuid = rule["content"]["ols-path-uuid"] + tenant_uuid = rule["tenant-uuid"] + layer_protocol_name = rule["content"]["layer-protocol-name"] + layer_protocol_qualifier = rule["content"]["layer-protocol-qualifier"] + lower_frequency_mhz = rule["content"]["lower-frequency-mhz"] + upper_frequency_mhz = rule["content"]["upper-frequency-mhz"] + link_uuid_path = rule["content"]["link-uuid-path"] + granularity = rule["content"]["adjustment-granularity"] + grid = rule["content"]["grid-type"] + + self.__load_template(2, os.path.join(TEMPLATES_PATH, "TAPI_service.json")) + tfs_request = json.loads(str(self.__teraflow_template)) + tfs_request["services"][0]["service_id"]["service_uuid"]["uuid"] = service_uuid + config_rules = tfs_request["services"][0]["service_config"]["config_rules"][0] + + config_rules["tapi_lsp"]["rule_set"]["src"] = origin_router_id + config_rules["tapi_lsp"]["rule_set"]["dst"] = destination_router_id + config_rules["tapi_lsp"]["rule_set"]["uuid"] = service_uuid + config_rules["tapi_lsp"]["rule_set"]["bw"] = str(bandwidth) + config_rules["tapi_lsp"]["rule_set"]["tenant_uuid"] = tenant_uuid + config_rules["tapi_lsp"]["rule_set"]["direction"] = direction + config_rules["tapi_lsp"]["rule_set"]["layer_protocol_name"] = layer_protocol_name + config_rules["tapi_lsp"]["rule_set"]["layer_protocol_qualifier"] = layer_protocol_qualifier + config_rules["tapi_lsp"]["rule_set"]["lower_frequency_mhz"] = str(lower_frequency_mhz) + config_rules["tapi_lsp"]["rule_set"]["upper_frequency_mhz"] = str(upper_frequency_mhz) + config_rules["tapi_lsp"]["rule_set"]["link_uuid_path"] = link_uuid_path + config_rules["tapi_lsp"]["rule_set"]["granularity"] = granularity + config_rules["tapi_lsp"]["rule_set"]["grid_type"] = grid + + + logging.info("Sending Media Channel Service to Orchestrator") + logging.info(f"DATOS A ENVIAR: {tfs_request}") + + self.tfs_post(self.tfs_ip, tfs_request) + + elif rule["type"] == "ACTIVATE_TRANSCEIVER": + params = { + "router_id": rule["content"]["node-uuid"], + "router_tp": rule["content"]["termination-point-uuid"], + "frequency": rule["content"]["frequency-ghz"], + "power": rule["content"]["tx-power-dbm"] + } + transceiver_params.append(params) + elif rule["type"] == "CONFIG_VPNL3": + src_router_id = rule["content"]["src-node-uuid"] + + if src_router_id == transceiver_params[0]["router_id"]: + src_power = transceiver_params[0]["power"] + src_frequency = transceiver_params[0]["frequency"] + dst_power = transceiver_params[1]["power"] + dst_frequency = transceiver_params[1]["frequency"] + else: + src_power = transceiver_params[1]["power"] + src_frequency = transceiver_params[1]["frequency"] + dst_power = transceiver_params[0]["power"] + dst_frequency = transceiver_params[0]["frequency"] + + src_router_id = rule["content"]["src-node-uuid"] + src_ip_address = rule["content"]["src-ip-address"] + src_ip_mask = rule["content"]["src-ip-mask"] + src_vlan_id = rule["content"]["src-vlan-id"] + vlan_value = src_vlan_id + + dst_router_id = rule["content"]["dest-node-uuid"] + dst_ip_address = rule["content"]["dest-ip-address"] + dst_ip_mask = rule["content"]["dest-ip-mask"] + dst_vlan_id = rule["content"]["dest-vlan-id"] + + service_uuid = rule["content"]["tunnel-uuid"] + self.__load_template(2, os.path.join(TEMPLATES_PATH, "IPoWDM_orchestrator.json")) + tfs_request = json.loads(str(self.__teraflow_template)) + tfs_request["services"][0]["service_id"]["service_uuid"]["uuid"] = service_uuid + config_rules = tfs_request["services"][0]["service_config"]["config_rules"][0] + src = config_rules["ipowdm"]["rule_set"]["src"] + src.append({ + 'uuid': src_router_id, + 'ip_address': src_ip_address, + 'ip_mask': src_ip_mask, + 'vlan_id': src_vlan_id, + 'power': src_power, + 'frequency': src_frequency + }) + + dst = config_rules["ipowdm"]["rule_set"]["dst"] + dst.append({ + 'uuid': dst_router_id, + 'ip_address': dst_ip_address, + 'ip_mask': dst_ip_mask, + 'vlan_id': dst_vlan_id, + 'power': dst_power, + 'frequency': dst_frequency + }) + + config_rules["ipowdm"]["rule_set"]["bw"] = bandwidth + config_rules["ipowdm"]["rule_set"]["uuid"] = service_uuid + + logging.info("Sending IPoWDM Service to Orchestrator") + self.tfs_post(self.tfs_ip, tfs_request) + else: + logging.info("Unsupported rule type for optical slice: %s", rule["type"]) + + self.answer[self.subnet]["VLAN"] = vlan_value + self.answer[self.subnet]["QoS Requirements"] = [] + # Add service constraints + + self.answer[self.subnet]["QoS Requirements"].append(bandwidth) + self.answer[self.subnet]["QoS Requirements"].append(latency) + + def optical_post(self, request): + """ + Send a POST request to the Optical Controller. + Args: + request (dict): The request payload to be sent. + Returns: + dict: The response from the Optical Controller. + """ + # logging.info("Sending request to Optical Controller: %s",request) + + url = "http://11.1.1.101:4900/restconf/data" + headers = { + "Content-Type": "application/json", + "Expect": "" + } + + data = json.dumps(request) + + requests.post(url, data=data, headers=headers, timeout=10) + + def tfs_post(self, ip, request): + """ + Send a POST request to the TeraFlow Service Orchestrator. + + Args: + ip (str): IP address of the TeraFlow Service Orchestrator. + request (dict): The request payload to be sent. + + Returns: + dict: The response from the TeraFlow Service Orchestrator. + """ + user="admin" + password="admin" + token="" + session = requests.Session() + session.auth = (user, password) + url=f'http://{ip}/webui' + response=session.get(url=url) + for item in response.iter_lines(): + if"csrf_token" in str(item): + string=str(item).split(' Date: Thu, 11 Sep 2025 12:22:58 +0000 Subject: [PATCH 2/6] Fix import case sensitivity for NSC_PORT in app.py --- app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.py b/app.py index 1e34ec5..1a7ec52 100644 --- a/app.py +++ b/app.py @@ -18,7 +18,7 @@ from flask import Flask from flask_restx import Api from flask_cors import CORS from swagger.slice_namespace import slice_ns -from src.Constants import NSC_PORT +from src.constants import NSC_PORT app = Flask(__name__) CORS(app) -- GitLab From 910cc1dd32deb6a22562fbc78613f6f2fc1b0eef Mon Sep 17 00:00:00 2001 From: armingol Date: Tue, 23 Sep 2025 06:58:58 +0000 Subject: [PATCH 3/6] Enhance NSController with new generate_rules function and refactor slice handling logic --- src/helpers.py | 154 ++++++++++++- src/network_slice_controller.py | 391 +++++++++++++++----------------- 2 files changed, 339 insertions(+), 206 deletions(-) diff --git a/src/helpers.py b/src/helpers.py index f8457bb..e65aac5 100644 --- a/src/helpers.py +++ b/src/helpers.py @@ -16,11 +16,11 @@ import json import logging import os +import uuid import requests from netmiko import ConnectHandler -from src.constants import DEFAULT_LOGGING_LEVEL, SRC_PATH +from src.constants import DEFAULT_LOGGING_LEVEL -# Configure logging to provide clear and informative log messages logging.basicConfig( level=DEFAULT_LOGGING_LEVEL, format='%(levelname)s - %(message)s') @@ -174,10 +174,156 @@ def send_network_slice_request(data: str, action: str = "create") -> dict: logging.error(f"HTTP request failed: {e}") return {} - - # Comprobar y devolver la respuesta if response.ok: return response.json() else: print(f"Request failed with status code {response.status_code}: {response.text}") response.raise_for_status() + +def group_block(group, action, group_id_override=None): + active = "true" if action == 'create' else "false" + group_id = group_id_override if group_id_override is not None else group["digital_sub_carriers_group_id"] + + return { + "digital_sub_carriers_group_id": group_id, + "digital_sub_carrier_id": [ + { + "sub_carrier_id": sid, + "active": active, + } + for sid in group["subcarrier-id"] + ] + } + +def generate_rules(connectivity_service, intent, action): + src_name = connectivity_service.get("source", "FALTA VALOR") + dest_list = connectivity_service.get("destination", ["FALTA VALOR"]) + dest_str = ",".join(dest_list) + config_rules = [] + + network_slice_uuid_str = f"{src_name}_to_{dest_str}" + tunnel_uuid = str(uuid.uuid5(uuid.NAMESPACE_DNS, network_slice_uuid_str)) + provisionamiento = { + "network-slice-uuid": network_slice_uuid_str, + "viability": True, + "actions": [] + } + + attributes = connectivity_service["connectivity-service"]["tapi-connectivity:connectivity-service"]["connection"][0]["optical-connection-attributes"] + groups = attributes["subcarrier-attributes"]["digital-subcarrier-group"] + # central_freq = (attributes["central-frequency"]) + # tx_power = (attributes["Tx-power"]) + # spacing = attributes["digital-subcarrier-spacing"] + operational_mode = attributes["modulation"]["operational-mode"] + # port = attributes["modulation"]["port"] + hub_groups = [ + group_block(group, action, group_id_override=index + 1) + for index, group in enumerate(groups) + ] + hub = { + "name": "channel-1", + "frequency": 195000000, + "target_output_power": 0, + "operational_mode": operational_mode, + "operation" : "merge", + "digital_sub_carriers_group": hub_groups + } + + leaves = [] + for dest, group in zip(connectivity_service["destination"], groups): + if dest == "T1.1": + name = "channel-1" + freq = 195006250 + elif dest == "T1.2": + name = "channel-3" + freq = 195018750 + else: + name = "channel-5" + freq = 195031250 + + leaf = { + "name": name, + "frequency": freq, + "target_output_power": group["Tx-power"], + "operational_mode": int(group["operational-mode"]), + "operation" : "merge", + "digital_sub_carriers_group": [group_block(group, action, group_id_override=1)] + } + leaves.append(leaf) + + final_json = {"components": [hub] + leaves} + + if action == 'create': + provisionamiento["actions"].append({ + "type": "XR_AGENT_ACTIVATE_TRANSCEIVER", + "layer": "OPTICAL", + "content": final_json, + "controller-uuid": "IPoWDM Controller" + }) + + nodes = {} + sdp_list = intent['ietf-network-slice-service:network-slice-services']['slice-service'][0]['sdps']['sdp'] + + for sdp in sdp_list: + node = sdp['node-id'] + attachments = sdp['attachment-circuits']['attachment-circuit'] + for ac in attachments: + ip = ac.get('ac-ipv4-address', None) + prefix = ac.get('ac-ipv4-prefix-length', None) + last_octet = int(ip.split('.')[-1]) if ip else 0 + vlan = 100 + last_octet + nodes[node] = { + "ip-address": ip, + "ip-mask": prefix, + "vlan-id": vlan + } + + provisionamiento["actions"].append({ + "type": "CONFIG_VPNL3", + "layer": "IP", + "content": { + "tunnel-uuid": tunnel_uuid, + "src-node-uuid": src_name, + "src-ip-address": nodes[src_name]["ip-address"], + "src-ip-mask": str(nodes[src_name]["ip-mask"]), + "src-vlan-id": nodes[src_name]["vlan-id"], + "dest1-node-uuid": dest_list[0], + "dest1-ip-address": nodes[dest_list[0]]["ip-address"], + "dest1-ip-mask": str(nodes[dest_list[0]]["ip-mask"]), + "dest1-vlan-id": nodes[dest_list[0]]["vlan-id"], + "dest2-node-uuid": dest_list[1], + "dest2-ip-address": nodes[dest_list[1]]["ip-address"], + "dest2-ip-mask": str(nodes[dest_list[1]]["ip-mask"]), + "dest2-vlan-id": nodes[dest_list[1]]["vlan-id"] + }, + "controller-uuid": "IP Controller" + }) + + config_rules.append(provisionamiento) + else: + nodes = [] + nodes.append(src_name) + for dst in dest_list: nodes.append(dst) + aux = tunnel_uuid + '-' + src_name + '-' + '-'.join(dest_list) + provisionamiento["actions"].append({ + "type": "DEACTIVATE_XR_AGENT_TRANSCEIVER", + "layer": "OPTICAL", + "content": final_json, + "controller-uuid": "IPoWDM Controller", + "uuid" : aux, + "nodes": nodes + }) + config_rules.append(provisionamiento) + + return config_rules + +def get_sip_from_name(context, node_name): + + context = context.json() + topologies = context.get("tapi-topology:topology-context", {}).get("topology", []) + for topology in topologies: + nodes = topology.get("nodes", []) + for node in nodes: + if node.get("name") == node_name: + return node.get("uuid") + return None diff --git a/src/network_slice_controller.py b/src/network_slice_controller.py index 08d70d7..a13144d 100644 --- a/src/network_slice_controller.py +++ b/src/network_slice_controller.py @@ -22,7 +22,7 @@ import uuid from datetime import datetime import requests -from src.helpers import TFSConnector, CiscoConnector, send_network_slice_request +from src.helpers import TFSConnector, CiscoConnector, send_network_slice_request, generate_rules, get_sip_from_name from src.constants import DEFAULT_LOGGING_LEVEL, TFS_UPLOAD, TFS_IP, TFS_L2VPN_SUPPORT, SRC_PATH, TEMPLATES_PATH # Configure logging to provide clear and informative log messages @@ -125,27 +125,18 @@ class NSController: Exception: For unexpected errors during file processing """ try: - # Read slice database from JSON file with open(os.path.join(SRC_PATH, "slice_ddbb.json"), 'r', encoding='utf-8') as file: content = json.load(file) - # If specific slice ID is provided, find and return matching slice if slice_id: for slice_item in content: if slice_item["slice_id"] == slice_id: return slice_item - # If no slices exist, raise an error if len(content) == 0: raise ValueError("Transport network slices not found") - - # Return all slices if no specific ID is given return content - - except ValueError as e: - # Handle case where no slices are found - return self.__send_response(False, code=404, message=str(e)) - except OSError as e: - # Handle file-related errors - return self.__send_response(False, code=500, message=str(e)) + + except ValueError as e: return self.__send_response(False, code=404, message=str(e)) + except OSError as e: return self.__send_response(False, code=500, message=str(e)) def modify_flow(self,slice_id, intent): """ @@ -191,39 +182,28 @@ class NSController: - If need_l2vpn_support is True, performs additional L2VPN cleanup """ try: - # Read current slice database with open(os.path.join(SRC_PATH, "slice_ddbb.json"), 'r') as file: content = json.load(file) id = None - # Delete specific slice if slice_id is provided if slice_id: return self.nsc(slice_id, action="delete") - - # Delete all slices else: - # Optional: Delete in Teraflow if configured if self.upload_to_tfs == True: # TODO: should send a delete request to Teraflow if self.need_l2vpn_support: self.__tfs_l2vpn_delete() - # Verify slices exist before deletion with open(os.path.join(SRC_PATH, "slice_ddbb.json"), 'r') as file: if len(json.load(file)) == 0: raise ValueError("Transport network slices not found") - - # Clear slice database with open(os.path.join(SRC_PATH, "slice_ddbb.json"), 'w') as file: json.dump([], file, indent=4) - logging.info("All slices removed successfully") return self.__send_response(False, code=200, status="success", message="All transport network slices deleted successfully.") - - except ValueError as e: - return self.__send_response(False, code=404, message=str(e)) - except Exception as e: - return self.__send_response(False, code=500, message=str(e)) + + except ValueError as e: return self.__send_response(False, code=404, message=str(e)) + except Exception as e: return self.__send_response(False, code=500, message=str(e)) # Main NSC Functionalities def nsc(self, intent_json, slice_id=None, action=None): @@ -248,47 +228,38 @@ class NSController: """ service_uuid = None + rules = None try: if action == 'delete': logging.info("\n\n--------NEW REQUEST--------\n") logging.info("DELETE REQUEST RECEIVED: %s", intent_json) - rules = send_network_slice_request(intent_json, action) - logging.info("Rules loaded: %s", rules) + rules = self.__planner(intent_json ,action = action) tfs_request = self.__realizer(rules=rules) else: - # Start performance tracking self.start_time = time.perf_counter() - # Reset requests and load IETF template self.__load_template(1, os.path.join(TEMPLATES_PATH, "ietf_template_empty.json")) tfs_requests = {"services":[]} - # Process intent (translate if 3GPP) ietf_intents = self.__nbi_processor(intent_json) if ietf_intents: for intent in ietf_intents: - # Extract and store slice request details self.__extract_data(intent) - # Mapper self.__mapper(intent, action) - # Realizer - rules = send_network_slice_request(intent, action) - logging.info("Rules loaded: %s", rules) - service_uuid = rules.get("network-slice-uuid") - - self.__store_data(intent, slice_id, service_uuid = service_uuid) - tfs_request = self.__realizer(intent, rules=rules) - tfs_requests["services"].append(tfs_request) + rules = self.__planner(intent,action = action) + for rule in rules: + service_uuid = rule.get("network-slice-uuid") + logging.info(f"Rules generated by planner: \n {rule}") + self.__store_data(intent, slice_id, service_uuid = service_uuid) + tfs_request = self.__realizer(intent, rules=rule) + tfs_requests["services"].append(tfs_request) else: return self.__send_response(False, code=404, message="No intents found") - - # Generated service logging.debug(json.dumps(tfs_requests, indent=2)) - # Optional: Upload template to Teraflow if self.upload_to_tfs is True: response = TFSConnector().simple_post(self.tfs_ip, tfs_requests) @@ -297,20 +268,15 @@ class NSController: code=response.status_code, message=f"Teraflow upload failed. Response: {response.text}") - # For deploying an L2VPN with path selection (not supported by Teraflow) if self.need_l2vpn_support: self.__tfs_l2vpn_support(tfs_requests["services"]) - logging.info("Request sent to Teraflow") - # End performance tracking self.end_time = time.perf_counter() return self.__send_response(True, code=200, service_uuid = service_uuid) - except ValueError as e: - return self.__send_response(False, code=400, message=str(e)) - except OSError as e: - return self.__send_response(False, code=500, message=str(e)) + except ValueError as e: return self.__send_response(False, code=400, message=str(e)) + except OSError as e: return self.__send_response(False, code=500, message=str(e)) def __nbi_processor(self, intent_json): """ @@ -410,46 +376,40 @@ class NSController: # TODO Here we should put how the slice is attached to the new nrp def __realizer(self, ietf_intent = None, need_nrp=False, order=None, nrp=None, rules=None, way=None): - if need_nrp: - # Perform NRP-related operations self.__nrp(order, nrp) else: - # Detect 'way' automatically if not specified selected_way = way if selected_way is None: + if isinstance(rules, list) and len(rules) > 0: rules = rules[0] actions = rules.get("actions", []) - has_optical = any(act.get("type", "").startswith("CREATE_OPTICAL_SLICE") for act in actions) - has_l2 = any(act.get("type", "").startswith("CONFIG_VPNL2") for act in actions) - has_l3 = any(act.get("type", "").startswith("CONFIG_VPNL3") for act in actions) - del_l3 = any(act.get("type", "").startswith("REMOVE_VPNL3") for act in actions) - del_l2 = any(act.get("type", "").startswith("REMOVE_VPNL2") for act in actions) - del_optical = any(act.get("type", "").startswith("DEPROVISION_OPTICAL_RESOURCE") for act in actions) - - if has_l2 and not (has_l3 or has_optical): - selected_way = "L2VPN" - elif has_l3 and not (has_l2 or has_optical): - selected_way = "L3VPN" - elif has_optical and not (has_l2 or has_l3): - selected_way = "OPTIC" - elif has_optical and has_l2: - selected_way = "L2oWDM" - elif has_optical and has_l3: - selected_way = "L3oWDM" - elif del_l2 and not (del_l3 or del_optical): - selected_way = "DEL_L2VPN" - elif del_l3 and not (del_l2 or del_optical): - selected_way = "DEL_L3VPN" - elif del_optical and not (del_l2 or del_l3): - selected_way = "DEL_OPTIC" - elif del_optical and del_l2: - selected_way = "DEL_L2oWDM" - elif del_optical and del_l3: - selected_way = "DEL_L3oWDM" - else: - raise ValueError("Cannot determine the realization way from rules") - logging.info("Selected Realizer: %s", selected_way) + has_transceiver = any(a.get("type", "").startswith("XR_AGENT_ACTIVATE_TRANSCEIVER") for a in actions) + has_optical = any(a.get("type", "").startswith("PROVISION_MEDIA_CHANNEL") for a in actions) + has_l3 = any(a.get("type", "").startswith("CONFIG_VPNL3") for a in actions) + has_l2 = any(a.get("type", "").startswith("CONFIG_VPNL2") for a in actions) + + del_transceiver = any(a.get("type", "").startswith("DEACTIVATE_XR_AGENT_TRANSCEIVER") for a in actions) + del_optical = any(a.get("type", "").startswith("DEPROVISION_OPTICAL_RESOURCE") for a in actions) + del_l3 = any(a.get("type", "").startswith("REMOVE_VPNL3") for a in actions) + del_l2 = any(a.get("type", "").startswith("REMOVE_VPNL2") for a in actions) + + if has_transceiver: selected_way = "L3oWDM" + elif has_optical and has_l3: selected_way = "L3oWDM" + elif has_optical and has_l2: selected_way = "L2oWDM" + elif has_optical: selected_way = "OPTIC" + elif has_l3: selected_way = "L3VPN" + elif has_l2: selected_way = "L2VPN" + + elif del_transceiver: selected_way = "DEL_L3oWDM" + elif del_optical and del_l3: selected_way = "DEL_L3oWDM" + elif del_optical and del_l2: selected_way = "DEL_L2oWDM" + elif del_optical: selected_way = "DEL_OPTIC" + elif del_l3: selected_way = "DEL_L3VPN" + elif del_l2: selected_way = "DEL_L2VPN" + else: raise ValueError("Cannot determine the realization way from rules") + + logging.info("Selected Realizer: %s", selected_way) return self.__select_way(selected_way, ietf_intent, rules=rules) ### Generic functionalities @@ -510,9 +470,9 @@ class NSController: "id": service_uuid if service_uuid else subnet, "source": subnet_info["Source"], "destination": subnet_info["Destination"], - "vlan": subnet_info["VLAN"], - "bandwidth(Mbps)": subnet_info["QoS Requirements"][0], - "latency(ms)": subnet_info["QoS Requirements"][1] + "vlan": 111, + "bandwidth(Mbps)": 1000, + "latency(ms)": 50 } answer["slices"].append(slice_info) @@ -777,59 +737,90 @@ class NSController: score = sum(flexibility_scores) / len(flexibility_scores) if flexibility_scores else 0 return True, score # Si pasó todas las verificaciones, la NRP es viable - def __planner(self, intent, nrp_view): - """ - TODO - Request slice viability from the Planner module. - - This method prepares and sends a viability request for network slice creation, - detaching the implementation from the main core thread. + def __planner(self, intent, action, nrp_view= None): - Args: - intent (dict): Network slice intent - nrp_view (list): Current Network Resource Pool view - - Returns: - tuple: A tuple containing: - - Viability status (str): "Good" or other status - - Reason (str): Explanation of viability + if action == 'delete': + with open(os.path.join(SRC_PATH, "slice_ddbb.json"), 'r', encoding='utf-8') as file: + slices = json.load(file) + + for slice_obj in slices: + if 'slice_id' in slice_obj and slice_obj['slice_id'] == intent: + source = None + destination = None + services = slice_obj['intent']['ietf-network-slice-service:network-slice-services']['slice-service'] + for service in services: + c_groups = service.get("connection-groups", {}).get("connection-group", []) + for cg in c_groups: + constructs = cg.get("connectivity-construct", []) + for construct in constructs: + if "p2mp-sdp" in construct: + source = construct["p2mp-sdp"]["root-sdp-id"] + destination = construct["p2mp-sdp"]["leaf-sdp-id"] + break + if source and destination: + break + response = self.__send_request(source, destination) + summary = { + "source": source, + "destination": destination, + "connectivity-service": response + } + rules = generate_rules(summary, intent, action) + else: + services = intent["ietf-network-slice-service:network-slice-services"]["slice-service"] + source = None + destination = None + for service in services: + c_groups = service.get("connection-groups", {}).get("connection-group", []) + for cg in c_groups: + constructs = cg.get("connectivity-construct", []) + for construct in constructs: + if "p2mp-sdp" in construct: + source = construct["p2mp-sdp"]["root-sdp-id"] + destination = construct["p2mp-sdp"]["leaf-sdp-id"] + break + if source and destination: + break + + response = self.__send_request(source, destination) + + summary = { + "source": source, + "destination": destination, + "connectivity-service": response + } + rules = generate_rules(summary, intent,action) + return rules - Notes: - - Calculates source and destination service delivery points (SDP) - - Extracts QoS requirements - - Performs viability check through internal methods - """ + def __send_request(self, source, destination): + url = "http://10.30.7.66:31060/OpticalTFS/restconf/operations/tapi-path-computation:compute-p2mp" - #Version 1 - matriz = {} - matriz["payloads"] = [] - #for i in intent: - # SI ya existe, suma, si no existe, lo crea - origen = self.__calculate_sdp(intent["ietf-network-slice-service:network-slice-services"][ - "slice-service"][0]["sdps"]["sdp"][0]["sdp-ip-address"]) + headers = { + "Content-Type": "application/json", + "Accept": "*/*" + } - destino = self.__calculate_sdp(intent["ietf-network-slice-service:network-slice-services"][ - "slice-service"][0]["sdps"]["sdp"][1]["sdp-ip-address"]) + if isinstance(source, str): + sources_list = [source] + else: + sources_list = list(source) - qos_req = intent["ietf-network-slice-service:network-slice-services"]["slo-sle-templates"][ - "slo-sle-template"][0]["slo-policy"]["metric-bound"][0]["bound"] + if isinstance(destination, str): + destinations_list = [destination] + else: + destinations_list = list(destination) payload = { - "Source": origen, - "Destination": destino, - "QoS requirements": qos_req + "sources": sources_list, + "destinations": destinations_list, + "bitrate": 100, + "bidirectional": True, + "band": 200, + "subcarriers_per_source": [4] * len(sources_list) } - matriz["payloads"].append(payload) - m_res = [] - m_qos = [] - for p in matriz["payloads"]: - res = p - m_res.append(res) - m_qos.append(p["QoS requirements"]) - m_res, m_qos = self.__viability(m_res,intent, m_qos) - reason="Viable" - viability = "Good" - return viability, reason + + response = requests.post(url, headers=headers, data=json.dumps(payload)) + return json.loads(response.text) def __viability(self, matrix, intent, qos): """ @@ -949,17 +940,13 @@ class NSController: dict: A realization request for the specified network slice type. """ - realizing_request = None # Ensure variable is always initialized - if way == "L2VPN": - realizing_request = self.__tfs_l2vpn(ietf_intent) - elif way == "L3VPN": - realizing_request = self.__tfs_l3vpn(ietf_intent) - elif way == "OPTIC": - self.__optic_slice(ietf_intent,rules=rules) - elif way == "L3oWDM": - self.__l3ipowdm_slice(ietf_intent,rules=rules) - elif way == "DEL_L3oWDM": - self.__del_l3ipowdm_slice(rules) + realizing_request = None + if way == "L2VPN": realizing_request = self.__tfs_l2vpn(ietf_intent) + elif way == "L3VPN": realizing_request = self.__tfs_l3vpn(ietf_intent) + elif way == "OPTIC": self.__optic_slice(ietf_intent,rules=rules) + elif way == "L3oWDM": self.__l3ipowdm_slice(ietf_intent,rules=rules) + elif way == "DEL_L3oWDM": self.__del_l3ipowdm_slice(rules) + return realizing_request def __del_l3ipowdm_slice(self, rules): @@ -976,9 +963,7 @@ class NSController: Returns: dict: A TeraFlow service request for deleting the optical slice. """ - transceiver_params = [] - for rule in rules["actions"]: if rule["type"] == "DEACTIVATE_TRANSCEIVER": params = { @@ -992,12 +977,13 @@ class NSController: logging.info("Sending DELETE Optical Slice request to Optical Controller") url = f'http://11.1.1.101:4900/restconf/data/tapi-common:context={rule["content"]["tenant-uuid"]}' response = requests.delete(url, timeout=10) - logging.info("Response from Optical Controller: %s", response) + logging.info("Response: %s", response) elif rule["type"] == "DEPROVISION_OPTICAL_RESOURCE": url = f'http://10.95.86.58/restconf/E2E/v1/service/tapi_lsp={rule["tenant-uuid"]}:{rule["content"]["connectivity-service-uuid"]:}' logging.info("Sending DELETE Media Channel request to Orchestrator") - requests.delete(url, timeout=10) + response = requests.delete(url, timeout=10) + logging.info("Response: %s", response) elif rule["type"] == "REMOVE_VPNL3": logging.info("Sending DELETE IPoWDM service request to Orchestrator") @@ -1008,7 +994,14 @@ class NSController: 'dst_router_tp' : transceiver_params[1]["router_tp"], } url = f'http://10.95.86.58/restconf/E2E/v1/service/ipowdm={rule["content"]["tunnel-uuid"]}:{data}' - requests.delete(url, json=data, timeout=10) + response = requests.delete(url, timeout=10) + logging.info("Response: %s", response) + + elif rule["type"] == "DEACTIVATE_XR_AGENT_TRANSCEIVER": + logging.info("Sending DELETE XR AGENT service request to Orchestrator") + url = f'http://10.95.89.50/restconf/E2E/v1/service/ipowdm={rule["uuid"]}[[{rule["nodes"]}]]:{json.dumps(rule["content"])}' + response = requests.delete(url, timeout=10) + logging.info("Response: %s", response) def __optic_slice(self, ietf_intent, rules=None): """ @@ -1122,6 +1115,7 @@ class NSController: dict: A TeraFlow service request for optical slice configuration. """ transceiver_params = [] + xr_agent_transceiver = "" bandwidth = 0 latency = 0 vlan_value = 0 @@ -1132,25 +1126,26 @@ class NSController: tfs_request = json.loads(str(self.__teraflow_template)) self.optical_slice_template(tfs_request, rules) logging.info("Sending Optical Slice to Optical Controller") - logging.info(f"DATOS A ENVIAR: {tfs_request}") + self.optical_post(tfs_request) - # self.optical_post(tfs_request) + elif rule["type"] == "XR_AGENT_ACTIVATE_TRANSCEIVER": + xr_agent_transceiver = rule["content"] elif rule["type"] == "PROVISION_MEDIA_CHANNEL_OLS_PATH": - origin_router_id = rule["content"]["src-sip-uuid"] - destination_router_id = rule["content"]["dest-sip-uuid"] - direction = rule["content"]["direction"] - bandwidth = rule["content"]["bandwidth-ghz"] - service_uuid = rule["content"]["ols-path-uuid"] - tenant_uuid = rule["tenant-uuid"] - layer_protocol_name = rule["content"]["layer-protocol-name"] + origin_router_id = rule["content"]["src-sip-uuid"] + destination_router_id = rule["content"]["dest-sip-uuid"] + direction = rule["content"]["direction"] + bandwidth = rule["content"]["bandwidth-ghz"] + service_uuid = rule["content"]["ols-path-uuid"] + tenant_uuid = rule["tenant-uuid"] + layer_protocol_name = rule["content"]["layer-protocol-name"] layer_protocol_qualifier = rule["content"]["layer-protocol-qualifier"] - lower_frequency_mhz = rule["content"]["lower-frequency-mhz"] - upper_frequency_mhz = rule["content"]["upper-frequency-mhz"] - link_uuid_path = rule["content"]["link-uuid-path"] - granularity = rule["content"]["adjustment-granularity"] - grid = rule["content"]["grid-type"] + lower_frequency_mhz = rule["content"]["lower-frequency-mhz"] + upper_frequency_mhz = rule["content"]["upper-frequency-mhz"] + link_uuid_path = rule["content"]["link-uuid-path"] + granularity = rule["content"]["adjustment-granularity"] + grid = rule["content"]["grid-type"] self.__load_template(2, os.path.join(TEMPLATES_PATH, "TAPI_service.json")) tfs_request = json.loads(str(self.__teraflow_template)) @@ -1162,19 +1157,16 @@ class NSController: config_rules["tapi_lsp"]["rule_set"]["uuid"] = service_uuid config_rules["tapi_lsp"]["rule_set"]["bw"] = str(bandwidth) config_rules["tapi_lsp"]["rule_set"]["tenant_uuid"] = tenant_uuid - config_rules["tapi_lsp"]["rule_set"]["direction"] = direction + config_rules["tapi_lsp"]["rule_set"]["direction"] = direction config_rules["tapi_lsp"]["rule_set"]["layer_protocol_name"] = layer_protocol_name config_rules["tapi_lsp"]["rule_set"]["layer_protocol_qualifier"] = layer_protocol_qualifier config_rules["tapi_lsp"]["rule_set"]["lower_frequency_mhz"] = str(lower_frequency_mhz) config_rules["tapi_lsp"]["rule_set"]["upper_frequency_mhz"] = str(upper_frequency_mhz) config_rules["tapi_lsp"]["rule_set"]["link_uuid_path"] = link_uuid_path - config_rules["tapi_lsp"]["rule_set"]["granularity"] = granularity - config_rules["tapi_lsp"]["rule_set"]["grid_type"] = grid - + config_rules["tapi_lsp"]["rule_set"]["granularity"] = granularity + config_rules["tapi_lsp"]["rule_set"]["grid_type"] = grid logging.info("Sending Media Channel Service to Orchestrator") - logging.info(f"DATOS A ENVIAR: {tfs_request}") - self.tfs_post(self.tfs_ip, tfs_request) elif rule["type"] == "ACTIVATE_TRANSCEIVER": @@ -1182,70 +1174,65 @@ class NSController: "router_id": rule["content"]["node-uuid"], "router_tp": rule["content"]["termination-point-uuid"], "frequency": rule["content"]["frequency-ghz"], - "power": rule["content"]["tx-power-dbm"] + "power": rule["content"]["tx-power-dbm"] } transceiver_params.append(params) elif rule["type"] == "CONFIG_VPNL3": src_router_id = rule["content"]["src-node-uuid"] - - if src_router_id == transceiver_params[0]["router_id"]: - src_power = transceiver_params[0]["power"] - src_frequency = transceiver_params[0]["frequency"] - dst_power = transceiver_params[1]["power"] - dst_frequency = transceiver_params[1]["frequency"] - else: - src_power = transceiver_params[1]["power"] - src_frequency = transceiver_params[1]["frequency"] - dst_power = transceiver_params[0]["power"] - dst_frequency = transceiver_params[0]["frequency"] - - src_router_id = rule["content"]["src-node-uuid"] src_ip_address = rule["content"]["src-ip-address"] src_ip_mask = rule["content"]["src-ip-mask"] src_vlan_id = rule["content"]["src-vlan-id"] vlan_value = src_vlan_id - dst_router_id = rule["content"]["dest-node-uuid"] - dst_ip_address = rule["content"]["dest-ip-address"] - dst_ip_mask = rule["content"]["dest-ip-mask"] - dst_vlan_id = rule["content"]["dest-vlan-id"] + dst1_router_id = rule["content"]["dest1-node-uuid"] + dst1_ip_address = rule["content"]["dest1-ip-address"] + dst1_ip_mask = rule["content"]["dest1-ip-mask"] + dst1_vlan_id = rule["content"]["dest1-vlan-id"] + dst2_router_id = rule["content"]["dest2-node-uuid"] + dst2_ip_address = rule["content"]["dest2-ip-address"] + dst2_ip_mask = rule["content"]["dest2-ip-mask"] + dst2_vlan_id = rule["content"]["dest2-vlan-id"] - service_uuid = rule["content"]["tunnel-uuid"] + service_uuid = rule["content"]["tunnel-uuid"] + '-' + src_router_id + '-' + dst1_router_id + '-' + dst2_router_id self.__load_template(2, os.path.join(TEMPLATES_PATH, "IPoWDM_orchestrator.json")) tfs_request = json.loads(str(self.__teraflow_template)) tfs_request["services"][0]["service_id"]["service_uuid"]["uuid"] = service_uuid config_rules = tfs_request["services"][0]["service_config"]["config_rules"][0] + src = config_rules["ipowdm"]["rule_set"]["src"] src.append({ 'uuid': src_router_id, 'ip_address': src_ip_address, 'ip_mask': src_ip_mask, 'vlan_id': src_vlan_id, - 'power': src_power, - 'frequency': src_frequency }) - dst = config_rules["ipowdm"]["rule_set"]["dst"] + dst = [] dst.append({ - 'uuid': dst_router_id, - 'ip_address': dst_ip_address, - 'ip_mask': dst_ip_mask, - 'vlan_id': dst_vlan_id, - 'power': dst_power, - 'frequency': dst_frequency + 'uuid': dst1_router_id, + 'ip_address': dst1_ip_address, + 'ip_mask': dst1_ip_mask, + 'vlan_id': dst1_vlan_id }) + dst.append({ + 'uuid': dst2_router_id, + 'ip_address': dst2_ip_address, + 'ip_mask': dst2_ip_mask, + 'vlan_id': dst2_vlan_id + }) + config_rules["ipowdm"]["rule_set"]["dst"] = dst + bandwidth = rule.get("bandwidth", 0) + config_rules["ipowdm"]["rule_set"]["bw"] = bandwidth + config_rules["ipowdm"]["rule_set"]["uuid"] = service_uuid + config_rules["ipowdm"]["rule_set"]["transceiver"] = xr_agent_transceiver - config_rules["ipowdm"]["rule_set"]["bw"] = bandwidth - config_rules["ipowdm"]["rule_set"]["uuid"] = service_uuid - - logging.info("Sending IPoWDM Service to Orchestrator") + logging.info(f"Sending IPoWDM Service to Orchestrator: \n {json.dumps(tfs_request)}") self.tfs_post(self.tfs_ip, tfs_request) else: logging.info("Unsupported rule type for optical slice: %s", rule["type"]) self.answer[self.subnet]["VLAN"] = vlan_value self.answer[self.subnet]["QoS Requirements"] = [] - # Add service constraints self.answer[self.subnet]["QoS Requirements"].append(bandwidth) self.answer[self.subnet]["QoS Requirements"].append(latency) -- GitLab From 1c9c24fe98b87f87d11c79af0904b705c97a9ded Mon Sep 17 00:00:00 2001 From: armingol Date: Wed, 1 Oct 2025 07:23:27 +0000 Subject: [PATCH 4/6] Refactor logging setup in app.py and enhance NSController with improved logging and rule generation handling --- app.py | 44 ++++++++++++++++------------- src/helpers.py | 50 ++++++++++++++++++++------------- src/network_slice_controller.py | 27 ++++++++++++++---- 3 files changed, 77 insertions(+), 44 deletions(-) diff --git a/app.py b/app.py index 1a7ec52..9b4df84 100644 --- a/app.py +++ b/app.py @@ -1,39 +1,43 @@ -# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# This file is an original contribution from Telefonica Innovación Digital S.L. - +import logging from flask import Flask from flask_restx import Api from flask_cors import CORS from swagger.slice_namespace import slice_ns from src.constants import NSC_PORT +# Configuración de logging básica para fichero +logging.basicConfig( + filename='nsc_controller.log', + level=logging.INFO, + format='%(asctime)s %(levelname)s %(name)s: %(message)s', + filemode='w' +) + +# Obtener logger raíz y logger Flask +logger = logging.getLogger() +flask_logger = logging.getLogger('werkzeug') # logger que usa Flask para accesos HTTP + +# Añadir handler de fichero a logger de Flask para asegurarnos que escribe en el log +file_handler = logging.FileHandler('nsc_controller.log') +file_handler.setLevel(logging.INFO) +formatter = logging.Formatter('%(asctime)s %(levelname)s %(name)s: %(message)s') +file_handler.setFormatter(formatter) +logger.addHandler(file_handler) +flask_logger.addHandler(file_handler) + app = Flask(__name__) CORS(app) -# Create API instance api = Api( app, version="1.0", title="Network Slice Controller (NSC) API", description="API for orchestrating and realizing transport network slice requests", - doc="/nsc" # Swagger UI URL + doc="/nsc" ) -# Register namespaces api.add_namespace(slice_ns, path="/slice") if __name__ == "__main__": - app.run(host="0.0.0.0", port=NSC_PORT, debug=True) + logger.info("Running Network Slice Controller in %s:%s", "192.168.202.50", NSC_PORT) + app.run(host="192.168.202.50", port=NSC_PORT, debug=True) diff --git a/src/helpers.py b/src/helpers.py index e65aac5..7248f5b 100644 --- a/src/helpers.py +++ b/src/helpers.py @@ -180,20 +180,30 @@ def send_network_slice_request(data: str, action: str = "create") -> dict: print(f"Request failed with status code {response.status_code}: {response.text}") response.raise_for_status() -def group_block(group, action, group_id_override=None): +def group_block(group, action, group_id_override=None, node = None): active = "true" if action == 'create' else "false" group_id = group_id_override if group_id_override is not None else group["digital_sub_carriers_group_id"] - - return { - "digital_sub_carriers_group_id": group_id, - "digital_sub_carrier_id": [ - { - "sub_carrier_id": sid, - "active": active, + if node == "leaf": + return { + "digital_sub_carriers_group_id": group_id, + "digital_sub_carrier_id": [ + {'sub_carrier_id': 1, 'active': active}, + {'sub_carrier_id': 2, 'active': active}, + {'sub_carrier_id': 3, 'active': active}, + {'sub_carrier_id': 4, 'active': active} + ] } - for sid in group["subcarrier-id"] - ] - } + else: + return { + "digital_sub_carriers_group_id": group_id, + "digital_sub_carrier_id": [ + { + "sub_carrier_id": sid, + "active": active, + } + for sid in group["subcarrier-id"] + ] + } def generate_rules(connectivity_service, intent, action): src_name = connectivity_service.get("source", "FALTA VALOR") @@ -234,25 +244,24 @@ def generate_rules(connectivity_service, intent, action): if dest == "T1.1": name = "channel-1" freq = 195006250 - elif dest == "T1.2": + if dest == "T1.2": name = "channel-3" freq = 195018750 - else: + if dest == "T1.3": name = "channel-5" freq = 195031250 - leaf = { "name": name, "frequency": freq, "target_output_power": group["Tx-power"], "operational_mode": int(group["operational-mode"]), "operation" : "merge", - "digital_sub_carriers_group": [group_block(group, action, group_id_override=1)] + "digital_sub_carriers_group": [group_block(group, action, group_id_override=1, node = "leaf")] } + leaves.append(leaf) final_json = {"components": [hub] + leaves} - if action == 'create': provisionamiento["actions"].append({ "type": "XR_AGENT_ACTIVATE_TRANSCEIVER", @@ -270,8 +279,7 @@ def generate_rules(connectivity_service, intent, action): for ac in attachments: ip = ac.get('ac-ipv4-address', None) prefix = ac.get('ac-ipv4-prefix-length', None) - last_octet = int(ip.split('.')[-1]) if ip else 0 - vlan = 100 + last_octet + vlan = 500 nodes[node] = { "ip-address": ip, "ip-mask": prefix, @@ -294,7 +302,11 @@ def generate_rules(connectivity_service, intent, action): "dest2-node-uuid": dest_list[1], "dest2-ip-address": nodes[dest_list[1]]["ip-address"], "dest2-ip-mask": str(nodes[dest_list[1]]["ip-mask"]), - "dest2-vlan-id": nodes[dest_list[1]]["vlan-id"] + "dest2-vlan-id": nodes[dest_list[1]]["vlan-id"], + "dest3-node-uuid": dest_list[2], + "dest3-ip-address": nodes[dest_list[2]]["ip-address"], + "dest3-ip-mask": str(nodes[dest_list[2]]["ip-mask"]), + "dest3-vlan-id": nodes[dest_list[2]]["vlan-id"] }, "controller-uuid": "IP Controller" }) diff --git a/src/network_slice_controller.py b/src/network_slice_controller.py index a13144d..afaca74 100644 --- a/src/network_slice_controller.py +++ b/src/network_slice_controller.py @@ -236,6 +236,7 @@ class NSController: logging.info("DELETE REQUEST RECEIVED: %s", intent_json) rules = self.__planner(intent_json ,action = action) + logging.info(f"Rules generated by planner: \n {rules}") tfs_request = self.__realizer(rules=rules) else: self.start_time = time.perf_counter() @@ -538,7 +539,7 @@ class NSController: content = json.load(file) # Determine the actual ID to use - slice_id = service_uuid if service_uuid is not None else slice_id + slice_id = "T2.1_to_T1.1,T1.2,T1.3" if slice_id is None: # If neither given, fallback to intent's internal id slice_id = intent["ietf-network-slice-service"]["slice_id"] if "slice_id" in intent["ietf-network-slice-service"] \ @@ -738,13 +739,14 @@ class NSController: return True, score # Si pasó todas las verificaciones, la NRP es viable def __planner(self, intent, action, nrp_view= None): - if action == 'delete': + logging.info("DELETE REQUEST RECEIVED: %s", intent) with open(os.path.join(SRC_PATH, "slice_ddbb.json"), 'r', encoding='utf-8') as file: slices = json.load(file) for slice_obj in slices: if 'slice_id' in slice_obj and slice_obj['slice_id'] == intent: + logging.info("Slice found: %s", slice_obj['slice_id']) source = None destination = None services = slice_obj['intent']['ietf-network-slice-service:network-slice-services']['slice-service'] @@ -789,6 +791,7 @@ class NSController: "destination": destination, "connectivity-service": response } + logging.info(summary) rules = generate_rules(summary, intent,action) return rules @@ -818,6 +821,7 @@ class NSController: "band": 200, "subcarriers_per_source": [4] * len(sources_list) } + logging.info(f"Payload for path computation: {json.dumps(payload, indent=2)}") response = requests.post(url, headers=headers, data=json.dumps(payload)) return json.loads(response.text) @@ -964,6 +968,7 @@ class NSController: dict: A TeraFlow service request for deleting the optical slice. """ transceiver_params = [] + response = None for rule in rules["actions"]: if rule["type"] == "DEACTIVATE_TRANSCEIVER": params = { @@ -994,13 +999,13 @@ class NSController: 'dst_router_tp' : transceiver_params[1]["router_tp"], } url = f'http://10.95.86.58/restconf/E2E/v1/service/ipowdm={rule["content"]["tunnel-uuid"]}:{data}' - response = requests.delete(url, timeout=10) + response = requests.delete(url, timeout=30) logging.info("Response: %s", response) elif rule["type"] == "DEACTIVATE_XR_AGENT_TRANSCEIVER": logging.info("Sending DELETE XR AGENT service request to Orchestrator") url = f'http://10.95.89.50/restconf/E2E/v1/service/ipowdm={rule["uuid"]}[[{rule["nodes"]}]]:{json.dumps(rule["content"])}' - response = requests.delete(url, timeout=10) + response = requests.delete(url, timeout=30) logging.info("Response: %s", response) def __optic_slice(self, ietf_intent, rules=None): @@ -1192,8 +1197,13 @@ class NSController: dst2_ip_address = rule["content"]["dest2-ip-address"] dst2_ip_mask = rule["content"]["dest2-ip-mask"] dst2_vlan_id = rule["content"]["dest2-vlan-id"] + dst3_router_id = rule["content"]["dest3-node-uuid"] + dst3_ip_address = rule["content"]["dest3-ip-address"] + dst3_ip_mask = rule["content"]["dest3-ip-mask"] + dst3_vlan_id = rule["content"]["dest3-vlan-id"] + - service_uuid = rule["content"]["tunnel-uuid"] + '-' + src_router_id + '-' + dst1_router_id + '-' + dst2_router_id + service_uuid = rule["content"]["tunnel-uuid"] + '-' + src_router_id + '-' + dst1_router_id + '-' + dst2_router_id+ '-' + dst3_router_id self.__load_template(2, os.path.join(TEMPLATES_PATH, "IPoWDM_orchestrator.json")) tfs_request = json.loads(str(self.__teraflow_template)) tfs_request["services"][0]["service_id"]["service_uuid"]["uuid"] = service_uuid @@ -1220,6 +1230,13 @@ class NSController: 'ip_mask': dst2_ip_mask, 'vlan_id': dst2_vlan_id }) + dst.append({ + 'uuid': dst3_router_id, + 'ip_address': dst3_ip_address, + 'ip_mask': dst3_ip_mask, + 'vlan_id': dst3_vlan_id + }) + config_rules["ipowdm"]["rule_set"]["dst"] = dst bandwidth = rule.get("bandwidth", 0) config_rules["ipowdm"]["rule_set"]["bw"] = bandwidth -- GitLab From 34fbb6bd4c4c2068c9fc96856b6f8845385d25b7 Mon Sep 17 00:00:00 2001 From: armingol Date: Wed, 1 Oct 2025 07:58:00 +0000 Subject: [PATCH 5/6] Update NSC_PORT to 8086 and remove unused constants from the project --- src/config/constants.py | 2 +- src/constants.py | 62 --------------------- src/realizer/tfs/helpers/cisco_connector.py | 52 ----------------- src/utils/dump_templates.py | 23 -------- 4 files changed, 1 insertion(+), 138 deletions(-) delete mode 100644 src/constants.py diff --git a/src/config/constants.py b/src/config/constants.py index 5f5bb85..bbf8ba1 100644 --- a/src/config/constants.py +++ b/src/config/constants.py @@ -17,7 +17,7 @@ from pathlib import Path # Default port for NSC deployment -NSC_PORT = 8081 +NSC_PORT = 8086 # Paths BASE_DIR = Path(__file__).resolve().parent.parent.parent diff --git a/src/constants.py b/src/constants.py deleted file mode 100644 index d56a7bc..0000000 --- a/src/constants.py +++ /dev/null @@ -1,62 +0,0 @@ -# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# This file is an original contribution from Telefonica Innovación Digital S.L. -""" This file contains constants used throughout the NSC application. """ - -<<<<<<<< HEAD:src/utils/dump_templates.py -import json, os -from src.config.constants import TEMPLATES_PATH -from flask import current_app - -def dump_templates(nbi_file, ietf_file, realizer_file): - """ - Dump multiple templates as JSON into the templates path. - """ - if not current_app.config["DUMP_TEMPLATES"]: - return -======== -import logging -import os -# Default logging level -DEFAULT_LOGGING_LEVEL = logging.INFO - -# Default port for NSC deployment -NSC_PORT = 8086 ->>>>>>>> 1c9c24fe98b87f87d11c79af0904b705c97a9ded:src/constants.py - - templates = { - "nbi_template.json": nbi_file, - "ietf_template.json": ietf_file, - "realizer_template.json": realizer_file, - } - -<<<<<<<< HEAD:src/utils/dump_templates.py - for filename, content in templates.items(): - path = os.path.join(TEMPLATES_PATH, filename) - with open(path, "w") as f: - json.dump(content, f, indent=2) -======== -# Create the path to the desired file relative to the current file -TEMPLATES_PATH = os.path.join(SRC_PATH, "templates") - -# TFS Flags -# Flag to determine if configurations should be uploaded to Teraflow -TFS_UPLOAD = False -# Teraflow IP -TFS_IP = "10.95.86.58" -# Flag to determine if additional L2VPN configuration support is -# required for deploying L2VPNs with path selection -TFS_L2VPN_SUPPORT = False ->>>>>>>> 1c9c24fe98b87f87d11c79af0904b705c97a9ded:src/constants.py diff --git a/src/realizer/tfs/helpers/cisco_connector.py b/src/realizer/tfs/helpers/cisco_connector.py index f6e6e5c..69953b8 100644 --- a/src/realizer/tfs/helpers/cisco_connector.py +++ b/src/realizer/tfs/helpers/cisco_connector.py @@ -13,64 +13,12 @@ # limitations under the License. """ This file is an original contribution from Telefonica Innovación Digital S.L. """ -<<<<<<< HEAD:src/realizer/tfs/helpers/cisco_connector.py # This file is an original contribution from Telefonica Innovación Digital S.L. import logging from netmiko import ConnectHandler class cisco_connector(): -======= -import json -import logging -import os -import uuid -import requests -from netmiko import ConnectHandler -from src.constants import DEFAULT_LOGGING_LEVEL - -logging.basicConfig( - level=DEFAULT_LOGGING_LEVEL, - format='%(levelname)s - %(message)s') - -#Teraflow -class TFSConnector(): - """Connector class for interacting with Teraflow SDN. - This class provides methods to send requests to - Teraflow SDN for network service configuration.""" - - def simple_post(self, tfs_ip, service): - """Send a simple POST request to Teraflow SDN. - This method sends a JSON payload to the Teraflow SDN - service endpoint to configure network services.""" - - user="admin" - password="admin" - token="" - session = requests.Session() - session.auth = (user, password) - url=f'http://{tfs_ip}/webui' - response=session.get(url=url) - for item in response.iter_lines(): - if "csrf_token" in str(item): - string=str(item).split('>>>>>> 1c9c24fe98b87f87d11c79af0904b705c97a9ded:src/helpers.py def __init__(self, address, configs=None): self.address=address self.configs=configs diff --git a/src/utils/dump_templates.py b/src/utils/dump_templates.py index d56a7bc..a460b54 100644 --- a/src/utils/dump_templates.py +++ b/src/utils/dump_templates.py @@ -15,7 +15,6 @@ # This file is an original contribution from Telefonica Innovación Digital S.L. """ This file contains constants used throughout the NSC application. """ -<<<<<<<< HEAD:src/utils/dump_templates.py import json, os from src.config.constants import TEMPLATES_PATH from flask import current_app @@ -26,15 +25,7 @@ def dump_templates(nbi_file, ietf_file, realizer_file): """ if not current_app.config["DUMP_TEMPLATES"]: return -======== -import logging -import os -# Default logging level -DEFAULT_LOGGING_LEVEL = logging.INFO -# Default port for NSC deployment -NSC_PORT = 8086 ->>>>>>>> 1c9c24fe98b87f87d11c79af0904b705c97a9ded:src/constants.py templates = { "nbi_template.json": nbi_file, @@ -42,21 +33,7 @@ NSC_PORT = 8086 "realizer_template.json": realizer_file, } -<<<<<<<< HEAD:src/utils/dump_templates.py for filename, content in templates.items(): path = os.path.join(TEMPLATES_PATH, filename) with open(path, "w") as f: json.dump(content, f, indent=2) -======== -# Create the path to the desired file relative to the current file -TEMPLATES_PATH = os.path.join(SRC_PATH, "templates") - -# TFS Flags -# Flag to determine if configurations should be uploaded to Teraflow -TFS_UPLOAD = False -# Teraflow IP -TFS_IP = "10.95.86.58" -# Flag to determine if additional L2VPN configuration support is -# required for deploying L2VPNs with path selection -TFS_L2VPN_SUPPORT = False ->>>>>>>> 1c9c24fe98b87f87d11c79af0904b705c97a9ded:src/constants.py -- GitLab From c37e2a0ef59d54d4af5ecad9bcb71179830a7680 Mon Sep 17 00:00:00 2001 From: armingol Date: Wed, 1 Oct 2025 08:01:12 +0000 Subject: [PATCH 6/6] Remove unnecessary docstrings and clean up comments in cisco_connector and dump_templates modules --- src/realizer/tfs/helpers/cisco_connector.py | 207 +------------------- src/utils/dump_templates.py | 2 - 2 files changed, 1 insertion(+), 208 deletions(-) diff --git a/src/realizer/tfs/helpers/cisco_connector.py b/src/realizer/tfs/helpers/cisco_connector.py index 69953b8..356012c 100644 --- a/src/realizer/tfs/helpers/cisco_connector.py +++ b/src/realizer/tfs/helpers/cisco_connector.py @@ -11,7 +11,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -""" This file is an original contribution from Telefonica Innovación Digital S.L. """ # This file is an original contribution from Telefonica Innovación Digital S.L. @@ -24,10 +23,6 @@ class cisco_connector(): self.configs=configs def execute_commands(self, commands): - """Execute a list of commands on the Cisco device via SSH. - Connects to the device and sends configuration commands. - This method is used to send configuration commands to the Cisco device.""" - try: # Configuración del dispositivo device = { @@ -51,10 +46,6 @@ class cisco_connector(): logging.error("Failed to execute commands on %s: %s",self.address, str(e)) def create_command_template(self, config): - """Create a command template for configuring L2VPN on a Cisco device. - This method generates the necessary commands to configure L2VPN - based on the provided configuration parameters.""" - commands = [ "l2vpn", f"pw-class l2vpn_vpws_profile_example_{config['number']}", @@ -84,10 +75,6 @@ class cisco_connector(): return commands def full_create_command_template(self): - """Create a full command template for configuring L2VPN on a Cisco device. - This method generates all necessary commands to configure L2VPN based on - the provided configurations.""" - commands =[] for config in self.configs: commands_temp = self.create_command_template(config) @@ -97,202 +84,10 @@ class cisco_connector(): return commands def create_command_template_delete(self): - """Create a command template for deleting L2VPN configuration on a Cisco device. - This method generates the necessary commands to remove L2VPN configurations.""" - commands = [ "no l2vpn", ] + commands.append("commit") commands.append("end") return commands - -def send_network_slice_request(data: str, action: str = "create") -> dict: - - url = 'http://192.168.1.143:9090/api/resource-allocation/transport-network-slice-l3' - headers = {'Content-Type': 'application/json'} - try: - if action == "delete": - data = { - "ietf-network-slice-service:network-slice-services": { - "slice-service": [ - { - "id": data - } - ] - } - } - response = requests.delete(url, headers=headers, json=data, timeout=15) - elif action == "create": - response = requests.post(url, headers=headers, json=data, timeout=15) - else: - raise ValueError("Invalid action. Use 'create' or 'delete'.") - except requests.exceptions.RequestException as e: - logging.error(f"HTTP request failed: {e}") - return {} - - if response.ok: - return response.json() - else: - print(f"Request failed with status code {response.status_code}: {response.text}") - response.raise_for_status() - -def group_block(group, action, group_id_override=None, node = None): - active = "true" if action == 'create' else "false" - group_id = group_id_override if group_id_override is not None else group["digital_sub_carriers_group_id"] - if node == "leaf": - return { - "digital_sub_carriers_group_id": group_id, - "digital_sub_carrier_id": [ - {'sub_carrier_id': 1, 'active': active}, - {'sub_carrier_id': 2, 'active': active}, - {'sub_carrier_id': 3, 'active': active}, - {'sub_carrier_id': 4, 'active': active} - ] - } - else: - return { - "digital_sub_carriers_group_id": group_id, - "digital_sub_carrier_id": [ - { - "sub_carrier_id": sid, - "active": active, - } - for sid in group["subcarrier-id"] - ] - } - -def generate_rules(connectivity_service, intent, action): - src_name = connectivity_service.get("source", "FALTA VALOR") - dest_list = connectivity_service.get("destination", ["FALTA VALOR"]) - dest_str = ",".join(dest_list) - config_rules = [] - - network_slice_uuid_str = f"{src_name}_to_{dest_str}" - tunnel_uuid = str(uuid.uuid5(uuid.NAMESPACE_DNS, network_slice_uuid_str)) - provisionamiento = { - "network-slice-uuid": network_slice_uuid_str, - "viability": True, - "actions": [] - } - - attributes = connectivity_service["connectivity-service"]["tapi-connectivity:connectivity-service"]["connection"][0]["optical-connection-attributes"] - groups = attributes["subcarrier-attributes"]["digital-subcarrier-group"] - # central_freq = (attributes["central-frequency"]) - # tx_power = (attributes["Tx-power"]) - # spacing = attributes["digital-subcarrier-spacing"] - operational_mode = attributes["modulation"]["operational-mode"] - # port = attributes["modulation"]["port"] - hub_groups = [ - group_block(group, action, group_id_override=index + 1) - for index, group in enumerate(groups) - ] - hub = { - "name": "channel-1", - "frequency": 195000000, - "target_output_power": 0, - "operational_mode": operational_mode, - "operation" : "merge", - "digital_sub_carriers_group": hub_groups - } - - leaves = [] - for dest, group in zip(connectivity_service["destination"], groups): - if dest == "T1.1": - name = "channel-1" - freq = 195006250 - if dest == "T1.2": - name = "channel-3" - freq = 195018750 - if dest == "T1.3": - name = "channel-5" - freq = 195031250 - leaf = { - "name": name, - "frequency": freq, - "target_output_power": group["Tx-power"], - "operational_mode": int(group["operational-mode"]), - "operation" : "merge", - "digital_sub_carriers_group": [group_block(group, action, group_id_override=1, node = "leaf")] - } - - leaves.append(leaf) - - final_json = {"components": [hub] + leaves} - if action == 'create': - provisionamiento["actions"].append({ - "type": "XR_AGENT_ACTIVATE_TRANSCEIVER", - "layer": "OPTICAL", - "content": final_json, - "controller-uuid": "IPoWDM Controller" - }) - - nodes = {} - sdp_list = intent['ietf-network-slice-service:network-slice-services']['slice-service'][0]['sdps']['sdp'] - - for sdp in sdp_list: - node = sdp['node-id'] - attachments = sdp['attachment-circuits']['attachment-circuit'] - for ac in attachments: - ip = ac.get('ac-ipv4-address', None) - prefix = ac.get('ac-ipv4-prefix-length', None) - vlan = 500 - nodes[node] = { - "ip-address": ip, - "ip-mask": prefix, - "vlan-id": vlan - } - - provisionamiento["actions"].append({ - "type": "CONFIG_VPNL3", - "layer": "IP", - "content": { - "tunnel-uuid": tunnel_uuid, - "src-node-uuid": src_name, - "src-ip-address": nodes[src_name]["ip-address"], - "src-ip-mask": str(nodes[src_name]["ip-mask"]), - "src-vlan-id": nodes[src_name]["vlan-id"], - "dest1-node-uuid": dest_list[0], - "dest1-ip-address": nodes[dest_list[0]]["ip-address"], - "dest1-ip-mask": str(nodes[dest_list[0]]["ip-mask"]), - "dest1-vlan-id": nodes[dest_list[0]]["vlan-id"], - "dest2-node-uuid": dest_list[1], - "dest2-ip-address": nodes[dest_list[1]]["ip-address"], - "dest2-ip-mask": str(nodes[dest_list[1]]["ip-mask"]), - "dest2-vlan-id": nodes[dest_list[1]]["vlan-id"], - "dest3-node-uuid": dest_list[2], - "dest3-ip-address": nodes[dest_list[2]]["ip-address"], - "dest3-ip-mask": str(nodes[dest_list[2]]["ip-mask"]), - "dest3-vlan-id": nodes[dest_list[2]]["vlan-id"] - }, - "controller-uuid": "IP Controller" - }) - - config_rules.append(provisionamiento) - else: - nodes = [] - nodes.append(src_name) - for dst in dest_list: nodes.append(dst) - aux = tunnel_uuid + '-' + src_name + '-' + '-'.join(dest_list) - provisionamiento["actions"].append({ - "type": "DEACTIVATE_XR_AGENT_TRANSCEIVER", - "layer": "OPTICAL", - "content": final_json, - "controller-uuid": "IPoWDM Controller", - "uuid" : aux, - "nodes": nodes - }) - config_rules.append(provisionamiento) - - return config_rules - -def get_sip_from_name(context, node_name): - - context = context.json() - topologies = context.get("tapi-topology:topology-context", {}).get("topology", []) - for topology in topologies: - nodes = topology.get("nodes", []) - for node in nodes: - if node.get("name") == node_name: - return node.get("uuid") - return None diff --git a/src/utils/dump_templates.py b/src/utils/dump_templates.py index a460b54..67cc73f 100644 --- a/src/utils/dump_templates.py +++ b/src/utils/dump_templates.py @@ -13,7 +13,6 @@ # limitations under the License. # This file is an original contribution from Telefonica Innovación Digital S.L. -""" This file contains constants used throughout the NSC application. """ import json, os from src.config.constants import TEMPLATES_PATH @@ -26,7 +25,6 @@ def dump_templates(nbi_file, ietf_file, realizer_file): if not current_app.config["DUMP_TEMPLATES"]: return - templates = { "nbi_template.json": nbi_file, "ietf_template.json": ietf_file, -- GitLab