Commit 315bd644 authored by Lluis Gifre Renom's avatar Lluis Gifre Renom
Browse files

Merge branch 'release/2.0.1' of https://labs.etsi.org/rep/tfs/controller into feat/slice-grouping

parents b6fdf507 0066c66f
Loading
Loading
Loading
Loading

src/common/tests/LoadScenario.py

deleted100644 → 0
+0 −50
Original line number Diff line number Diff line
# Copyright 2022-2023 ETSI TeraFlowSDN - TFS OSG (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 common.tools.descriptor.Loader import DescriptorLoader, compose_notifications
from context.client.ContextClient import ContextClient
from device.client.DeviceClient import DeviceClient
from service.client.ServiceClient import ServiceClient
from slice.client.SliceClient import SliceClient

LOGGER = logging.getLogger(__name__)
LOGGERS = {
    'success': LOGGER.info,
    'danger' : LOGGER.error,
    'error'  : LOGGER.error,
}

def load_scenario_from_descriptor(
    descriptor_file : str, context_client : ContextClient, device_client : DeviceClient,
    service_client : ServiceClient, slice_client : SliceClient
) -> DescriptorLoader:
    with open(descriptor_file, 'r', encoding='UTF-8') as f:
        descriptors = f.read()

    descriptor_loader = DescriptorLoader(
        descriptors,
        context_client=context_client, device_client=device_client,
        service_client=service_client, slice_client=slice_client)
    results = descriptor_loader.process()

    num_errors = 0
    for message,level in compose_notifications(results):
        LOGGERS.get(level)(message)
        if level != 'success': num_errors += 1
    if num_errors > 0:
        MSG = 'Failed to load descriptors in file {:s}'
        raise Exception(MSG.format(str(descriptor_file)))

    return descriptor_loader
 No newline at end of file
+133 −24
Original line number Diff line number Diff line
@@ -15,25 +15,30 @@
# SDN controller descriptor loader

# Usage example (WebUI):
#    descriptors = json.loads(descriptors_data_from_client)
#    descriptors = json.loads(
#       descriptors=descriptors_data_from_client, num_workers=10,
#       context_client=..., device_client=..., service_client=..., slice_client=...)
#    descriptor_loader = DescriptorLoader(descriptors)
#    results = descriptor_loader.process()
#    for message,level in compose_notifications(results):
#        flash(message, level)

# Usage example (pytest):
#    with open('path/to/descriptor.json', 'r', encoding='UTF-8') as f:
#        descriptors = json.loads(f.read())
#    descriptor_loader = DescriptorLoader(
#       descriptors, context_client=..., device_client=..., service_client=..., slice_client=...)
#       descriptors_file='path/to/descriptor.json', num_workers=10,
#       context_client=..., device_client=..., service_client=..., slice_client=...)
#    results = descriptor_loader.process()
#    loggers = {'success': LOGGER.info, 'danger': LOGGER.error, 'error': LOGGER.error}
#    for message,level in compose_notifications(results):
#        loggers.get(level)(message)
#    check_results(results, descriptor_loader)
#    descriptor_loader.validate()
#    # do test ...
#    descriptor_loader.unload()

