Commit d34a1c29 authored by Pablo Armingol's avatar Pablo Armingol
Browse files

refactor: Remove TAPI client, request builder, and NBI services, streamlining...

refactor: Remove TAPI client, request builder, and NBI services, streamlining TransportApiDriver to focus on generic connectivity services.
parent ab76ad52
Loading
Loading
Loading
Loading
+0 −88
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 os

LOGGER = logging.getLogger(__name__)

def create_tapi_request(resource_value):
    """Create TAPI connectivity service request from resource value.
    
    Args:
        resource_value: Tuple of (resource_key, resource_data_dict)
        
    Returns:
        dict: TAPI connectivity service request payload
    """
    LOGGER.info("Creating TAPI request for resource_value: %s", resource_value)
    try:
        # Load JSON template
        base_dir = os.path.dirname(os.path.abspath(__file__))
        json_path = os.path.join(base_dir, 'templates', 'lsp.json')
        with open(json_path, 'r', encoding='utf-8') as f:
            template = json.load(f)
        
        # Extract service data from resource
        resource_key = resource_value[0]
        resource_data = json.loads(resource_value[1]) if isinstance(resource_value[1], str) else resource_value[1]
        
        LOGGER.info("Processing resource_key: %s", resource_key)
        LOGGER.info("Resource data: %s", resource_data)
        
        # Populate service-level fields
        svc = template["tapi-connectivity:connectivity-service"][0]
        svc["connectivity-direction"] = resource_data["direction"]
        svc["layer-protocol-name"] = resource_data["layer_protocol_name"]
        svc["layer-protocol-qualifier"] = resource_data["layer_protocol_qualifier"]
        svc["requested-capacity"]["total-size"]["unit"] = "GHz"
        svc["requested-capacity"]["total-size"]["value"] = resource_data["bw"]
        svc["include-link"] = resource_data.get("link_uuid_path", [])
        svc["uuid"] = resource_data["uuid"]
        
        # Configure source endpoint
        ep0 = svc["end-point"][0]
        ep0["service-interface-point"]["service-interface-point-uuid"] = resource_data["input_sip"]
        ep0["direction"] = resource_data["direction"]
        ep0["layer-protocol-name"] = resource_data["layer_protocol_name"]
        ep0["layer-protocol-qualifier"] = resource_data["layer_protocol_qualifier"]
        ep0["local-id"] = resource_data["input_sip"]
        
        # Configure spectrum for source endpoint
        media_spec = ep0["tapi-photonic-media:media-channel-connectivity-service-end-point-spec"]
        mc_config = media_spec["mc-config"]
        spectrum = mc_config["spectrum"]
        spectrum["lower-frequency"] = resource_data["lower_frequency_mhz"]
        spectrum["upper-frequency"] = resource_data["upper_frequency_mhz"]
        spectrum["frequency-constraint"]["adjustment-granularity"] = resource_data["granularity"]
        spectrum["frequency-constraint"]["grid-type"] = resource_data["grid_type"]
        
        # Configure destination endpoint
        ep1 = svc["end-point"][1]
        ep1["service-interface-point"]["service-interface-point-uuid"] = resource_data["output_sip"]
        ep1["direction"] = resource_data["direction"]
        ep1["layer-protocol-name"] = resource_data["layer_protocol_name"]
        ep1["layer-protocol-qualifier"] = resource_data["layer_protocol_qualifier"]
        ep1["local-id"] = resource_data["output_sip"]
        
        LOGGER.info("Created TAPI request: %s", json.dumps(template, indent=2))
        
        # Extract URL if present
        url = resource_data.get("url", "")
        return template, url
        
    except (OSError, json.JSONDecodeError, KeyError) as e:
        LOGGER.error("Error creating TAPI request: %s", str(e), exc_info=True)
        raise
+0 −135
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, requests
from typing import Dict, List, Optional
from common.tools.client.RestClient import RestClient
from device.service.driver_api.ImportTopologyEnum import ImportTopologyEnum

GET_CONTEXT_IDS_URL = '/tfs-api/context_ids'
GET_DEVICES_URL     = '/tfs-api/devices'
GET_LINKS_URL       = '/tfs-api/links'

