Loading app.py +24 −20 Original line number Diff line number Diff line # 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) src/constants.py +1 −1 Original line number Diff line number Diff line Loading @@ -34,7 +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 = "10.95.86.58" TFS_IP = "172.24.36.55" # Flag to determine if additional L2VPN configuration support is # required for deploying L2VPNs with path selection TFS_L2VPN_SUPPORT = False src/helpers.py +162 −4 Original line number Diff line number Diff line Loading @@ -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') Loading Loading @@ -174,10 +174,168 @@ 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, 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 Loading
app.py +24 −20 Original line number Diff line number Diff line # 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)
src/constants.py +1 −1 Original line number Diff line number Diff line Loading @@ -34,7 +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 = "10.95.86.58" TFS_IP = "172.24.36.55" # Flag to determine if additional L2VPN configuration support is # required for deploying L2VPNs with path selection TFS_L2VPN_SUPPORT = False
src/helpers.py +162 −4 Original line number Diff line number Diff line Loading @@ -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') Loading Loading @@ -174,10 +174,168 @@ 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, 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