Commit 26d98495 authored by Pablo Armingol's avatar Pablo Armingol
Browse files

Merge branch 'develop' of https://labs.etsi.org/rep/tfs/controller into...

Merge branch 'develop' of https://labs.etsi.org/rep/tfs/controller into feat/325-tid-nbi-e2e-to-manage-e2e-path-computation
parents d1956045 89a09e01
Loading
Loading
Loading
Loading
+54 −17
Original line number Diff line number Diff line
@@ -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": {
            "service_config": {
                "config_rules": [
                    {
                        "action": 1,
                        "ipowdm": {
                            "endpoint_id": {
                        "device_id": {"device_uuid": {"uuid": "IP1"}},
                        "endpoint_uuid": {"uuid": "PORT-xe4"}
                                "device_id": {
                                    "device_uuid": {
                                        "uuid": "IP1"
                                    }
                                },
                                "endpoint_uuid": {
                                    "uuid": "PORT-xe4"
                                }
                            },
                            "rule_set": {
                                "src": [],
                                "dst": []
                            }
                }}
            ]}
                        }
                    }
                ]
            }
        }
    ]
}
 No newline at end of file
+2 −0
Original line number Diff line number Diff line
@@ -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
@@ -108,6 +109,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)
register_e2e_path_computation(nbi_app)
+406 −0
Original line number Diff line number Diff line
# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

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
+39 −0
Original line number Diff line number Diff line
# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

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/<string:serviceId>',
        endpoint='ipowdm_service'
    )
    nbi_app.add_rest_api_resource(
        PluggablesService,
        f'{URL_PREFIX}/pluggables/<string:serviceId>',
        endpoint='ipowdm_pluggables'
    )
    nbi_app.add_rest_api_resource(
        L3NMService,
        f'{URL_PREFIX}/l3nm/<string:serviceId>',
        endpoint='ipowdm_l3nm'
    )
    LOGGER.info('IPoWDM Service NBI registered')