MAPPING_STATUS = {
    'DEVICEOPERATIONALSTATUS_UNDEFINED': 0,
    'DEVICEOPERATIONALSTATUS_DISABLED' : 1,
    'DEVICEOPERATIONALSTATUS_ENABLED'  : 2,
}

MAPPING_DRIVER = {
    'DEVICEDRIVER_UNDEFINED'            : 0,
    'DEVICEDRIVER_OPENCONFIG'           : 1,
    'DEVICEDRIVER_TRANSPORT_API'        : 2,
    'DEVICEDRIVER_P4'                   : 3,
    'DEVICEDRIVER_IETF_NETWORK_TOPOLOGY': 4,
    'DEVICEDRIVER_ONF_TR_532'           : 5,
    'DEVICEDRIVER_XR'                   : 6,
    'DEVICEDRIVER_IETF_L2VPN'           : 7,
    'DEVICEDRIVER_GNMI_OPENCONFIG'      : 8,
    'DEVICEDRIVER_OPTICAL_TFS'          : 9,
    'DEVICEDRIVER_IETF_ACTN'            : 10,
    'DEVICEDRIVER_OC'                   : 11,
    'DEVICEDRIVER_QKD'                  : 12,
    'DEVICEDRIVER_IETF_L3VPN'           : 13,
    'DEVICEDRIVER_IETF_SLICE'           : 14,
    'DEVICEDRIVER_NCE'                  : 15,
    'DEVICEDRIVER_SMARTNIC'             : 16,
    'DEVICEDRIVER_MORPHEUS'             : 17,
    'DEVICEDRIVER_RYU'                  : 18,
}

LOGGER = logging.getLogger(__name__)

class TfsApiClient(RestClient):
    def __init__(
        self, address : str, port : int, scheme : str = 'http',
        username : Optional[str] = None, password : Optional[str] = None,
        timeout : Optional[int] = 30
    ) -> None:
        super().__init__(
            address, port, scheme=scheme, username=username, password=password,
            timeout=timeout, verify_certs=False, allow_redirects=True, logger=LOGGER
        )

    def check_credentials(self) -> None:
        self.get(GET_CONTEXT_IDS_URL, expected_status_codes={requests.codes['OK']})
        LOGGER.info('Credentials checked')

    def get_devices_endpoints(
        self, import_topology : ImportTopologyEnum = ImportTopologyEnum.DEVICES
    ) -> List[Dict]:
        LOGGER.debug('[get_devices_endpoints] begin')
        MSG = '[get_devices_endpoints] import_topology={:s}'
        LOGGER.debug(MSG.format(str(import_topology)))

        if import_topology == ImportTopologyEnum.DISABLED:
            MSG = 'Unsupported import_topology mode: {:s}'
            raise Exception(MSG.format(str(import_topology)))

        devices = self.get(GET_DEVICES_URL, expected_status_codes={requests.codes['OK']})

        result = list()
        for json_device in devices['devices']:
            device_uuid : str = json_device['device_id']['device_uuid']['uuid']
            device_type : str = json_device['device_type']
            device_status = json_device['device_operational_status']
            device_url = '/devices/device[{:s}]'.format(device_uuid)
            device_data = {
                'uuid': json_device['device_id']['device_uuid']['uuid'],
                'name': json_device['name'],
                'type': device_type,
                'status': MAPPING_STATUS[device_status],
                'drivers': [
                    MAPPING_DRIVER[driver]
                    for driver in json_device['device_drivers']
                ],
            }
            result.append((device_url, device_data))

            for json_endpoint in json_device['device_endpoints']:
                endpoint_uuid = json_endpoint['endpoint_id']['endpoint_uuid']['uuid']
                endpoint_url = '/endpoints/endpoint[{:s}]'.format(endpoint_uuid)
                endpoint_data = {
                    'device_uuid': device_uuid,
                    'uuid': endpoint_uuid,
                    'name': json_endpoint['name'],
                    'type': json_endpoint['endpoint_type'],
                }
                result.append((endpoint_url, endpoint_data))

        if import_topology == ImportTopologyEnum.DEVICES:
            LOGGER.debug('[get_devices_endpoints] devices only; returning')
            return result

        links = self.get(GET_LINKS_URL, expected_status_codes={requests.codes['OK']})

        for json_link in links['links']:
            link_uuid : str = json_link['link_id']['link_uuid']['uuid']
            link_url = '/links/link[{:s}]'.format(link_uuid)
            link_endpoint_ids = [
                (
                    json_endpoint_id['device_id']['device_uuid']['uuid'],
                    json_endpoint_id['endpoint_uuid']['uuid'],
                )
                for json_endpoint_id in json_link['link_endpoint_ids']
            ]
            link_data = {
                'uuid': json_link['link_id']['link_uuid']['uuid'],
                'name': json_link['name'],
                'endpoints': link_endpoint_ids,
            }
            result.append((link_url, link_data))

        LOGGER.debug('[get_devices_endpoints] topology; returning')
        return result
