Commit f26c169d authored by Leandro Campos's avatar Leandro Campos
Browse files

NBI

parent dc405d13
Loading
Loading
Loading
Loading
+46 −0
Original line number Original line Diff line number Diff line
# 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.

from typing import Dict, Tuple
from common.proto.context_pb2 import Device, DeviceId, EndPoint, EndPointId

class NameMappings:
    def __init__(self) -> None:
        self._device_uuid_to_name   : Dict[str,             str] = dict()
        self._endpoint_uuid_to_name : Dict[Tuple[str, str], str] = dict()

    def store_device_name(self, device : Device) -> None:
        device_uuid = device.device_id.device_uuid.uuid
        device_name = device.name
        self._device_uuid_to_name[device_uuid] = device_name
        self._device_uuid_to_name[device_name] = device_name

    def store_endpoint_name(self, device : Device, endpoint : EndPoint) -> None:
        device_uuid = device.device_id.device_uuid.uuid
        device_name = device.name
        endpoint_uuid = endpoint.endpoint_id.endpoint_uuid.uuid
        endpoint_name = endpoint.name
        self._endpoint_uuid_to_name[(device_uuid, endpoint_uuid)] = endpoint_name
        self._endpoint_uuid_to_name[(device_name, endpoint_uuid)] = endpoint_name
        self._endpoint_uuid_to_name[(device_uuid, endpoint_name)] = endpoint_name
        self._endpoint_uuid_to_name[(device_name, endpoint_name)] = endpoint_name

    def get_device_name(self, device_id : DeviceId) -> str:
        device_uuid = device_id.device_uuid.uuid
        return self._device_uuid_to_name.get(device_uuid, device_uuid)

    def get_endpoint_name(self, endpoint_id : EndPointId) -> str:
        device_uuid = endpoint_id.device_id.device_uuid.uuid
        endpoint_uuid = endpoint_id.endpoint_uuid.uuid
        return self._endpoint_uuid_to_name.get((device_uuid, endpoint_uuid), endpoint_uuid)
+23 −0
Original line number Original line Diff line number Diff line
# 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.

from enum import Enum
from typing import Optional

class NetworkTypeEnum(Enum):
    TE_OTN_TOPOLOGY      = 'otn'
    TE_ETH_TRAN_TOPOLOGY = 'eth-tran'

def get_network_topology_type(topology_id : str) -> Optional[NetworkTypeEnum]:
    return NetworkTypeEnum._value2member_map_.get(topology_id)
+85 −0
Original line number Original line Diff line number Diff line
# 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 enum, json, logging
import pyangbind.lib.pybindJSON as pybindJSON
from flask import request
from flask.json import jsonify
from flask_restful import Resource
from common.Constants import DEFAULT_CONTEXT_NAME, DEFAULT_TOPOLOGY_NAME
from common.Settings import get_setting
from common.proto.context_pb2 import ContextId, Empty
from common.tools.context_queries.Topology import get_topology_details
from common.tools.object_factory.Context import json_context_id
from context.client.ContextClient import ContextClient
from nbi.service.rest_server.nbi_plugins.tools.Authentication import HTTP_AUTH
from nbi.service.rest_server.nbi_plugins.tools.HttpStatusCodes import HTTP_OK, HTTP_SERVERERROR
from .YangHandler import YangHandler

LOGGER = logging.getLogger(__name__)

TE_TOPOLOGY_NAMES = [
    'providerId-10-clientId-0-topologyId-1',
    'providerId-10-clientId-0-topologyId-2'
]

class Renderer(enum.Enum):
    LIBYANG   = 'LIBYANG'
    PYANGBIND = 'PYANGBIND'

DEFAULT_RENDERER = Renderer.LIBYANG
USE_RENDERER = get_setting('IETF_NETWORK_RENDERER', default=DEFAULT_RENDERER.value)


class Networks_bw(Resource):
    @HTTP_AUTH.login_required
    def get(self):
        LOGGER.info('Request: {:s}'.format(str(request)))
        topology_id = ''
        try:
            context_client = ContextClient()

            if USE_RENDERER == Renderer.LIBYANG.value:
                yang_handler = YangHandler()
                json_response = []

                contexts = context_client.ListContexts(Empty()).contexts
                context_names = [context.name for context in contexts]
                LOGGER.info(f'Contexts detected: {context_names}')

                for context_name in context_names:
                    topologies = context_client.ListTopologies(ContextId(**json_context_id(context_name))).topologies
                    topology_names = [topology.name for topology in topologies]
                    LOGGER.info(f'Topologies detected for context {context_name}: {topology_names}')

                    for topology_name in topology_names:
                        topology_details = get_topology_details(context_client, topology_name, context_name)
                        if topology_details is None:
                            raise Exception(f'Topology({context_name}/{topology_name}) not found')

                        network_reply = yang_handler.compose_network(topology_name, topology_details)
                        json_response.append(network_reply)

                yang_handler.destroy()
            else:
                raise Exception('Unsupported Renderer: {:s}'.format(str(USE_RENDERER)))

            response = jsonify(json_response)
            response.status_code = HTTP_OK

        except Exception as e: # pylint: disable=broad-except
            LOGGER.exception('Something went wrong Retrieving Topology({:s})'.format(str(topology_id)))
            response = jsonify({'error': str(e)})
            response.status_code = HTTP_SERVERERROR
        return response
