diff --git a/src/device/service/drivers/ietf_l3vpn/templates/ipowdm.json b/src/device/service/drivers/ietf_l3vpn/templates/ipowdm.json index db700293f442d8b10b16318ec8d388d818d2e61e..3063397f943fcf1c837f5b8d071489cddff3c815 100644 --- a/src/device/service/drivers/ietf_l3vpn/templates/ipowdm.json +++ b/src/device/service/drivers/ietf_l3vpn/templates/ipowdm.json @@ -2,28 +2,65 @@ "services": [ { "service_id": { - "context_id": {"context_uuid": {"uuid": "admin"}}, - "service_uuid": {"uuid": "IPoWDM"} + "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": "IP1"}},"endpoint_uuid": {"uuid": "PORT-xe4"}}, - {"device_id": {"device_uuid": {"uuid": "IP2"}},"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": "IP1"}}, - "endpoint_uuid": {"uuid": "PORT-xe4"} - }, - "rule_set": { - "src" : [], - "dst" : [] + "service_config": { + "config_rules": [ + { + "action": 1, + "ipowdm": { + "endpoint_id": { + "device_id": { + "device_uuid": { + "uuid": "IP1" + } + }, + "endpoint_uuid": { + "uuid": "PORT-xe4" + } + }, + "rule_set": { + "src": [], + "dst": [] + } + } } - }} - ]} + ] + } } ] -} +} \ No newline at end of file diff --git a/src/nbi/service/app.py b/src/nbi/service/app.py index b8c5b73d7b2dfacb2482e55f32c368dffef1f8df..b5b83a61091e0e777df90bad2050d2250dd95d76 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 .optical_slice import register_optical_slice from .osm_nbi import register_osm_api from .qkd_app import register_qkd_app @@ -107,6 +108,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_media_channel (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 0000000000000000000000000000000000000000..dbe1dd3ccb5ca60d0825284492a4a5b044eb382e --- /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 0000000000000000000000000000000000000000..39cadfb1464e3b02fe0075f4230ae739610aa05d --- /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')