From f27a291faebe7a9e853b0322171fb4f46f5b0bc5 Mon Sep 17 00:00:00 2001 From: PedroDuarte536 Date: Thu, 24 Jul 2025 18:58:45 +0100 Subject: [PATCH 1/4] add restconf checkbox to TFS UI --- proto/context.proto | 1 + src/context/service/database/models/enums/DeviceDriver.py | 1 + src/service/service/service_handler_api/FilterFields.py | 1 + src/webui/service/device/forms.py | 1 + src/webui/service/device/routes.py | 2 ++ src/webui/service/templates/device/add.html | 1 + 6 files changed, 7 insertions(+) diff --git a/proto/context.proto b/proto/context.proto index b33750e80..00c3325b0 100644 --- a/proto/context.proto +++ b/proto/context.proto @@ -232,6 +232,7 @@ enum DeviceDriverEnum { DEVICEDRIVER_SMARTNIC = 16; DEVICEDRIVER_MORPHEUS = 17; DEVICEDRIVER_RYU = 18; + DEVICEDRIVER_RESTCONF = 19; } enum DeviceOperationalStatusEnum { diff --git a/src/context/service/database/models/enums/DeviceDriver.py b/src/context/service/database/models/enums/DeviceDriver.py index 02c423111..ecf8b46d5 100644 --- a/src/context/service/database/models/enums/DeviceDriver.py +++ b/src/context/service/database/models/enums/DeviceDriver.py @@ -41,6 +41,7 @@ class ORM_DeviceDriverEnum(enum.Enum): SMARTNIC = DeviceDriverEnum.DEVICEDRIVER_SMARTNIC MORPHEUS = DeviceDriverEnum.DEVICEDRIVER_MORPHEUS RYU = DeviceDriverEnum.DEVICEDRIVER_RYU + RESTCONF = DeviceDriverEnum.DEVICEDRIVER_RESTCONF grpc_to_enum__device_driver = functools.partial( grpc_to_enum, DeviceDriverEnum, ORM_DeviceDriverEnum) diff --git a/src/service/service/service_handler_api/FilterFields.py b/src/service/service/service_handler_api/FilterFields.py index a56bcf0f9..6f70a6306 100644 --- a/src/service/service/service_handler_api/FilterFields.py +++ b/src/service/service/service_handler_api/FilterFields.py @@ -53,6 +53,7 @@ DEVICE_DRIVER_VALUES = { DeviceDriverEnum.DEVICEDRIVER_IETF_L3VPN, DeviceDriverEnum.DEVICEDRIVER_SMARTNIC, DeviceDriverEnum.DEVICEDRIVER_RYU, + DeviceDriverEnum.DEVICEDRIVER_RESTCONF, } # Map allowed filter fields to allowed values per Filter field. If no restriction (free text) None is specified diff --git a/src/webui/service/device/forms.py b/src/webui/service/device/forms.py index ddcfac1d5..b56d34aa5 100644 --- a/src/webui/service/device/forms.py +++ b/src/webui/service/device/forms.py @@ -36,6 +36,7 @@ class AddDeviceForm(FlaskForm): device_drivers_qkd = BooleanField('QKD') device_drivers_smartnic = BooleanField('SMARTNIC') device_drivers_morpheus = BooleanField('MORPHEUS') + device_drivers_restconf = BooleanField('RESTCONF') device_config_address = StringField('connect/address',default='127.0.0.1',validators=[DataRequired(), Length(min=5)]) device_config_port = StringField('connect/port',default='0',validators=[DataRequired(), Length(min=1)]) diff --git a/src/webui/service/device/routes.py b/src/webui/service/device/routes.py index 2397cb39e..54b1d1d45 100644 --- a/src/webui/service/device/routes.py +++ b/src/webui/service/device/routes.py @@ -139,6 +139,8 @@ def add(): device_drivers.append(DeviceDriverEnum.DEVICEDRIVER_SMARTNIC) if form.device_drivers_morpheus.data: device_drivers.append(DeviceDriverEnum.DEVICEDRIVER_MORPHEUS) + if form.device_drivers_restconf.data: + device_drivers.append(DeviceDriverEnum.DEVICEDRIVER_RESTCONF) device_obj.device_drivers.extend(device_drivers) # pylint: disable=no-member try: diff --git a/src/webui/service/templates/device/add.html b/src/webui/service/templates/device/add.html index c18fcc74b..6314af815 100644 --- a/src/webui/service/templates/device/add.html +++ b/src/webui/service/templates/device/add.html @@ -99,6 +99,7 @@
{{ form.device_drivers_smartnic }} {{ form.device_drivers_smartnic.label(class="col-sm-3 col-form-label") }} {{ form.device_drivers_morpheus }} {{ form.device_drivers_morpheus.label(class="col-sm-3 col-form-label") }} + {{ form.device_drivers_restconf }} {{ form.device_drivers_restconf.label(class="col-sm-3 col-form-label") }} {% endif %} -- GitLab From 51718d83553fa66cb38d06a3e478e9fbca7d11a6 Mon Sep 17 00:00:00 2001 From: PedroDuarte536 Date: Thu, 24 Jul 2025 19:08:22 +0100 Subject: [PATCH 2/4] add initial restconf driver implementation --- .../drivers/restconf/RestconfDriver.py | 78 ++++++++++ .../drivers/restconf/RestconfHandler.py | 72 +++++++++ .../drivers/restconf/handlers/Component.py | 52 +++++++ .../drivers/restconf/handlers/Interface.py | 145 ++++++++++++++++++ .../restconf/handlers/InterfaceLimit.py | 54 +++++++ .../restconf/handlers/NetworkInstance.py | 117 ++++++++++++++ .../handlers/NetworkInstanceInterface.py | 51 ++++++ .../handlers/NetworkInstanceProtocol.py | 46 ++++++ .../handlers/NetworkInstanceStaticRoute.py | 50 ++++++ .../restconf/handlers/NetworkInstanceVlan.py | 52 +++++++ .../drivers/restconf/handlers/_Handler.py | 23 +++ .../drivers/restconf/handlers/__init__.py | 125 +++++++++++++++ 12 files changed, 865 insertions(+) create mode 100644 src/device/service/drivers/restconf/RestconfDriver.py create mode 100644 src/device/service/drivers/restconf/RestconfHandler.py create mode 100644 src/device/service/drivers/restconf/handlers/Component.py create mode 100644 src/device/service/drivers/restconf/handlers/Interface.py create mode 100644 src/device/service/drivers/restconf/handlers/InterfaceLimit.py create mode 100644 src/device/service/drivers/restconf/handlers/NetworkInstance.py create mode 100644 src/device/service/drivers/restconf/handlers/NetworkInstanceInterface.py create mode 100644 src/device/service/drivers/restconf/handlers/NetworkInstanceProtocol.py create mode 100644 src/device/service/drivers/restconf/handlers/NetworkInstanceStaticRoute.py create mode 100644 src/device/service/drivers/restconf/handlers/NetworkInstanceVlan.py create mode 100644 src/device/service/drivers/restconf/handlers/_Handler.py create mode 100644 src/device/service/drivers/restconf/handlers/__init__.py diff --git a/src/device/service/drivers/restconf/RestconfDriver.py b/src/device/service/drivers/restconf/RestconfDriver.py new file mode 100644 index 000000000..2f57acbeb --- /dev/null +++ b/src/device/service/drivers/restconf/RestconfDriver.py @@ -0,0 +1,78 @@ +import logging, json, threading +from typing import List, Tuple, Any, Union, Optional, Iterator + +from .handlers import ALL_RESOURCE_KEYS, get_path, parse, compose +from .RestconfHandler import RestconfHandler + +from device.service.driver_api._Driver import _Driver + + +DRIVER_NAME = 'restconf' + +class RestconfDriver(_Driver): + def __init__(self, address : str, port : int, **settings) -> None: + super().__init__(DRIVER_NAME, address, port, **settings) + self.__logger = logging.getLogger('{:s}:[{:s}:{:s}]'.format(str(__name__), str(self.address), str(self.port))) + self.__restconf_handler = RestconfHandler(self.address, self.port, **(self.settings)) + + def Connect(self) -> bool: + return self.__restconf_handler.is_endpoint_available + + def Disconnect(self) -> bool: + return True + + def GetInitialConfig(self) -> List[Tuple[str, Any]]: + return [] + + def GetConfig(self, resource_keys : List[str] = []) -> List[Tuple[str, Union[Any, None, Exception]]]: + results = [] + + if len(resource_keys) == 0: resource_keys = ALL_RESOURCE_KEYS + for i,resource_key in enumerate(resource_keys): + str_resource_name = 'resource_key[#{:d}]'.format(i) + try: + resource_path = get_path(resource_key) + if resource_path is None: resource_path = resource_key + data = self.__restconf_handler.get(resource_path) + results.extend(parse(resource_path, data)) + except Exception as e: + MSG = 'Exception retrieving {:s}: {:s}' + self.__logger.exception(MSG.format(str_resource_name, str(resource_key))) + results.append((resource_key, e)) + return results + + + def SetConfig(self, resources: List[Tuple[str, Any]]) -> List[Union[bool, Exception]]: + if len(resources) == 0: return [] + + results = [] + + for resource, data in resources: + if type(data) == str: + try: + data = json.loads(data) + except json.decoder.JSONDecodeError: + results.append((resource, True)) + continue + + compose_result = compose(resource, data) + results.append(compose_result if compose_result is not None else (resource, Exception('Unexpected'))) + + return self.__restconf_handler.set(results) + + + def DeleteConfig(self, resources: List[Tuple[str, Any]]) -> List[Union[bool, Exception]]: + if len(resources) == 0: return [] + return self.__restconf_handler.delete([ key for key, _ in resources ]) + + + def SubscribeState(self, subscriptions: List[Tuple[str, float, float]]) -> List[Union[bool, Exception]]: + return [ True for _ in subscriptions ] + + + def UnsubscribeState(self, subscriptions: List[Tuple[str, float, float]]) -> List[Union[bool, Exception]]: + return [ True for _ in subscriptions ] + + + def GetState(self, blocking=False, terminate : Optional[threading.Event] = None) -> Iterator[Tuple[float, str, Any]]: + return [] diff --git a/src/device/service/drivers/restconf/RestconfHandler.py b/src/device/service/drivers/restconf/RestconfHandler.py new file mode 100644 index 000000000..15ba0502c --- /dev/null +++ b/src/device/service/drivers/restconf/RestconfHandler.py @@ -0,0 +1,72 @@ +import requests, logging, json +import xml.etree.ElementTree as ET + +from typing import List, Tuple, Any, Union + + +class RestconfHandler: + def __init__(self, address : str, port : int, **settings) -> None: + self.__address = address + self.__port = int(port) + self.__base_path = '' + + self.__logger = logging.getLogger('{:s}:[{:s}:{:s}]'.format(str(__name__), str(self.__address), str(self.__port))) + + @property + def _base_path(self) -> str: + return f"http://{self.__address}:{self.__port}/{self.__base_path}".strip('/') + + @property + def is_endpoint_available(self) -> bool: + response = requests.get(f"{self._base_path}/.well-known/host-meta") + if response.status_code != 200: return False + + response = ET.fromstring(response.text) + restconf_link = response.find("xrd:Link[@rel='restconf']", {'xrd': 'http://docs.oasis-open.org/ns/xri/xrd-1.0'}) + + if restconf_link is None or 'href' not in restconf_link.attrib: return False + + self.__base_path = restconf_link.attrib.get('href').strip('/') + + return True + + def get(self, resource: str) -> dict: + resource = resource.strip('/') + response = requests.get(f"{self._base_path}/data/{resource}") + return response.json() + + def set(self, resources : List[Tuple[str, Any]]) -> List[Union[bool, Exception]]: + is_valid_data = lambda resource_key, resource_value: (not resource_key.startswith('_connect/') )and ((not isinstance(resource_value, bool)) and (not isinstance(resource_value, Exception))) + + results = [ (resource_key, resource_value) for resource_key, resource_value in resources if not is_valid_data(resource_key, resource_value) ] + + resources = [ (resource_key, json.loads(resource_value)) if isinstance(resource_value, str) else (resource_key, resource_value) for resource_key, resource_value in resources if is_valid_data(resource_key, resource_value) ] + delete_resources = [ resource_key for resource_key, resource_value in resources if len(resource_value) == 0 ] + + response = self.delete(delete_resources) + self.__logger.debug(response) + + for resource_key, resource_value in resources: + if resource_key in delete_resources: continue + + if isinstance(resource_value, bool) or isinstance(resource_value, Exception): + results.append((resource_key, resource_value)) + continue + + response = requests.put(f"{self._base_path}/data/{resource_key.strip('/')}", json=resource_value) + + response_success = response.status_code in [200, 201] + results.append((resource_key, True if response_success else Exception('Unexpected'))) + + return results + + def delete(self, resources : List[str]) -> List[Union[bool, Exception]]: + results = [] + + for resource_key in resources: + response = requests.delete(path) + + response_success = response.status_code in [200, 201] + results.append((resource_key, True if response_success else Exception('Unexpected'))) + + return results diff --git a/src/device/service/drivers/restconf/handlers/Component.py b/src/device/service/drivers/restconf/handlers/Component.py new file mode 100644 index 000000000..74c7c8eaa --- /dev/null +++ b/src/device/service/drivers/restconf/handlers/Component.py @@ -0,0 +1,52 @@ +import re +import json +import logging +from typing import Dict, Tuple + +from ._Handler import _Handler + +from common.proto.kpi_sample_types_pb2 import KpiSampleType + + +PATH_IF_CTR = '/oci:interfaces/interface={:s}/state/counters/{:s}' + +class ComponentHandler(_Handler): + def __init__(self): + self.__logger = logging.getLogger(str(__name__)) + + def get_resource_key(self): return '/endpoints/endpoint' + def get_path(self): return '/ocp:components' + + def compose(self, resource_key : str, resource_value : Dict, delete : bool = False) -> Tuple[str, str]: + return resource_key, resource_value + + def parse(self, json_data : Dict): + self.__logger.debug('json_data = {:s}'.format(json.dumps(json_data))) + + entries = [] + for component in json_data['components']: + self.__logger.debug('component={:s}'.format(str(component))) + + component_name = component['name'] + + component_state = component.get('state', {}) + component_type = component_state.get('type') + if component_type is None: continue + component_type = component_type.split(':')[-1] + if component_type not in {'PORT'}: continue + + # TODO: improve mapping between interface name and component name + # By now, computed by time for the sake of saving time for the Hackfest. + interface_name = re.sub(r'\-[pP][oO][rR][tT]', '', component_name) + + endpoint = {'uuid': interface_name, 'type': '-'} + endpoint['sample_types'] = { + KpiSampleType.KPISAMPLETYPE_BYTES_RECEIVED : PATH_IF_CTR.format(interface_name, 'in-octets' ), + KpiSampleType.KPISAMPLETYPE_BYTES_TRANSMITTED : PATH_IF_CTR.format(interface_name, 'out-octets'), + KpiSampleType.KPISAMPLETYPE_PACKETS_RECEIVED : PATH_IF_CTR.format(interface_name, 'in-pkts' ), + KpiSampleType.KPISAMPLETYPE_PACKETS_TRANSMITTED: PATH_IF_CTR.format(interface_name, 'out-pkts' ), + } + + entries.append(('/endpoints/endpoint[{:s}]'.format(endpoint['uuid']), endpoint)) + + return entries diff --git a/src/device/service/drivers/restconf/handlers/Interface.py b/src/device/service/drivers/restconf/handlers/Interface.py new file mode 100644 index 000000000..bb25433f5 --- /dev/null +++ b/src/device/service/drivers/restconf/handlers/Interface.py @@ -0,0 +1,145 @@ +import json +import logging +from typing import Dict, Tuple + +from ._Handler import _Handler + + +class InterfaceHandler(_Handler): + def __init__(self): + self.__logger = logging.getLogger(str(__name__)) + + def get_resource_key(self): return '/interface' + def get_path(self): return '/oci:interfaces' + + def compose(self, resource_key : str, resource_value : Dict, delete : bool = False) -> Tuple[str, str]: + if_name = resource_value.get('name') + if_index = int(resource_value.get('index', 0)) + + str_path = '/interfaces/interface={:s}'.format(if_name) + + if delete: + return str_path, {} + + prefix = resource_value.get('prefix') + enabled = bool(resource_value.get('enabled', True)) + address_ip = resource_value.get('address_ip') + address_prefix = int(resource_value.get('address_prefix', 32)) + mtu = int(resource_value.get('mtu')) + + data = { + 'config': { 'index': if_index, 'name': if_name, 'enabled': enabled, 'mtu': mtu }, + 'ipv4': { + 'config': { 'enabled': enabled }, + 'addresses': [{ 'prefix': prefix, 'ip': address_ip, 'prefix-length': address_prefix, 'mtu': mtu }] + }, + } + + return str_path, data + + def parse(self, json_data : Dict): + self.__logger.debug('json_data = {:s}'.format(json.dumps(json_data))) + + entries = [] + for interface in json_data['interfaces']: + self.__logger.debug('interface={:s}'.format(str(interface))) + + interface_name = interface['name'] + interface_config = interface.get('config', {}) + + interface_state = interface.get('state', {}) + interface_type = interface_state.get('type') + if interface_type is None: continue + interface_type = interface_type.split(':')[-1] + if interface_type not in {'ether'}: continue + + _interface = { + 'name' : interface_name, + 'type' : interface_type, + 'mtu' : interface_state['mtu'], + 'admin-status' : interface_state['admin-status'], + 'oper-status' : interface_state['oper-status'], + 'management' : interface_state['management'], + } + if not interface_state['management'] and 'ifindex' in interface_state: + _interface['ifindex'] = interface_state['ifindex'] + if 'description' in interface_config: + _interface['description'] = interface_config['description'] + if 'enabled' in interface_config: + _interface['enabled'] = interface_config['enabled'] + if 'hardware-port' in interface_state: + _interface['hardware-port'] = interface_state['hardware-port'] + if 'transceiver' in interface_state: + _interface['transceiver'] = interface_state['transceiver'] + + entry_interface_key = '/interface[{:s}]'.format(interface_name) + entries.append((entry_interface_key, _interface)) + + if interface_type == 'ether': + ethernet_state = interface['ethernet']['state'] + + _ethernet = { + # TODO Fill these fields in the wrapper + # 'mac-address' : ethernet_state['mac-address'], + # 'hw-mac-address' : ethernet_state['hw-mac-address'], + # 'port-speed' : ethernet_state['port-speed'].split(':')[-1], + # 'negotiated-port-speed' : ethernet_state['negotiated-port-speed'].split(':')[-1], + } + entry_ethernet_key = '{:s}/ethernet'.format(entry_interface_key) + entries.append((entry_ethernet_key, _ethernet)) + + # TODO Validate need for subinterfaces + # subinterfaces = interface.get('subinterfaces', {}).get('subinterface', []) + # for subinterface in subinterfaces: + # self.__logger.debug('subinterface={:s}'.format(str(subinterface))) + + # subinterface_index = subinterface['index'] + # subinterface_state = subinterface.get('state', {}) + + # _subinterface = {'index': subinterface_index} + # if 'name' in subinterface_state: + # _subinterface['name'] = subinterface_state['name'] + # if 'enabled' in subinterface_state: + # _subinterface['enabled'] = subinterface_state['enabled'] + + # if 'vlan' in subinterface: + # vlan = subinterface['vlan'] + # vlan_match = vlan['match'] + + # single_tagged = vlan_match.pop('single-tagged', None) + # if single_tagged is not None: + # single_tagged_config = single_tagged['config'] + # vlan_id = single_tagged_config['vlan-id'] + # _subinterface['vlan_id'] = vlan_id + + # if len(vlan_match) > 0: + # raise Exception('Unsupported VLAN schema: {:s}'.format(str(vlan))) + + # ipv4_addresses = subinterface.get('ipv4', {}).get('addresses', {}).get('address', []) + # if len(ipv4_addresses) > 1: + # raise Exception('Multiple IPv4 Addresses not supported: {:s}'.format(str(ipv4_addresses))) + # for ipv4_address in ipv4_addresses: + # self.__logger.debug('ipv4_address={:s}'.format(str(ipv4_address))) + # _subinterface['address_ip'] = ipv4_address['ip'] + # ipv4_address_state = ipv4_address.get('state', {}) + # #if 'origin' in ipv4_address_state: + # # _subinterface['origin'] = ipv4_address_state['origin'] + # if 'prefix-length' in ipv4_address_state: + # _subinterface['address_prefix'] = ipv4_address_state['prefix-length'] + + # ipv6_addresses = subinterface.get('ipv6', {}).get('addresses', {}).get('address', []) + # if len(ipv6_addresses) > 1: + # raise Exception('Multiple IPv6 Addresses not supported: {:s}'.format(str(ipv6_addresses))) + # for ipv6_address in ipv6_addresses: + # self.__logger.debug('ipv6_address={:s}'.format(str(ipv6_address))) + # _subinterface['address_ipv6'] = ipv6_address['ip'] + # ipv6_address_state = ipv6_address.get('state', {}) + # #if 'origin' in ipv6_address_state: + # # _subinterface['origin_ipv6'] = ipv6_address_state['origin'] + # if 'prefix-length' in ipv6_address_state: + # _subinterface['address_prefix_ipv6'] = ipv6_address_state['prefix-length'] + + # entry_subinterface_key = '{:s}/subinterface[{:d}]'.format(entry_interface_key, subinterface_index) + # entries.append((entry_subinterface_key, _subinterface)) + + return entries diff --git a/src/device/service/drivers/restconf/handlers/InterfaceLimit.py b/src/device/service/drivers/restconf/handlers/InterfaceLimit.py new file mode 100644 index 000000000..719e14379 --- /dev/null +++ b/src/device/service/drivers/restconf/handlers/InterfaceLimit.py @@ -0,0 +1,54 @@ +import logging +from typing import Dict, Tuple + +from ._Handler import _Handler + + +SERVICE_LIMIT_NAMES = ("latency", "corrupt", "duplicate", "loss", "reorder", "rate") + +class InterfaceLimitHandler(_Handler): + def __init__(self): + self.__logger = logging.getLogger(str(__name__)) + + def get_resource_key(self): return '/interface/service-limits' + def get_path(self): return '/oci:interfaces/interface/service-limits' + + def compose(self, resource_key : str, resource_value : Dict, delete : bool = False) -> Tuple[str, str]: + if_name = resource_value['interface'] + + if delete: + PATH_TMPL = '/interfaces/interface={:s}/service-limits' + str_path = PATH_TMPL.format(if_name) + return str_path, {} + + data = { 'interface': if_name } + + for key, value in resource_value.items(): + if key not in SERVICE_LIMIT_NAMES: continue + + if key == 'latency': + config = { + 'delay': value['delay'], + 'jitter': value.get('jitter', ''), + 'correlation': value.get('correlation', '') + } + + elif key == 'rate': + config = { + 'size': value['rate'] + } + + else: + config = { + 'percent': value['percent'], + 'correlation': value.get('correlation', ''), + } + + data[key] = config + + str_path = '/interfaces/interface={:s}/service-limits'.format(if_name) + return str_path, data + + def parse(self, json_data : Dict): + # TODO Implement + return [] diff --git a/src/device/service/drivers/restconf/handlers/NetworkInstance.py b/src/device/service/drivers/restconf/handlers/NetworkInstance.py new file mode 100644 index 000000000..9d6ad94cc --- /dev/null +++ b/src/device/service/drivers/restconf/handlers/NetworkInstance.py @@ -0,0 +1,117 @@ +import json, logging +from typing import Dict, Tuple + +from ._Handler import _Handler + + +MAP_NETWORK_INSTANCE_TYPE = { + # special routing instance; acts as default/global routing instance for a network device + 'DEFAULT': 'openconfig-network-instance-types:DEFAULT_INSTANCE', + + # private L3-only routing instance; formed of one or more RIBs + 'L3VRF': 'openconfig-network-instance-types:L3VRF', + + # private L2-only switch instance; formed of one or more L2 forwarding tables + 'L2VSI': 'openconfig-network-instance-types:L2VSI', + + # private L2-only forwarding instance; point to point connection between two endpoints + 'L2P2P': 'openconfig-network-instance-types:L2P2P', + + # private Layer 2 and Layer 3 forwarding instance + 'L2L3': 'openconfig-network-instance-types:L2L3', +} + +class NetworkInstanceHandler(_Handler): + def __init__(self): + self.__logger = logging.getLogger(str(__name__)) + + def get_resource_key(self): return '/network_instance' + def get_path(self): return '/ocni:network-instances' + + def compose(self, resource_key : str, resource_value : Dict, delete : bool = False) -> Tuple[str, Dict]: + ni_name = resource_value.get('name') + + if delete: + PATH_TMPL = '/network-instances/network-instance={:s}' + str_path = PATH_TMPL.format(ni_name) + str_data = json.dumps({}) + return str_path, str_data + + ni_type = resource_value.get('type') # L3VRF / L2VSI / ... + ni_type = MAP_NETWORK_INSTANCE_TYPE.get(ni_type, ni_type) + + str_path = '/network-instances/network-instance={:s}'.format(ni_name) + + data = { 'name': ni_name, 'config': { 'type': ni_type } } + + return str_path, data + + def parse(self, json_data : Dict): + self.__logger.debug('json_data = {:s}'.format(json.dumps(json_data))) + + entries = [] + for network_instance in json_data['network-instances']: + self.__logger.debug('network_instance={:s}'.format(str(network_instance))) + ni_name = network_instance['name'] + + ni_config = network_instance['config'] + ni_type = ni_config['type'].split(':')[-1] + + _net_inst = {'name': ni_name, 'type': ni_type} + entry_net_inst_key = '/network_instance[{:s}]'.format(ni_name) + entries.append((entry_net_inst_key, _net_inst)) + + for ni_interface in network_instance.get('interfaces', []): + ni_if_config = ni_interface['config'] + ni_if_name = ni_if_config['interface'] + ni_sif_index = ni_if_config['subinterface'] + ni_if_id = '{:s}.{:d}'.format(ni_if_name, ni_sif_index) + + _interface = {'name': ni_name, 'id': ni_if_id, 'if_name': ni_if_name, 'sif_index': ni_sif_index} + entry_interface_key = '{:s}/interface[{:s}]'.format(entry_net_inst_key, ni_if_id) + entries.append((entry_interface_key, _interface)) + + for ni_protocol in network_instance.get('protocols', []): + ni_protocol_id = ni_protocol['identifier'].split(':')[-1] + ni_protocol_name = ni_protocol['name'] + + _protocol = {'name': ni_name, 'identifier': ni_protocol_id, 'protocol_name': ni_protocol_name} + entry_protocol_key = '{:s}/protocols[{:s}]'.format(entry_net_inst_key, ni_protocol_id) + entries.append((entry_protocol_key, _protocol)) + + static_routes = ni_protocol.get('static-routes', []) + for static_route in static_routes: + static_route_prefix = static_route['prefix'] + for next_hop in static_route.get('next-hops', []): + static_route_metric = int(next_hop['config']['metric']) + _static_route = { + 'prefix' : static_route_prefix, + 'next_hop': next_hop['config']['next-hop'], + 'metric' : static_route_metric, + } + _static_route.update(_protocol) + entry_static_route_key = '{:s}/static_route[{:s}:{:d}]'.format( + entry_protocol_key, static_route_prefix, static_route_metric + ) + entries.append((entry_static_route_key, _static_route)) + + for ni_table in network_instance.get('tables', []): + ni_table_protocol = ni_table['protocol'].split(':')[-1] + ni_table_address_family = ni_table['address-family'].split(':')[-1] + _table = {'protocol': ni_table_protocol, 'address_family': ni_table_address_family} + entry_table_key = '{:s}/table[{:s},{:s}]'.format( + entry_net_inst_key, ni_table_protocol, ni_table_address_family + ) + entries.append((entry_table_key, _table)) + + for ni_vlan in network_instance.get('vxlans', []): + ni_vlan_id = ni_vlan['config']['id'] + + ni_vlan_name = ni_vlan['name'] + + _members = [ ni_vlan['config']['link'] ] + _vlan = {'vlan_id': ni_vlan_id, 'name': ni_vlan_name, 'members': _members} + entry_vlan_key = '{:s}/vlan[{:d}]'.format(entry_net_inst_key, ni_vlan_id) + entries.append((entry_vlan_key, _vlan)) + + return entries diff --git a/src/device/service/drivers/restconf/handlers/NetworkInstanceInterface.py b/src/device/service/drivers/restconf/handlers/NetworkInstanceInterface.py new file mode 100644 index 000000000..a27b04f42 --- /dev/null +++ b/src/device/service/drivers/restconf/handlers/NetworkInstanceInterface.py @@ -0,0 +1,51 @@ +# Copyright 2022-2024 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 typing import Any, Dict, List, Tuple +from ._Handler import _Handler + + +IS_CEOS = True + +class NetworkInstanceInterfaceHandler(_Handler): + def __init__(self): + self.__logger = logging.getLogger(str(__name__)) + + def get_resource_key(self) -> str: return '/network_instance/interface' + def get_path(self) -> str: return '/openconfig-network-instance:network-instances/network-instance/interfaces' + + def compose(self, resource_key : str, resource_value : Dict, delete : bool = False) -> Tuple[str, str]: + ni_name = resource_value.get('name') + ni_if_id = resource_value.get('id') + if_name = resource_value.get('interface') + sif_index = int(resource_value.get('subinterface', 0)) + + if IS_CEOS: ni_if_id = if_name + + if delete: + PATH_TMPL = '/network-instances/network-instance={:s}/interfaces/interface={:s}' + str_path = PATH_TMPL.format(ni_name, ni_if_id) + return str_path, {} + + str_path = '/network-instances/network-instance={:s}/interfaces/interface={:s}'.format( + ni_name, ni_if_id + ) + + data = { + 'id': ni_if_id, + 'config': {'id': ni_if_id, 'interface': if_name, 'subinterface': sif_index}, + } + + return str_path, data diff --git a/src/device/service/drivers/restconf/handlers/NetworkInstanceProtocol.py b/src/device/service/drivers/restconf/handlers/NetworkInstanceProtocol.py new file mode 100644 index 000000000..8b65cfce2 --- /dev/null +++ b/src/device/service/drivers/restconf/handlers/NetworkInstanceProtocol.py @@ -0,0 +1,46 @@ +# Copyright 2022-2024 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 typing import Any, Dict, List, Tuple +from ._Handler import _Handler + + +class NetworkInstanceProtocolHandler(_Handler): + def __init__(self): + self.__logger = logging.getLogger(str(__name__)) + + def get_resource_key(self) -> str: return '/network_instance/protocols' + def get_path(self) -> str: + return '/openconfig-network-instance:network-instances/network-instance/protocols/protocol' + + def compose(self, resource_key : str, resource_value : Dict, delete : bool = False) -> Tuple[str, str]: + ni_name = resource_value.get('name') + identifier = resource_value.get('identifier') + proto_name = resource_value.get('protocol_name') + + if ':' not in identifier: + identifier = 'openconfig-policy-types:{:s}'.format(identifier) + PATH_TMPL = '/network-instances/network-instance={:s}/protocols/protocol={:s}' + str_path = PATH_TMPL.format(ni_name, proto_name) + + if delete: + return str_path, {} + + data = { + 'identifier': identifier, 'name': proto_name, + 'config': {'identifier': identifier, 'name': proto_name, 'enabled': True} + } + + return str_path, data diff --git a/src/device/service/drivers/restconf/handlers/NetworkInstanceStaticRoute.py b/src/device/service/drivers/restconf/handlers/NetworkInstanceStaticRoute.py new file mode 100644 index 000000000..935a823a3 --- /dev/null +++ b/src/device/service/drivers/restconf/handlers/NetworkInstanceStaticRoute.py @@ -0,0 +1,50 @@ +# Copyright 2022-2024 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 typing import Any, Dict, List, Tuple +from ._Handler import _Handler + + +class NetworkInstanceStaticRouteHandler(_Handler): + def __init__(self): + self.__logger = logging.getLogger(str(__name__)) + + def get_resource_key(self) -> str: return '/network_instance/protocols/static_route' + def get_path(self) -> str: + return '/openconfig-network-instance:network-instances/network-instance/protocols/protocol/static-routes' + + def compose(self, resource_key : str, resource_value : Dict, delete : bool = False) -> Tuple[str, str]: + ni_name = resource_value.get('name') + proto_name = resource_value.get('protocol_name') + prefix = resource_value.get('prefix') + + PATH_TMPL = '/network-instances/network-instance={:s}/protocols/protocol={:s}/static-routes/static-route?static_route={:s}' + str_path = PATH_TMPL.format(ni_name, proto_name, prefix) + + if delete: + return str_path, {} + + data = { 'prefix': prefix } + + next_hops = resource_value.get('next_hops') + interface = resource_value.get('interface') + + if next_hops is not None: + data['next-hops'] = [{ 'config': {'metric': int(route.get('metric')), 'next-hop': route.get('next_hops')} } for route in next_hops] + + if interface is not None: + data['interface'] = interface + + return str_path, data diff --git a/src/device/service/drivers/restconf/handlers/NetworkInstanceVlan.py b/src/device/service/drivers/restconf/handlers/NetworkInstanceVlan.py new file mode 100644 index 000000000..998547984 --- /dev/null +++ b/src/device/service/drivers/restconf/handlers/NetworkInstanceVlan.py @@ -0,0 +1,52 @@ +# Copyright 2022-2024 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 typing import Any, Dict, List, Tuple +from ._Handler import _Handler + + +class NetworkInstanceVlanHandler(_Handler): + def __init__(self): + self.__logger = logging.getLogger(str(__name__)) + + def get_resource_key(self) -> str: return '/network_instance/vxlan' + def get_path(self) -> str: + return '/openconfig-network-instance:network-instances/network-instance/vxlans' + + def compose(self, resource_key : str, resource_value : Dict, delete : bool = False) -> Tuple[str, str]: + config = resource_value.get('config') + + identifier = config.get('id') + ip = resource_value.get('ip') + src_if = config.get('source-interface') + ni_name = config.get('network_instance') + + vlan_name = f"vlan{identifier}" + + PATH_TMPL = '/network-instances/network-instance={:s}/vxlans/vxlan={:s}' + str_path = PATH_TMPL.format(ni_name, vlan_name) + + if delete: + return str_path, {} + + data = { + 'config': { + "source-interface": src_if, + "id": identifier, + "ip": ip + } + } + + return str_path, data diff --git a/src/device/service/drivers/restconf/handlers/_Handler.py b/src/device/service/drivers/restconf/handlers/_Handler.py new file mode 100644 index 000000000..de3589b4f --- /dev/null +++ b/src/device/service/drivers/restconf/handlers/_Handler.py @@ -0,0 +1,23 @@ +from typing import Any, Dict, List, Tuple + + +class _Handler: + def get_resource_key(self) -> str: + # Retrieve the TeraFlowSDN resource_key path schema used to point this handler + raise NotImplementedError() + + def get_path(self) -> str: + # Retrieve the OpenConfig path schema used to interrogate the device + raise NotImplementedError() + + def compose( + self, resource_key : str, resource_value : Dict, delete : bool = False + ) -> Tuple[str, str]: + # Compose a Set/Delete message based on the resource_key/resource_value fields, and the delete flag + raise NotImplementedError() + + def parse( + self, json_data : Dict + ) -> List[Tuple[str, Dict[str, Any]]]: + # Parse a Reply from the device and return a list of resource_key/resource_value pairs + raise NotImplementedError() diff --git a/src/device/service/drivers/restconf/handlers/__init__.py b/src/device/service/drivers/restconf/handlers/__init__.py new file mode 100644 index 000000000..987ae8959 --- /dev/null +++ b/src/device/service/drivers/restconf/handlers/__init__.py @@ -0,0 +1,125 @@ +import re +import logging +from typing import Optional, Any, Union, Dict, List, Tuple + +from device.service.driver_api._Driver import ( + RESOURCE_ENDPOINTS, RESOURCE_INTERFACES, RESOURCE_NETWORK_INSTANCES, RESOURCE_ROUTING_POLICIES, RESOURCE_ACL, RESOURCE_INVENTORY) + +from ._Handler import _Handler +from .Interface import InterfaceHandler +from .InterfaceLimit import InterfaceLimitHandler +from .Component import ComponentHandler +from .NetworkInstance import NetworkInstanceHandler +from .NetworkInstanceInterface import NetworkInstanceInterfaceHandler +from .NetworkInstanceProtocol import NetworkInstanceProtocolHandler +from .NetworkInstanceStaticRoute import NetworkInstanceStaticRouteHandler +from .NetworkInstanceVlan import NetworkInstanceVlanHandler + + +LOGGER = logging.getLogger(__name__) + +comph = ComponentHandler() +ifh = InterfaceHandler() +iflh = InterfaceLimitHandler() +nih = NetworkInstanceHandler() +niifh = NetworkInstanceInterfaceHandler() +niph = NetworkInstanceProtocolHandler() +nisrh = NetworkInstanceStaticRouteHandler() +nivh = NetworkInstanceVlanHandler() + +ALL_RESOURCE_KEYS = [ + RESOURCE_ENDPOINTS, + RESOURCE_INTERFACES, + RESOURCE_NETWORK_INSTANCES, +] + +RESOURCE_KEY_MAPPER = { + RESOURCE_ENDPOINTS : comph.get_resource_key(), + RESOURCE_INTERFACES : ifh.get_resource_key(), + RESOURCE_NETWORK_INSTANCES : nih.get_resource_key(), +} + +PATH_MAPPER = { + '/components' : comph.get_path(), + '/components/component' : comph.get_path(), + '/interfaces' : ifh.get_path(), + '/network-instances' : nih.get_path(), +} + +RESOURCE_KEY_TO_HANDLER = { + comph.get_resource_key() : comph, + ifh.get_resource_key() : ifh, + iflh.get_resource_key() : iflh, + nih.get_resource_key() : nih, + niifh.get_resource_key() : niifh, + niph.get_resource_key() : niph, + nisrh.get_resource_key() : nisrh, + nivh.get_resource_key() : nivh, +} + +PATH_TO_HANDLER = { + comph.get_path() : comph, + ifh.get_path() : ifh, + iflh.get_path() : iflh, + nih.get_path() : nih, + niifh.get_path() : niifh, + niph.get_path() : niph, + nisrh.get_path() : nisrh, + nivh.get_path() : nivh, +} + + +RE_REMOVE_FILTERS = re.compile(r'\[[^\]]+\]') +RE_REMOVE_NAMESPACES = re.compile(r'\/[a-zA-Z0-9\_\-]+:') + +def get_schema(resource_key : str): + resource_key = RE_REMOVE_FILTERS.sub('', resource_key) + resource_key = RE_REMOVE_NAMESPACES.sub('/', resource_key) + return resource_key + +def get_handler( + resource_key : Optional[str] = None, path : Optional[str] = None, + raise_if_not_found=True +) -> Optional[_Handler]: + if (resource_key is None) == (path is None): + MSG = 'Exactly one of resource_key({:s}) or path({:s}) must be specified' + raise Exception(MSG.format(str(resource_key), str(path))) # pylint: disable=broad-exception-raised + if resource_key is not None: + resource_key_schema = get_schema(resource_key) + resource_key_schema = RESOURCE_KEY_MAPPER.get(resource_key_schema, resource_key_schema) + handler = RESOURCE_KEY_TO_HANDLER.get(resource_key_schema) + if handler is None and raise_if_not_found: + MSG = 'Handler not found: resource_key={:s} resource_key_schema={:s}' + # pylint: disable=broad-exception-raised + raise Exception(MSG.format(str(resource_key), str(resource_key_schema))) + elif path is not None: + path_schema = get_schema(path) + path_schema = PATH_MAPPER.get(path_schema, path_schema) + handler = PATH_TO_HANDLER.get(path_schema) + if handler is None and raise_if_not_found: + MSG = 'Handler not found: path={:s} path_schema={:s}' + # pylint: disable=broad-exception-raised + raise Exception(MSG.format(str(path), str(path_schema))) + return handler + +def get_path(resource_key : str) -> str: + handler = get_handler(resource_key=resource_key) + return handler.get_path() + +def parse(str_path : str, value : Union[Dict, List]) -> List[Tuple[str, Dict[str, Any]]]: + handler = get_handler(path=str_path) + value = validate_data(value) + return handler.parse(validate_data(value)) + +def validate_data(value : Any) -> Union[Dict, List]: + if type(value) == dict: + return { ckey.split(':')[-1]: validate_data(cvalue) for ckey, cvalue in value.items()} + + if type(value) == list: + return [ validate_data(data) for data in value ] + + return value + +def compose(resource_key : str, resource_value : Union[Dict, List], delete : bool = False) -> Tuple[str, str]: + handler = get_handler(resource_key=resource_key, raise_if_not_found=False) + return handler.compose(resource_key, resource_value, delete=(delete or len(resource_value) == 0)) if handler is not None else None \ No newline at end of file -- GitLab From 1ba46b6e3ae7eb92a3e2d589ed0c5aec4f4ab5f0 Mon Sep 17 00:00:00 2001 From: PedroDuarte536 Date: Thu, 24 Jul 2025 19:14:15 +0100 Subject: [PATCH 3/4] add initial restconf service handler implementation --- .../l3nm_restconf/ConfigRules.py | 316 ++++++++++++++++++ .../L3NMRestconfServiceHandler.py | 166 +++++++++ .../l3nm_restconf/__init__.py | 14 + 3 files changed, 496 insertions(+) create mode 100644 src/service/service/service_handlers/l3nm_restconf/ConfigRules.py create mode 100644 src/service/service/service_handlers/l3nm_restconf/L3NMRestconfServiceHandler.py create mode 100644 src/service/service/service_handlers/l3nm_restconf/__init__.py diff --git a/src/service/service/service_handlers/l3nm_restconf/ConfigRules.py b/src/service/service/service_handlers/l3nm_restconf/ConfigRules.py new file mode 100644 index 000000000..4ce6426c1 --- /dev/null +++ b/src/service/service/service_handlers/l3nm_restconf/ConfigRules.py @@ -0,0 +1,316 @@ +# Copyright 2022-2024 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 typing import Any, Dict, List, Optional, Tuple +from common.tools.object_factory.ConfigRule import json_config_rule_delete, json_config_rule_set +from service.service.service_handler_api.AnyTreeTools import TreeNode + + +LOGGER = logging.getLogger(__name__) + +def get_value(field_name : str, *containers, default=None) -> Optional[Any]: + if len(containers) == 0: raise Exception('No containers specified') + for container in containers: + if field_name not in container: continue + return container[field_name] + return default + +def setup_config_rules( + service_uuid : str, connection_uuid : str, device_uuid : str, endpoint_uuid : str, endpoint_name : str, + service_settings : TreeNode, device_settings : TreeNode, endpoint_settings : TreeNode, endpoint_acls : List [Tuple] +) -> List[Dict]: + + LOGGER.info(f"service_settings : {service_settings}") + LOGGER.info(f"endpoint_settings : {endpoint_settings}") + + if service_settings is None: return [] + if device_settings is None: + device_settings = TreeNode('settings') + device_settings.value = {} + if endpoint_settings is None: return [] + + json_settings : Dict = service_settings.value + json_device_settings : Dict = device_settings.value + json_endpoint_settings : Dict = endpoint_settings.value + + settings = (json_settings, json_endpoint_settings, json_device_settings) + + mtu = get_value('mtu', *settings, default=1450) # 1512 + #address_families = json_settings.get('address_families', [] ) # ['IPV4'] + bgp_as = get_value('bgp_as', *settings, default=65000) # 65000 + + router_id = json_endpoint_settings.get('router_id', '0.0.0.0') # '10.95.0.10' + route_distinguisher = json_settings.get('route_distinguisher', '65000:101' ) # '60001:801' + sub_interface_index = json_endpoint_settings.get('sub_interface_index', 0 ) # 1 + vlan_id = json_endpoint_settings.get('vlan_id', 1 ) # 400 + address_ip = json_endpoint_settings.get('address_ip', '0.0.0.0') # '2.2.2.1' + address_prefix = json_endpoint_settings.get('address_prefix', 24 ) # 30 + + policy_import = json_endpoint_settings.get('policy_AZ', '2' ) # 2 + policy_export = json_endpoint_settings.get('policy_ZA', '7' ) # 30 + #network_interface_desc = '{:s}-NetIf'.format(service_uuid) + network_interface_desc = json_endpoint_settings.get('ni_description','') + #network_subinterface_desc = '{:s}-NetSubIf'.format(service_uuid) + network_subinterface_desc = json_endpoint_settings.get('subif_description','') + #service_short_uuid = service_uuid.split('-')[-1] + #network_instance_name = '{:s}-NetInst'.format(service_short_uuid) + network_instance_name = json_endpoint_settings.get('ni_name', service_uuid.split('-')[-1]) #ELAN-AC:1 + + mtu = int(mtu) + bgp_as = int(bgp_as) + sub_interface_index = int(sub_interface_index) + vlan_id = int(vlan_id) + address_prefix = int(address_prefix) + + + if_subif_name = '{:s}.{:d}'.format(endpoint_name, vlan_id) + + json_config_rules = [ + # Configure Interface (not used) + #json_config_rule_set( + # '/interface[{:s}]'.format(endpoint_name), { + # 'name': endpoint_name, + # 'description': network_interface_desc, + # 'mtu': mtu, + #}), + + #Create network instance + json_config_rule_set( + '/network_instance[{:s}]'.format(network_instance_name), { + 'name': network_instance_name, + 'description': network_interface_desc, + 'type': 'L3VRF', + 'route_distinguisher': route_distinguisher, + #'router_id': router_id, + #'address_families': address_families, + }), + + #Add BGP protocol to network instance + json_config_rule_set( + '/network_instance[{:s}]/protocols[BGP]'.format(network_instance_name), { + 'name': network_instance_name, + 'protocol_name': 'BGP', + 'identifier': 'BGP', + 'type': 'L3VRF', + 'as': bgp_as, + 'router_id': router_id, + }), + + #Add DIRECTLY CONNECTED protocol to network instance + json_config_rule_set( + '/network_instance[{:s}]/protocols[DIRECTLY_CONNECTED]'.format(network_instance_name), { + 'name': network_instance_name, + 'identifier': 'DIRECTLY_CONNECTED', + 'protocol_name': 'DIRECTLY_CONNECTED', + }), + + #Add STATIC protocol to network instance + json_config_rule_set( + '/network_instance[{:s}]/protocols[STATIC]'.format(network_instance_name), { + 'name': network_instance_name, + 'identifier': 'STATIC', + 'protocol_name': 'STATIC', + }), + + #Create interface with subinterface + json_config_rule_set( + '/interface[{:s}]/subinterface[{:d}]'.format(if_subif_name, sub_interface_index), { + 'name' : if_subif_name, + 'type' :'l3ipvlan', + 'mtu' : mtu, + 'index' : sub_interface_index, + 'description' : network_subinterface_desc, + 'vlan_id' : vlan_id, + 'address_ip' : address_ip, + 'address_prefix': address_prefix, + }), + + #Associate interface to network instance + json_config_rule_set( + '/network_instance[{:s}]/interface[{:s}]'.format(network_instance_name, if_subif_name), { + 'name' : network_instance_name, + 'type' : 'L3VRF', + 'id' : if_subif_name, + 'interface' : if_subif_name, + 'subinterface': sub_interface_index, + }), + + # TODO Validate need + #Create routing policy + # json_config_rule_set( + # '/routing_policy/bgp_defined_set[{:s}_rt_import][{:s}]'.format(policy_import,route_distinguisher), { + # 'ext_community_set_name': 'set_{:s}'.format(policy_import), + # 'ext_community_member' : route_distinguisher, + # }), + # json_config_rule_set( + # # pylint: disable=duplicate-string-formatting-argument + # '/routing_policy/policy_definition[{:s}_import]/statement[{:s}]'.format(policy_import, policy_import), { + # 'policy_name' : policy_import, + # 'statement_name' : 'stm_{:s}'.format(policy_import), + # 'ext_community_set_name': 'set_{:s}'.format(policy_import), + # 'policy_result' : 'ACCEPT_ROUTE', + # }), + + #Associate routing policy to network instance + # json_config_rule_set( + # '/network_instance[{:s}]/inter_instance_policies[{:s}]'.format(network_instance_name, policy_import), { + # 'name' : network_instance_name, + # 'import_policy': policy_import, + # }), + + # TODO Validate need + #Create routing policy + # json_config_rule_set( + # '/routing_policy/bgp_defined_set[{:s}_rt_export][{:s}]'.format(policy_export, route_distinguisher), { + # 'ext_community_set_name': 'set_{:s}'.format(policy_export), + # 'ext_community_member' : route_distinguisher, + # }), + # json_config_rule_set( + # # pylint: disable=duplicate-string-formatting-argument + # '/routing_policy/policy_definition[{:s}_export]/statement[{:s}]'.format(policy_export, policy_export), { + # 'policy_name' : policy_export, + # 'statement_name' : 'stm_{:s}'.format(policy_export), + # 'ext_community_set_name': 'set_{:s}'.format(policy_export), + # 'policy_result' : 'ACCEPT_ROUTE', + # }), + + # #Associate routing policy to network instance + # json_config_rule_set( + # '/network_instance[{:s}]/inter_instance_policies[{:s}]'.format(network_instance_name, policy_export),{ + # 'name' : network_instance_name, + # 'export_policy': policy_export, + # }), + + #Create table connections + # json_config_rule_set( + # '/network_instance[{:s}]/table_connections[DIRECTLY_CONNECTED][BGP][IPV4]'.format(network_instance_name), { + # 'name' : network_instance_name, + # 'src_protocol' : 'DIRECTLY_CONNECTED', + # 'dst_protocol' : 'BGP', + # 'address_family' : 'IPV4', + # 'default_import_policy': 'ACCEPT_ROUTE', + # }), + + # json_config_rule_set( + # '/network_instance[{:s}]/table_connections[STATIC][BGP][IPV4]'.format(network_instance_name), { + # 'name' : network_instance_name, + # 'src_protocol' : 'STATIC', + # 'dst_protocol' : 'BGP', + # 'address_family' : 'IPV4', + # 'default_import_policy': 'ACCEPT_ROUTE', + # }), + + ] + + for res_key, res_value in endpoint_acls: + json_config_rules.append( + {'action': 1, 'acl': res_value} + ) + return json_config_rules + +def teardown_config_rules( + service_uuid : str, connection_uuid : str, device_uuid : str, endpoint_uuid : str, endpoint_name : str, + service_settings : TreeNode, device_settings : TreeNode, endpoint_settings : TreeNode +) -> List[Dict]: + + if service_settings is None: return [] + if device_settings is None: return [] + if endpoint_settings is None: return [] + + json_settings : Dict = service_settings.value + json_device_settings : Dict = device_settings.value + json_endpoint_settings : Dict = endpoint_settings.value + + settings = (json_settings, json_endpoint_settings, json_device_settings) + + service_short_uuid = service_uuid.split('-')[-1] + network_instance_name = '{:s}-NetInst'.format(service_short_uuid) + #network_interface_desc = '{:s}-NetIf'.format(service_uuid) + #network_subinterface_desc = '{:s}-NetSubIf'.format(service_uuid) + + #mtu = get_value('mtu', *settings, default=1450) # 1512 + #address_families = json_settings.get('address_families', [] ) # ['IPV4'] + #bgp_as = get_value('bgp_as', *settings, default=65000) # 65000 + route_distinguisher = json_settings.get('route_distinguisher', '0:0' ) # '60001:801' + #sub_interface_index = json_endpoint_settings.get('sub_interface_index', 0 ) # 1 + #router_id = json_endpoint_settings.get('router_id', '0.0.0.0') # '10.95.0.10' + vlan_id = json_endpoint_settings.get('vlan_id', 1 ) # 400 + #address_ip = json_endpoint_settings.get('address_ip', '0.0.0.0') # '2.2.2.1' + #address_prefix = json_endpoint_settings.get('address_prefix', 24 ) # 30 + policy_import = json_endpoint_settings.get('policy_AZ', '2' ) # 2 + policy_export = json_endpoint_settings.get('policy_ZA', '7' ) # 30 + + if_subif_name = '{:s}.{:d}'.format(endpoint_name, vlan_id) + + json_config_rules = [ + #Delete table connections + # json_config_rule_delete( + # '/network_instance[{:s}]/table_connections[DIRECTLY_CONNECTED][BGP][IPV4]'.format(network_instance_name),{ + # 'name' : network_instance_name, + # 'src_protocol' : 'DIRECTLY_CONNECTED', + # 'dst_protocol' : 'BGP', + # 'address_family': 'IPV4', + # }), + + + # json_config_rule_delete( + # '/network_instance[{:s}]/table_connections[STATIC][BGP][IPV4]'.format(network_instance_name), { + # 'name' : network_instance_name, + # 'src_protocol' : 'STATIC', + # 'dst_protocol' : 'BGP', + # 'address_family': 'IPV4', + # }), + + + # TODO Validate need + #Delete export routing policy + # json_config_rule_delete( + # '/routing_policy/policy_definition[{:s}_export]'.format(network_instance_name), { + # 'policy_name': '{:s}_export'.format(network_instance_name), + # }), + # json_config_rule_delete( + # '/routing_policy/bgp_defined_set[{:s}_rt_export][{:s}]'.format(policy_export, route_distinguisher), { + # 'ext_community_set_name': 'set_{:s}'.format(policy_export), + # }), + + # #Delete import routing policy + + # json_config_rule_delete( + # '/routing_policy/policy_definition[{:s}_import]'.format(network_instance_name), { + # 'policy_name': '{:s}_import'.format(network_instance_name), + # }), + # json_config_rule_delete( + # '/routing_policy/bgp_defined_set[{:s}_rt_import][{:s}]'.format(policy_import, route_distinguisher), { + # 'ext_community_set_name': 'set_{:s}'.format(policy_import), + # }), + + #Delete interface; automatically deletes: + # - /interface[]/subinterface[] + json_config_rule_delete('/interface[{:s}]/subinterface[0]'.format(if_subif_name), + { + 'name': if_subif_name, + }), + + #Delete network instance; automatically deletes: + # - /network_instance[]/interface[] + # - /network_instance[]/protocols[] + # - /network_instance[]/inter_instance_policies[] + json_config_rule_delete('/network_instance[{:s}]'.format(network_instance_name), + { + 'name': network_instance_name + }), + ] + return json_config_rules diff --git a/src/service/service/service_handlers/l3nm_restconf/L3NMRestconfServiceHandler.py b/src/service/service/service_handlers/l3nm_restconf/L3NMRestconfServiceHandler.py new file mode 100644 index 000000000..eb72a72c3 --- /dev/null +++ b/src/service/service/service_handlers/l3nm_restconf/L3NMRestconfServiceHandler.py @@ -0,0 +1,166 @@ +# Copyright 2022-2024 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, logging +from typing import Any, List, Optional, Tuple, Union +from common.method_wrappers.Decorator import MetricsPool, metered_subclass_method +from common.proto.context_pb2 import ConfigRule, DeviceId, Service +from common.tools.object_factory.Device import json_device_id +from common.type_checkers.Checkers import chk_type +from service.service.service_handler_api.Tools import get_device_endpoint_uuids, get_endpoint_matching +from service.service.service_handler_api._ServiceHandler import _ServiceHandler +from service.service.service_handler_api.SettingsHandler import SettingsHandler +from service.service.task_scheduler.TaskExecutor import TaskExecutor +from .ConfigRules import setup_config_rules, teardown_config_rules + +LOGGER = logging.getLogger(__name__) + +METRICS_POOL = MetricsPool('Service', 'Handler', labels={'handler': 'l3nm_restconf'}) + +class L3NMRestconfServiceHandler(_ServiceHandler): + def __init__( # pylint: disable=super-init-not-called + self, service : Service, task_executor : TaskExecutor, **settings + ) -> None: + self.__service = service + self.__task_executor = task_executor + self.__settings_handler = SettingsHandler(service.service_config, **settings) + + @metered_subclass_method(METRICS_POOL) + def SetEndpoint( + self, endpoints : List[Tuple[str, str, Optional[str]]], connection_uuid : Optional[str] = None + ) -> List[Union[bool, Exception]]: + chk_type('endpoints', endpoints, list) + if len(endpoints) == 0: return [] + + service_uuid = self.__service.service_id.service_uuid.uuid + settings = self.__settings_handler.get('/settings') + + results = [] + for endpoint in endpoints: + try: + device_uuid, endpoint_uuid = get_device_endpoint_uuids(endpoint) + + device_obj = self.__task_executor.get_device(DeviceId(**json_device_id(device_uuid))) + device_settings = self.__settings_handler.get_device_settings(device_obj) + endpoint_obj = get_endpoint_matching(device_obj, endpoint_uuid) + endpoint_settings = self.__settings_handler.get_endpoint_settings(device_obj, endpoint_obj) + endpoint_acls = self.__settings_handler.get_endpoint_acls(device_obj, endpoint_obj) + endpoint_name = endpoint_obj.name + + json_config_rules = setup_config_rules( + service_uuid, connection_uuid, device_uuid, endpoint_uuid, endpoint_name, + settings, device_settings, endpoint_settings, endpoint_acls) + + LOGGER.info(f"json_config_rules : {json_config_rules}") + + if len(json_config_rules) > 0: + del device_obj.device_config.config_rules[:] + for json_config_rule in json_config_rules: + device_obj.device_config.config_rules.append(ConfigRule(**json_config_rule)) + self.__task_executor.configure_device(device_obj) + + results.append(True) + except Exception as e: # pylint: disable=broad-except + LOGGER.exception('Unable to SetEndpoint({:s})'.format(str(endpoint))) + results.append(e) + + return results + + @metered_subclass_method(METRICS_POOL) + def DeleteEndpoint( + self, endpoints : List[Tuple[str, str, Optional[str]]], connection_uuid : Optional[str] = None + ) -> List[Union[bool, Exception]]: + chk_type('endpoints', endpoints, list) + if len(endpoints) == 0: return [] + + service_uuid = self.__service.service_id.service_uuid.uuid + settings = self.__settings_handler.get('/settings') + + results = [] + for endpoint in endpoints: + try: + device_uuid, endpoint_uuid = get_device_endpoint_uuids(endpoint) + + device_obj = self.__task_executor.get_device(DeviceId(**json_device_id(device_uuid))) + device_settings = self.__settings_handler.get_device_settings(device_obj) + endpoint_obj = get_endpoint_matching(device_obj, endpoint_uuid) + endpoint_settings = self.__settings_handler.get_endpoint_settings(device_obj, endpoint_obj) + endpoint_name = endpoint_obj.name + + json_config_rules = teardown_config_rules( + service_uuid, connection_uuid, device_uuid, endpoint_uuid, endpoint_name, + settings, device_settings, endpoint_settings) + + if len(json_config_rules) > 0: + del device_obj.device_config.config_rules[:] + for json_config_rule in json_config_rules: + device_obj.device_config.config_rules.append(ConfigRule(**json_config_rule)) + self.__task_executor.configure_device(device_obj) + + results.append(True) + except Exception as e: # pylint: disable=broad-except + LOGGER.exception('Unable to DeleteEndpoint({:s})'.format(str(endpoint))) + results.append(e) + + return results + + @metered_subclass_method(METRICS_POOL) + def SetConstraint(self, constraints : List[Tuple[str, Any]]) -> List[Union[bool, Exception]]: + chk_type('constraints', constraints, list) + if len(constraints) == 0: return [] + + msg = '[SetConstraint] Method not implemented. Constraints({:s}) are being ignored.' + LOGGER.warning(msg.format(str(constraints))) + return [True for _ in range(len(constraints))] + + @metered_subclass_method(METRICS_POOL) + def DeleteConstraint(self, constraints : List[Tuple[str, Any]]) -> List[Union[bool, Exception]]: + chk_type('constraints', constraints, list) + if len(constraints) == 0: return [] + + msg = '[DeleteConstraint] Method not implemented. Constraints({:s}) are being ignored.' + LOGGER.warning(msg.format(str(constraints))) + return [True for _ in range(len(constraints))] + + @metered_subclass_method(METRICS_POOL) + def SetConfig(self, resources : List[Tuple[str, Any]]) -> List[Union[bool, Exception]]: + chk_type('resources', resources, list) + if len(resources) == 0: return [] + + results = [] + for resource in resources: + try: + resource_value = json.loads(resource[1]) + self.__settings_handler.set(resource[0], resource_value) + results.append(True) + except Exception as e: # pylint: disable=broad-except + LOGGER.exception('Unable to SetConfig({:s})'.format(str(resource))) + results.append(e) + + return results + + @metered_subclass_method(METRICS_POOL) + def DeleteConfig(self, resources : List[Tuple[str, Any]]) -> List[Union[bool, Exception]]: + chk_type('resources', resources, list) + if len(resources) == 0: return [] + + results = [] + for resource in resources: + try: + self.__settings_handler.delete(resource[0]) + except Exception as e: # pylint: disable=broad-except + LOGGER.exception('Unable to DeleteConfig({:s})'.format(str(resource))) + results.append(e) + + return results diff --git a/src/service/service/service_handlers/l3nm_restconf/__init__.py b/src/service/service/service_handlers/l3nm_restconf/__init__.py new file mode 100644 index 000000000..53d5157f7 --- /dev/null +++ b/src/service/service/service_handlers/l3nm_restconf/__init__.py @@ -0,0 +1,14 @@ +# Copyright 2022-2024 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. + -- GitLab From 59384a712bc9c104882cc329e659a409310e5961 Mon Sep 17 00:00:00 2001 From: PedroDuarte536 Date: Thu, 24 Jul 2025 19:17:16 +0100 Subject: [PATCH 4/4] add driver imports --- src/device/service/drivers/__init__.py | 10 ++++++++++ src/service/service/service_handlers/__init__.py | 7 +++++++ 2 files changed, 17 insertions(+) diff --git a/src/device/service/drivers/__init__.py b/src/device/service/drivers/__init__.py index 788b09edd..b34c54be9 100644 --- a/src/device/service/drivers/__init__.py +++ b/src/device/service/drivers/__init__.py @@ -252,3 +252,13 @@ if LOAD_ALL_DEVICE_DRIVERS: FilterFieldEnum.DRIVER : DeviceDriverEnum.DEVICEDRIVER_QKD, } ])) + +if LOAD_ALL_DEVICE_DRIVERS: + from .restconf.RestconfDriver import RestconfDriver + DRIVERS.append( + (RestconfDriver, [ + { + FilterFieldEnum.DEVICE_TYPE: DeviceTypeEnum.PACKET_ROUTER, + FilterFieldEnum.DRIVER : DeviceDriverEnum.DEVICEDRIVER_RESTCONF, + } + ])) \ No newline at end of file diff --git a/src/service/service/service_handlers/__init__.py b/src/service/service/service_handlers/__init__.py index 1aba88e30..7d6205e4e 100644 --- a/src/service/service/service_handlers/__init__.py +++ b/src/service/service/service_handlers/__init__.py @@ -24,6 +24,7 @@ from .l3nm_gnmi_openconfig.L3NMGnmiOpenConfigServiceHandler import L3NMGnmiOpenC from .l3nm_ietf_actn.L3NMIetfActnServiceHandler import L3NMIetfActnServiceHandler from .l3nm_nce.L3NMNCEServiceHandler import L3NMNCEServiceHandler from .l3slice_ietfslice.L3SliceIETFSliceServiceHandler import L3NMSliceIETFSliceServiceHandler +from .l3nm_restconf.L3NMRestconfServiceHandler import L3NMRestconfServiceHandler from .microwave.MicrowaveServiceHandler import MicrowaveServiceHandler from .p4_dummy_l1.p4_dummy_l1_service_handler import P4DummyL1ServiceHandler from .p4_fabric_tna_int.p4_fabric_tna_int_service_handler import P4FabricINTServiceHandler @@ -92,6 +93,12 @@ SERVICE_HANDLERS = [ FilterFieldEnum.DEVICE_DRIVER : DeviceDriverEnum.DEVICEDRIVER_IETF_SLICE, } ]), + (L3NMRestconfServiceHandler, [ + { + FilterFieldEnum.SERVICE_TYPE : ServiceTypeEnum.SERVICETYPE_L3NM, + FilterFieldEnum.DEVICE_DRIVER : DeviceDriverEnum.DEVICEDRIVER_RESTCONF, + } + ]), (TapiServiceHandler, [ { FilterFieldEnum.SERVICE_TYPE : ServiceTypeEnum.SERVICETYPE_TAPI_CONNECTIVITY_SERVICE, -- GitLab