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/helpers.py +31 −19 Original line number Diff line number Diff line Loading @@ -180,10 +180,20 @@ 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"] 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": [ Loading Loading @@ -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", Loading @@ -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, Loading @@ -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" }) Loading src/network_slice_controller.py +22 −5 Original line number Diff line number Diff line Loading @@ -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() Loading Loading @@ -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"] \ Loading Loading @@ -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'] Loading Loading @@ -789,6 +791,7 @@ class NSController: "destination": destination, "connectivity-service": response } logging.info(summary) rules = generate_rules(summary, intent,action) return rules Loading Loading @@ -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) Loading Loading @@ -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 = { Loading Loading @@ -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): Loading Loading @@ -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 Loading @@ -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 Loading 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/helpers.py +31 −19 Original line number Diff line number Diff line Loading @@ -180,10 +180,20 @@ 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"] 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": [ Loading Loading @@ -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", Loading @@ -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, Loading @@ -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" }) Loading
src/network_slice_controller.py +22 −5 Original line number Diff line number Diff line Loading @@ -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() Loading Loading @@ -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"] \ Loading Loading @@ -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'] Loading Loading @@ -789,6 +791,7 @@ class NSController: "destination": destination, "connectivity-service": response } logging.info(summary) rules = generate_rules(summary, intent,action) return rules Loading Loading @@ -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) Loading Loading @@ -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 = { Loading Loading @@ -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): Loading Loading @@ -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 Loading @@ -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 Loading