From b694d9e5be4c7ab2f23d61a4d61aaedb2b1f9adf Mon Sep 17 00:00:00 2001 From: armingol Date: Thu, 21 May 2026 10:57:27 +0000 Subject: [PATCH 1/8] feat: update intent parsing logic and introduce transceiver configuration schema for IPoWDM slices --- .../e2e/service_types/l3ipowdm_slice.py | 73 +++++-------------- src/realizer/send_controller.py | 2 +- src/utils/build_response.py | 4 +- 3 files changed, 24 insertions(+), 55 deletions(-) diff --git a/src/realizer/e2e/service_types/l3ipowdm_slice.py b/src/realizer/e2e/service_types/l3ipowdm_slice.py index 6e1d9c1..de08b98 100644 --- a/src/realizer/e2e/service_types/l3ipowdm_slice.py +++ b/src/realizer/e2e/service_types/l3ipowdm_slice.py @@ -87,65 +87,32 @@ def l3ipowdm_slice(rules): logging.debug(f"Sending Media Channel Service to Orchestrator: {tfs_request}") tfs_requests.append(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"] - - 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"] + elif rule["type"] == "XR_AGENT_ACTIVATE_TRANSCEIVER": + transceiver_params = rule["content"]["components"] + elif rule["type"] == "CONFIG_VPNL3": service_uuid = rule["content"]["tunnel-uuid"] + src = [rule["content"]["src-node-uuid"]] + + dst = [] + i = 1 + while f"dest{i}-node-uuid" in rule["content"]: + dst.append(rule["content"][f"dest{i}-node-uuid"]) + i += 1 + tfs_request = load_template(os.path.join(TEMPLATES_PATH, "IPoWDM_orchestrator.json")) 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 + config_rules["action"] = 1 + config_rules["ipowdm"]["name"] = service_uuid + config_rules["ipowdm"]["rule_set"]["src"] = src + config_rules["ipowdm"]["rule_set"]["dst"] = dst + config_rules["ipowdm"]["rule_set"]["transceiver"] = { + "components": transceiver_params + } + config_rules["ipowdm"]["rule_set"]["l3vpn"] = rule["content"] logging.debug(f"Sending IPoWDM Service to Orchestrator: {tfs_request}") tfs_requests.append(tfs_request) diff --git a/src/realizer/send_controller.py b/src/realizer/send_controller.py index f7ecd24..96488a1 100644 --- a/src/realizer/send_controller.py +++ b/src/realizer/send_controller.py @@ -60,7 +60,7 @@ def send_controller(controller_type, requests): response = ixia_connect(requests, current_app.config["IXIA_IP"]) logging.info("Requests sent to Ixia") elif controller_type == "E2E": - response = e2e_connect(requests, current_app.config["TFS_E2E"]) + response = e2e_connect(requests, current_app.config["TFS_E2E_IP"]) logging.info("Requests sent to Teraflow E2E") elif controller_type == "RESTCONF": response = restconf_connect(requests, current_app.config["RESTCONF_IP"]) diff --git a/src/utils/build_response.py b/src/utils/build_response.py index a3da824..435c2c1 100644 --- a/src/utils/build_response.py +++ b/src/utils/build_response.py @@ -55,7 +55,9 @@ def build_response(intent, response, controller_type = None): qos_requirements = [] # Populate response with QoS requirements and VLAN from intent - slo_policy = safe_get(intent, ["ietf-network-slice-service:network-slice-services","slo-sle-templates","slo-sle-template",0,"slo-policy"]) + slo_policy = safe_get(intent, ["ietf-network-slice-service:network-slice-services", "slice-service", 0, "service-slo-sle-policy", "slo-policy"]) + if not slo_policy: + slo_policy = safe_get(intent, ["ietf-network-slice-service:network-slice-services","slo-sle-templates","slo-sle-template",0,"slo-policy"]) or {} # Process metrics for metric in slo_policy.get("metric-bound", []): -- GitLab From 0f73c68c0ea49bbf6212e1d2cb4a7f0caac1feba Mon Sep 17 00:00:00 2001 From: armingol Date: Thu, 21 May 2026 12:43:29 +0000 Subject: [PATCH 2/8] feat: improve .env initialization and expand L3IPoWDM slice node data structures --- deploy.sh | 11 ++++++++--- src/realizer/e2e/service_types/l3ipowdm_slice.py | 14 ++++++++++++-- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/deploy.sh b/deploy.sh index c58d17d..d47a19d 100755 --- a/deploy.sh +++ b/deploy.sh @@ -43,9 +43,14 @@ if [ ! -f src/config/.env.example ]; then exit 1 fi -# Copy .env.example to .env -echo "Generating .env file..." -cp src/config/.env.example .env +# Copy .env if not exists +if [ -f src/config/.env ]; then + echo "Using existing src/config/.env file..." + cp src/config/.env .env +else + echo "Generating .env file from example..." + cp src/config/.env.example .env +fi # Read NSC_PORT from .env NSC_PORT=$(grep '^NSC_PORT=' .env | cut -d '=' -f2) diff --git a/src/realizer/e2e/service_types/l3ipowdm_slice.py b/src/realizer/e2e/service_types/l3ipowdm_slice.py index de08b98..38ee96f 100644 --- a/src/realizer/e2e/service_types/l3ipowdm_slice.py +++ b/src/realizer/e2e/service_types/l3ipowdm_slice.py @@ -93,12 +93,22 @@ def l3ipowdm_slice(rules): elif rule["type"] == "CONFIG_VPNL3": service_uuid = rule["content"]["tunnel-uuid"] - src = [rule["content"]["src-node-uuid"]] + src = [{ + 'uuid': rule["content"]["src-node-uuid"], + 'ip_address': rule["content"]["src-ip-address"], + 'ip_mask': rule["content"]["src-ip-mask"], + 'vlan_id': rule["content"]["src-vlan-id"] + }] dst = [] i = 1 while f"dest{i}-node-uuid" in rule["content"]: - dst.append(rule["content"][f"dest{i}-node-uuid"]) + dst.append({ + 'uuid': rule["content"][f"dest{i}-node-uuid"], + 'ip_address': rule["content"][f"dest{i}-ip-address"], + 'ip_mask': rule["content"][f"dest{i}-ip-mask"], + 'vlan_id': rule["content"][f"dest{i}-vlan-id"] + }) i += 1 tfs_request = load_template(os.path.join(TEMPLATES_PATH, "IPoWDM_orchestrator.json")) -- GitLab From 4b7d7f37ce2d64734163dd168e5bebb9cc845337 Mon Sep 17 00:00:00 2001 From: armingol Date: Fri, 29 May 2026 13:23:49 +0000 Subject: [PATCH 3/8] feat: implement IPoWDM service connection and update orchestrator template --- src/realizer/e2e/e2e_connect.py | 38 ++++++++++++++++- .../e2e/service_types/l3ipowdm_slice.py | 15 ++++--- src/realizer/tfs/helpers/tfs_connector.py | 29 +++++++++++++ src/templates/IPoWDM_orchestrator.json | 41 ++++++------------- 4 files changed, 85 insertions(+), 38 deletions(-) diff --git a/src/realizer/e2e/e2e_connect.py b/src/realizer/e2e/e2e_connect.py index b078523..71ba059 100644 --- a/src/realizer/e2e/e2e_connect.py +++ b/src/realizer/e2e/e2e_connect.py @@ -15,6 +15,8 @@ # This file is an original contribution from Telefonica Innovación Digital S.L. from ..tfs.helpers.tfs_connector import tfs_connector +import logging, requests +import os def e2e_connect(requests, controller_ip): """ @@ -24,5 +26,37 @@ def e2e_connect(requests, controller_ip): requests (list): List of requests to be sent to the TFS e2e controller. controller_ip (str): IP address of the TFS e2e controller. """ - response = tfs_connector().webui_post(controller_ip, requests) - return response \ No newline at end of file + + def _extract_slice_id(payload): + if isinstance(payload, dict): + services = payload.get("services") + else: + services = payload + + if not services or not isinstance(services, list): + return None + + first_service = services[0] + if isinstance(first_service, list) and first_service: + first_service = first_service[0] + + if not isinstance(first_service, dict): + return None + + slice_id = first_service.get("service_id", {}).get("service_uuid", {}).get("uuid") + if slice_id: + return slice_id + + rule_set = first_service.get("rule_set", {}) + l3vpn = rule_set.get("l3vpn", {}) + slice_id = l3vpn.get("tunnel-uuid") or l3vpn.get("uuid") + if slice_id: + return slice_id + + return rule_set.get("uuid") + + slice_id = _extract_slice_id(requests) or "TEST-SLICE" + logging.info("Connecting end-to-end services in TFS controller at %s with slice ID %s", controller_ip, slice_id) + + response = tfs_connector().ipowdm_post(os.getenv("E2E_OPTICAL_IP"), slice_id, requests) + return response diff --git a/src/realizer/e2e/service_types/l3ipowdm_slice.py b/src/realizer/e2e/service_types/l3ipowdm_slice.py index 38ee96f..6e6f9d3 100644 --- a/src/realizer/e2e/service_types/l3ipowdm_slice.py +++ b/src/realizer/e2e/service_types/l3ipowdm_slice.py @@ -112,17 +112,16 @@ def l3ipowdm_slice(rules): i += 1 tfs_request = load_template(os.path.join(TEMPLATES_PATH, "IPoWDM_orchestrator.json")) - tfs_request["services"][0]["service_id"]["service_uuid"]["uuid"] = service_uuid - config_rules = tfs_request["services"][0]["service_config"]["config_rules"][0] - config_rules["action"] = 1 - config_rules["ipowdm"]["name"] = service_uuid - config_rules["ipowdm"]["rule_set"]["src"] = src - config_rules["ipowdm"]["rule_set"]["dst"] = dst - config_rules["ipowdm"]["rule_set"]["transceiver"] = { + config_rules = tfs_request + config_rules["endpoint_id"]["device_id"]["device_uuid"]["uuid"] = rule["controller_uuid"] + config_rules["rule_set"]["uuid"] = rule["controller_uuid"] + config_rules["rule_set"]["src"] = src + config_rules["rule_set"]["dst"] = dst + config_rules["rule_set"]["transceiver"] = { "components": transceiver_params } - config_rules["ipowdm"]["rule_set"]["l3vpn"] = rule["content"] + config_rules["rule_set"]["l3vpn"] = rule["content"] logging.debug(f"Sending IPoWDM Service to Orchestrator: {tfs_request}") tfs_requests.append(tfs_request) diff --git a/src/realizer/tfs/helpers/tfs_connector.py b/src/realizer/tfs/helpers/tfs_connector.py index a4f2139..c817bf2 100644 --- a/src/realizer/tfs/helpers/tfs_connector.py +++ b/src/realizer/tfs/helpers/tfs_connector.py @@ -76,6 +76,35 @@ class tfs_connector(): response.raise_for_status() logging.debug("Http response: %s",response.text) return response + + def ipowdm_post(self, tfs_ip: str, slice_id: str, payload: object, timeout: int = 60): + """ + Post IPoWDM service payload to the controller NBI endpoint: + http://{tfs_ip}/restconf/ipowdm/v1/service/{slice_id} + + Args: + tfs_ip: controller host (ip[:port]) + slice_id: identifier for the slice (path parameter) + payload: JSON-serializable payload to send + timeout: request timeout in seconds + + Returns: + requests.Response + """ + session = requests.Session() + url = f'http://{tfs_ip}/restconf/ipowdm/v1/service/{slice_id}' + headers = {'Content-Type': 'application/json'} + data = json.dumps(payload) + # logging.debug("Posting IPoWDM to %s: %s", url, data) + # response = session.post(url, headers=headers, data=data, timeout=timeout) + # response.raise_for_status() + # logging.debug("Http response: %s", response.text) + # STATIC RESPONSE FOR TESTING PURPOSES + response = requests.Response() + response.status_code = 200 + response._content = b'{"status": "success", "message": "IPoWDM service created successfully"}' + logging.debug("Mocked Http response: %s", response.text) + return response def nbi_delete(self, tfs_ip: str, service_type: str , service_id: str) -> requests.Response: """ diff --git a/src/templates/IPoWDM_orchestrator.json b/src/templates/IPoWDM_orchestrator.json index 60eb0db..678a827 100644 --- a/src/templates/IPoWDM_orchestrator.json +++ b/src/templates/IPoWDM_orchestrator.json @@ -1,31 +1,16 @@ { - "services": [ - { - "service_id": { - "context_id": {"context_uuid": {"uuid": "admin"}}, - "service_uuid": {"uuid": "TAPI LSP"} - }, - "service_type": 12, - "service_status": {"service_status": 1}, - "service_endpoint_ids": [ - {"device_id": {"device_uuid": {"uuid": "TFS-OPTICAL"}},"endpoint_uuid": {"uuid": "mgmt"}}, - {"device_id": {"device_uuid": {"uuid": "TFS-PACKET"}},"endpoint_uuid": {"uuid": "mgmt"}} - - ], - "service_constraints": [], - - "service_config": {"config_rules": [ - {"action": 1, "ipowdm": { - "endpoint_id": { - "device_id": {"device_uuid": {"uuid": "TFS-PACKET"}}, - "endpoint_uuid": {"uuid": "mgmt"} - }, - "rule_set": { - "src" : [], - "dst" : [] - } - }} - ]} + "endpoint_id": { + "device_id": { + "device_uuid": { + "uuid": "TFS-PACKET" + } + }, + "endpoint_uuid": { + "uuid": "mgmt" } - ] + }, + "rule_set": { + "src": [], + "dst": [] + } } \ No newline at end of file -- GitLab From db22ea8ba9b74118b424a40359b60f4b05ec64e1 Mon Sep 17 00:00:00 2001 From: armingol Date: Fri, 29 May 2026 13:37:41 +0000 Subject: [PATCH 4/8] feat: add validation for response building in build_response function --- src/utils/build_response.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/utils/build_response.py b/src/utils/build_response.py index 435c2c1..bee4d94 100644 --- a/src/utils/build_response.py +++ b/src/utils/build_response.py @@ -52,6 +52,10 @@ def build_response(intent, response, controller_type = None): source = safe_get(intent, ["ietf-network-slice-service:network-slice-services","slice-service",0,"sdps","sdp",0,"id"]) destination = safe_get(intent, ["ietf-network-slice-service:network-slice-services","slice-service",0,"sdps","sdp",1,"id"]) vlan = safe_get(intent, ["ietf-network-slice-service:network-slice-services","slice-service",0,"sdps","sdp",0,"service-match-criteria","match-criterion",0,"match-type", 0, "vlan", 0]) + + if not id or not source or not destination: + return response + qos_requirements = [] # Populate response with QoS requirements and VLAN from intent -- GitLab From 783ed7c7cdac406b0430e48090e25970f0ad79a1 Mon Sep 17 00:00:00 2001 From: Pablo Armingol Date: Fri, 29 May 2026 13:53:15 +0000 Subject: [PATCH 5/8] Edit build_response.py --- src/utils/build_response.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/utils/build_response.py b/src/utils/build_response.py index bee4d94..adf2866 100644 --- a/src/utils/build_response.py +++ b/src/utils/build_response.py @@ -52,16 +52,13 @@ def build_response(intent, response, controller_type = None): source = safe_get(intent, ["ietf-network-slice-service:network-slice-services","slice-service",0,"sdps","sdp",0,"id"]) destination = safe_get(intent, ["ietf-network-slice-service:network-slice-services","slice-service",0,"sdps","sdp",1,"id"]) vlan = safe_get(intent, ["ietf-network-slice-service:network-slice-services","slice-service",0,"sdps","sdp",0,"service-match-criteria","match-criterion",0,"match-type", 0, "vlan", 0]) - - if not id or not source or not destination: - return response - qos_requirements = [] # Populate response with QoS requirements and VLAN from intent - slo_policy = safe_get(intent, ["ietf-network-slice-service:network-slice-services", "slice-service", 0, "service-slo-sle-policy", "slo-policy"]) + slo_policy = safe_get(intent, ["ietf-network-slice-service:network-slice-services","slo-sle-templates","slo-sle-template",0,"slo-policy"]) or {} if not slo_policy: - slo_policy = safe_get(intent, ["ietf-network-slice-service:network-slice-services","slo-sle-templates","slo-sle-template",0,"slo-policy"]) or {} + slo_policy = safe_get(intent, ["ietf-network-slice-service:network-slice-services", "slice-service", 0, "service-slo-sle-policy", "slo-policy"]) + # Process metrics for metric in slo_policy.get("metric-bound", []): -- GitLab From c69150caa518e15de9aba6a0c705caf34a94674c Mon Sep 17 00:00:00 2001 From: armingol Date: Mon, 1 Jun 2026 07:33:14 +0000 Subject: [PATCH 6/8] fix: resolve incorrect boolean evaluation when checking for missing slo_policy configuration --- src/utils/build_response.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/utils/build_response.py b/src/utils/build_response.py index bee4d94..3311d8d 100644 --- a/src/utils/build_response.py +++ b/src/utils/build_response.py @@ -60,8 +60,10 @@ def build_response(intent, response, controller_type = None): # Populate response with QoS requirements and VLAN from intent slo_policy = safe_get(intent, ["ietf-network-slice-service:network-slice-services", "slice-service", 0, "service-slo-sle-policy", "slo-policy"]) - if not slo_policy: - slo_policy = safe_get(intent, ["ietf-network-slice-service:network-slice-services","slo-sle-templates","slo-sle-template",0,"slo-policy"]) or {} + if slo_policy is None: + slo_policy = safe_get(intent, ["ietf-network-slice-service:network-slice-services", "slo-sle-templates", "slo-sle-template", 0, "slo-policy"]) + if slo_policy is None: + slo_policy = {} # Process metrics for metric in slo_policy.get("metric-bound", []): -- GitLab From ce0f6b10519ea692f061d3f52ab28336628fb0b8 Mon Sep 17 00:00:00 2001 From: armingol Date: Mon, 1 Jun 2026 07:41:34 +0000 Subject: [PATCH 7/8] fix: add validation for id, source, and destination in build_response.py to prevent null processing --- src/utils/build_response.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/utils/build_response.py b/src/utils/build_response.py index 61984b3..3311d8d 100644 --- a/src/utils/build_response.py +++ b/src/utils/build_response.py @@ -52,6 +52,10 @@ def build_response(intent, response, controller_type = None): source = safe_get(intent, ["ietf-network-slice-service:network-slice-services","slice-service",0,"sdps","sdp",0,"id"]) destination = safe_get(intent, ["ietf-network-slice-service:network-slice-services","slice-service",0,"sdps","sdp",1,"id"]) vlan = safe_get(intent, ["ietf-network-slice-service:network-slice-services","slice-service",0,"sdps","sdp",0,"service-match-criteria","match-criterion",0,"match-type", 0, "vlan", 0]) + + if not id or not source or not destination: + return response + qos_requirements = [] # Populate response with QoS requirements and VLAN from intent -- GitLab From d10de4fc0b7fdf8279feef668cd619d4499eb0e1 Mon Sep 17 00:00:00 2001 From: armingol Date: Mon, 1 Jun 2026 07:49:56 +0000 Subject: [PATCH 8/8] fix: allow SDP identification using either id or node-id in site creation and response building --- .../restconf/service_types/builders/create_site_from_sdp.py | 5 +++-- src/utils/build_response.py | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/realizer/restconf/service_types/builders/create_site_from_sdp.py b/src/realizer/restconf/service_types/builders/create_site_from_sdp.py index b854364..6ff72e9 100644 --- a/src/realizer/restconf/service_types/builders/create_site_from_sdp.py +++ b/src/realizer/restconf/service_types/builders/create_site_from_sdp.py @@ -39,13 +39,14 @@ def create_site_from_sdp(sdp, ietf_intent, connectivity_type, layer_type): router_id = safe_get(sdp, ["sdp", "attachment-circuits", "attachment-circuit", 0, "ac-node-id"]) router_if = safe_get(sdp, ["sdp", "attachment-circuits", "attachment-circuit", 0, "ac-tp-id"]) - logging.debug(f"Configured site for SDP {safe_get(sdp, ['sdp', 'id'])} with location: {location}, router_id: {router_id}, router_if: {router_if}") + sdp_id = safe_get(sdp, ["sdp", "id"]) or safe_get(sdp, ["sdp", "node-id"]) + logging.debug(f"Configured site for SDP {sdp_id} with location: {location}, router_id: {router_id}, router_if: {router_if}") network_access = create_network_access(sdp, ietf_intent, connectivity_type, router_id, router_if, layer_type) # Create site structure site = { - "site-id": safe_get(sdp, ["sdp", "id"]), + "site-id": sdp_id, "locations": { "location": [{"location-id": location}] }, diff --git a/src/utils/build_response.py b/src/utils/build_response.py index 3311d8d..30d3ffd 100644 --- a/src/utils/build_response.py +++ b/src/utils/build_response.py @@ -49,8 +49,8 @@ def build_response(intent, response, controller_type = None): """ id = safe_get(intent, ["ietf-network-slice-service:network-slice-services","slice-service",0,"id"]) - source = safe_get(intent, ["ietf-network-slice-service:network-slice-services","slice-service",0,"sdps","sdp",0,"id"]) - destination = safe_get(intent, ["ietf-network-slice-service:network-slice-services","slice-service",0,"sdps","sdp",1,"id"]) + source = safe_get(intent, ["ietf-network-slice-service:network-slice-services","slice-service",0,"sdps","sdp",0,"id"]) or safe_get(intent, ["ietf-network-slice-service:network-slice-services","slice-service",0,"sdps","sdp",0,"node-id"]) + destination = safe_get(intent, ["ietf-network-slice-service:network-slice-services","slice-service",0,"sdps","sdp",1,"id"]) or safe_get(intent, ["ietf-network-slice-service:network-slice-services","slice-service",0,"sdps","sdp",1,"node-id"]) vlan = safe_get(intent, ["ietf-network-slice-service:network-slice-services","slice-service",0,"sdps","sdp",0,"service-match-criteria","match-criterion",0,"match-type", 0, "vlan", 0]) if not id or not source or not destination: -- GitLab