Commit 5ece61dc authored by Lluis Gifre Renom's avatar Lluis Gifre Renom
Browse files

Merge branch 'develop' of ssh://gifrerenom_labs.etsi.org/tfs/controller into...

Merge branch 'develop' of ssh://gifrerenom_labs.etsi.org/tfs/controller into feat/199-cttc-add-missing-ci-cd-descriptor-for-analytics-component
parents caea0622 b830b87a
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -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"]
+82 −6
Original line number Diff line number Diff line
@@ -33,20 +33,25 @@
#    # do test ...
#    descriptor_loader.unload()

import concurrent.futures, json, logging, operator
import concurrent.futures, copy, json, logging, operator
from typing import Any, Dict, List, Optional, Tuple, Union
from common.proto.context_pb2 import (
    Connection, Context, ContextId, Device, DeviceId, Empty, Link, LinkId, Service, ServiceId, Slice, SliceId,
    Topology, TopologyId)
    Connection, Context, ContextId, Device, DeviceId, Empty,
    Link, LinkId, Service, ServiceId, Slice, SliceId,
    Topology, TopologyId
)
from common.tools.object_factory.Context import json_context_id
from context.client.ContextClient import ContextClient
from device.client.DeviceClient import DeviceClient
from service.client.ServiceClient import ServiceClient
from slice.client.SliceClient import SliceClient
from .Tools import (
    format_device_custom_config_rules, format_service_custom_config_rules, format_slice_custom_config_rules,
    get_descriptors_add_contexts, get_descriptors_add_services, get_descriptors_add_slices,
    get_descriptors_add_topologies, split_controllers_and_network_devices, split_devices_by_rules)
    format_device_custom_config_rules, format_service_custom_config_rules,
    format_slice_custom_config_rules, get_descriptors_add_contexts,
    get_descriptors_add_services, get_descriptors_add_slices,
    get_descriptors_add_topologies, split_controllers_and_network_devices,
    split_devices_by_rules
)

LOGGER = logging.getLogger(__name__)
LOGGERS = {
@@ -78,6 +83,30 @@ TypeResults = List[Tuple[str, str, int, List[str]]] # entity_name, action, num_o
TypeNotification = Tuple[str, str] # message, level
TypeNotificationList = List[TypeNotification]

SLICE_TEMPLATE = {
    "slice_id": {
        "context_id": {"context_uuid": {"uuid": "admin"}},
        "slice_uuid": {"uuid": None}
    },
    "name": {},
    "slice_config": {"config_rules": [
        {"action": 1, "custom": {"resource_key": "/settings", "resource_value": {
            "address_families": ["IPV4"], "bgp_as": 65000,
            "bgp_route_target": "65000:333", "mtu": 1512
        }}}
    ]},
    "slice_constraints": [
        {"sla_capacity": {"capacity_gbps": 20.0}},
        {"sla_availability": {"availability": 20.0, "num_disjoint_paths": 1, "all_active": True}},
        {"sla_isolation": {"isolation_level": [0]}}
    ],
    "slice_endpoint_ids": [

    ],
    "slice_status": {"slice_status": 1}
}


class DescriptorLoader:
    def __init__(
        self, descriptors : Optional[Union[str, Dict]] = None, descriptors_file : Optional[str] = None,
@@ -106,8 +135,53 @@ class DescriptorLoader:
        self.__links       = self.__descriptors.get('links'      , [])
        self.__services    = self.__descriptors.get('services'   , [])
        self.__slices      = self.__descriptors.get('slices'     , [])
        self.__ietf_slices = self.__descriptors.get('ietf-network-slice-service:network-slice-services', {})
        self.__connections = self.__descriptors.get('connections', [])

        if len(self.__ietf_slices) > 0:
            for slice_service in self.__ietf_slices["slice-service"]:
                tfs_slice = copy.deepcopy(SLICE_TEMPLATE)
                tfs_slice["slice_id"]["slice_uuid"]["uuid"] = slice_service["id"]
                tfs_slice["name"] = slice_service["description"]
                for sdp in slice_service["sdps"]["sdp"]:
                    sdp_id = sdp["id"]
                    for attcircuit in sdp["attachment-circuits"]["attachment-circuit"]:
                        att_cir_tp_id = attcircuit["ac-tp-id"]
                        RESOURCE_KEY = "/device[{:s}]/endpoint[{:s}]/settings"
                        resource_key = RESOURCE_KEY.format(str(sdp_id), str(att_cir_tp_id))

                        for tag in attcircuit['ac-tags']['ac-tag']:
                            if tag.get('tag-type') == 'ietf-nss:vlan-id':
                                vlan_id = tag.get('value')
                            else:
                                vlan_id = 0

                        tfs_slice["slice_config"]["config_rules"].append({
                            "action": 1, "custom": {
                                "resource_key": resource_key, "resource_value": {
                                    "router_id": sdp.get("node-id",[]),
                                    "sub_interface_index": 0,
                                    "vlan_id": vlan_id
                                }
                            }
                        })
                        tfs_slice["slice_endpoint_ids"].append({
                            "device_id": {"device_uuid": {"uuid": sdp_id}},
                            "endpoint_uuid": {"uuid": att_cir_tp_id},
                            "topology_id": {"context_id": {"context_uuid": {"uuid": "admin"}}, 
                            "topology_uuid": {"uuid": "admin"}}
                        })
                        #tfs_slice["slice_constraints"].append({
                        #    "endpoint_location": {
                        #        "endpoint_id": {
                        #            "device_id": {"device_uuid": {"uuid": sdp["id"]}},
                        #            "endpoint_uuid": {"uuid": attcircuit["ac-tp-id"]}
                        #        },
                        #        "location": {"region": "4"}
                        #    }
                        #})
                self.__slices.append(tfs_slice)

        self.__contexts_add   = None
        self.__topologies_add = None
        self.__devices_add    = None
@@ -232,7 +306,9 @@ class DescriptorLoader:

    def _load_dummy_mode(self) -> None:
        # Dummy Mode: used to pre-load databases (WebUI debugging purposes) with no smart or automated tasks.

        controllers, network_devices = split_controllers_and_network_devices(self.__devices)

        self.__ctx_cli.connect()
        self._process_descr('context',    'add',    self.__ctx_cli.SetContext,    Context,    self.__contexts_add  )
        self._process_descr('topology',   'add',    self.__ctx_cli.SetTopology,   Topology,   self.__topologies_add)
+59 −19
Original line number Diff line number Diff line
@@ -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,6 +52,8 @@ class Networks(Resource):
        topology_id = ''
        try:
            context_client = ContextClient()

            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)))
@@ -47,7 +61,8 @@ class Networks(Resource):
                ietf_nets = ietf_network()

                topology_details = get_topology_details(
                context_client, DEFAULT_TOPOLOGY_NAME, context_uuid=DEFAULT_CONTEXT_NAME, #rw_copy=True
                    context_client, DEFAULT_TOPOLOGY_NAME, context_uuid=DEFAULT_CONTEXT_NAME,
                    #rw_copy=True
                )
                if topology_details is None:
                    MSG = 'Topology({:s}/{:s}) not found'
@@ -62,9 +77,34 @@ class Networks(Resource):
                
                # 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 = []

                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)})
+117 −0
Original line number Diff line number Diff line
# 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()
+359 −0

File added.

Preview size limit exceeded, changes collapsed.

Loading