diff --git a/manifests/nbiservice.yaml b/manifests/nbiservice.yaml index d3892118a3d8330335b58459a0953bb45e4854ea..70f553e6425ca7972b8af185f432842b4e184790 100644 --- a/manifests/nbiservice.yaml +++ b/manifests/nbiservice.yaml @@ -38,6 +38,8 @@ spec: env: - name: LOG_LEVEL value: "INFO" + - name: IETF_NETWORK_RENDERER + value: "LIBYANG" readinessProbe: exec: command: ["/bin/grpc_health_probe", "-addr=:9090"] diff --git a/src/nbi/service/rest_server/nbi_plugins/ietf_network/NameMapping.py b/src/nbi/service/rest_server/nbi_plugins/ietf_network/NameMapping.py index 0c10559115f4e4ba9e5b2468e36cf7f917c25f51..94e4723a5c7ca83fb382bb70cb241cb69b66ce0e 100644 --- a/src/nbi/service/rest_server/nbi_plugins/ietf_network/NameMapping.py +++ b/src/nbi/service/rest_server/nbi_plugins/ietf_network/NameMapping.py @@ -19,7 +19,7 @@ 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 diff --git a/src/nbi/service/rest_server/nbi_plugins/ietf_network/Networks.py b/src/nbi/service/rest_server/nbi_plugins/ietf_network/Networks.py index 5d663b8b3071856bc9cd204ee911c61b368ebe97..b53dc0fc242ff220e19091eab103902488ae2a3c 100644 --- a/src/nbi/service/rest_server/nbi_plugins/ietf_network/Networks.py +++ b/src/nbi/service/rest_server/nbi_plugins/ietf_network/Networks.py @@ -12,19 +12,23 @@ # See the License for the specific language governing permissions and # limitations under the License. -import json, logging +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 .bindings import ietf_network from .ComposeNetwork import compose_network from .ManualFixes import manual_fixes +from .YangHandler import YangHandler LOGGER = logging.getLogger(__name__) @@ -33,6 +37,14 @@ TE_TOPOLOGY_NAMES = [ '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_RENDERER.value) + + class Networks(Resource): @HTTP_AUTH.login_required def get(self): @@ -40,31 +52,59 @@ class Networks(Resource): topology_id = '' try: context_client = ContextClient() - #target = get_slice_by_uuid(context_client, vpn_id, rw_copy=True) - #if target is None: - # raise Exception('VPN({:s}) not found in database'.format(str(vpn_id))) - ietf_nets = ietf_network() + if USE_RENDERER == Renderer.PYANGBIND.value: + #target = get_slice_by_uuid(context_client, vpn_id, rw_copy=True) + #if target is None: + # raise Exception('VPN({:s}) not found in database'.format(str(vpn_id))) + + ietf_nets = ietf_network() + + topology_details = get_topology_details( + context_client, DEFAULT_TOPOLOGY_NAME, context_uuid=DEFAULT_CONTEXT_NAME, + #rw_copy=True + ) + if topology_details is None: + MSG = 'Topology({:s}/{:s}) not found' + raise Exception(MSG.format(DEFAULT_CONTEXT_NAME, DEFAULT_TOPOLOGY_NAME)) - topology_details = get_topology_details( - context_client, DEFAULT_TOPOLOGY_NAME, context_uuid=DEFAULT_CONTEXT_NAME, #rw_copy=True - ) - if topology_details is None: - MSG = 'Topology({:s}/{:s}) not found' - raise Exception(MSG.format(DEFAULT_CONTEXT_NAME, DEFAULT_TOPOLOGY_NAME)) + for te_topology_name in TE_TOPOLOGY_NAMES: + ietf_net = ietf_nets.networks.network.add(te_topology_name) + compose_network(ietf_net, te_topology_name, topology_details) - for te_topology_name in TE_TOPOLOGY_NAMES: - ietf_net = ietf_nets.networks.network.add(te_topology_name) - compose_network(ietf_net, te_topology_name, topology_details) + # TODO: improve these workarounds to enhance performance + json_response = json.loads(pybindJSON.dumps(ietf_nets, mode='ietf')) + + # Workaround; pyangbind does not allow to set otn_topology / eth-tran-topology + manual_fixes(json_response) + elif USE_RENDERER == Renderer.LIBYANG.value: + yang_handler = YangHandler() + json_response = [] - # TODO: improve these workarounds to enhance performance - json_response = json.loads(pybindJSON.dumps(ietf_nets, mode='ietf')) - - # Workaround; pyangbind does not allow to set otn_topology / eth-tran-topology - manual_fixes(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)}) diff --git a/src/nbi/service/rest_server/nbi_plugins/ietf_network/YangHandler.py b/src/nbi/service/rest_server/nbi_plugins/ietf_network/YangHandler.py new file mode 100644 index 0000000000000000000000000000000000000000..a5dda280c98c060c2f872df5ab17152880b522d5 --- /dev/null +++ b/src/nbi/service/rest_server/nbi_plugins/ietf_network/YangHandler.py @@ -0,0 +1,117 @@ +# Copyright 2022-2024 ETSI OSG/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 + +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'] + +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) + + 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() diff --git a/src/nbi/service/rest_server/nbi_plugins/ietf_network/yang/ietf-l3-unicast-topology@2018-02-26.yang b/src/nbi/service/rest_server/nbi_plugins/ietf_network/yang/ietf-l3-unicast-topology@2018-02-26.yang new file mode 100644 index 0000000000000000000000000000000000000000..39fcebd767bf7ea687de523b0dd0ba731d3c80e7 --- /dev/null +++ b/src/nbi/service/rest_server/nbi_plugins/ietf_network/yang/ietf-l3-unicast-topology@2018-02-26.yang @@ -0,0 +1,359 @@ +module ietf-l3-unicast-topology { + yang-version 1.1; + namespace + "urn:ietf:params:xml:ns:yang:ietf-l3-unicast-topology"; + prefix "l3t"; + import ietf-network { + prefix "nw"; + } + import ietf-network-topology { + prefix "nt"; + } + import ietf-inet-types { + prefix "inet"; + } + import ietf-routing-types { + prefix "rt-types"; + } + organization + "IETF I2RS (Interface to the Routing System) Working Group"; + contact + "WG Web: + WG List: + Editor: Alexander Clemm + + Editor: Jan Medved + + Editor: Robert Varga + + Editor: Xufeng Liu + + Editor: Nitin Bahadur + + Editor: Hariharan Ananthakrishnan + "; + description + "This module defines a model for Layer 3 Unicast + topologies. + + Copyright (c) 2018 IETF Trust and the persons identified as + authors of the code. All rights reserved. + + Redistribution and use in source and binary forms, with or + without modification, is permitted pursuant to, and subject + to the license terms contained in, the Simplified BSD License + set forth in Section 4.c of the IETF Trust's Legal Provisions + Relating to IETF Documents + (https://trustee.ietf.org/license-info). + + This version of this YANG module is part of + RFC 8346; see the RFC itself for full legal notices."; + revision "2018-02-26" { + description + "Initial revision."; + reference + "RFC 8346: A YANG Data Model for Layer 3 Topologies"; + } + + identity flag-identity { + description "Base type for flags"; + } + + typedef l3-event-type { + type enumeration { + enum "add" { + description + "A Layer 3 node, link, prefix, or termination point has + been added"; + } + enum "remove" { + description + "A Layer 3 node, link, prefix, or termination point has + been removed"; + } + enum "update" { + description + "A Layer 3 node, link, prefix, or termination point has + been updated"; + } + } + description "Layer 3 event type for notifications"; + } + + typedef prefix-flag-type { + type identityref { + base "flag-identity"; + } + description "Prefix flag attributes"; + } + + typedef node-flag-type { + type identityref { + base "flag-identity"; + } + description "Node flag attributes"; + } + + typedef link-flag-type { + type identityref { + base "flag-identity"; + } + description "Link flag attributes"; + } + + typedef l3-flag-type { + type identityref { + base "flag-identity"; + } + description "L3 flag attributes"; + } + + grouping l3-prefix-attributes { + description + "L3 prefix attributes"; + leaf prefix { + type inet:ip-prefix; + description + "IP prefix value"; + } + leaf metric { + type uint32; + description + "Prefix metric"; + } + leaf-list flag { + type prefix-flag-type; + description + "Prefix flags"; + } + } + grouping l3-unicast-topology-type { + description "Identifies the topology type to be L3 Unicast."; + container l3-unicast-topology { + presence "indicates L3 Unicast topology"; + description + "The presence of the container node indicates L3 Unicast + topology"; + } + } + grouping l3-topology-attributes { + description "Topology scope attributes"; + container l3-topology-attributes { + description "Contains topology attributes"; + leaf name { + type string; + description + "Name of the topology"; + } + leaf-list flag { + type l3-flag-type; + description + "Topology flags"; + } + } + } + grouping l3-node-attributes { + description "L3 node scope attributes"; + container l3-node-attributes { + description + "Contains node attributes"; + leaf name { + type inet:domain-name; + description + "Node name"; + } + leaf-list flag { + type node-flag-type; + description + "Node flags"; + } + leaf-list router-id { + type rt-types:router-id; + description + "Router-id for the node"; + } + list prefix { + key "prefix"; + description + "A list of prefixes along with their attributes"; + uses l3-prefix-attributes; + } + } + } + grouping l3-link-attributes { + description + "L3 link scope attributes"; + container l3-link-attributes { + description + "Contains link attributes"; + leaf name { + type string; + description + "Link Name"; + } + leaf-list flag { + type link-flag-type; + description + "Link flags"; + } + leaf metric1 { + type uint64; + description + "Link Metric 1"; + } + leaf metric2 { + type uint64; + description + "Link Metric 2"; + } + } + } + grouping l3-termination-point-attributes { + description "L3 termination point scope attributes"; + container l3-termination-point-attributes { + description + "Contains termination point attributes"; + choice termination-point-type { + description + "Indicates the termination point type"; + case ip { + leaf-list ip-address { + type inet:ip-address; + description + "IPv4 or IPv6 address."; + } + } + case unnumbered { + leaf unnumbered-id { + type uint32; + description + "Unnumbered interface identifier. + The identifier will correspond to the ifIndex value + of the interface, i.e., the ifIndex value of the + ifEntry that represents the interface in + implementations where the Interfaces Group MIB + (RFC 2863) is supported."; + reference + "RFC 2863: The Interfaces Group MIB"; + } + } + case interface-name { + leaf interface-name { + type string; + description + "Name of the interface. The name can (but does not + have to) correspond to an interface reference of a + containing node's interface, i.e., the path name of a + corresponding interface data node on the containing + node reminiscent of data type interface-ref defined + in RFC 8343. It should be noted that data type + interface-ref of RFC 8343 cannot be used directly, + + as this data type is used to reference an interface + in a datastore of a single node in the network, not + to uniquely reference interfaces across a network."; + reference + "RFC 8343: A YANG Data Model for Interface Management"; + } + } + } + } + } + augment "/nw:networks/nw:network/nw:network-types" { + description + "Introduces new network type for L3 Unicast topology"; + uses l3-unicast-topology-type; + } + augment "/nw:networks/nw:network" { + when "nw:network-types/l3t:l3-unicast-topology" { + description + "Augmentation parameters apply only for networks with + L3 Unicast topology"; + } + description + "L3 Unicast for the network as a whole"; + uses l3-topology-attributes; + } + augment "/nw:networks/nw:network/nw:node" { + when "../nw:network-types/l3t:l3-unicast-topology" { + description + "Augmentation parameters apply only for networks with + L3 Unicast topology"; + } + description + "L3 Unicast node-level attributes "; + uses l3-node-attributes; + } + augment "/nw:networks/nw:network/nt:link" { + when "../nw:network-types/l3t:l3-unicast-topology" { + description + "Augmentation parameters apply only for networks with + L3 Unicast topology"; + } + description + "Augments topology link attributes"; + uses l3-link-attributes; + } + augment "/nw:networks/nw:network/nw:node/" + +"nt:termination-point" { + when "../../nw:network-types/l3t:l3-unicast-topology" { + description + "Augmentation parameters apply only for networks with + L3 Unicast topology"; + } + description "Augments topology termination point configuration"; + uses l3-termination-point-attributes; + } + notification l3-node-event { + description + "Notification event for L3 node"; + leaf l3-event-type { + type l3-event-type; + description + "Event type"; + } + uses nw:node-ref; + uses l3-unicast-topology-type; + uses l3-node-attributes; + } + notification l3-link-event { + description + "Notification event for L3 link"; + leaf l3-event-type { + type l3-event-type; + description + "Event type"; + } + uses nt:link-ref; + uses l3-unicast-topology-type; + uses l3-link-attributes; + } + notification l3-prefix-event { + description + "Notification event for L3 prefix"; + leaf l3-event-type { + type l3-event-type; + description + "Event type"; + } + uses nw:node-ref; + uses l3-unicast-topology-type; + container prefix { + description + "Contains L3 prefix attributes"; + uses l3-prefix-attributes; + } + } + notification termination-point-event { + description + "Notification event for L3 termination point"; + leaf l3-event-type { + type l3-event-type; + description + "Event type"; + } + uses nt:tp-ref; + uses l3-unicast-topology-type; + uses l3-termination-point-attributes; + } +} diff --git a/src/nbi/tests/test_ietf_network.py b/src/nbi/tests/test_ietf_network.py index 9a25e1b3b5e0ee202a0af945e88794f8aa9b0ec4..ec03d3798ded3efd027a0b8237becc865441fc98 100644 --- a/src/nbi/tests/test_ietf_network.py +++ b/src/nbi/tests/test_ietf_network.py @@ -12,14 +12,20 @@ # See the License for the specific language governing permissions and # limitations under the License. -import deepdiff, json, logging, operator +import deepdiff, json, logging, operator, os from typing import Dict from common.Constants import DEFAULT_CONTEXT_NAME from common.proto.context_pb2 import ContextId -from common.tools.descriptor.Loader import DescriptorLoader, check_descriptor_load_results, validate_empty_scenario +from common.tools.descriptor.Loader import ( + DescriptorLoader, check_descriptor_load_results, validate_empty_scenario +) from common.tools.object_factory.Context import json_context_id from context.client.ContextClient import ContextClient from nbi.service.rest_server import RestServer + +# Explicitly state NBI to use PyangBind Renderer for this test +os.environ['IETF_NETWORK_RENDERER'] = 'PYANGBIND' + from .PrepareTestScenario import ( # pylint: disable=unused-import # be careful, order of symbols is important here! do_rest_get_request, mock_service, nbi_service_rest, osm_wim, context_client