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