+13 −15
Original line number Diff line number Diff line
@@ -170,21 +170,19 @@ def create_connectivity_service(
        ]
    }
    results = []
    LOGGER.info('Would create Connectivity service {:s}: {:s}'.format(str(uuid), str(data)))
    # try:
    #     LOGGER.info('Connectivity service {:s}: {:s}'.format(str(uuid), str(data)))
    #     response = requests.post(
    #         url=url, data=json.dumps(data), timeout=timeout, headers=headers, verify=False, auth=auth)
    #     LOGGER.info('TAPI response: {:s}'.format(str(response)))
    # except Exception as e:  # pylint: disable=broad-except
    #     LOGGER.exception('Exception creating ConnectivityService(uuid={:s}, data={:s})'.format(str(uuid), str(data)))
    #     results.append(e)
    # else:
    #     if response.status_code not in HTTP_OK_CODES:
    #         msg = 'Could not create ConnectivityService(uuid={:s}, data={:s}). status_code={:s} reply={:s}'\
    #         LOGGER.error(msg.format(str(uuid), str(data), str(response.status_code), str(response)))
    #     results.append(response.status_code in HTTP_OK_CODES)
    results.append(True)  # Always return success for now
    try:
        LOGGER.info('Connectivity service {:s}: {:s}'.format(str(uuid), str(data)))
        response = requests.post(
            url=url, data=json.dumps(data), timeout=timeout, headers=headers, verify=False, auth=auth)
        LOGGER.info('TAPI response: {:s}'.format(str(response)))
    except Exception as e:  # pylint: disable=broad-except
        LOGGER.exception('Exception creating ConnectivityService(uuid={:s}, data={:s})'.format(str(uuid), str(data)))
        results.append(e)
    else:
        if response.status_code not in HTTP_OK_CODES:
            msg = 'Could not create ConnectivityService(uuid={:s}, data={:s}). status_code={:s} reply={:s}'
            LOGGER.error(msg.format(str(uuid), str(data), str(response.status_code), str(response)))
        results.append(response.status_code in HTTP_OK_CODES)
    return results

def delete_connectivity_service(root_url, uuid, auth : Optional[HTTPBasicAuth] = None, timeout : Optional[int] = None):
+25 −67
Original line number Diff line number Diff line
@@ -12,15 +12,14 @@
# See the License for the specific language governing permissions and
# limitations under the License.

import json, logging, requests, threading
import logging, requests, threading
from requests.auth import HTTPBasicAuth
from typing import Any, Iterator, List, Optional, Tuple, Union
from common.method_wrappers.Decorator import MetricsPool, metered_subclass_method
from common.type_checkers.Checkers import chk_string, chk_type
from device.service.driver_api._Driver import _Driver, RESOURCE_ENDPOINTS, RESOURCE_SERVICES
from device.service.driver_api._Driver import _Driver
from device.service.driver_api.ImportTopologyEnum import ImportTopologyEnum, get_import_topology
from . import ALL_RESOURCE_KEYS
from .TfsApiClient import TfsApiClient
from .Tools import create_connectivity_service, find_key, config_getter, delete_connectivity_service, tapi_tequest