import concurrent.futures, json, logging, operator
from typing import Any, Dict, List, Optional, Tuple, Union
from common.proto.context_pb2 import Connection, Context, Device, Link, Service, Slice, Topology
from common.proto.context_pb2 import (
    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
@@ -44,6 +49,11 @@ from .Tools import (
    get_descriptors_add_topologies, split_devices_by_rules)

LOGGER = logging.getLogger(__name__)
LOGGERS = {
    'success': LOGGER.info,
    'danger' : LOGGER.error,
    'error'  : LOGGER.error,
}

ENTITY_TO_TEXT = {
    # name   => singular,    plural
@@ -67,25 +77,26 @@ TypeResults = List[Tuple[str, str, int, List[str]]] # entity_name, action, num_o
TypeNotification = Tuple[str, str] # message, level
TypeNotificationList = List[TypeNotification]

def compose_notifications(results : TypeResults) -> TypeNotificationList:
    notifications = []
    for entity_name, action_name, num_ok, error_list in results:
        entity_name_singluar,entity_name_plural = ENTITY_TO_TEXT[entity_name]
        action_infinitive, action_past = ACTION_TO_TEXT[action_name]
        num_err = len(error_list)
        for error in error_list:
            notifications.append((f'Unable to {action_infinitive} {entity_name_singluar} {error}', 'error'))
        if num_ok : notifications.append((f'{str(num_ok)} {entity_name_plural} {action_past}', 'success'))
        if num_err: notifications.append((f'{str(num_err)} {entity_name_plural} failed', 'danger'))
    return notifications

class DescriptorLoader:
    def __init__(
        self, descriptors : Union[str, Dict], num_workers : int = 1,
        self, descriptors : Optional[Union[str, Dict]] = None, descriptors_file : Optional[str] = None,
        num_workers : int = 1,
        context_client : Optional[ContextClient] = None, device_client : Optional[DeviceClient] = None,
        service_client : Optional[ServiceClient] = None, slice_client : Optional[SliceClient] = None
    ) -> None:
        if (descriptors is None) == (descriptors_file is None):
            raise Exception('Exactly one of "descriptors" or "descriptors_file" is required')
        
        if descriptors_file is not None:
            with open(descriptors_file, 'r', encoding='UTF-8') as f:
                self.__descriptors = json.loads(f.read())
            self.__descriptor_file_path = descriptors_file
        else: # descriptors is not None
            self.__descriptors = json.loads(descriptors) if isinstance(descriptors, str) else descriptors
            self.__descriptor_file_path = '<dict>'

        self.__num_workers = num_workers

        self.__dummy_mode  = self.__descriptors.get('dummy_mode' , False)
        self.__contexts    = self.__descriptors.get('contexts'   , [])
        self.__topologies  = self.__descriptors.get('topologies' , [])
@@ -95,8 +106,6 @@ class DescriptorLoader:
        self.__slices      = self.__descriptors.get('slices'     , [])
        self.__connections = self.__descriptors.get('connections', [])

        self.__num_workers = num_workers

        self.__contexts_add   = None
        self.__topologies_add = None
        self.__devices_add    = None
@@ -111,6 +120,24 @@ class DescriptorLoader:

        self.__results : TypeResults = list()

    @property
    def descriptor_file_path(self) -> Optional[str]: return self.__descriptor_file_path

    @property
    def num_workers(self) -> int: return self.__num_workers

    @property
    def context_client(self) -> Optional[ContextClient]: return self.__ctx_cli

    @property
    def device_client(self) -> Optional[DeviceClient]: return self.__dev_cli

    @property
    def service_client(self) -> Optional[ServiceClient]: return self.__svc_cli

    @property
    def slice_client(self) -> Optional[SliceClient]: return self.__slc_cli

    @property
    def contexts(self) -> List[Dict]: return self.__contexts

@@ -269,3 +296,85 @@ class DescriptorLoader:

        error_list = [str_error for _,str_error in sorted(error_list, key=operator.itemgetter(0))]
        self.__results.append((entity_name, action_name, num_ok, error_list))

    def validate(self) -> None:
        self.__ctx_cli.connect()

        contexts = self.__ctx_cli.ListContexts(Empty())
        assert len(contexts.contexts) == self.num_contexts

        for context_uuid, num_topologies in self.num_topologies.items():
            response = self.__ctx_cli.ListTopologies(ContextId(**json_context_id(context_uuid)))
            assert len(response.topologies) == num_topologies

        response = self.__ctx_cli.ListDevices(Empty())
        assert len(response.devices) == self.num_devices

        response = self.__ctx_cli.ListLinks(Empty())
        assert len(response.links) == self.num_links

        for context_uuid, num_services in self.num_services.items():
            response = self.__ctx_cli.ListServices(ContextId(**json_context_id(context_uuid)))
            assert len(response.services) == num_services

        for context_uuid, num_slices in self.num_slices.items():
            response = self.__ctx_cli.ListSlices(ContextId(**json_context_id(context_uuid)))
            assert len(response.slices) == num_slices

    def unload(self) -> None:
        self.__ctx_cli.connect()
        self.__dev_cli.connect()
        self.__svc_cli.connect()
        self.__slc_cli.connect()

        for _, slice_list in self.slices.items():
            for slice_ in slice_list:
                self.__slc_cli.DeleteSlice(SliceId(**slice_['slice_id']))

        for _, service_list in self.services.items():
            for service in service_list:
                self.__svc_cli.DeleteService(ServiceId(**service['service_id']))

        for link in self.links:
            self.__ctx_cli.RemoveLink(LinkId(**link['link_id']))

        for device in self.devices:
            self.__dev_cli.DeleteDevice(DeviceId(**device['device_id']))

        for _, topology_list in self.topologies.items():
            for topology in topology_list:
                self.__ctx_cli.RemoveTopology(TopologyId(**topology['topology_id']))

        for context in self.contexts:
            self.__ctx_cli.RemoveContext(ContextId(**context['context_id']))

def compose_notifications(results : TypeResults) -> TypeNotificationList:
    notifications = []
    for entity_name, action_name, num_ok, error_list in results:
        entity_name_singluar,entity_name_plural = ENTITY_TO_TEXT[entity_name]
        action_infinitive, action_past = ACTION_TO_TEXT[action_name]
        num_err = len(error_list)
        for error in error_list:
            notifications.append((f'Unable to {action_infinitive} {entity_name_singluar} {error}', 'error'))
        if num_ok : notifications.append((f'{str(num_ok)} {entity_name_plural} {action_past}', 'success'))
        if num_err: notifications.append((f'{str(num_err)} {entity_name_plural} failed', 'danger'))
    return notifications

def check_descriptor_load_results(results : TypeResults, descriptor_loader : DescriptorLoader) -> None:
    num_errors = 0
    for message,level in compose_notifications(results):
        LOGGERS.get(level)(message)
        if level != 'success': num_errors += 1
    if num_errors > 0:
        MSG = 'Failed to load descriptors from "{:s}"'
        raise Exception(MSG.format(str(descriptor_loader.descriptor_file_path)))

def validate_empty_scenario(context_client : ContextClient) -> None:
    response = context_client.ListContexts(Empty())
    assert len(response.contexts) == 0

    response = context_client.ListDevices(Empty())
    assert len(response.devices) == 0

    response = context_client.ListLinks(Empty())
    assert len(response.links) == 0
+13 −32
Original line number Diff line number Diff line
@@ -13,10 +13,10 @@
# limitations under the License.

import logging, time
from common.Constants import DEFAULT_CONTEXT_NAME
from common.proto.context_pb2 import ContextId, Empty
from common.proto.monitoring_pb2 import KpiDescriptorList
from common.tests.LoadScenario import load_scenario_from_descriptor
from common.tools.grpc.Tools import grpc_message_to_json_string
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 device.client.DeviceClient import DeviceClient
@@ -27,44 +27,25 @@ LOGGER = logging.getLogger(__name__)
LOGGER.setLevel(logging.DEBUG)

DESCRIPTOR_FILE = 'ofc22/descriptors_emulated.json'
ADMIN_CONTEXT_ID = ContextId(**json_context_id(DEFAULT_CONTEXT_NAME))

def test_scenario_bootstrap(
    context_client : ContextClient, # pylint: disable=redefined-outer-name
    device_client : DeviceClient,   # pylint: disable=redefined-outer-name
) -> None:
    # ----- List entities - Ensure database is empty -------------------------------------------------------------------
    response = context_client.ListContexts(Empty())
    assert len(response.contexts) == 0
    validate_empty_scenario(context_client)

    response = context_client.ListDevices(Empty())
    assert len(response.devices) == 0

    response = context_client.ListLinks(Empty())
    assert len(response.links) == 0


    # ----- Load Scenario ----------------------------------------------------------------------------------------------
    descriptor_loader = load_scenario_from_descriptor(
        DESCRIPTOR_FILE, context_client, device_client, None, None)


    # ----- List entities - Ensure scenario is ready -------------------------------------------------------------------
    response = context_client.ListContexts(Empty())
    assert len(response.contexts) == descriptor_loader.num_contexts

    for context_uuid, num_topologies in descriptor_loader.num_topologies.items():
        response = context_client.ListTopologies(ContextId(**json_context_id(context_uuid)))
        assert len(response.topologies) == num_topologies

    response = context_client.ListDevices(Empty())
    assert len(response.devices) == descriptor_loader.num_devices
    descriptor_loader = DescriptorLoader(
        descriptors_file=DESCRIPTOR_FILE, context_client=context_client, device_client=device_client)
    results = descriptor_loader.process()
    check_descriptor_load_results(results, descriptor_loader)
    descriptor_loader.validate()

    response = context_client.ListLinks(Empty())
    assert len(response.links) == descriptor_loader.num_links
    # Verify the scenario has no services/slices
    response = context_client.GetContext(ADMIN_CONTEXT_ID)
    assert len(response.service_ids) == 0
    assert len(response.slice_ids) == 0

    for context_uuid, _ in descriptor_loader.num_services.items():
        response = context_client.ListServices(ContextId(**json_context_id(context_uuid)))
        assert len(response.services) == 0

def test_scenario_kpis_created(
    context_client : ContextClient,         # pylint: disable=redefined-outer-name
+16 −52
Original line number Diff line number Diff line
@@ -13,9 +13,10 @@
# limitations under the License.

import logging
from common.tools.descriptor.Loader import DescriptorLoader
from common.Constants import DEFAULT_CONTEXT_NAME
from common.proto.context_pb2 import ContextId
from common.tools.descriptor.Loader import DescriptorLoader, validate_empty_scenario
from common.tools.object_factory.Context import json_context_id
from common.proto.context_pb2 import ContextId, DeviceId, Empty, LinkId, TopologyId
from context.client.ContextClient import ContextClient
from device.client.DeviceClient import DeviceClient
from tests.Fixtures import context_client, device_client    # pylint: disable=unused-import
@@ -24,57 +25,20 @@ LOGGER = logging.getLogger(__name__)
LOGGER.setLevel(logging.DEBUG)

DESCRIPTOR_FILE = 'ofc22/descriptors_emulated.json'
ADMIN_CONTEXT_ID = ContextId(**json_context_id(DEFAULT_CONTEXT_NAME))


def test_services_removed(
def test_scenario_cleanup(
    context_client : ContextClient, # pylint: disable=redefined-outer-name
    device_client : DeviceClient,   # pylint: disable=redefined-outer-name
) -> None:
    # ----- List entities - Ensure service is removed ------------------------------------------------------------------
    with open(DESCRIPTOR_FILE, 'r', encoding='UTF-8') as f:
        descriptors = f.read()

    descriptor_loader = DescriptorLoader(descriptors)

    response = context_client.ListContexts(Empty())
    assert len(response.contexts) == descriptor_loader.num_contexts

    for context_uuid, num_topologies in descriptor_loader.num_topologies.items():
        response = context_client.ListTopologies(ContextId(**json_context_id(context_uuid)))
        assert len(response.topologies) == num_topologies

    response = context_client.ListDevices(Empty())
    assert len(response.devices) == descriptor_loader.num_devices

    response = context_client.ListLinks(Empty())
    assert len(response.links) == descriptor_loader.num_links

    for context_uuid, _ in descriptor_loader.num_services.items():
        response = context_client.ListServices(ContextId(**json_context_id(context_uuid)))
        assert len(response.services) == 0


    # ----- Delete Links, Devices, Topologies, Contexts ----------------------------------------------------------------
    for link in descriptor_loader.links:
        context_client.RemoveLink(LinkId(**link['link_id']))

    for device in descriptor_loader.devices:
        device_client .DeleteDevice(DeviceId(**device['device_id']))

    for context_uuid, topology_list in descriptor_loader.topologies.items():
        for topology in topology_list:
            context_client.RemoveTopology(TopologyId(**topology['topology_id']))

    for context in descriptor_loader.contexts:
        context_client.RemoveContext(ContextId(**context['context_id']))


    # ----- List entities - Ensure database is empty again -------------------------------------------------------------
    response = context_client.ListContexts(Empty())
    assert len(response.contexts) == 0

    response = context_client.ListDevices(Empty())
    assert len(response.devices) == 0

    response = context_client.ListLinks(Empty())
    assert len(response.links) == 0
    # Verify the scenario has no services/slices
    response = context_client.GetContext(ADMIN_CONTEXT_ID)
    assert len(response.service_ids) == 0
    assert len(response.slice_ids) == 0

    # Load descriptors and validate the base scenario
    descriptor_loader = DescriptorLoader(
        descriptors_file=DESCRIPTOR_FILE, context_client=context_client, device_client=device_client)
    descriptor_loader.validate()
    descriptor_loader.unload()
    validate_empty_scenario(context_client)
+35 −57
Original line number Diff line number Diff line
@@ -13,15 +13,15 @@
# limitations under the License.

import logging, random
from common.DeviceTypes import DeviceTypeEnum
from common.proto.context_pb2 import ContextId, Empty
from common.Constants import DEFAULT_CONTEXT_NAME
from common.proto.context_pb2 import ContextId, Empty, ServiceTypeEnum
from common.proto.kpi_sample_types_pb2 import KpiSampleType
from common.tools.descriptor.Loader import DescriptorLoader
from common.tools.grpc.Tools import grpc_message_to_json_string
from common.tools.object_factory.Context import json_context_id
from context.client.ContextClient import ContextClient
from monitoring.client.MonitoringClient import MonitoringClient
from tests.Fixtures import context_client, device_client, monitoring_client # pylint: disable=unused-import
from tests.Fixtures import context_client, monitoring_client                    # pylint: disable=unused-import
from tests.tools.mock_osm.MockOSM import MockOSM
from .Fixtures import osm_wim                                                   # pylint: disable=unused-import
from .Objects import WIM_SERVICE_CONNECTION_POINTS, WIM_SERVICE_TYPE
@@ -29,67 +29,45 @@ from .Objects import WIM_SERVICE_CONNECTION_POINTS, WIM_SERVICE_TYPE
LOGGER = logging.getLogger(__name__)
LOGGER.setLevel(logging.DEBUG)

DEVTYPE_EMU_PR  = DeviceTypeEnum.EMULATED_PACKET_ROUTER.value
DEVTYPE_EMU_OLS = DeviceTypeEnum.EMULATED_OPEN_LINE_SYSTEM.value

DESCRIPTOR_FILE = 'ofc22/descriptors_emulated.json'
ADMIN_CONTEXT_ID = ContextId(**json_context_id(DEFAULT_CONTEXT_NAME))

def test_service_creation(context_client : ContextClient, osm_wim : MockOSM): # pylint: disable=redefined-outer-name
    # ----- List entities - Ensure scenario is ready -------------------------------------------------------------------
    with open(DESCRIPTOR_FILE, 'r', encoding='UTF-8') as f:
        descriptors = f.read()

    descriptor_loader = DescriptorLoader(descriptors)

    response = context_client.ListContexts(Empty())
    assert len(response.contexts) == descriptor_loader.num_contexts

    for context_uuid, num_topologies in descriptor_loader.num_topologies.items():
        response = context_client.ListTopologies(ContextId(**json_context_id(context_uuid)))
        assert len(response.topologies) == num_topologies

    response = context_client.ListDevices(Empty())
    assert len(response.devices) == descriptor_loader.num_devices

    response = context_client.ListLinks(Empty())
    assert len(response.links) == descriptor_loader.num_links

    for context_uuid, num_services in descriptor_loader.num_services.items():
        response = context_client.ListServices(ContextId(**json_context_id(context_uuid)))
        assert len(response.services) == 0
    # Load descriptors and validate the base scenario
    descriptor_loader = DescriptorLoader(descriptors_file=DESCRIPTOR_FILE, context_client=context_client)
    descriptor_loader.validate()

    # Verify the scenario has no services/slices
    response = context_client.GetContext(ADMIN_CONTEXT_ID)
    assert len(response.service_ids) == 0
    assert len(response.slice_ids) == 0

    # ----- Create Service ---------------------------------------------------------------------------------------------
    # Create Connectivity Service
    service_uuid = osm_wim.create_connectivity_service(WIM_SERVICE_TYPE, WIM_SERVICE_CONNECTION_POINTS)
    osm_wim.get_connectivity_service_status(service_uuid)

    # Ensure slices and services are created
    response = context_client.ListSlices(ADMIN_CONTEXT_ID)
    LOGGER.info('Slices[{:d}] = {:s}'.format(len(response.slices), grpc_message_to_json_string(response)))
    assert len(response.slices) == 1 # OSM slice

    # ----- List entities - Ensure service is created ------------------------------------------------------------------
    response = context_client.ListContexts(Empty())
    assert len(response.contexts) == descriptor_loader.num_contexts

    for context_uuid, num_topologies in descriptor_loader.num_topologies.items():
        response = context_client.ListTopologies(ContextId(**json_context_id(context_uuid)))
        assert len(response.topologies) == num_topologies

    response = context_client.ListDevices(Empty())
    assert len(response.devices) == descriptor_loader.num_devices

    response = context_client.ListLinks(Empty())
    assert len(response.links) == descriptor_loader.num_links

    for context_uuid, num_services in descriptor_loader.num_services.items():
        response = context_client.ListServices(ContextId(**json_context_id(context_uuid)))
    response = context_client.ListServices(ADMIN_CONTEXT_ID)
    LOGGER.info('Services[{:d}] = {:s}'.format(len(response.services), grpc_message_to_json_string(response)))
        assert len(response.services) == 2*num_services # OLS & L3NM => (L3NM + TAPI)
    assert len(response.services) == 2 # 1xL3NM + 1xTAPI

    for service in response.services:
        service_id = service.service_id
        response = context_client.ListConnections(service_id)
        LOGGER.info('  ServiceId[{:s}] => Connections[{:d}] = {:s}'.format(
                grpc_message_to_json_string(service_id), len(response.connections),
                grpc_message_to_json_string(response)))
            assert len(response.connections) == 1 # one connection per service
            grpc_message_to_json_string(service_id), len(response.connections), grpc_message_to_json_string(response)))

        if service.service_type == ServiceTypeEnum.SERVICETYPE_L3NM:
            assert len(response.connections) == 1 # 1 connection per service
        elif service.service_type == ServiceTypeEnum.SERVICETYPE_TAPI_CONNECTIVITY_SERVICE:
            assert len(response.connections) == 1 # 1 connection per service
        else:
            str_service = grpc_message_to_json_string(service)
            raise Exception('Unexpected ServiceType: {:s}'.format(str_service))


def test_scenario_kpi_values_created(
Loading