From 08cb94c60138739ed6c9f4c56220b248502d0c1d Mon Sep 17 00:00:00 2001 From: armingol Date: Mon, 9 Mar 2026 10:38:11 +0000 Subject: [PATCH 1/4] Update NBI + DEVICE NBI: * New NBI for IPoWDM services DEVICE: * Management of IPoWDM services in Emulated driver * Management of IPoWDM services in ietf_l3vpn driver --- .../drivers/emulated/EmulatedDriver.py | 112 +++- src/device/service/drivers/emulated/Tools.py | 27 +- .../drivers/ietf_l3vpn/IetfL3VpnDriver.py | 8 +- .../drivers/ietf_l3vpn/TfsApiClient.py | 5 +- .../drivers/ietf_l3vpn/templates/ipowdm.json | 34 +- .../drivers/ietf_l3vpn/templates/tools.py | 571 ++++++++++-------- src/nbi/service/app.py | 2 + src/nbi/service/ipowdm/Resources.py | 406 +++++++++++++ src/nbi/service/ipowdm/__init__.py | 39 ++ 9 files changed, 909 insertions(+), 295 deletions(-) create mode 100644 src/nbi/service/ipowdm/Resources.py create mode 100644 src/nbi/service/ipowdm/__init__.py diff --git a/src/device/service/drivers/emulated/EmulatedDriver.py b/src/device/service/drivers/emulated/EmulatedDriver.py index 125724c0f..e35e234e5 100644 --- a/src/device/service/drivers/emulated/EmulatedDriver.py +++ b/src/device/service/drivers/emulated/EmulatedDriver.py @@ -25,7 +25,8 @@ from device.service.driver_api._Driver import _Driver from device.service.driver_api.AnyTreeTools import TreeNode, dump_subtree, get_subnode, set_subnode_value from .Constants import SPECIAL_RESOURCE_MAPPINGS from .SyntheticSamplingParameters import SyntheticSamplingParameters, do_sampling -from .Tools import compose_resource_endpoint +from .Tools import compose_resource_endpoint,connect_to_xr_agent +import requests LOGGER = logging.getLogger(__name__) @@ -38,6 +39,7 @@ class EmulatedDriver(_Driver): def __init__(self, address : str, port : int, **settings) -> None: super().__init__(DRIVER_NAME, address, port, **settings) self.__lock = threading.Lock() + self.__address = address self.__initial = TreeNode('.') self.__running = TreeNode('.') self.__subscriptions = TreeNode('.') @@ -117,35 +119,85 @@ class EmulatedDriver(_Driver): chk_type('resources', resources, list) if len(resources) == 0: return [] results = [] - resolver = anytree.Resolver(pathattr='name') - with self.__lock: - for i,resource in enumerate(resources): - str_resource_name = 'resources[#{:d}]'.format(i) - try: - chk_type(str_resource_name, resource, (list, tuple)) - chk_length(str_resource_name, resource, min_length=2, max_length=2) - resource_key,resource_value = resource - chk_string(str_resource_name, resource_key, allow_empty=False) - resource_path = resource_key.split('/') - except Exception as e: # pylint: disable=broad-except - LOGGER.exception('Exception validating {:s}: {:s}'.format(str_resource_name, str(resource_key))) - results.append(e) # if validation fails, store the exception - continue - - try: - resource_value = json.loads(resource_value) - except: # pylint: disable=bare-except - pass - - set_subnode_value(resolver, self.__running, resource_path, resource_value) - - match = RE_GET_ENDPOINT_FROM_INTERFACE.match(resource_key) - if match is not None: - endpoint_uuid = match.group(1) - if '.' in endpoint_uuid: endpoint_uuid = endpoint_uuid.split('.')[0] - self.__synthetic_sampling_parameters.set_endpoint_configured(endpoint_uuid) - - results.append(True) + if 'ipowdm_ruleset' in str(resources): + connect_to_xr_agent(resources) + results.append(True) + else: + resolver = anytree.Resolver(pathattr='name') + with self.__lock: + for i,resource in enumerate(resources): + str_resource_name = 'resources[#{:d}]'.format(i) + try: + chk_type(str_resource_name, resource, (list, tuple)) + chk_length(str_resource_name, resource, min_length=2, max_length=2) + resource_key,resource_value = resource + chk_string(str_resource_name, resource_key, allow_empty=False) + resource_path = resource_key.split('/') + except Exception as e: # pylint: disable=broad-except + LOGGER.exception('Exception validating {:s}: {:s}'.format(str_resource_name, str(resource_key))) + results.append(e) # if validation fails, store the exception + continue + try: + resource_value = json.loads(resource_value) + except: # pylint: disable=bare-except + pass + + if resource_key.startswith('/ipowdm/service/'): + LOGGER.info('[%s] Legacy IPoWDM Service Provisioning: Payload=%s', self.__address, str(resource_value)) + elif resource_key.startswith('/ipowdm/l3nm/'): + LOGGER.info('[%s] L3NM Service Provisioning: Payload=%s', self.__address, str(resource_value)) + elif resource_key.startswith('/ipowdm/pluggables/'): + LOGGER.info('[%s] Pluggables Service Provisioning: Payload=%s', self.__address, str(resource_value)) + try: + key_parts = resource_key.split('/') + if len(key_parts) >= 5: + service_uuid_raw = key_parts[3] + # Normalize UUID: if serviceId contains '-pluggable-', take the part before it + if "-pluggable-" in service_uuid_raw: + service_uuid = service_uuid_raw.split("-pluggable-")[0] + else: + service_uuid = service_uuid_raw + + if not hasattr(EmulatedDriver, 'pluggables_pending'): + EmulatedDriver.pluggables_pending = {} + + if service_uuid in EmulatedDriver.pluggables_pending: + stored_payload = EmulatedDriver.pluggables_pending.pop(service_uuid) + current_payload = resource_value + + def format_entry(payload): + config = payload.get('config', {}) + return { + 'uuid': payload.get('device'), + 'power': config.get('target-output-power', 0.0), + 'frequency': config.get('frequency', 0.0) + } + combined_data = [ + {'src': format_entry(stored_payload)}, + {'dst': format_entry(current_payload)} + ] + LOGGER.info('[%s] Pluggables Service Provisioning Aggregated: %s', self.__address, json.dumps(combined_data, indent=2)) + # TODO Dynamic IP + url = "http://192.168.88.17:9849/api-v0/transponders" + headers = {'Content-Type': 'application/json'} + response = requests.post(url, json=combined_data, headers=headers) + LOGGER.info('[%s] Pluggables Service Provisioning Response: %s', self.__address, str(response.text)) + else: + EmulatedDriver.pluggables_pending[service_uuid] = resource_value + LOGGER.debug('[%s] Pluggables Service Partial Provisioning stored for %s', self.__address, service_uuid) + except Exception as e: + LOGGER.warning("Error processing Pluggables aggregation: %s", str(e)) + LOGGER.info('[%s] Pluggables Service Provisioning: Payload=%s', self.__address, str(resource_value)) + + set_subnode_value(resolver, self.__running, resource_path, resource_value) + + match = RE_GET_ENDPOINT_FROM_INTERFACE.match(resource_key) + if match is not None: + endpoint_uuid = match.group(1) + if '.' in endpoint_uuid: endpoint_uuid = endpoint_uuid.split('.')[0] + self.__synthetic_sampling_parameters.set_endpoint_configured(endpoint_uuid) + + results.append(True) return results @metered_subclass_method(METRICS_POOL) diff --git a/src/device/service/drivers/emulated/Tools.py b/src/device/service/drivers/emulated/Tools.py index 697a5995a..89f7724f9 100644 --- a/src/device/service/drivers/emulated/Tools.py +++ b/src/device/service/drivers/emulated/Tools.py @@ -12,12 +12,14 @@ # See the License for the specific language governing permissions and # limitations under the License. +import json import logging -from typing import Any, Dict, Optional, Tuple +import requests from common.proto.kpi_sample_types_pb2 import KpiSampleType from common.type_checkers.Checkers import chk_attribute, chk_string, chk_type from device.service.driver_api._Driver import RESOURCE_ENDPOINTS from .Constants import SPECIAL_RESOURCE_MAPPINGS +from typing import Any, Dict, Optional, Tuple LOGGER = logging.getLogger(__name__) @@ -107,3 +109,26 @@ def compose_resource_endpoint(endpoint_data : Dict[str, Any]) -> Optional[Tuple[ except: # pylint: disable=bare-except LOGGER.exception('Problem composing endpoint({:s})'.format(str(endpoint_data))) return None + +# TODO Dynamic IP +def connect_to_xr_agent(resources): + rule_set = resources[0][1]['rule_set'] + nodes = [ + {'src': { + 'uuid': rule_set['src'][0]['uuid'], + 'power': rule_set['src'][0]['power'], + 'frequency': rule_set['src'][0]['frequency'] + }}, + {'dst': { + 'uuid': rule_set['dst'][0]['uuid'], + 'power': rule_set['dst'][0]['power'], + 'frequency': rule_set['dst'][0]['frequency'] + }} + ] + url = "http://192.168.88.17:9849/api-v0/transponders" + headers = { + "Content-Type": "application/json", + "Expect": "" + } + json_data = json.dumps(nodes) + requests.post(url, data=json_data, headers=headers, timeout=10) diff --git a/src/device/service/drivers/ietf_l3vpn/IetfL3VpnDriver.py b/src/device/service/drivers/ietf_l3vpn/IetfL3VpnDriver.py index 08f34b8ad..dbab1c98b 100644 --- a/src/device/service/drivers/ietf_l3vpn/IetfL3VpnDriver.py +++ b/src/device/service/drivers/ietf_l3vpn/IetfL3VpnDriver.py @@ -190,10 +190,16 @@ class IetfL3VpnDriver(_Driver): if len(resources) == 0: return results with self.__lock: if 'ipowdm' in str(resources): + scheme = self.settings.get('scheme', 'http') + controller_url = f"{scheme}://{self.address}:{self.port}" + LOGGER.info('=' * 80) + LOGGER.info('IPoWDM SERVICE RECEIVED') + LOGGER.info('=' * 80) + for resource in resources: if 'ipowdm' in str(resource): try: - create_request(resource) + create_request(resource, controller_url) LOGGER.info('Request created successfully') results.append((resource, True)) except Exception as e: diff --git a/src/device/service/drivers/ietf_l3vpn/TfsApiClient.py b/src/device/service/drivers/ietf_l3vpn/TfsApiClient.py index c984c1adf..070c4361b 100644 --- a/src/device/service/drivers/ietf_l3vpn/TfsApiClient.py +++ b/src/device/service/drivers/ietf_l3vpn/TfsApiClient.py @@ -144,8 +144,9 @@ class TfsApiClient(RestApiClient): cr_rk : str = cr['custom']['resource_key'] if not cr_rk.startswith('/endpoints/endpoint['): continue settings = json.loads(cr['custom']['resource_value']) - ep_name = settings['name'] - config_rule_dict[ep_name] = settings + ep_name = settings.get('name', settings.get('uuid')) + if ep_name: + config_rule_dict[ep_name] = settings for json_endpoint in json_device['device_endpoints']: endpoint_uuid = json_endpoint['endpoint_id']['endpoint_uuid']['uuid'] diff --git a/src/device/service/drivers/ietf_l3vpn/templates/ipowdm.json b/src/device/service/drivers/ietf_l3vpn/templates/ipowdm.json index db700293f..ac22e80c6 100644 --- a/src/device/service/drivers/ietf_l3vpn/templates/ipowdm.json +++ b/src/device/service/drivers/ietf_l3vpn/templates/ipowdm.json @@ -3,25 +3,45 @@ { "service_id": { "context_id": {"context_uuid": {"uuid": "admin"}}, - "service_uuid": {"uuid": "IPoWDM"} + "service_uuid": {"uuid": "644c4aa6-c2e2-4db0-9d6e-869522c4141c"} }, "service_type": 12, "service_status": {"service_status": 1}, "service_endpoint_ids": [ - {"device_id": {"device_uuid": {"uuid": "IP1"}},"endpoint_uuid": {"uuid": "PORT-xe4"}}, - {"device_id": {"device_uuid": {"uuid": "IP2"}},"endpoint_uuid": {"uuid": "PORT-xe4"}} + {"device_id": {"device_uuid": {"uuid": "Phoenix1"}},"endpoint_uuid": {"uuid": "PORT-xe4"}}, + {"device_id": {"device_uuid": {"uuid": "Phoenix2"}},"endpoint_uuid": {"uuid": "PORT-xe4"}} ], "service_constraints": [], "service_config": {"config_rules": [ {"action": 1, "ipowdm": { "endpoint_id": { - "device_id": {"device_uuid": {"uuid": "IP1"}}, + "device_id": {"device_uuid": {"uuid": "Phoenix1"}}, "endpoint_uuid": {"uuid": "PORT-xe4"} }, "rule_set": { - "src" : [], - "dst" : [] - } + "src": [ + { + "uuid": "Phoenix-1", + "ip_address": "10.10.1.1", + "ip_mask": "/24", + "vlan_id": 100, + "power": 0.0, + "frequency": 194700.0 + } + ], + "dst": [ + { + "uuid": "Phoenix-2", + "ip_address": "10.10.2.1", + "ip_mask": "/24", + "vlan_id": 100, + "power": 0.0, + "frequency": 194700.0 + } + ], + "bw": 100, + "uuid": "644c4aa6-c2e2-4db0-9d6e-869522c4141c" + } }} ]} } diff --git a/src/device/service/drivers/ietf_l3vpn/templates/tools.py b/src/device/service/drivers/ietf_l3vpn/templates/tools.py index a9a4adbb2..e9791406e 100644 --- a/src/device/service/drivers/ietf_l3vpn/templates/tools.py +++ b/src/device/service/drivers/ietf_l3vpn/templates/tools.py @@ -16,10 +16,14 @@ import json import logging import os import requests +import uuid +from typing import Dict, List, Optional, Tuple from concurrent.futures import ThreadPoolExecutor LOGGER = logging.getLogger(__name__) +DEFAULT_CONTROLLER_URL = 'http://127.0.0.1:80' + HEADERS = { "Accept": "application/yang-data+json", "Content-Type": "application/yang-data+json" @@ -27,42 +31,91 @@ HEADERS = { executor = ThreadPoolExecutor() -site_template = { - "site-id": "", - "devices": { - "device": [ - { - "device-id": "", - "location": "" +def generate_l3vpn_site(endpoint: Dict, vpn_id: str, role: str = "spoke-role") -> Dict: + """ + Generate a single L3VPN site configuration from an endpoint. + + Args: + endpoint: Dict with uuid, ip_address, ip_mask, vlan_id + vpn_id: VPN identifier + role: "spoke-role" or "hub-role" + + Returns: + Dict with L3VPN site configuration + """ + return { + "site-id": endpoint["uuid"], + "management": {"type": "ietf-l3vpn-svc:provider-managed"}, + "locations": {"location": [{"location-id": f"location-{endpoint['uuid']}"}]}, + "devices": {"device": [{ + "device-id": endpoint.get("ip_address", "127.0.0.1"), + "location": f"location-{endpoint['uuid']}" + }]}, + "routing-protocols": {"routing-protocol": [{ + "type": "ietf-l3vpn-svc:static", + "static": { + "cascaded-lan-prefixes": { + "ipv4-lan-prefixes": [{ + "lan": f"{endpoint['ip_address']}{endpoint['ip_mask']}", + "lan-tag": f"vlan{endpoint['vlan_id']}", + "next-hop": endpoint.get("gateway", endpoint['ip_address']) + }] + } } - ] - }, - "site-network-accesses": { - "site-network-access": [ - { - "site-network-access-id": "", - "device-reference": "", + }]}, + "site-network-accesses": { + "site-network-access": [{ + "site-network-access-id": str(endpoint['vlan_id']), + "site-network-access-type": "ietf-l3vpn-svc:multipoint", + "device-reference": endpoint.get("ip_address", "127.0.0.1"), + "vpn-attachment": { + "vpn-id": vpn_id, + "site-role": f"ietf-l3vpn-svc:{role}" + }, "ip-connection": { "ipv4": { "address-allocation-type": "ietf-l3vpn-svc:static-address", "addresses": { - "provider-address": "", - "customer-address": "", - "prefix-length": "" + "provider-address": endpoint.get("provider_ip", endpoint['ip_address']), + "customer-address": endpoint['ip_address'], + "prefix-length": int(endpoint['ip_mask'].replace('/', '')) if isinstance(endpoint['ip_mask'], str) else endpoint['ip_mask'] } } }, - "vpn-attachment": { - "vpn-id": "vpn-p2mp" - }, - "site-network-access-type": "ietf-l3vpn-svc:multipoint" - } - ] + "service": { + "svc-mtu": 1500, + "svc-input-bandwidth": 1000000000, + "svc-output-bandwidth": 1000000000, + "qos": { + "qos-profile": { + "classes": { + "class": [{ + "class-id": "qos-realtime", + "direction": "ietf-l3vpn-svc:both", + "latency": {"latency-boundary": 10}, + "bandwidth": {"guaranteed-bw-percent": 100} + }] + } + } + } + } + }] + } } -} -def generate_l3vpn_template_pair(src, dst, vpn_id): +def generate_l3vpn_service(src: Dict, dst: Dict, vpn_id: str) -> Dict: + """ + Generate a complete L3VPN service with source and destination sites. + + Args: + src: Source endpoint configuration + dst: Destination endpoint configuration + vpn_id: VPN service identifier + + Returns: + Dict with complete L3VPN service configuration + """ return { "ietf-l3vpn-svc:l3vpn-svc": { "vpn-services": { @@ -70,247 +123,257 @@ def generate_l3vpn_template_pair(src, dst, vpn_id): }, "sites": { "site": [ - { - "site-id": src["uuid"], - "management": {"type": "ietf-l3vpn-svc:provider-managed"}, - "locations": {"location": [{"location-id": f"location-{src['uuid']}"}]}, - "devices": {"device": [{ - "device-id": "10.0.30.1", - "location": f"location-{src['uuid']}" - }]}, - "routing-protocols": {"routing-protocol": [{ - "type": "ietf-l3vpn-svc:static", - "static": { - "cascaded-lan-prefixes": { - "ipv4-lan-prefixes": [ - { - "lan": "128.32.10.1/24", - "lan-tag": f"vlan{src['vlan_id']}", - "next-hop": "10.0.30.10" - } - ] - } - } - }]}, - "site-network-accesses": { - "site-network-access": [{ - "site-network-access-id": f"{src['vlan_id']}", - "site-network-access-type": "ietf-l3vpn-svc:multipoint", - "device-reference": "10.0.30.1", - "vpn-attachment": { - "vpn-id": vpn_id, "site-role": "ietf-l3vpn-svc:spoke-role" - }, - "ip-connection": { - "ipv4": { - "address-allocation-type": "ietf-l3vpn-svc:static-address", - "addresses": { - "provider-address": "10.0.30.254", - "customer-address": "10.0.30.10", - "prefix-length": 24 - } - } - }, - "routing-protocols": {"routing-protocol": [{ - "type": "ietf-l3vpn-svc:static", - "static": { - "cascaded-lan-prefixes": { - "ipv4-lan-prefixes": [ - { - "lan": "172.1.101.1/24", - "lan-tag": "vlan100", - "next-hop": "10.0.30.254" - } - ] - } - } - }]}, - "service": { - "svc-mtu": 1500, - "svc-input-bandwidth": 1000000000, - "svc-output-bandwidth": 1000000000, - "qos": { - "qos-profile": { - "classes": { - "class": [ - { - "class-id": "qos-realtime", - "direction": "ietf-l3vpn-svc:both", - "latency": { - "latency-boundary": 10 - }, - "bandwidth": { - "guaranteed-bw-percent": 100 - } - } - ] - } - } - } - } - }] - } - }, - { - "site-id": dst["uuid"], - "management": {"type": "ietf-l3vpn-svc:provider-managed"}, - "locations": {"location": [{"location-id": f"location-{dst['uuid']}"}]}, - "devices": {"device": [{ - "device-id": "10.0.20.1", - "location": f"location-{dst['uuid']}" - }]}, - "routing-protocols": {"routing-protocol": [{ - "type": "ietf-l3vpn-svc:static", - "static": { - "cascaded-lan-prefixes": { - "ipv4-lan-prefixes": [ - { - "lan": "172.1.101.1/24", - "lan-tag": "vlan200", - "next-hop": "172.10.33.2" - } - ] - } - } - }]}, - "site-network-accesses": { - "site-network-access": [{ - "site-network-access-id": f"{dst['vlan_id']}", - "site-network-access-type": "ietf-l3vpn-svc:multipoint", - "device-reference": "10.0.20.1", - "vpn-attachment": { - "vpn-id": vpn_id, "site-role": "ietf-l3vpn-svc:hub-role" - }, - "ip-connection": { - "ipv4": { - "address-allocation-type": "ietf-l3vpn-svc:static-address", - "addresses": { - "provider-address": "172.10.33.254", - "customer-address": "172.10.33.2", - "prefix-length": 24 - } - } - }, - "routing-protocols": {"routing-protocol": [{ - "type": "ietf-l3vpn-svc:static", - "static": { - "cascaded-lan-prefixes": { - "ipv4-lan-prefixes": [ - { - "lan": "128.32.10.1/24", - "lan-tag": "vlan200", - "next-hop": "172.10.33.254" - } - ] - } - } - }]}, - "service": { - "svc-mtu": 1500, - "svc-input-bandwidth": 1000000000, - "svc-output-bandwidth": 1000000000, - "qos": { - "qos-profile": { - "classes": { - "class": [ - { - "class-id": "qos-realtime", - "direction": "ietf-l3vpn-svc:both", - "latency": { - "latency-boundary": 10 - }, - "bandwidth": { - "guaranteed-bw-percent": 100 - } - } - ] - } - } - } - } - }] - } - } + generate_l3vpn_site(src, vpn_id, "spoke-role"), + generate_l3vpn_site(dst, vpn_id, "hub-role") ] } } } -def create_request(resource_value): - """ Create and send HTTP request based on a JSON template and provided resource value. - The JSON template is expected to be in the same directory as this script, named 'ipowdm.json'. - Example resource_value: - {"rule_set": { - "uuid": "unique-service-uuid", - "bw": 100, - "src": [{"uuid": "src-device-uuid", "ip_address": "192.168.1.1", "ip_mask": "24", "vlan_id": 100, "power": 10, "frequency": 193100}], - "dst": [{"uuid": "dst-device-uuid", "ip_address": "192.168.3.3", "ip_mask": "24", "vlan_id": 100, "power": 10, "frequency": 193100}] - }} - The src and dst fields are lists to accommodate future extensions for multi-endpoint scenarios. - The request is sent to a predefined URL with appropriate headers. - Returns a response-like object with status_code and text attributes. - In case of error, returns a SimpleNamespace with status_code 500 and the error message in text. - - Note: The actual HTTP request sending is currently mocked for testing purposes. - The URL and headers are hardcoded for demonstration and should be adapted as needed. + +def send_l3vpn_service(service_config: Dict, controller_url: str, service_uuid: str) -> requests.Response: """ + Send L3VPN/L3NM service configuration to the NBI. + + Args: + service_config: L3VPN service configuration dict + controller_url: Controller URL (required) + service_uuid: Service UUID for the endpoint - LOGGER.info("Creating request for resource_value: %s", resource_value) - - node_src = resource_value[1]['rule_set']['src'][0] - src = [{ - 'uuid': node_src["uuid"], - 'ip_address': node_src["ip_address"], - 'ip_mask': node_src["ip_mask"], - 'vlan_id': node_src["vlan_id"] - }] - dst_list = resource_value[1]['rule_set']['dst'] - dsts = [] - for node in dst_list: - dsts.append({ - 'uuid': node["uuid"], - 'ip_address': node["ip_address"], - 'ip_mask': node["ip_mask"], - 'vlan_id': node["vlan_id"] - }) - - sites_input = src + dsts - - components = resource_value[1]['rule_set']['transceiver']['components'] - for i, device in enumerate(components): - name = sites_input[i]['uuid'] - - if name == "T2.1":device["frequency"]= 195000000 - if name == "T1.1":device["frequency"]= 195006250 - if name == "T1.2":device["frequency"]= 195018750 - if name == "T1.3":device["frequency"]= 195031250 - - LOGGER.debug(f"NODE TO CONFIGURE: \n{name}: {json.dumps(device, indent=2)}") - response = patch_optical_channel_frequency(device, name) - LOGGER.debug(f"RESPONSE :\n {response}") - templates = [] - for dst in dsts: - vpn = "L3VPN_"+src[0]['uuid']+"_"+dst['uuid'] - templates.append(generate_l3vpn_template_pair(src[0], dst,vpn)) - - url = "http://192.168.202.254:80/restconf/data/ietf-l3vpn-svc:l3vpn-svc/vpn-services" + Returns: + requests.Response object or status code + """ + url = f"{controller_url}/restconf/ipowdm/v1/l3nm/{service_uuid}" headers = { 'accept': 'application/json', 'Content-Type': 'application/json' } - for template in templates: - LOGGER.info("Generated L3VPN P2MP service JSON:\n%s", json.dumps(template, indent=2)) + LOGGER.info("Sending L3NM service to %s", url) + LOGGER.debug("L3NM payload: %s", json.dumps(service_config, indent=2)) + + try: + response = requests.post(url=url, headers=headers, json=service_config, timeout=30) + LOGGER.info("L3NM response: %s - %s", response.status_code, response.text) + return response + except Exception as e: + LOGGER.error("Failed to send L3NM service: %s", str(e)) + raise + +def generate_pluggable_config(component: Dict, device_id: str) -> Dict: + """ + Generate pluggable/transceiver configuration from a component. + + Args: + component: Dict with name, frequency, target_output_power, operational_mode, operation + device_id: Device identifier + + Returns: + Dict with pluggable configuration in format {device, config} + """ + return { + "device": device_id, + "config": { + "frequency": component.get("frequency", 193100000), + "target-output-power": component.get("target_output_power", 0.0), + "operational-mode": component.get("operational_mode", 0), + "name": component.get("name", ""), + "operation": component.get("operation", "activate") + } + } + + +def configure_pluggable(component: Dict, device_id: str, controller_url: str, service_uuid: str): + """ + Configure a pluggable/transceiver via NBI. + + Args: + component: Component configuration dict + device_id: Device identifier + controller_url: Controller URL (required) + service_uuid: Service UUID for the endpoint + + Returns: + Response object or status code + """ + url = f"{controller_url}/restconf/ipowdm/v1/pluggables/{service_uuid}" + + config = generate_pluggable_config(component, device_id) + + LOGGER.info("Configuring pluggable for service %s on device %s", service_uuid, device_id) + LOGGER.debug("Pluggable config: %s", json.dumps(config, indent=2)) + + try: + response = requests.post(url, json=config, headers=HEADERS, timeout=30) + LOGGER.info("Pluggable response: %s - %s", response.status_code, response.text) + return response + except Exception as e: + LOGGER.error("Failed to configure pluggable on %s: %s", device_id, str(e)) + return None + + +def configure_all_pluggables(components: List[Dict], device_ids: List[str], controller_url: str = None) -> List[Optional[requests.Response]]: + """ + Configure all pluggables/transceivers in parallel. + + Args: + components: List of component configurations + device_ids: List of device identifiers (same order as components) + controller_url: Optional controller URL override + + Returns: + List of response objects + """ + responses = [] + for component, device_id in zip(components, device_ids): + response = configure_pluggable(component, device_id, controller_url) + responses.append(response) + return responses + +def parse_ipowdm_data(resource_value) -> Tuple[List[Dict], List[Dict], List[Dict], str]: + """ + Parse IPoWDM resource data into structured components. + + Args: + resource_value: Raw resource value (tuple or dict) + + Returns: + Tuple of (src_endpoints, dst_endpoints, transceiver_components, service_uuid) + """ + if isinstance(resource_value, tuple): + data = resource_value[1] if isinstance(resource_value[1], dict) else json.loads(resource_value[1]) + elif isinstance(resource_value, dict): + data = resource_value + else: + data = json.loads(resource_value) + + if 'rule_set' in data: + data = data['rule_set'] + + src_endpoints = data.get('src', []) + dst_endpoints = data.get('dst', []) + components = data.get('transceiver', {}).get('components', []) + service_uuid = data.get('uuid', 'unknown') + + return src_endpoints, dst_endpoints, components, service_uuid + + +def process_ipowdm_service(resource_value, controller_url: str) -> Dict: + """ + Process an IPoWDM service request, generating L3NM services and configuring pluggables. + + This is the main entry point for IPoWDM processing. It: + 1. Parses the incoming IPoWDM data + 2. Configures all pluggables/transceivers + 3. Generates and sends L3VPN services for each src-dst pair + + Args: + resource_value: IPoWDM resource data + controller_url: Controller URL for L3VPN and pluggable configuration + + Returns: + Dict with processing results + """ + LOGGER.info("Processing IPoWDM service request") + + src_endpoints, dst_endpoints, components, service_uuid = parse_ipowdm_data(resource_value) + + LOGGER.info("IPoWDM Service UUID: %s", service_uuid) + LOGGER.info("Source endpoints: %d, Destination endpoints: %d, Components: %d", + len(src_endpoints), len(dst_endpoints), len(components)) + + results = { + "service_uuid": service_uuid, + "pluggable_results": [], + "l3vpn_results": [] + } + + if components: + all_endpoints = src_endpoints + dst_endpoints + device_ids = [ep.get('uuid', f'device-{i}') for i, ep in enumerate(all_endpoints)] + + LOGGER.info("Configuring %d pluggables", len(components)) + for i, (component, device_id) in enumerate(zip(components, device_ids)): + LOGGER.debug("Configuring pluggable %d on device %s: %s", i, device_id, component) + try: + pluggable_uuid = f"{service_uuid}-pluggable-{device_id}" + response = configure_pluggable(component, device_id, controller_url, pluggable_uuid) + status_code = response if isinstance(response, int) else (response.status_code if response else 0) + results["pluggable_results"].append({ + "device_id": device_id, + "status": status_code, + "success": status_code in [200, 201, 204] + }) + except Exception as e: + LOGGER.error("Pluggable configuration failed for %s: %s", device_id, str(e)) + results["pluggable_results"].append({ + "device_id": device_id, + "status": "error", + "success": False, + "error": str(e) + }) + + for src in src_endpoints: + for dst in dst_endpoints: + vpn_id = f"L3VPN_{src['uuid']}_{dst['uuid']}" + LOGGER.info("Creating L3VPN service: %s", vpn_id) - response = requests.post(url = url, headers= headers, json=template) - LOGGER.debug(response) - return None + try: + service_config = generate_l3vpn_service(src, dst, vpn_id) + l3nm_uuid = f"{service_uuid}-l3nm-{src['uuid']}-{dst['uuid']}" + response = send_l3vpn_service(service_config, controller_url, l3nm_uuid) + status_code = response if isinstance(response, int) else (response.status_code if response else 0) + results["l3vpn_results"].append({ + "vpn_id": vpn_id, + "src": src['uuid'], + "dst": dst['uuid'], + "status": status_code, + "success": status_code in [200, 201, 204] + }) + except Exception as e: + LOGGER.error("L3VPN creation failed for %s: %s", vpn_id, str(e)) + results["l3vpn_results"].append({ + "vpn_id": vpn_id, + "src": src['uuid'], + "dst": dst['uuid'], + "status": "error", + "success": False, + "error": str(e) + }) -def patch_optical_channel_frequency(data, DEVICE_ID): - encoded_path = f"http://192.168.202.254:80/restconf/data/device={DEVICE_ID}/openconfig-platform:components/component=channel-1/optical-channel/config" + LOGGER.info("IPoWDM processing complete. Results: %s", json.dumps(results, indent=2)) + return results + +def create_request(resource_value, controller_url: str = None): + """ + Legacy entry point for IPoWDM processing. + Maintained for backward compatibility with existing driver code. - patch_data = data - response = requests.patch(f"{encoded_path}", - json=patch_data, - headers=HEADERS) - assert response.status_code == 200 - return response + Args: + resource_value: IPoWDM resource data + controller_url: Controller URL (IP:port of the L3VPN controller device) + + Returns: + Processing results dict + """ + LOGGER.info("create_request called") + LOGGER.info("Resource value: %s", resource_value) + LOGGER.info("Controller URL: %s", controller_url) + + url = controller_url or DEFAULT_CONTROLLER_URL + return process_ipowdm_service(resource_value, url) + +def generate_l3vpn_template_pair(src: Dict, dst: Dict, vpn_id: str) -> Dict: + """ + Legacy function for generating L3VPN template. + Wrapper around generate_l3vpn_service for backward compatibility. + """ + return generate_l3vpn_service(src, dst, vpn_id) + + +def patch_optical_channel_frequency(data: Dict, device_id: str) -> Optional[requests.Response]: + """ + Legacy function for patching optical channel frequency. + Wrapper around configure_pluggable for backward compatibility. + """ + return configure_pluggable(data, device_id) diff --git a/src/nbi/service/app.py b/src/nbi/service/app.py index 7cb7cb3e7..6dc7cd923 100644 --- a/src/nbi/service/app.py +++ b/src/nbi/service/app.py @@ -40,6 +40,7 @@ from .ietf_l2vpn import register_ietf_l2vpn from .ietf_l3vpn import register_ietf_l3vpn from .ietf_network import register_ietf_network from .ietf_network_slice import register_ietf_nss +from .ipowdm import register_ipowdm from .osm_nbi import register_osm_api from .qkd_app import register_qkd_app from .restconf_root import register_restconf_root @@ -104,6 +105,7 @@ register_telemetry_subscription(nbi_app) register_tfs_api (nbi_app) #register_topology_updates(nbi_app) # does not work; check if eventlet-grpc side effects register_vntm_recommend (nbi_app) +register_ipowdm (nbi_app) register_well_known (nbi_app) LOGGER.info('All connectors registered') diff --git a/src/nbi/service/ipowdm/Resources.py b/src/nbi/service/ipowdm/Resources.py new file mode 100644 index 000000000..dbe1dd3cc --- /dev/null +++ b/src/nbi/service/ipowdm/Resources.py @@ -0,0 +1,406 @@ +# 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. + +import json +import logging +import requests +from flask_restful import Resource, request +from common.proto.context_pb2 import ConfigActionEnum, ConfigRule, Device, Service, ServiceTypeEnum, ServiceStatusEnum, ContextId +from device.client.DeviceClient import DeviceClient +from service.client.ServiceClient import ServiceClient +from context.client.ContextClient import ContextClient + +LOGGER = logging.getLogger(__name__) + +class IPoWDMService(Resource): + def __init__(self): + super().__init__() + self.device_client = DeviceClient() + self.service_client = ServiceClient() + + def post(self, serviceId: str): + LOGGER.info("Received POST request for IPoWDM service: %s", serviceId) + + request_data = request.get_json() + LOGGER.info("IPoWDM request data: %s", json.dumps(request_data, indent=2)) + + if 'src' not in request_data or 'dst' not in request_data: + return {'status': 'error', 'message': 'Missing required fields: src and dst'}, 400 + + src_endpoints = request_data.get('src', []) + dst_endpoints = request_data.get('dst', []) + bandwidth = request_data.get('bw', 100) + device_id = request_data.get('device_id', 'TFS-PACKET') + + LOGGER.info(f"Service UUID: {serviceId}") + LOGGER.info(f"Bandwidth: {bandwidth}") + LOGGER.info(f"Source endpoints: {len(src_endpoints)}") + LOGGER.info(f"Destination endpoints: {len(dst_endpoints)}") + LOGGER.info(f"Device ID: {device_id}") + + try: + service = Service() + service.service_id.service_uuid.uuid = serviceId + service.service_id.context_id.context_uuid.uuid = "admin" + service.service_type = ServiceTypeEnum.SERVICETYPE_L3NM + service.service_status.service_status = ServiceStatusEnum.SERVICESTATUS_ACTIVE + service.name = f"IPoWDM-{serviceId}" + + service_response = self.service_client.CreateService(service) + LOGGER.info("Created TFS IPoWDM service: %s", service_response) + + except Exception as e: + LOGGER.error("Failed to create TFS IPoWDM service: %s", str(e), exc_info=True) + return {'status': 'error', 'message': f'Failed to create TFS service: {str(e)}'}, 500 + + try: + device = Device() + device.device_id.device_uuid.uuid = device_id + + config_rule = ConfigRule() + config_rule.action = ConfigActionEnum.CONFIGACTION_SET + config_rule.custom.resource_key = f'/ipowdm/service/{serviceId}' + + config_rule.custom.resource_value = json.dumps(request_data) + + device.device_config.config_rules.append(config_rule) + self.device_client.ConfigureDevice(device) + LOGGER.info("Configured device %s with IPoWDM service %s", device_id, serviceId) + + except Exception as e: + LOGGER.error("Failed to configure device: %s", str(e)) + return {'status': 'error', 'message': f'Failed to configure device: {str(e)}'}, 500 + + return { + 'status': 'success', + 'message': f'IPoWDM service created for {serviceId}', + 'serviceId': serviceId, + 'device_id': device_id + }, 201 + + def delete(self, serviceId: str): + LOGGER.info("Received DELETE request for IPoWDM service: %s", serviceId) + + data = request.get_json() or {} + device_id = data.get('device_id', 'TFS-PACKET') + + try: + from common.proto.context_pb2 import ServiceId + + service_id = ServiceId() + service_id.service_uuid.uuid = serviceId + service_id.context_id.context_uuid.uuid = "admin" + + self.service_client.DeleteService(service_id) + LOGGER.info("Deleted TFS IPoWDM service: %s", serviceId) + + except Exception as e: + LOGGER.error("Failed to delete TFS IPoWDM service: %s", str(e), exc_info=True) + return {'status': 'error', 'message': f'Failed to delete TFS service: {str(e)}'}, 500 + + if device_id: + try: + device = Device() + device.device_id.device_uuid.uuid = device_id + + config_rule = ConfigRule() + config_rule.action = ConfigActionEnum.CONFIGACTION_DELETE + config_rule.custom.resource_key = f'/ipowdm/service/{serviceId}' + config_rule.custom.resource_value = serviceId + + device.device_config.config_rules.append(config_rule) + self.device_client.ConfigureDevice(device) + LOGGER.info("Deleted IPoWDM service from device %s", device_id) + + except Exception as e: + LOGGER.warning("Failed to delete from device: %s", str(e)) + + headers = { + "Content-Type": "application/json", + "Expect": "" + } + try: + # TODO Dynamic IP address + url = f'http://10.95.86.62/restconf/ipowdm/v1/pluggables/{serviceId}' + requests.delete(url, headers=headers, timeout=10) + LOGGER.info("Deleted pluggables from controller %s: %s", serviceId, url) + + url = f'http://10.95.86.62/restconf/ipowdm/v1/l3nm/{serviceId}' + requests.delete(url, headers=headers, timeout=10) + LOGGER.info("Deleted services from controller %s: %s", serviceId, url) + + except Exception as e: + LOGGER.warning("Failed to delete from controller: %s", str(e)) + return {'status': 'error', 'message': f'Failed to delete from controller: {str(e)}'}, 500 + + return { + 'status': 'success', + 'message': f'IPoWDM service deleted for {serviceId}', + 'serviceId': serviceId + }, 200 + +class PluggablesService(Resource): + def __init__(self): + super().__init__() + self.device_client = DeviceClient() + self.service_client = ServiceClient() + self.context_client = ContextClient() + + def post(self, serviceId: str): + LOGGER.info("Received POST request for Pluggables service: %s", serviceId) + + request_data = request.get_json() + + device_id = request_data.get('device') + + LOGGER.info(f"Service UUID: {serviceId}") + LOGGER.info(f"Device ID: {device_id}") + + try: + service = Service() + service.service_id.service_uuid.uuid = serviceId + service.service_id.context_id.context_uuid.uuid = "admin" + service.service_type = ServiceTypeEnum.SERVICETYPE_IPOWDM + service.service_status.service_status = ServiceStatusEnum.SERVICESTATUS_ACTIVE + service.name = f"Pluggables-{serviceId}" + + service_response = self.service_client.CreateService(service) + LOGGER.info("Created TFS Pluggables service: %s", service_response) + except Exception as e: + LOGGER.error("Failed to create TFS Pluggables service: %s", str(e), exc_info=True) + return {'status': 'error', 'message': f'Failed to create TFS service: {str(e)}'}, 500 + + if device_id: + try: + device = Device() + device.device_id.device_uuid.uuid = device_id + + config_rule = ConfigRule() + config_rule.action = ConfigActionEnum.CONFIGACTION_SET + config_rule.custom.resource_key = f'/ipowdm/pluggables/{serviceId}/{device_id}' + config_rule.custom.resource_value = json.dumps(request_data) + + device.device_config.config_rules.append(config_rule) + self.device_client.ConfigureDevice(device) + LOGGER.info("Configured device %s with Pluggables service %s", device_id, serviceId) + except Exception as e: + LOGGER.error("Failed to configure device: %s", str(e)) + return {'status': 'error', 'message': f'Failed to configure device: {str(e)}'}, 500 + else: + LOGGER.warning("No device_id provided for Pluggables service.") + + return { + 'status': 'success', + 'message': f'Pluggables service created for {serviceId}', + 'serviceId': serviceId, + 'device_id': device_id + }, 201 + + def delete(self, serviceId: str): + LOGGER.info("Received DELETE request for Pluggables service: %s", serviceId) + + try: + context_id = ContextId() + context_id.context_uuid.uuid = "admin" + services = self.context_client.ListServices(context_id) + + services_to_delete = [] + endpoints_data = [] + + for service in services.services: + if serviceId in service.name and "Pluggables-" in service.name: + LOGGER.info("Found matching Pluggables service to delete: %s", service.name) + services_to_delete.append(service.service_id) + + try: + if "-pluggable-" in service.name: + parts = service.name.split("-pluggable-") + device_name = parts[-1] + + service_suffix = service.name.replace("Pluggables-", "", 1) + resource_key_to_find = f'/ipowdm/pluggables/{service_suffix}/{device_name}' + + LOGGER.info("Searching for config rule with key: %s on device: %s", resource_key_to_find, device_name) + from common.proto.context_pb2 import DeviceId + device_id_obj = DeviceId() + device_id_obj.device_uuid.uuid = device_name + + try: + device = self.context_client.GetDevice(device_id_obj) + rule_found = False + for rule in device.device_config.config_rules: + if rule.custom.resource_key == resource_key_to_find: + LOGGER.info("Found Config Rule Payload: %s", rule.custom.resource_value) + rule_found = True + + try: + config_data = json.loads(rule.custom.resource_value) + router_id = config_data.get('device') + router_tp = config_data.get('config', {}).get('name') + if router_id and router_tp: + endpoints_data.append({ + 'router_id': router_id, + 'router_tp': router_tp + }) + except Exception as e: + LOGGER.warning("Failed to parse config rule JSON: %s", str(e)) + break + if not rule_found: + LOGGER.warning("Config rule not found for key: %s", resource_key_to_find) + except Exception as e: + LOGGER.warning("Failed to get device %s to read config rule: %s", device_name, str(e)) + + except Exception as e: + LOGGER.warning("Error while trying to log config rule for service %s: %s", service.name, str(e)) + + if len(endpoints_data) == 2: + endpoints_data.sort(key=lambda x: x['router_id']) + + combined_data = { + 'src_router_id': endpoints_data[0]['router_id'], + 'src_router_tp': endpoints_data[0]['router_tp'], + 'dst_router_id': endpoints_data[1]['router_id'], + 'dst_router_tp': endpoints_data[1]['router_tp'] + } + LOGGER.info("Aggregated Config Rules: %s", json.dumps(combined_data, indent=2)) + + # TODO Dynamic IP address + url = "http://192.168.88.17:9849/api-v0/transponders" + headers = {'Content-Type': 'application/json'} + response = requests.post(url, json=combined_data, headers=headers) + LOGGER.info('Pluggables Service Provisioning Response: %s', str(response.text)) + elif len(endpoints_data) > 0: + LOGGER.warning("Found %d endpoints, expected 2 for aggregation. Data: %s", len(endpoints_data), endpoints_data) + + deleted_count = 0 + for service_id_to_del in services_to_delete: + self.service_client.DeleteService(service_id_to_del) + deleted_count += 1 + + LOGGER.info("Deleted %d matching Pluggables services for UUID: %s", deleted_count, serviceId) + + if deleted_count == 0: + LOGGER.warning("No matching Pluggables services found for UUID: %s", serviceId) + + except Exception as e: + LOGGER.error("Failed to delete TFS Pluggables service: %s", str(e), exc_info=True) + return {'status': 'error', 'message': f'Failed to delete TFS service: {str(e)}'}, 500 + + return { + 'status': 'success', + 'message': f'Pluggables service deleted for {serviceId}', + 'serviceId': serviceId + }, 200 + +class L3NMService(Resource): + def __init__(self): + super().__init__() + self.device_client = DeviceClient() + self.service_client = ServiceClient() + self.context_client = ContextClient() + + def post(self, serviceId: str): + LOGGER.info("Received POST request for L3NM service: %s", serviceId) + + request_data = request.get_json() + + device_ids = set() + try: + l3vpn_svc = request_data.get('ietf-l3vpn-svc:l3vpn-svc', {}) + sites = l3vpn_svc.get('sites', {}).get('site', []) + for site in sites: + site_devices = site.get('devices', {}).get('device', []) + for device in site_devices: + dev_id = device.get('device-id') + site_id = site.get('site-id') + if site_id: + device_ids.add(site_id) + + LOGGER.info("Extracted device IDs from payload: %s", device_ids) + except Exception as e: + LOGGER.warning("Failed to extract device IDs from payload: %s", str(e)) + + LOGGER.info(f"Service UUID: {serviceId}") + LOGGER.info(f"Target Devices: {device_ids}") + + try: + service = Service() + service.service_id.service_uuid.uuid = serviceId + service.service_id.context_id.context_uuid.uuid = "admin" + service.service_type = ServiceTypeEnum.SERVICETYPE_L3NM + service.service_status.service_status = ServiceStatusEnum.SERVICESTATUS_ACTIVE + service.name = f"L3NM-{serviceId}" + + service_response = self.service_client.CreateService(service) + LOGGER.info("Created TFS L3NM service: %s", service_response) + except Exception as e: + LOGGER.error("Failed to create TFS L3NM service: %s", str(e), exc_info=True) + return {'status': 'error', 'message': f'Failed to create TFS service: {str(e)}'}, 500 + + if device_ids: + target_device_id = list(device_ids)[0] + try: + device = Device() + device.device_id.device_uuid.uuid = target_device_id + + config_rule = ConfigRule() + config_rule.action = ConfigActionEnum.CONFIGACTION_SET + config_rule.custom.resource_key = f'/ipowdm/l3nm/{serviceId}/{target_device_id}' + config_rule.custom.resource_value = json.dumps(request_data) + + device.device_config.config_rules.append(config_rule) + self.device_client.ConfigureDevice(device) + LOGGER.info("Configured device %s with L3NM service %s", target_device_id, serviceId) + except Exception as e: + LOGGER.error("Failed to configure device %s: %s", target_device_id, str(e)) + return {'status': 'error', 'message': f'Failed to configure device: {str(e)}'}, 500 + else: + LOGGER.warning("No devices identified for L3NM service configuration.") + + return { + 'status': 'success', + 'message': f'L3NM service created for {serviceId}', + 'serviceId': serviceId, + 'device_ids': list(device_ids) + }, 201 + + def delete(self, serviceId: str): + LOGGER.info("Received DELETE request for L3NM service: %s", serviceId) + + try: + context_id = ContextId() + context_id.context_uuid.uuid = "admin" + services = self.context_client.ListServices(context_id) + + deleted_count = 0 + for service in services.services: + if serviceId in service.name and "L3NM-" in service.name: + LOGGER.info("Found matching L3NM service to delete: %s", service.name) + self.service_client.DeleteService(service.service_id) + deleted_count += 1 + + LOGGER.info("Deleted %d matching L3NM services for UUID: %s", deleted_count, serviceId) + + if deleted_count == 0: + LOGGER.warning("No matching L3NM services found for UUID: %s", serviceId) + + except Exception as e: + LOGGER.error("Failed to delete TFS L3NM service: %s", str(e), exc_info=True) + return {'status': 'error', 'message': f'Failed to delete TFS service: {str(e)}'}, 500 + + return { + 'status': 'success', + 'message': f'L3NM service deleted for {serviceId}', + 'serviceId': serviceId + }, 200 diff --git a/src/nbi/service/ipowdm/__init__.py b/src/nbi/service/ipowdm/__init__.py new file mode 100644 index 000000000..39cadfb14 --- /dev/null +++ b/src/nbi/service/ipowdm/__init__.py @@ -0,0 +1,39 @@ +# 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. + +import logging +from .Resources import IPoWDMService, PluggablesService, L3NMService + +LOGGER = logging.getLogger(__name__) + +URL_PREFIX = '/restconf/ipowdm/v1' + +def register_ipowdm(nbi_app): + LOGGER.info('Registering IPoWDM Service NBI') + nbi_app.add_rest_api_resource( + IPoWDMService, + f'{URL_PREFIX}/service/', + endpoint='ipowdm_service' + ) + nbi_app.add_rest_api_resource( + PluggablesService, + f'{URL_PREFIX}/pluggables/', + endpoint='ipowdm_pluggables' + ) + nbi_app.add_rest_api_resource( + L3NMService, + f'{URL_PREFIX}/l3nm/', + endpoint='ipowdm_l3nm' + ) + LOGGER.info('IPoWDM Service NBI registered') -- GitLab From 749e9201a83e739167f96b7149c8b42d223c7fcc Mon Sep 17 00:00:00 2001 From: armingol Date: Mon, 1 Jun 2026 08:22:30 +0000 Subject: [PATCH 2/4] Clean emulate driver --- .../drivers/emulated/EmulatedDriver.py | 112 +++++------------- src/device/service/drivers/emulated/Tools.py | 27 +---- 2 files changed, 31 insertions(+), 108 deletions(-) diff --git a/src/device/service/drivers/emulated/EmulatedDriver.py b/src/device/service/drivers/emulated/EmulatedDriver.py index e35e234e5..650eeb68d 100644 --- a/src/device/service/drivers/emulated/EmulatedDriver.py +++ b/src/device/service/drivers/emulated/EmulatedDriver.py @@ -25,8 +25,7 @@ from device.service.driver_api._Driver import _Driver from device.service.driver_api.AnyTreeTools import TreeNode, dump_subtree, get_subnode, set_subnode_value from .Constants import SPECIAL_RESOURCE_MAPPINGS from .SyntheticSamplingParameters import SyntheticSamplingParameters, do_sampling -from .Tools import compose_resource_endpoint,connect_to_xr_agent -import requests +from .Tools import compose_resource_endpoint LOGGER = logging.getLogger(__name__) @@ -39,7 +38,6 @@ class EmulatedDriver(_Driver): def __init__(self, address : str, port : int, **settings) -> None: super().__init__(DRIVER_NAME, address, port, **settings) self.__lock = threading.Lock() - self.__address = address self.__initial = TreeNode('.') self.__running = TreeNode('.') self.__subscriptions = TreeNode('.') @@ -119,85 +117,35 @@ class EmulatedDriver(_Driver): chk_type('resources', resources, list) if len(resources) == 0: return [] results = [] - if 'ipowdm_ruleset' in str(resources): - connect_to_xr_agent(resources) - results.append(True) - else: - resolver = anytree.Resolver(pathattr='name') - with self.__lock: - for i,resource in enumerate(resources): - str_resource_name = 'resources[#{:d}]'.format(i) - try: - chk_type(str_resource_name, resource, (list, tuple)) - chk_length(str_resource_name, resource, min_length=2, max_length=2) - resource_key,resource_value = resource - chk_string(str_resource_name, resource_key, allow_empty=False) - resource_path = resource_key.split('/') - except Exception as e: # pylint: disable=broad-except - LOGGER.exception('Exception validating {:s}: {:s}'.format(str_resource_name, str(resource_key))) - results.append(e) # if validation fails, store the exception - continue - try: - resource_value = json.loads(resource_value) - except: # pylint: disable=bare-except - pass - - if resource_key.startswith('/ipowdm/service/'): - LOGGER.info('[%s] Legacy IPoWDM Service Provisioning: Payload=%s', self.__address, str(resource_value)) - elif resource_key.startswith('/ipowdm/l3nm/'): - LOGGER.info('[%s] L3NM Service Provisioning: Payload=%s', self.__address, str(resource_value)) - elif resource_key.startswith('/ipowdm/pluggables/'): - LOGGER.info('[%s] Pluggables Service Provisioning: Payload=%s', self.__address, str(resource_value)) - try: - key_parts = resource_key.split('/') - if len(key_parts) >= 5: - service_uuid_raw = key_parts[3] - # Normalize UUID: if serviceId contains '-pluggable-', take the part before it - if "-pluggable-" in service_uuid_raw: - service_uuid = service_uuid_raw.split("-pluggable-")[0] - else: - service_uuid = service_uuid_raw - - if not hasattr(EmulatedDriver, 'pluggables_pending'): - EmulatedDriver.pluggables_pending = {} - - if service_uuid in EmulatedDriver.pluggables_pending: - stored_payload = EmulatedDriver.pluggables_pending.pop(service_uuid) - current_payload = resource_value - - def format_entry(payload): - config = payload.get('config', {}) - return { - 'uuid': payload.get('device'), - 'power': config.get('target-output-power', 0.0), - 'frequency': config.get('frequency', 0.0) - } - combined_data = [ - {'src': format_entry(stored_payload)}, - {'dst': format_entry(current_payload)} - ] - LOGGER.info('[%s] Pluggables Service Provisioning Aggregated: %s', self.__address, json.dumps(combined_data, indent=2)) - # TODO Dynamic IP - url = "http://192.168.88.17:9849/api-v0/transponders" - headers = {'Content-Type': 'application/json'} - response = requests.post(url, json=combined_data, headers=headers) - LOGGER.info('[%s] Pluggables Service Provisioning Response: %s', self.__address, str(response.text)) - else: - EmulatedDriver.pluggables_pending[service_uuid] = resource_value - LOGGER.debug('[%s] Pluggables Service Partial Provisioning stored for %s', self.__address, service_uuid) - except Exception as e: - LOGGER.warning("Error processing Pluggables aggregation: %s", str(e)) - LOGGER.info('[%s] Pluggables Service Provisioning: Payload=%s', self.__address, str(resource_value)) - - set_subnode_value(resolver, self.__running, resource_path, resource_value) - - match = RE_GET_ENDPOINT_FROM_INTERFACE.match(resource_key) - if match is not None: - endpoint_uuid = match.group(1) - if '.' in endpoint_uuid: endpoint_uuid = endpoint_uuid.split('.')[0] - self.__synthetic_sampling_parameters.set_endpoint_configured(endpoint_uuid) - - results.append(True) + resolver = anytree.Resolver(pathattr='name') + with self.__lock: + for i,resource in enumerate(resources): + str_resource_name = 'resources[#{:d}]'.format(i) + try: + chk_type(str_resource_name, resource, (list, tuple)) + chk_length(str_resource_name, resource, min_length=2, max_length=2) + resource_key,resource_value = resource + chk_string(str_resource_name, resource_key, allow_empty=False) + resource_path = resource_key.split('/') + except Exception as e: # pylint: disable=broad-except + LOGGER.exception('Exception validating {:s}: {:s}'.format(str_resource_name, str(resource_key))) + results.append(e) # if validation fails, store the exception + continue + + try: + resource_value = json.loads(resource_value) + except: # pylint: disable=bare-except + pass + + set_subnode_value(resolver, self.__running, resource_path, resource_value) + + match = RE_GET_ENDPOINT_FROM_INTERFACE.match(resource_key) + if match is not None: + endpoint_uuid = match.group(1) + if '.' in endpoint_uuid: endpoint_uuid = endpoint_uuid.split('.')[0] + self.__synthetic_sampling_parameters.set_endpoint_configured(endpoint_uuid) + + results.append(True) return results @metered_subclass_method(METRICS_POOL) diff --git a/src/device/service/drivers/emulated/Tools.py b/src/device/service/drivers/emulated/Tools.py index 89f7724f9..697a5995a 100644 --- a/src/device/service/drivers/emulated/Tools.py +++ b/src/device/service/drivers/emulated/Tools.py @@ -12,14 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. -import json import logging -import requests +from typing import Any, Dict, Optional, Tuple from common.proto.kpi_sample_types_pb2 import KpiSampleType from common.type_checkers.Checkers import chk_attribute, chk_string, chk_type from device.service.driver_api._Driver import RESOURCE_ENDPOINTS from .Constants import SPECIAL_RESOURCE_MAPPINGS -from typing import Any, Dict, Optional, Tuple LOGGER = logging.getLogger(__name__) @@ -109,26 +107,3 @@ def compose_resource_endpoint(endpoint_data : Dict[str, Any]) -> Optional[Tuple[ except: # pylint: disable=bare-except LOGGER.exception('Problem composing endpoint({:s})'.format(str(endpoint_data))) return None - -# TODO Dynamic IP -def connect_to_xr_agent(resources): - rule_set = resources[0][1]['rule_set'] - nodes = [ - {'src': { - 'uuid': rule_set['src'][0]['uuid'], - 'power': rule_set['src'][0]['power'], - 'frequency': rule_set['src'][0]['frequency'] - }}, - {'dst': { - 'uuid': rule_set['dst'][0]['uuid'], - 'power': rule_set['dst'][0]['power'], - 'frequency': rule_set['dst'][0]['frequency'] - }} - ] - url = "http://192.168.88.17:9849/api-v0/transponders" - headers = { - "Content-Type": "application/json", - "Expect": "" - } - json_data = json.dumps(nodes) - requests.post(url, data=json_data, headers=headers, timeout=10) -- GitLab From 591c987a8cf8cb246b8709a6be1cb4b34dbd6929 Mon Sep 17 00:00:00 2001 From: Pablo Armingol Date: Mon, 1 Jun 2026 08:23:59 +0000 Subject: [PATCH 3/4] Edit EmulatedDriver.py --- src/device/service/drivers/emulated/EmulatedDriver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/device/service/drivers/emulated/EmulatedDriver.py b/src/device/service/drivers/emulated/EmulatedDriver.py index b88ae6b9d..7e26056cf 100644 --- a/src/device/service/drivers/emulated/EmulatedDriver.py +++ b/src/device/service/drivers/emulated/EmulatedDriver.py @@ -117,7 +117,7 @@ class EmulatedDriver(_Driver): chk_type('resources', resources, list) if len(resources) == 0: return [] results = [] - resolver = anytree.Resolver(pathattr='name') + resolver = anytree.Resolver(pathattr='name') with self.__lock: for i,resource in enumerate(resources): str_resource_name = 'resources[#{:d}]'.format(i) -- GitLab From e1a4e6ae72ceba46ce03af5b11266b7e2e8b4a5e Mon Sep 17 00:00:00 2001 From: armingol Date: Mon, 1 Jun 2026 08:39:15 +0000 Subject: [PATCH 4/4] Clean L3IETf driver --- .../drivers/ietf_l3vpn/IetfL3VpnDriver.py | 8 +- .../drivers/ietf_l3vpn/templates/ipowdm.json | 89 +-- .../drivers/ietf_l3vpn/templates/tools.py | 571 ++++++++---------- 3 files changed, 308 insertions(+), 360 deletions(-) diff --git a/src/device/service/drivers/ietf_l3vpn/IetfL3VpnDriver.py b/src/device/service/drivers/ietf_l3vpn/IetfL3VpnDriver.py index a35f03804..cc78bbbd3 100644 --- a/src/device/service/drivers/ietf_l3vpn/IetfL3VpnDriver.py +++ b/src/device/service/drivers/ietf_l3vpn/IetfL3VpnDriver.py @@ -191,16 +191,10 @@ class IetfL3VpnDriver(_Driver): if len(resources) == 0: return results with self.__lock: if 'ipowdm' in str(resources): - scheme = self.settings.get('scheme', 'http') - controller_url = f"{scheme}://{self.address}:{self.port}" - LOGGER.info('=' * 80) - LOGGER.info('IPoWDM SERVICE RECEIVED') - LOGGER.info('=' * 80) - for resource in resources: if 'ipowdm' in str(resource): try: - create_request(resource, controller_url) + create_request(resource) LOGGER.info('Request created successfully') results.append((resource, True)) except Exception as e: diff --git a/src/device/service/drivers/ietf_l3vpn/templates/ipowdm.json b/src/device/service/drivers/ietf_l3vpn/templates/ipowdm.json index ac22e80c6..3063397f9 100644 --- a/src/device/service/drivers/ietf_l3vpn/templates/ipowdm.json +++ b/src/device/service/drivers/ietf_l3vpn/templates/ipowdm.json @@ -2,48 +2,65 @@ "services": [ { "service_id": { - "context_id": {"context_uuid": {"uuid": "admin"}}, - "service_uuid": {"uuid": "644c4aa6-c2e2-4db0-9d6e-869522c4141c"} + "context_id": { + "context_uuid": { + "uuid": "admin" + } + }, + "service_uuid": { + "uuid": "IPoWDM" + } }, "service_type": 12, - "service_status": {"service_status": 1}, + "service_status": { + "service_status": 1 + }, "service_endpoint_ids": [ - {"device_id": {"device_uuid": {"uuid": "Phoenix1"}},"endpoint_uuid": {"uuid": "PORT-xe4"}}, - {"device_id": {"device_uuid": {"uuid": "Phoenix2"}},"endpoint_uuid": {"uuid": "PORT-xe4"}} + { + "device_id": { + "device_uuid": { + "uuid": "IP1" + } + }, + "endpoint_uuid": { + "uuid": "PORT-xe4" + } + }, + { + "device_id": { + "device_uuid": { + "uuid": "IP2" + } + }, + "endpoint_uuid": { + "uuid": "PORT-xe4" + } + } ], "service_constraints": [], - "service_config": {"config_rules": [ - {"action": 1, "ipowdm": { - "endpoint_id": { - "device_id": {"device_uuid": {"uuid": "Phoenix1"}}, - "endpoint_uuid": {"uuid": "PORT-xe4"} - }, - "rule_set": { - "src": [ - { - "uuid": "Phoenix-1", - "ip_address": "10.10.1.1", - "ip_mask": "/24", - "vlan_id": 100, - "power": 0.0, - "frequency": 194700.0 - } - ], - "dst": [ - { - "uuid": "Phoenix-2", - "ip_address": "10.10.2.1", - "ip_mask": "/24", - "vlan_id": 100, - "power": 0.0, - "frequency": 194700.0 + "service_config": { + "config_rules": [ + { + "action": 1, + "ipowdm": { + "endpoint_id": { + "device_id": { + "device_uuid": { + "uuid": "IP1" } - ], - "bw": 100, - "uuid": "644c4aa6-c2e2-4db0-9d6e-869522c4141c" + }, + "endpoint_uuid": { + "uuid": "PORT-xe4" + } + }, + "rule_set": { + "src": [], + "dst": [] } - }} - ]} + } + } + ] + } } ] -} +} \ No newline at end of file diff --git a/src/device/service/drivers/ietf_l3vpn/templates/tools.py b/src/device/service/drivers/ietf_l3vpn/templates/tools.py index d64a8aa6a..ec9d6d310 100644 --- a/src/device/service/drivers/ietf_l3vpn/templates/tools.py +++ b/src/device/service/drivers/ietf_l3vpn/templates/tools.py @@ -16,14 +16,10 @@ import json import logging import os import requests -import uuid -from typing import Dict, List, Optional, Tuple from concurrent.futures import ThreadPoolExecutor LOGGER = logging.getLogger(__name__) -DEFAULT_CONTROLLER_URL = 'http://127.0.0.1:80' - HEADERS = { "Accept": "application/yang-data+json", "Content-Type": "application/yang-data+json" @@ -31,91 +27,42 @@ HEADERS = { executor = ThreadPoolExecutor() -def generate_l3vpn_site(endpoint: Dict, vpn_id: str, role: str = "spoke-role") -> Dict: - """ - Generate a single L3VPN site configuration from an endpoint. - - Args: - endpoint: Dict with uuid, ip_address, ip_mask, vlan_id - vpn_id: VPN identifier - role: "spoke-role" or "hub-role" - - Returns: - Dict with L3VPN site configuration - """ - return { - "site-id": endpoint["uuid"], - "management": {"type": "ietf-l3vpn-svc:provider-managed"}, - "locations": {"location": [{"location-id": f"location-{endpoint['uuid']}"}]}, - "devices": {"device": [{ - "device-id": endpoint.get("ip_address", "127.0.0.1"), - "location": f"location-{endpoint['uuid']}" - }]}, - "routing-protocols": {"routing-protocol": [{ - "type": "ietf-l3vpn-svc:static", - "static": { - "cascaded-lan-prefixes": { - "ipv4-lan-prefixes": [{ - "lan": f"{endpoint['ip_address']}{endpoint['ip_mask']}", - "lan-tag": f"vlan{endpoint['vlan_id']}", - "next-hop": endpoint.get("gateway", endpoint['ip_address']) - }] - } +site_template = { + "site-id": "", + "devices": { + "device": [ + { + "device-id": "", + "location": "" } - }]}, - "site-network-accesses": { - "site-network-access": [{ - "site-network-access-id": str(endpoint['vlan_id']), - "site-network-access-type": "ietf-l3vpn-svc:multipoint", - "device-reference": endpoint.get("ip_address", "127.0.0.1"), - "vpn-attachment": { - "vpn-id": vpn_id, - "site-role": f"ietf-l3vpn-svc:{role}" - }, + ] + }, + "site-network-accesses": { + "site-network-access": [ + { + "site-network-access-id": "", + "device-reference": "", "ip-connection": { "ipv4": { "address-allocation-type": "ietf-l3vpn-svc:static-address", "addresses": { - "provider-address": endpoint.get("provider_ip", endpoint['ip_address']), - "customer-address": endpoint['ip_address'], - "prefix-length": int(endpoint['ip_mask'].replace('/', '')) if isinstance(endpoint['ip_mask'], str) else endpoint['ip_mask'] + "provider-address": "", + "customer-address": "", + "prefix-length": "" } } }, - "service": { - "svc-mtu": 1500, - "svc-input-bandwidth": 1000000000, - "svc-output-bandwidth": 1000000000, - "qos": { - "qos-profile": { - "classes": { - "class": [{ - "class-id": "qos-realtime", - "direction": "ietf-l3vpn-svc:both", - "latency": {"latency-boundary": 10}, - "bandwidth": {"guaranteed-bw-percent": 100} - }] - } - } - } - } - }] - } + "vpn-attachment": { + "vpn-id": "vpn-p2mp" + }, + "site-network-access-type": "ietf-l3vpn-svc:multipoint" + } + ] } +} +def generate_l3vpn_template_pair(src, dst, vpn_id): -def generate_l3vpn_service(src: Dict, dst: Dict, vpn_id: str) -> Dict: - """ - Generate a complete L3VPN service with source and destination sites. - - Args: - src: Source endpoint configuration - dst: Destination endpoint configuration - vpn_id: VPN service identifier - - Returns: - Dict with complete L3VPN service configuration - """ return { "ietf-l3vpn-svc:l3vpn-svc": { "vpn-services": { @@ -123,257 +70,247 @@ def generate_l3vpn_service(src: Dict, dst: Dict, vpn_id: str) -> Dict: }, "sites": { "site": [ - generate_l3vpn_site(src, vpn_id, "spoke-role"), - generate_l3vpn_site(dst, vpn_id, "hub-role") + { + "site-id": src["uuid"], + "management": {"type": "ietf-l3vpn-svc:provider-managed"}, + "locations": {"location": [{"location-id": f"location-{src['uuid']}"}]}, + "devices": {"device": [{ + "device-id": "10.0.30.1", + "location": f"location-{src['uuid']}" + }]}, + "routing-protocols": {"routing-protocol": [{ + "type": "ietf-l3vpn-svc:static", + "static": { + "cascaded-lan-prefixes": { + "ipv4-lan-prefixes": [ + { + "lan": "128.32.10.1/24", + "lan-tag": f"vlan{src['vlan_id']}", + "next-hop": "10.0.30.10" + } + ] + } + } + }]}, + "site-network-accesses": { + "site-network-access": [{ + "site-network-access-id": f"{src['vlan_id']}", + "site-network-access-type": "ietf-l3vpn-svc:multipoint", + "device-reference": "10.0.30.1", + "vpn-attachment": { + "vpn-id": vpn_id, "site-role": "ietf-l3vpn-svc:spoke-role" + }, + "ip-connection": { + "ipv4": { + "address-allocation-type": "ietf-l3vpn-svc:static-address", + "addresses": { + "provider-address": "10.0.30.254", + "customer-address": "10.0.30.10", + "prefix-length": 24 + } + } + }, + "routing-protocols": {"routing-protocol": [{ + "type": "ietf-l3vpn-svc:static", + "static": { + "cascaded-lan-prefixes": { + "ipv4-lan-prefixes": [ + { + "lan": "172.1.101.1/24", + "lan-tag": "vlan100", + "next-hop": "10.0.30.254" + } + ] + } + } + }]}, + "service": { + "svc-mtu": 1500, + "svc-input-bandwidth": 1000000000, + "svc-output-bandwidth": 1000000000, + "qos": { + "qos-profile": { + "classes": { + "class": [ + { + "class-id": "qos-realtime", + "direction": "ietf-l3vpn-svc:both", + "latency": { + "latency-boundary": 10 + }, + "bandwidth": { + "guaranteed-bw-percent": 100 + } + } + ] + } + } + } + } + }] + } + }, + { + "site-id": dst["uuid"], + "management": {"type": "ietf-l3vpn-svc:provider-managed"}, + "locations": {"location": [{"location-id": f"location-{dst['uuid']}"}]}, + "devices": {"device": [{ + "device-id": "10.0.20.1", + "location": f"location-{dst['uuid']}" + }]}, + "routing-protocols": {"routing-protocol": [{ + "type": "ietf-l3vpn-svc:static", + "static": { + "cascaded-lan-prefixes": { + "ipv4-lan-prefixes": [ + { + "lan": "172.1.101.1/24", + "lan-tag": "vlan200", + "next-hop": "172.10.33.2" + } + ] + } + } + }]}, + "site-network-accesses": { + "site-network-access": [{ + "site-network-access-id": f"{dst['vlan_id']}", + "site-network-access-type": "ietf-l3vpn-svc:multipoint", + "device-reference": "10.0.20.1", + "vpn-attachment": { + "vpn-id": vpn_id, "site-role": "ietf-l3vpn-svc:hub-role" + }, + "ip-connection": { + "ipv4": { + "address-allocation-type": "ietf-l3vpn-svc:static-address", + "addresses": { + "provider-address": "172.10.33.254", + "customer-address": "172.10.33.2", + "prefix-length": 24 + } + } + }, + "routing-protocols": {"routing-protocol": [{ + "type": "ietf-l3vpn-svc:static", + "static": { + "cascaded-lan-prefixes": { + "ipv4-lan-prefixes": [ + { + "lan": "128.32.10.1/24", + "lan-tag": "vlan200", + "next-hop": "172.10.33.254" + } + ] + } + } + }]}, + "service": { + "svc-mtu": 1500, + "svc-input-bandwidth": 1000000000, + "svc-output-bandwidth": 1000000000, + "qos": { + "qos-profile": { + "classes": { + "class": [ + { + "class-id": "qos-realtime", + "direction": "ietf-l3vpn-svc:both", + "latency": { + "latency-boundary": 10 + }, + "bandwidth": { + "guaranteed-bw-percent": 100 + } + } + ] + } + } + } + } + }] + } + } ] } } } - -def send_l3vpn_service(service_config: Dict, controller_url: str, service_uuid: str) -> requests.Response: +def create_request(resource_value): + """ Create and send HTTP request based on a JSON template and provided resource value. + The JSON template is expected to be in the same directory as this script, named 'ipowdm.json'. + Example resource_value: + {"rule_set": { + "uuid": "unique-service-uuid", + "bw": 100, + "src": [{"uuid": "src-device-uuid", "ip_address": "192.168.1.1", "ip_mask": "24", "vlan_id": 100, "power": 10, "frequency": 193100}], + "dst": [{"uuid": "dst-device-uuid", "ip_address": "192.168.3.3", "ip_mask": "24", "vlan_id": 100, "power": 10, "frequency": 193100}] + }} + The src and dst fields are lists to accommodate future extensions for multi-endpoint scenarios. + The request is sent to a predefined URL with appropriate headers. + Returns a response-like object with status_code and text attributes. + In case of error, returns a SimpleNamespace with status_code 500 and the error message in text. + + Note: The actual HTTP request sending is currently mocked for testing purposes. + The URL and headers are hardcoded for demonstration and should be adapted as needed. """ - Send L3VPN/L3NM service configuration to the NBI. - - Args: - service_config: L3VPN service configuration dict - controller_url: Controller URL (required) - service_uuid: Service UUID for the endpoint - Returns: - requests.Response object or status code - """ - url = f"{controller_url}/restconf/ipowdm/v1/l3nm/{service_uuid}" + LOGGER.info("Creating request for resource_value: %s", resource_value) + + node_src = resource_value[1]['rule_set']['src'][0] + src = [{ + 'uuid': node_src["uuid"], + 'ip_address': node_src["ip_address"], + 'ip_mask': node_src["ip_mask"], + 'vlan_id': node_src["vlan_id"] + }] + dst_list = resource_value[1]['rule_set']['dst'] + dsts = [] + for node in dst_list: + dsts.append({ + 'uuid': node["uuid"], + 'ip_address': node["ip_address"], + 'ip_mask': node["ip_mask"], + 'vlan_id': node["vlan_id"] + }) + + sites_input = src + dsts + + components = resource_value[1]['rule_set']['transceiver']['components'] + for i, device in enumerate(components): + name = sites_input[i]['uuid'] + + if name == "T2.1":device["frequency"]= 195000000 + if name == "T1.1":device["frequency"]= 195006250 + if name == "T1.2":device["frequency"]= 195018750 + if name == "T1.3":device["frequency"]= 195031250 + + LOGGER.debug(f"NODE TO CONFIGURE: \n{name}: {json.dumps(device, indent=2)}") + response = patch_optical_channel_frequency(device, name) + LOGGER.debug(f"RESPONSE :\n {response}") + templates = [] + for dst in dsts: + vpn = "L3VPN_"+src[0]['uuid']+"_"+dst['uuid'] + templates.append(generate_l3vpn_template_pair(src[0], dst,vpn)) + + url = "http://192.168.202.254:80/restconf/data/ietf-l3vpn-svc:l3vpn-svc/vpn-services" headers = { 'accept': 'application/json', 'Content-Type': 'application/json' } - LOGGER.info("Sending L3NM service to %s", url) - LOGGER.debug("L3NM payload: %s", json.dumps(service_config, indent=2)) - - try: - response = requests.post(url=url, headers=headers, json=service_config, timeout=30) - LOGGER.info("L3NM response: %s - %s", response.status_code, response.text) - return response - except Exception as e: - LOGGER.error("Failed to send L3NM service: %s", str(e)) - raise - -def generate_pluggable_config(component: Dict, device_id: str) -> Dict: - """ - Generate pluggable/transceiver configuration from a component. - - Args: - component: Dict with name, frequency, target_output_power, operational_mode, operation - device_id: Device identifier - - Returns: - Dict with pluggable configuration in format {device, config} - """ - return { - "device": device_id, - "config": { - "frequency": component.get("frequency", 193100000), - "target-output-power": component.get("target_output_power", 0.0), - "operational-mode": component.get("operational_mode", 0), - "name": component.get("name", ""), - "operation": component.get("operation", "activate") - } - } - - -def configure_pluggable(component: Dict, device_id: str, controller_url: str, service_uuid: str): - """ - Configure a pluggable/transceiver via NBI. - - Args: - component: Component configuration dict - device_id: Device identifier - controller_url: Controller URL (required) - service_uuid: Service UUID for the endpoint - - Returns: - Response object or status code - """ - url = f"{controller_url}/restconf/ipowdm/v1/pluggables/{service_uuid}" - - config = generate_pluggable_config(component, device_id) - - LOGGER.info("Configuring pluggable for service %s on device %s", service_uuid, device_id) - LOGGER.debug("Pluggable config: %s", json.dumps(config, indent=2)) - - try: - response = requests.post(url, json=config, headers=HEADERS, timeout=30) - LOGGER.info("Pluggable response: %s - %s", response.status_code, response.text) - return response - except Exception as e: - LOGGER.error("Failed to configure pluggable on %s: %s", device_id, str(e)) - return None - - -def configure_all_pluggables(components: List[Dict], device_ids: List[str], controller_url: str = None) -> List[Optional[requests.Response]]: - """ - Configure all pluggables/transceivers in parallel. - - Args: - components: List of component configurations - device_ids: List of device identifiers (same order as components) - controller_url: Optional controller URL override - - Returns: - List of response objects - """ - responses = [] - for component, device_id in zip(components, device_ids): - response = configure_pluggable(component, device_id, controller_url) - responses.append(response) - return responses - -def parse_ipowdm_data(resource_value) -> Tuple[List[Dict], List[Dict], List[Dict], str]: - """ - Parse IPoWDM resource data into structured components. - - Args: - resource_value: Raw resource value (tuple or dict) - - Returns: - Tuple of (src_endpoints, dst_endpoints, transceiver_components, service_uuid) - """ - if isinstance(resource_value, tuple): - data = resource_value[1] if isinstance(resource_value[1], dict) else json.loads(resource_value[1]) - elif isinstance(resource_value, dict): - data = resource_value - else: - data = json.loads(resource_value) - - if 'rule_set' in data: - data = data['rule_set'] - - src_endpoints = data.get('src', []) - dst_endpoints = data.get('dst', []) - components = data.get('transceiver', {}).get('components', []) - service_uuid = data.get('uuid', 'unknown') - - return src_endpoints, dst_endpoints, components, service_uuid - - -def process_ipowdm_service(resource_value, controller_url: str) -> Dict: - """ - Process an IPoWDM service request, generating L3NM services and configuring pluggables. - - This is the main entry point for IPoWDM processing. It: - 1. Parses the incoming IPoWDM data - 2. Configures all pluggables/transceivers - 3. Generates and sends L3VPN services for each src-dst pair - - Args: - resource_value: IPoWDM resource data - controller_url: Controller URL for L3VPN and pluggable configuration - - Returns: - Dict with processing results - """ - LOGGER.info("Processing IPoWDM service request") - - src_endpoints, dst_endpoints, components, service_uuid = parse_ipowdm_data(resource_value) - - LOGGER.info("IPoWDM Service UUID: %s", service_uuid) - LOGGER.info("Source endpoints: %d, Destination endpoints: %d, Components: %d", - len(src_endpoints), len(dst_endpoints), len(components)) - - results = { - "service_uuid": service_uuid, - "pluggable_results": [], - "l3vpn_results": [] - } - - if components: - all_endpoints = src_endpoints + dst_endpoints - device_ids = [ep.get('uuid', f'device-{i}') for i, ep in enumerate(all_endpoints)] - - LOGGER.info("Configuring %d pluggables", len(components)) - for i, (component, device_id) in enumerate(zip(components, device_ids)): - LOGGER.debug("Configuring pluggable %d on device %s: %s", i, device_id, component) - try: - pluggable_uuid = f"{service_uuid}-pluggable-{device_id}" - response = configure_pluggable(component, device_id, controller_url, pluggable_uuid) - status_code = response if isinstance(response, int) else (response.status_code if response else 0) - results["pluggable_results"].append({ - "device_id": device_id, - "status": status_code, - "success": status_code in [200, 201, 204] - }) - except Exception as e: - LOGGER.error("Pluggable configuration failed for %s: %s", device_id, str(e)) - results["pluggable_results"].append({ - "device_id": device_id, - "status": "error", - "success": False, - "error": str(e) - }) - - for src in src_endpoints: - for dst in dst_endpoints: - vpn_id = f"L3VPN_{src['uuid']}_{dst['uuid']}" - LOGGER.info("Creating L3VPN service: %s", vpn_id) + for template in templates: + LOGGER.info("Generated L3VPN P2MP service JSON:\n%s", json.dumps(template, indent=2)) - try: - service_config = generate_l3vpn_service(src, dst, vpn_id) - l3nm_uuid = f"{service_uuid}-l3nm-{src['uuid']}-{dst['uuid']}" - response = send_l3vpn_service(service_config, controller_url, l3nm_uuid) - status_code = response if isinstance(response, int) else (response.status_code if response else 0) - results["l3vpn_results"].append({ - "vpn_id": vpn_id, - "src": src['uuid'], - "dst": dst['uuid'], - "status": status_code, - "success": status_code in [200, 201, 204] - }) - except Exception as e: - LOGGER.error("L3VPN creation failed for %s: %s", vpn_id, str(e)) - results["l3vpn_results"].append({ - "vpn_id": vpn_id, - "src": src['uuid'], - "dst": dst['uuid'], - "status": "error", - "success": False, - "error": str(e) - }) + response = requests.post(url = url, headers= headers, json=template) + LOGGER.debug(response) + return None - LOGGER.info("IPoWDM processing complete. Results: %s", json.dumps(results, indent=2)) - return results - -def create_request(resource_value, controller_url: str = None): - """ - Legacy entry point for IPoWDM processing. - Maintained for backward compatibility with existing driver code. +def patch_optical_channel_frequency(data, DEVICE_ID): + encoded_path = f"http://192.168.202.254:80/restconf/data/device={DEVICE_ID}/openconfig-platform:components/component=channel-1/optical-channel/config" - Args: - resource_value: IPoWDM resource data - controller_url: Controller URL (IP:port of the L3VPN controller device) - - Returns: - Processing results dict - """ - LOGGER.info("create_request called") - LOGGER.info("Resource value: %s", resource_value) - LOGGER.info("Controller URL: %s", controller_url) - - url = controller_url or DEFAULT_CONTROLLER_URL - return process_ipowdm_service(resource_value, url) - -def generate_l3vpn_template_pair(src: Dict, dst: Dict, vpn_id: str) -> Dict: - """ - Legacy function for generating L3VPN template. - Wrapper around generate_l3vpn_service for backward compatibility. - """ - return generate_l3vpn_service(src, dst, vpn_id) - - -def patch_optical_channel_frequency(data: Dict, device_id: str) -> Optional[requests.Response]: - """ - Legacy function for patching optical channel frequency. - Wrapper around configure_pluggable for backward compatibility. - """ - return configure_pluggable(data, device_id) + patch_data = data + response = requests.patch(f"{encoded_path}", + json=patch_data, + headers=HEADERS) + assert response.status_code == 200 + return response -- GitLab