Commit df7d5bfd authored by Lluis Gifre Renom's avatar Lluis Gifre Renom
Browse files

Device component - NCE-FAN Driver:

- Add topology discovery
- Multiple bug fixes
- Code polishing
parent 6a3609d9
Loading
Loading
Loading
Loading
+34 −57
Original line number Diff line number Diff line
@@ -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,
@@ -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 = []
@@ -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()
@@ -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)
@@ -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
@@ -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)))
+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
+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.