LOGGER = logging.getLogger(__name__)
@@ -110,48 +109,7 @@ class TransportApiDriver(_Driver):
        if len(resources) == 0:
            return results
        with self.__lock:
            for resource in resources:
                LOGGER.info('resource = {:s}'.format(str(resource)))

                resource_key = resource[0]
                # Handle optical slice resources
                if '/optical_slice/context/' in resource_key:
                    LOGGER.info('=' * 80)
                    LOGGER.info('OPTICAL SLICE RECEIVED')
                    LOGGER.info('=' * 80)
                    try:
                        optical_slice_data = json.loads(resource[1])

                        url = optical_slice_data.get('url', '')
                        LOGGER.info('URL: %s', url)
                        LOGGER.info('-' * 80)

                        LOGGER.info('Optical Slice Data:')
                        optical_slice = optical_slice_data.get('data', {})
                        LOGGER.info(json.dumps(optical_slice, indent=2))
                        LOGGER.info('=' * 80)
                        results.append(True)
                    except Exception as e:
                        LOGGER.error(f'Failed to parse optical slice data: {str(e)}')
                        results.append(e)
                    continue

                # Handle media channel resources
                elif '/media_channel/service/' in resource_key:
                    try:
                        from .TapiRequestBuilder import create_tapi_request

                        tapi_request, url = create_tapi_request(resource)
                        LOGGER.info('URL: {:s}'.format(url))
                        LOGGER.info('Generated TAPI request: {:s}'.format(json.dumps(tapi_request, indent=2)))

                        results.append(True)

                    except Exception as e:
                        LOGGER.error('Failed to create TAPI request: {:s}'.format(str(e)), exc_info=True)
                        results.append(e)

                elif "tapi_lsp" in str(resources):
            if "tapi_lsp" in str(resources):
                for resource in resources:
                    try:
                        tapi_tequest(resource)
+0 −186
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
import json
from flask_restful import Resource, request
from common.proto.context_pb2 import ConfigActionEnum, ConfigRule, DeviceId, Device, Service, ServiceTypeEnum, ServiceStatusEnum
from device.client.DeviceClient import DeviceClient
from service.client.ServiceClient import ServiceClient

LOGGER = logging.getLogger(__name__)