+14 −0
Original line number Original line Diff line number Diff line
# IETF Network

This NBI plugin implements a basic skeleton for the IETF Network data model.

## DISCLAIMER
__USE WITH CARE! This plugin is NOT production ready, it contains many hardcodings based on ongoing tests.__

## IETF RFCs/drafts:
- RFC 8795 - YANG Data Model for Traffic Engineering (TE) Topologies (https://datatracker.ietf.org/doc/html/rfc8795)
- RFC 8776 - Common YANG Data Types for Traffic Engineering (https://datatracker.ietf.org/doc/html/rfc8776)
- RFC 8345 - A YANG Data Model for Network Topologies (https://datatracker.ietf.org/doc/html/rfc8345)
- RFC 6991 - Common YANG Data Types (https://datatracker.ietf.org/doc/html/rfc6991)
- RFC draft-ietf-ccamp-eth-client-te-topo-yang-05 - A YANG Data Model for Ethernet TE Topology (https://datatracker.ietf.org/doc/draft-ietf-ccamp-eth-client-te-topo-yang/)
- RFC draft-ietf-ccamp-client-signal-yang-10 - A YANG Data Model for Transport Network Client Signals (https://datatracker.ietf.org/doc/draft-ietf-ccamp-client-signal-yang/)
+183 −0
Original line number Original line Diff line number Diff line
# 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
import libyang, logging, os
from typing import Any
from common.proto.context_pb2 import TopologyDetails, Device, Link
from .NameMapping import NameMappings
from context.client.ContextClient import ContextClient
from common.tools.object_factory.Device import json_device_id
from common.proto.context_pb2 import DeviceId
import requests



LOGGER = logging.getLogger(__name__)

YANG_DIR = os.path.join(os.path.dirname(__file__), 'yang')
YANG_MODULES = ['ietf-network', 'ietf-network-topology', 'ietf-l3-unicast-topology', 'ietf-te-types', 'ietf-te-packet-types',  'ietf-te-topology', 'ietf-l3-te-topology','ietf-te-topology-packet' ]

current_dir = os.path.dirname(__file__)
data_path = os.path.join(current_dir, "linksdata.json")

class YangHandler:
    def __init__(self) -> None:
        self._yang_context = libyang.Context(YANG_DIR)
        for yang_module_name in YANG_MODULES:
            LOGGER.info('Loading module: {:s}'.format(str(yang_module_name)))
            self._yang_context.load_module(yang_module_name).feature_enable_all()

        



    def compose_network(self, te_topology_name: str, topology_details: TopologyDetails) -> dict:
        networks = self._yang_context.create_data_path('/ietf-network:networks')
        network = networks.create_path(f'network[network-id="{te_topology_name}"]')
        network.create_path('network-id', te_topology_name)

        network_types = network.create_path('network-types') 
        network_types.create_path('ietf-l3-unicast-topology:l3-unicast-topology') 

        name_mappings = NameMappings()

        for device in topology_details.devices: 
            self.compose_node(device, name_mappings, network)

        for link in topology_details.links:
            self.compose_link(link, name_mappings, network)

        return json.loads(networks.print_mem('json'))

    def compose_node(self, dev: Device, name_mappings: NameMappings, network: Any) -> None:                                     
        device_name = dev.name
        name_mappings.store_device_name(dev)

        node = network.create_path(f'node[node-id="{device_name}"]')
        node.create_path('node-id', device_name)
        node_attributes = node.create_path('ietf-l3-unicast-topology:l3-node-attributes')
        node_attributes.create_path('name', device_name)

        context_client = ContextClient()
        device = context_client.GetDevice(DeviceId(**json_device_id(device_name)))

        for endpoint in device.device_endpoints:
            name_mappings.store_endpoint_name(dev, endpoint)

        self._process_device_config(device, node)

    def _process_device_config(self, device: Device, node: Any) -> None:
        for config in device.device_config.config_rules:
            if config.WhichOneof('config_rule') != 'custom' or '/interface[' not in config.custom.resource_key:
                continue

            for endpoint in device.device_endpoints:
                endpoint_name = endpoint.name
                if f'/interface[{endpoint_name}]' in config.custom.resource_key or f'/interface[{endpoint_name}.' in config.custom.resource_key:
                    interface_name = config.custom.resource_key.split('interface[')[1].split(']')[0]
                    self._create_termination_point(node, interface_name, endpoint_name, config.custom.resource_value)

    def _create_termination_point(self, node: Any, interface_name: str, endpoint_name: str, resource_value: str) -> None:
        ip_addresses = self._extract_ip_addresses(json.loads(resource_value))
        if ip_addresses:
            tp = node.create_path(f'ietf-network-topology:termination-point[tp-id="{interface_name}"]')
            tp.create_path('tp-id', interface_name)
            tp_attributes = tp.create_path('ietf-l3-unicast-topology:l3-termination-point-attributes')

            for ip in ip_addresses:
                tp_attributes.create_path('ip-address', ip)
            tp_attributes.create_path('interface-name', endpoint_name)

    @staticmethod
    def _extract_ip_addresses(resource_value: dict) -> list:
        ip_addresses = []
        if 'address_ip' in resource_value:
            ip_addresses.append(resource_value['address_ip'])
        if 'address_ipv6' in resource_value:
            ip_addresses.append(resource_value['address_ipv6'])
        return ip_addresses

    def compose_link(self, link_specs: Link, name_mappings: NameMappings, network: Any) -> None:
        link_name = link_specs.name
        links = network.create_path(f'ietf-network-topology:link[link-id="{link_name}"]')
        links.create_path('link-id', link_name)

        self._create_link_endpoint(links, 'source', link_specs.link_endpoint_ids[0], name_mappings)
        self._create_link_endpoint(links, 'destination', link_specs.link_endpoint_ids[-1], name_mappings)


        
        # Cargar los datos del archivo JSON una sola vez
        try:
            response = requests.get("http://192.168.165.44:8080/linksdata.json")
            response.raise_for_status()
            link_data = response.json()
            LOGGER.debug(f"Link data loaded from external API for {link_specs.name}")

            # Obtener los datos específicos para este enlace
            data = link_data.get(link_name)

            te = links.create_path('ietf-te-topology:te')
            te_link_attributes = te.create_path('te-link-attributes')

            # Asignar valores predeterminados si no se encuentran en el JSON
            max_bw = data.get("Maximum link bandwidth", 10000000000)
            max_resv_bw = data.get("Max. Reservable link bandwidth", 10000000000)
            te_link_attributes.create_path('max-link-bandwidth', max_bw)
            te_link_attributes.create_path('max-resv-link-bandwidth', max_resv_bw)

            te_data = data.get("ietf-te-topology:te", {}).get("te-link-attributes", {})

            # Unreserved bandwidth por prioridad
            for priority in range(8):
                key = f"Unreserved bandwidth (priority {priority})"
                value = te_data.get(key, 0)

                entry = te_link_attributes.create_path(f'unreserved-bandwidth[priority="{priority}"]')
                entry.create_path('priority', priority)
                bw = entry.create_path('te-bandwidth')
                bw.create_path('generic', value)

            # Métricas de rendimiento unidireccional
            perf_data = data["ietf-te-topology:te"]["te-link-attributes"]["ietf-te-topology-packet:performance-metrics-one-way"]
            performance_metrics = te_link_attributes.create_path('ietf-te-topology-packet:performance-metrics-one-way')
            performance_metrics.create_path('one-way-delay', perf_data.get("one-way-delay", 0))
            performance_metrics.create_path('one-way-residual-bandwidth', perf_data.get("one-way-residual-bandwidth", "0x0p0"))
            performance_metrics.create_path('one-way-available-bandwidth', perf_data.get("one-way-available-bandwidth", "0x0p0"))
            performance_metrics.create_path('one-way-utilized-bandwidth', perf_data.get("one-way-utilized-bandwidth", "0x0p0"))
            performance_metrics.create_path('one-way-min-delay', perf_data.get("one-way-min-delay", 0))
            performance_metrics.create_path('one-way-max-delay', perf_data.get("one-way-max-delay", 0))
            performance_metrics.create_path('one-way-delay-variation', perf_data.get("one-way-delay-variation", 0))





        except Exception as e:
            LOGGER.error(f"Failed to fetch link data from external API: {e}")
            link_data = {}

            

    def _create_link_endpoint(self, links: Any, endpoint_type: str, endpoint_id: Any, name_mappings: NameMappings) -> None:
        endpoint = links.create_path(endpoint_type)
        if endpoint_type == 'destination': endpoint_type = 'dest'
        endpoint.create_path(f'{endpoint_type}-node', name_mappings.get_device_name(endpoint_id.device_id))
        endpoint.create_path(f'{endpoint_type}-tp', name_mappings.get_endpoint_name(endpoint_id))



    def destroy(self) -> None:
        self._yang_context.destroy()
Loading