Loading src/device/service/drivers/nce/driver.py +34 −57 Original line number Diff line number Diff line Loading @@ -12,30 +12,16 @@ # See the License for the specific language governing permissions and # limitations under the License. import json import logging import re import threading import anytree, copy, json, logging, re, requests, threading from typing import Any, Iterator, List, Optional, Tuple, Union import anytree import requests from requests.auth import HTTPBasicAuth from common.method_wrappers.Decorator import MetricsPool, metered_subclass_method from common.tools.client.RestConfClient import RestConfClient from common.type_checkers.Checkers import chk_length, chk_string, chk_type from device.service.driver_api._Driver import _Driver from device.service.driver_api.AnyTreeTools import ( TreeNode, dump_subtree, get_subnode, set_subnode_value, ) from device.service.driver_api.ImportTopologyEnum import ( ImportTopologyEnum, get_import_topology, TreeNode, dump_subtree, get_subnode, set_subnode_value, ) from .handlers.NetworkTopologyHandler import NetworkTopologyHandler from .Constants import SPECIAL_RESOURCE_MAPPINGS from .nce_fan_client import ( NCEClient, Loading @@ -44,43 +30,38 @@ from .nce_fan_client import ( ) from .Tools import compose_resource_endpoint LOGGER = logging.getLogger(__name__) RE_NCE_APP_FLOW_DATA = re.compile(r'^\/service\[[^\]]+\]\/AppFlow$') RE_NCE_APP_FLOW_OPERATION = re.compile(r'^\/service\[[^\]]+\]\/AppFlow\/operation$') DRIVER_NAME = 'nce' METRICS_POOL = MetricsPool('Device', 'Driver', labels={'driver': DRIVER_NAME}) class NCEDriver(_Driver): def __init__(self, address: str, port: str, **settings) -> None: super().__init__(DRIVER_NAME, address, int(port), **settings) def __init__(self, address: str, port: int, **settings) -> None: super().__init__(DRIVER_NAME, address, port, **settings) self.__lock = threading.Lock() self.__started = threading.Event() self.__terminate = threading.Event() restconf_settings = copy.deepcopy(settings) restconf_settings.pop('base_url', None) restconf_settings.pop('import_topology', None) restconf_settings['logger'] = logging.getLogger(__name__ + '.RestConfClient') self._rest_conf_client = RestConfClient(address, port=port, **restconf_settings) self._handler_net_topology = NetworkTopologyHandler(self._rest_conf_client, **settings) self.__running = TreeNode('.') scheme = self.settings.get('scheme', 'http') username = self.settings.get('username') password = self.settings.get('password') self.nce = NCEClient( self.address, self.port, scheme=scheme, username=username, password=password, ) self.__auth = None # ( # HTTPBasicAuth(username, password) # if username is not None and password is not None # else None # ) self.__tfs_nbi_root = '{:s}://{:s}:{:d}'.format(scheme, self.address, int(self.port)) self.__timeout = int(self.settings.get('timeout', 120)) self.__import_topology = get_import_topology( self.settings, default=ImportTopologyEnum.DEVICES self.address, self.port, scheme=scheme, username=username, password=password, ) endpoints = self.settings.get('endpoints', []) endpoint_resources = [] Loading Loading @@ -127,13 +108,14 @@ class NCEDriver(_Driver): def Connect(self) -> bool: with self.__lock: if self.__started.is_set(): return True if self.__started.is_set(): return True try: ... self._rest_conf_client._discover_base_url() except requests.exceptions.Timeout: LOGGER.exception('Timeout exception checking connectivity') return False except Exception: # pylint: disable=broad-except LOGGER.exception('Unhandled exception checking connectivity') return False else: self.__started.set() Loading @@ -150,28 +132,27 @@ class NCEDriver(_Driver): return [] @metered_subclass_method(METRICS_POOL) def GetConfig( self, resource_keys: List[str] = [] ) -> List[Tuple[str, Union[Any, None, Exception]]]: def GetConfig(self, resource_keys : List[str] = []) -> List[Tuple[str, Union[Any, None, Exception]]]: chk_type('resources', resource_keys, list) results = list() with self.__lock: if len(resource_keys) == 0: return dump_subtree(self.__running) results = [] resolver = anytree.Resolver(pathattr='name') for i, resource_key in enumerate(resource_keys): str_resource_name = 'resource_key[#{:d}]'.format(i) try: chk_string(str_resource_name, resource_key, allow_empty=False) if resource_key == RESOURCE_ENDPOINTS: results.extend(self._handler_net_topology.get()) else: resource_key = SPECIAL_RESOURCE_MAPPINGS.get(resource_key, resource_key) resource_path = resource_key.split('/') except Exception as e: # pylint: disable=broad-except LOGGER.exception( 'Exception validating {:s}: {:s}'.format( str_resource_name, str(resource_key) ) ) results.append((resource_key, e)) # if validation fails, store the exception MSG = 'Error processing resource_key({:s}, {:s})' LOGGER.exception(MSG.format(str_resource_name, str(resource_key))) results.append((resource_key, e)) # if processing fails, store the exception continue resource_node = get_subnode(resolver, self.__running, resource_path, default=None) Loading @@ -185,10 +166,7 @@ class NCEDriver(_Driver): @metered_subclass_method(METRICS_POOL) def SetConfig(self, resources: List[Tuple[str, Any]]) -> List[Union[bool, Exception]]: results = [] if len(resources) == 0: return results if len(resources) == 0: return results with self.__lock: for resource in resources: resource_key, resource_value = resource Loading Loading @@ -224,8 +202,7 @@ class NCEDriver(_Driver): @metered_subclass_method(METRICS_POOL) def DeleteConfig(self, resources: List[Tuple[str, Any]]) -> List[Union[bool, Exception]]: results = [] if len(resources) == 0: return results if len(resources) == 0: return results with self.__lock: for resource in resources: LOGGER.info('resource = {:s}'.format(str(resource))) Loading src/device/service/drivers/nce/handlers/NetworkTopologyHandler.py 0 → 100644 +162 −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 typing import Dict, List, Optional from common.Constants import DEFAULT_TOPOLOGY_NAME from common.DeviceTypes import DeviceTypeEnum from common.proto.context_pb2 import ( DEVICEDRIVER_UNDEFINED, DEVICEOPERATIONALSTATUS_DISABLED, DEVICEOPERATIONALSTATUS_ENABLED ) from common.tools.client.RestConfClient import RestConfClient from device.service.driver_api.ImportTopologyEnum import ( ImportTopologyEnum, get_import_topology ) LOGGER = logging.getLogger(__name__) class NetworkTopologyHandler: def __init__(self, rest_conf_client : RestConfClient, **settings) -> None: self._rest_conf_client = rest_conf_client self._subpath_root = '/ietf-network:networks' self._subpath_item = self._subpath_root + '/network={network_id:s}' # Options are: # disabled --> just import endpoints as usual # devices --> imports sub-devices but not links connecting them. # (a remotely-controlled transport domain might exist between them) # topology --> imports sub-devices and links connecting them. # (not supported by XR driver) self._import_topology = get_import_topology(settings, default=ImportTopologyEnum.TOPOLOGY) def get(self, network_id : Optional[str] = None) -> List[Dict]: if network_id is None: network_id = DEFAULT_TOPOLOGY_NAME endpoint = self._subpath_item.format(network_id=network_id) reply = self._rest_conf_client.get(endpoint) if 'ietf-network:network' not in reply: raise Exception('Malformed reply. "ietf-network:network" missing') networks = reply['ietf-network:network'] if len(networks) == 0: MSG = '[get] Network({:s}) not found; returning' LOGGER.debug(MSG.format(str(network_id))) return list() if len(networks) > 1: MSG = '[get] Multiple occurrences for Network({:s}); returning' LOGGER.debug(MSG.format(str(network_id))) return list() network = networks[0] MSG = '[get] import_topology={:s}' LOGGER.debug(MSG.format(str(self._import_topology))) result = list() if self._import_topology == ImportTopologyEnum.DISABLED: LOGGER.debug('[get] abstract controller; returning') return result device_type = DeviceTypeEnum.EMULATED_PACKET_SWITCH.value endpoint_type = '' if 'network-types' in network: nnt = network['network-types'] if 'ietf-te-topology:te-topology' in nnt: nnt_tet = nnt['ietf-te-topology:te-topology'] if 'ietf-otn-topology:otn-topology' in nnt_tet: device_type = DeviceTypeEnum.EMULATED_OPTICAL_ROADM.value endpoint_type = 'optical' elif 'ietf-eth-te-topology:eth-tran-topology' in nnt_tet: device_type = DeviceTypeEnum.EMULATED_PACKET_SWITCH.value endpoint_type = 'copper' elif 'ietf-l3-unicast-topology:l3-unicast-topology' in nnt_tet: device_type = DeviceTypeEnum.EMULATED_PACKET_ROUTER.value endpoint_type = 'copper' for node in network['node']: node_id = node['node-id'] node_name = node_id node_is_up = True if 'ietf-te-topology:te' in node: nte = node['ietf-te-topology:te'] if 'oper-status' in nte: node_is_up = nte['oper-status'] == 'up' if 'te-node-attributes' in nte: ntea = nte['te-node-attributes'] if 'name' in ntea: node_name = ntea['name'] device_url = '/devices/device[{:s}]'.format(node_id) device_data = { 'uuid': node_id, 'name': node_name, 'type': device_type, 'status': DEVICEOPERATIONALSTATUS_ENABLED if node_is_up else DEVICEOPERATIONALSTATUS_DISABLED, 'drivers': [DEVICEDRIVER_UNDEFINED], } result.append((device_url, device_data)) for tp in node['ietf-network-topology:termination-point']: tp_id = tp['tp-id'] tp_name = tp_id if 'ietf-te-topology:te' in tp: tpte = tp['ietf-te-topology:te'] if 'name' in tpte: tp_name = tpte['name'] endpoint_url = '/endpoints/endpoint[{:s}, {:s}]'.format(node_id, tp_id) endpoint_data = { 'device_uuid': node_id, 'uuid': tp_id, 'name': tp_name, 'type': endpoint_type, } result.append((endpoint_url, endpoint_data)) if self._import_topology == ImportTopologyEnum.DEVICES: LOGGER.debug('[get] devices only; returning') return result for link in network['ietf-network-topology:link']: link_uuid = link['link-id'] link_src = link['source'] link_dst = link['destination'] link_src_dev_id = link_src['source-node'] link_src_ep_id = link_src['source-tp'] link_dst_dev_id = link_dst['dest-node'] link_dst_ep_id = link_dst['dest-tp'] link_url = '/links/link[{:s}]'.format(link_uuid) link_endpoint_ids = [ (link_src_dev_id, link_src_ep_id), (link_dst_dev_id, link_dst_ep_id), ] link_data = { 'uuid': link_uuid, 'name': link_uuid, 'endpoints': link_endpoint_ids, } result.append((link_url, link_data)) LOGGER.debug('[get] topology; returning') return result src/device/service/drivers/nce/handlers/__init__.py 0 → 100644 +13 −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. Loading
src/device/service/drivers/nce/driver.py +34 −57 Original line number Diff line number Diff line Loading @@ -12,30 +12,16 @@ # See the License for the specific language governing permissions and # limitations under the License. import json import logging import re import threading import anytree, copy, json, logging, re, requests, threading from typing import Any, Iterator, List, Optional, Tuple, Union import anytree import requests from requests.auth import HTTPBasicAuth from common.method_wrappers.Decorator import MetricsPool, metered_subclass_method from common.tools.client.RestConfClient import RestConfClient from common.type_checkers.Checkers import chk_length, chk_string, chk_type from device.service.driver_api._Driver import _Driver from device.service.driver_api.AnyTreeTools import ( TreeNode, dump_subtree, get_subnode, set_subnode_value, ) from device.service.driver_api.ImportTopologyEnum import ( ImportTopologyEnum, get_import_topology, TreeNode, dump_subtree, get_subnode, set_subnode_value, ) from .handlers.NetworkTopologyHandler import NetworkTopologyHandler from .Constants import SPECIAL_RESOURCE_MAPPINGS from .nce_fan_client import ( NCEClient, Loading @@ -44,43 +30,38 @@ from .nce_fan_client import ( ) from .Tools import compose_resource_endpoint LOGGER = logging.getLogger(__name__) RE_NCE_APP_FLOW_DATA = re.compile(r'^\/service\[[^\]]+\]\/AppFlow$') RE_NCE_APP_FLOW_OPERATION = re.compile(r'^\/service\[[^\]]+\]\/AppFlow\/operation$') DRIVER_NAME = 'nce' METRICS_POOL = MetricsPool('Device', 'Driver', labels={'driver': DRIVER_NAME}) class NCEDriver(_Driver): def __init__(self, address: str, port: str, **settings) -> None: super().__init__(DRIVER_NAME, address, int(port), **settings) def __init__(self, address: str, port: int, **settings) -> None: super().__init__(DRIVER_NAME, address, port, **settings) self.__lock = threading.Lock() self.__started = threading.Event() self.__terminate = threading.Event() restconf_settings = copy.deepcopy(settings) restconf_settings.pop('base_url', None) restconf_settings.pop('import_topology', None) restconf_settings['logger'] = logging.getLogger(__name__ + '.RestConfClient') self._rest_conf_client = RestConfClient(address, port=port, **restconf_settings) self._handler_net_topology = NetworkTopologyHandler(self._rest_conf_client, **settings) self.__running = TreeNode('.') scheme = self.settings.get('scheme', 'http') username = self.settings.get('username') password = self.settings.get('password') self.nce = NCEClient( self.address, self.port, scheme=scheme, username=username, password=password, ) self.__auth = None # ( # HTTPBasicAuth(username, password) # if username is not None and password is not None # else None # ) self.__tfs_nbi_root = '{:s}://{:s}:{:d}'.format(scheme, self.address, int(self.port)) self.__timeout = int(self.settings.get('timeout', 120)) self.__import_topology = get_import_topology( self.settings, default=ImportTopologyEnum.DEVICES self.address, self.port, scheme=scheme, username=username, password=password, ) endpoints = self.settings.get('endpoints', []) endpoint_resources = [] Loading Loading @@ -127,13 +108,14 @@ class NCEDriver(_Driver): def Connect(self) -> bool: with self.__lock: if self.__started.is_set(): return True if self.__started.is_set(): return True try: ... self._rest_conf_client._discover_base_url() except requests.exceptions.Timeout: LOGGER.exception('Timeout exception checking connectivity') return False except Exception: # pylint: disable=broad-except LOGGER.exception('Unhandled exception checking connectivity') return False else: self.__started.set() Loading @@ -150,28 +132,27 @@ class NCEDriver(_Driver): return [] @metered_subclass_method(METRICS_POOL) def GetConfig( self, resource_keys: List[str] = [] ) -> List[Tuple[str, Union[Any, None, Exception]]]: def GetConfig(self, resource_keys : List[str] = []) -> List[Tuple[str, Union[Any, None, Exception]]]: chk_type('resources', resource_keys, list) results = list() with self.__lock: if len(resource_keys) == 0: return dump_subtree(self.__running) results = [] resolver = anytree.Resolver(pathattr='name') for i, resource_key in enumerate(resource_keys): str_resource_name = 'resource_key[#{:d}]'.format(i) try: chk_string(str_resource_name, resource_key, allow_empty=False) if resource_key == RESOURCE_ENDPOINTS: results.extend(self._handler_net_topology.get()) else: resource_key = SPECIAL_RESOURCE_MAPPINGS.get(resource_key, resource_key) resource_path = resource_key.split('/') except Exception as e: # pylint: disable=broad-except LOGGER.exception( 'Exception validating {:s}: {:s}'.format( str_resource_name, str(resource_key) ) ) results.append((resource_key, e)) # if validation fails, store the exception MSG = 'Error processing resource_key({:s}, {:s})' LOGGER.exception(MSG.format(str_resource_name, str(resource_key))) results.append((resource_key, e)) # if processing fails, store the exception continue resource_node = get_subnode(resolver, self.__running, resource_path, default=None) Loading @@ -185,10 +166,7 @@ class NCEDriver(_Driver): @metered_subclass_method(METRICS_POOL) def SetConfig(self, resources: List[Tuple[str, Any]]) -> List[Union[bool, Exception]]: results = [] if len(resources) == 0: return results if len(resources) == 0: return results with self.__lock: for resource in resources: resource_key, resource_value = resource Loading Loading @@ -224,8 +202,7 @@ class NCEDriver(_Driver): @metered_subclass_method(METRICS_POOL) def DeleteConfig(self, resources: List[Tuple[str, Any]]) -> List[Union[bool, Exception]]: results = [] if len(resources) == 0: return results if len(resources) == 0: return results with self.__lock: for resource in resources: LOGGER.info('resource = {:s}'.format(str(resource))) Loading
src/device/service/drivers/nce/handlers/NetworkTopologyHandler.py 0 → 100644 +162 −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 typing import Dict, List, Optional from common.Constants import DEFAULT_TOPOLOGY_NAME from common.DeviceTypes import DeviceTypeEnum from common.proto.context_pb2 import ( DEVICEDRIVER_UNDEFINED, DEVICEOPERATIONALSTATUS_DISABLED, DEVICEOPERATIONALSTATUS_ENABLED ) from common.tools.client.RestConfClient import RestConfClient from device.service.driver_api.ImportTopologyEnum import ( ImportTopologyEnum, get_import_topology ) LOGGER = logging.getLogger(__name__) class NetworkTopologyHandler: def __init__(self, rest_conf_client : RestConfClient, **settings) -> None: self._rest_conf_client = rest_conf_client self._subpath_root = '/ietf-network:networks' self._subpath_item = self._subpath_root + '/network={network_id:s}' # Options are: # disabled --> just import endpoints as usual # devices --> imports sub-devices but not links connecting them. # (a remotely-controlled transport domain might exist between them) # topology --> imports sub-devices and links connecting them. # (not supported by XR driver) self._import_topology = get_import_topology(settings, default=ImportTopologyEnum.TOPOLOGY) def get(self, network_id : Optional[str] = None) -> List[Dict]: if network_id is None: network_id = DEFAULT_TOPOLOGY_NAME endpoint = self._subpath_item.format(network_id=network_id) reply = self._rest_conf_client.get(endpoint) if 'ietf-network:network' not in reply: raise Exception('Malformed reply. "ietf-network:network" missing') networks = reply['ietf-network:network'] if len(networks) == 0: MSG = '[get] Network({:s}) not found; returning' LOGGER.debug(MSG.format(str(network_id))) return list() if len(networks) > 1: MSG = '[get] Multiple occurrences for Network({:s}); returning' LOGGER.debug(MSG.format(str(network_id))) return list() network = networks[0] MSG = '[get] import_topology={:s}' LOGGER.debug(MSG.format(str(self._import_topology))) result = list() if self._import_topology == ImportTopologyEnum.DISABLED: LOGGER.debug('[get] abstract controller; returning') return result device_type = DeviceTypeEnum.EMULATED_PACKET_SWITCH.value endpoint_type = '' if 'network-types' in network: nnt = network['network-types'] if 'ietf-te-topology:te-topology' in nnt: nnt_tet = nnt['ietf-te-topology:te-topology'] if 'ietf-otn-topology:otn-topology' in nnt_tet: device_type = DeviceTypeEnum.EMULATED_OPTICAL_ROADM.value endpoint_type = 'optical' elif 'ietf-eth-te-topology:eth-tran-topology' in nnt_tet: device_type = DeviceTypeEnum.EMULATED_PACKET_SWITCH.value endpoint_type = 'copper' elif 'ietf-l3-unicast-topology:l3-unicast-topology' in nnt_tet: device_type = DeviceTypeEnum.EMULATED_PACKET_ROUTER.value endpoint_type = 'copper' for node in network['node']: node_id = node['node-id'] node_name = node_id node_is_up = True if 'ietf-te-topology:te' in node: nte = node['ietf-te-topology:te'] if 'oper-status' in nte: node_is_up = nte['oper-status'] == 'up' if 'te-node-attributes' in nte: ntea = nte['te-node-attributes'] if 'name' in ntea: node_name = ntea['name'] device_url = '/devices/device[{:s}]'.format(node_id) device_data = { 'uuid': node_id, 'name': node_name, 'type': device_type, 'status': DEVICEOPERATIONALSTATUS_ENABLED if node_is_up else DEVICEOPERATIONALSTATUS_DISABLED, 'drivers': [DEVICEDRIVER_UNDEFINED], } result.append((device_url, device_data)) for tp in node['ietf-network-topology:termination-point']: tp_id = tp['tp-id'] tp_name = tp_id if 'ietf-te-topology:te' in tp: tpte = tp['ietf-te-topology:te'] if 'name' in tpte: tp_name = tpte['name'] endpoint_url = '/endpoints/endpoint[{:s}, {:s}]'.format(node_id, tp_id) endpoint_data = { 'device_uuid': node_id, 'uuid': tp_id, 'name': tp_name, 'type': endpoint_type, } result.append((endpoint_url, endpoint_data)) if self._import_topology == ImportTopologyEnum.DEVICES: LOGGER.debug('[get] devices only; returning') return result for link in network['ietf-network-topology:link']: link_uuid = link['link-id'] link_src = link['source'] link_dst = link['destination'] link_src_dev_id = link_src['source-node'] link_src_ep_id = link_src['source-tp'] link_dst_dev_id = link_dst['dest-node'] link_dst_ep_id = link_dst['dest-tp'] link_url = '/links/link[{:s}]'.format(link_uuid) link_endpoint_ids = [ (link_src_dev_id, link_src_ep_id), (link_dst_dev_id, link_dst_ep_id), ] link_data = { 'uuid': link_uuid, 'name': link_uuid, 'endpoints': link_endpoint_ids, } result.append((link_url, link_data)) LOGGER.debug('[get] topology; returning') return result
src/device/service/drivers/nce/handlers/__init__.py 0 → 100644 +13 −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.