class MediaChannelService(Resource):
    def __init__(self):
        super().__init__()
        self.device_client = DeviceClient()
        self.service_client = ServiceClient()

    def post(self, allocationId: str):
        LOGGER.info("Received POST request for allocationId: %s", allocationId)
        
        data = request.get_json()
        LOGGER.info("Request data: %s", data)
        
        # Validate required fields and their types (following proto format)
        required_str_fields = ['input_sip', 'output_sip', 'uuid', 'tenant_uuid', 'direction', 
                               'layer_protocol_name', 'layer_protocol_qualifier', 
                               'granularity', 'grid_type','bw', 'lower_frequency_mhz', 
                               'upper_frequency_mhz']
        
        # Optional string fields (will be included if present)
        optional_str_fields = ['capacity_unit', 'capacity_value', 'route_objective_function', 'url']
        
        # Validate required string fields
        for field in required_str_fields:
            if field not in data:
                return {'status': 'error', 'message': f'Missing required field: {field}'}, 400
            if not isinstance(data[field], str):
                return {'status': 'error', 'message': f'Field {field} must be a string'}, 400
        
        # Validate optional string fields if present
        for field in optional_str_fields:
            if field in data and not isinstance(data[field], str):
                return {'status': 'error', 'message': f'Field {field} must be a string'}, 400
        
        # Validate link_uuid_path is a list if present
        if 'link_uuid_path' in data and not isinstance(data['link_uuid_path'], list):
            return {'status': 'error', 'message': 'Field link_uuid_path must be a list'}, 400
        
        # Create TFS Service (without config rules) to show in WebUI
        try:
            service = Service()
            service.service_id.service_uuid.uuid = data["uuid"]
            service.service_id.context_id.context_uuid.uuid = "admin"  # Use admin context
            service.service_type = ServiceTypeEnum.SERVICETYPE_TAPI_LSP
            service.service_status.service_status = ServiceStatusEnum.SERVICESTATUS_ACTIVE
            service.name = f"MediaChannel-{allocationId}"
            
            # Note: Endpoints should NOT be added here - CreateService doesn't accept them
            # They would need to be configured separately after service creation if needed
            
            # Create service in TFS
            service_response = self.service_client.CreateService(service)
            LOGGER.info("Created TFS service: %s", service_response)
            
        except Exception as e:
            LOGGER.error("Failed to create TFS service: %s", str(e), exc_info=True)
            return {'status': 'error', 'message': f'Failed to create TFS service: {str(e)}'}, 500
        
        device_id_str = data.get('device_id')
        if device_id_str:
            LOGGER.info("Processing device_id: %s", device_id_str)
            try:
                # Create Device object
                device = Device()
                device.device_id.device_uuid.uuid = device_id_str
                
                # Create config rule with validated TAPI data
                config_rule = ConfigRule()
                config_rule.action = ConfigActionEnum.CONFIGACTION_SET
                config_rule.custom.resource_key = f'/media_channel/service/{data["uuid"]}'
                
                # Prepare data for TransportApiDriver following proto format
                service_data = {
                    "input_sip": data["input_sip"],
                    "output_sip": data["output_sip"],
                    "uuid": data["uuid"],
                    "bw": str(data["bw"]),
                    "tenant_uuid": data["tenant_uuid"],
                    "layer_protocol_name": data["layer_protocol_name"],
                    "layer_protocol_qualifier": data["layer_protocol_qualifier"],
                    "lower_frequency_mhz": str(data["lower_frequency_mhz"]),
                    "upper_frequency_mhz": str(data["upper_frequency_mhz"]),
                    "link_uuid_path": data.get("link_uuid_path", []),
                    "granularity": data["granularity"],
                    "grid_type": data["grid_type"],
                    "direction": data["direction"]
                }
                
                # Add optional parameters if present
                if "capacity_unit" in data:
                    service_data["capacity_unit"] = data["capacity_unit"]
                if "capacity_value" in data:
                    service_data["capacity_value"] = data["capacity_value"]
                if "route_objective_function" in data:
                    service_data["route_objective_function"] = data["route_objective_function"]
                if "url" in data:
                    service_data["url"] = data["url"]
                
                config_rule.custom.resource_value = json.dumps(service_data)
                
                # Add rule and configure device
                device.device_config.config_rules.append(config_rule)
                self.device_client.ConfigureDevice(device)
                LOGGER.info("Configured device %s with media channel service %s", device_id_str, data["uuid"])
                
            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'Media channel service created for {allocationId}',
            'allocationId': allocationId,
            'service_uuid': data.get('uuid')
        }, 201

    def delete(self, allocationId: str):
        LOGGER.info("Received DELETE request for allocationId: %s", allocationId)
        
        data = request.get_json() or {}
        service_uuid = data.get('uuid', allocationId)
        
        # Delete TFS Service
        try:
            from common.proto.context_pb2 import ServiceId
            
            service_id = ServiceId()
            service_id.service_uuid.uuid = service_uuid
            service_id.context_id.context_uuid.uuid = "admin"
            
            # Delete service from TFS
            self.service_client.DeleteService(service_id)
            LOGGER.info("Deleted TFS service: %s", service_uuid)
            
        except Exception as e:
            LOGGER.error("Failed to delete TFS service: %s", str(e), exc_info=True)
            return {'status': 'error', 'message': f'Failed to delete TFS service: {str(e)}'}, 500
        
        # Optionally delete from device if device_id is provided
        device_id_str = data.get('device_id')
        if device_id_str:
            LOGGER.info("Processing device_id for deletion: %s", device_id_str)
            try:
                device = Device()
                device.device_id.device_uuid.uuid = device_id_str
                
                # Create DELETE config rule
                config_rule = ConfigRule()
                config_rule.action = ConfigActionEnum.CONFIGACTION_DELETE
                config_rule.custom.resource_key = f'/media_channel/service/{service_uuid}'
                
                device.device_config.config_rules.append(config_rule)
                self.device_client.ConfigureDevice(device)
                LOGGER.info("Deleted media channel from device %s", device_id_str)
                
            except Exception as e:
                LOGGER.warning("Failed to delete from device: %s", str(e))
        
        return {
            'status': 'success',
            'message': f'Media channel service deleted for {allocationId}',
            'allocationId': allocationId,
            'service_uuid': service_uuid
        }, 200
Loading