Commit 414f41fa authored by Lluis Gifre Renom's avatar Lluis Gifre Renom
Browse files

OFC25 test:

- Implemented vlink / optical connection validation logic.
parent 2a29d661
Loading
Loading
Loading
Loading
+143 −51
Original line number Diff line number Diff line
# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/)
# Copyright 2022-2026 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.
@@ -12,72 +12,164 @@
# See the License for the specific language governing permissions and
# limitations under the License.

import logging, os
import logging
import os
import time
from typing import List, Set

from common.Constants import DEFAULT_CONTEXT_NAME
from common.proto.context_pb2 import (
    ContextId, ServiceStatusEnum, ServiceTypeEnum,
)
from common.tools.descriptor.Loader import (
    DescriptorLoader, check_descriptor_load_results,
)
from common.proto.context_pb2 import ContextId, Empty, LinkTypeEnum, ServiceStatusEnum, ServiceTypeEnum
from common.tools.descriptor.Loader import DescriptorLoader, check_descriptor_load_results
from common.tools.grpc.Tools import grpc_message_to_json_string
from common.tools.object_factory.Context import json_context_id

from .Fixtures import (
    PROFILE_IP,
    tfs_clients,
)  # pylint: disable=unused-import
from .Fixtures import PROFILE_E2E, PROFILE_IP, PROFILE_OPT, tfs_clients  # pylint: disable=unused-import

LOGGER = logging.getLogger(__name__)
LOGGER.setLevel(logging.DEBUG)

DESCRIPTOR_FILES = [
    os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'descriptors', fname)
    for fname in ['virtual_link_01.json', 'virtual_link_02.json', 'virtual_link_03.json']
]
ADMIN_CONTEXT_ID = ContextId(**json_context_id(DEFAULT_CONTEXT_NAME))

def test_create_virtual_link(
    tfs_clients,
) -> None:
    context_client = tfs_clients[PROFILE_IP].context
    device_client = tfs_clients[PROFILE_IP].device
    for descriptor_file in DESCRIPTOR_FILES:
        # Load descriptors and validate the base scenario
        pass

    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)

    # Verify the scenario has 1 service and 0 slices
    response = context_client.GetContext(ADMIN_CONTEXT_ID)
    assert len(response.service_ids) == 1
    assert len(response.slice_ids) == 0
VIRTUAL_LINK_DESCRIPTORS = [
    ('virtual_link_01.json', 'IP1/PORT-xe1==IP2/PORT-xe1'),
    ('virtual_link_02.json', 'IP1/PORT-xe2==IP2/PORT-xe2'),
    ('virtual_link_03.json', 'IP1/PORT-xe3==IP2/PORT-xe3'),
]
DESCRIPTORS_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'descriptors')

    # Check there are no slices
    response = context_client.ListSlices(ADMIN_CONTEXT_ID)
    LOGGER.warning('Slices[{:d}] = {:s}'.format(len(response.slices), grpc_message_to_json_string(response)))
    assert len(response.slices) == 0

    # Check there is 1 service
def _list_active_optical_services(context_client) -> List:
    response = context_client.ListServices(ADMIN_CONTEXT_ID)
    LOGGER.warning('Services[{:d}] = {:s}'.format(len(response.services), grpc_message_to_json_string(response)))
    assert len(response.services) == 1
    LOGGER.info('Services[%d] = %s', len(response.services), grpc_message_to_json_string(response))

    active_optical_services = []
    for service in response.services:
        service_id = service.service_id
        assert service.service_type == ServiceTypeEnum.SERVICETYPE_OPTICAL_CONNECTIVITY
        assert service.service_status.service_status == ServiceStatusEnum.SERVICESTATUS_ACTIVE
        active_optical_services.append(service)
    return active_optical_services


def _count_service_connections(context_client, service) -> int:
    response = context_client.ListConnections(service.service_id)
    LOGGER.info(
        'ServiceId[%s] => Connections[%d] = %s',
        grpc_message_to_json_string(service.service_id),
        len(response.connections),
        grpc_message_to_json_string(response),
    )
    return len(response.connections)


def _get_virtual_link_ids(context_client) -> Set[str]:
    response = context_client.ListLinks(Empty())
    virtual_link_ids = {
        link.link_id.link_uuid.uuid
        for link in response.links
        if link.link_type == LinkTypeEnum.LINKTYPE_VIRTUAL
    }
    LOGGER.info('VirtualLinks[%d] = %s', len(virtual_link_ids), str(sorted(virtual_link_ids)))
    return virtual_link_ids


def _assert_global_state(
    ip_context_client,
    e2e_context_client,
    opt_context_client,
    expected_virtual_link_ids: Set[str],
    expected_e2e_services: int,
    expected_opt_connections: int,
) -> None:
    # IP should not have services in this OFC25 workflow.
    response = ip_context_client.ListServices(ADMIN_CONTEXT_ID)
    assert len(response.services) == 0

        response = context_client.ListConnections(service_id)
        LOGGER.warning('  ServiceId[{:s}] => Connections[{:d}] = {:s}'.format(
            grpc_message_to_json_string(service_id), len(response.connections), grpc_message_to_json_string(response)))
    virtual_link_ids = _get_virtual_link_ids(ip_context_client)
    assert virtual_link_ids == expected_virtual_link_ids

        if service.service_type == ServiceTypeEnum.SERVICETYPE_OPTICAL_CONNECTIVITY:
            assert len(response.connections) == 2
    e2e_services = _list_active_optical_services(e2e_context_client)
    if expected_e2e_services == 0:
        assert len(e2e_services) == 0
    else:
            str_service = grpc_message_to_json_string(service)
            raise Exception('Unexpected ServiceType: {:s}'.format(str_service))
        assert len(e2e_services) == expected_e2e_services
        for service in e2e_services:
            assert _count_service_connections(e2e_context_client, service) == 1

    opt_services = _list_active_optical_services(opt_context_client)
    if expected_opt_connections == 0:
        assert len(opt_services) == 0
    else:
        assert len(opt_services) == 1
        assert _count_service_connections(opt_context_client, opt_services[0]) == expected_opt_connections


def _wait_for_state_or_raise(
    ip_context_client,
    e2e_context_client,
    opt_context_client,
    expected_virtual_link_ids: Set[str],
    expected_e2e_services: int,
    expected_opt_connections: int,
    max_retry: int = 12,
    wait_seconds: float = 1.0,
) -> None:
    last_error: Exception = Exception('state not reached')
    for _ in range(max_retry):
        try:
            _assert_global_state(
                ip_context_client=ip_context_client,
                e2e_context_client=e2e_context_client,
                opt_context_client=opt_context_client,
                expected_virtual_link_ids=expected_virtual_link_ids,
                expected_e2e_services=expected_e2e_services,
                expected_opt_connections=expected_opt_connections,
            )
            return
        except Exception as error:  # pylint: disable=broad-except
            last_error = error
            time.sleep(wait_seconds)

    msg = (
        'Timed out waiting expected state: virtual_links={:s} e2e_services={:d} opt_connections={:d}; error={:s}'
    )
    raise Exception(msg.format(str(sorted(expected_virtual_link_ids)), expected_e2e_services, expected_opt_connections, str(last_error)))


def test_create_virtual_link(
    tfs_clients,
) -> None:
    ip_context_client = tfs_clients[PROFILE_IP].context
    ip_device_client = tfs_clients[PROFILE_IP].device
    e2e_context_client = tfs_clients[PROFILE_E2E].context
    opt_context_client = tfs_clients[PROFILE_OPT].context

    # Initial state: no services in any TFS and no virtual links in IP.
    _wait_for_state_or_raise(
        ip_context_client=ip_context_client,
        e2e_context_client=e2e_context_client,
        opt_context_client=opt_context_client,
        expected_virtual_link_ids=set(),
        expected_e2e_services=0,
        expected_opt_connections=0,
    )

    expected_virtual_link_ids: Set[str] = set()
    for index, (descriptor_name, virtual_link_id) in enumerate(VIRTUAL_LINK_DESCRIPTORS, start=1):
        descriptor_file = os.path.join(DESCRIPTORS_DIR, descriptor_name)
        descriptor_loader = DescriptorLoader(
            descriptors_file=descriptor_file,
            context_client=ip_context_client,
            device_client=ip_device_client,
        )
        results = descriptor_loader.process()
        check_descriptor_load_results(results, descriptor_loader)

        expected_virtual_link_ids.add(virtual_link_id)
        _wait_for_state_or_raise(
            ip_context_client=ip_context_client,
            e2e_context_client=e2e_context_client,
            opt_context_client=opt_context_client,
            expected_virtual_link_ids=expected_virtual_link_ids,
            expected_e2e_services=index,
            expected_opt_connections=1,
        )
+41 −57
Original line number Diff line number Diff line
# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/)
# Copyright 2022-2026 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.
@@ -12,69 +12,53 @@
# See the License for the specific language governing permissions and
# limitations under the License.

import logging
from typing import Set, Tuple
import os

from common.Constants import DEFAULT_CONTEXT_NAME
from common.proto.context_pb2 import (
    ContextId, ServiceId, ServiceStatusEnum, ServiceTypeEnum,
)
from common.tools.grpc.Tools import grpc_message_to_json_string
from common.tools.object_factory.Context import json_context_id
from .Fixtures import PROFILE_IP, tfs_clients # pylint: disable=unused-import
from common.tools.descriptor.Loader import DescriptorLoader

LOGGER = logging.getLogger(__name__)
LOGGER.setLevel(logging.DEBUG)
from .Fixtures import PROFILE_E2E, PROFILE_IP, PROFILE_OPT, tfs_clients  # pylint: disable=unused-import
from .test_functional_create_vlinks import (
    DESCRIPTORS_DIR,
    VIRTUAL_LINK_DESCRIPTORS,
    _wait_for_state_or_raise,
)

ADMIN_CONTEXT_ID = ContextId(**json_context_id(DEFAULT_CONTEXT_NAME))

def test_service_removal_bidir(
    tfs_clients,
):
    context_client = tfs_clients[PROFILE_IP].context
    # Verify the scenario has 1 service and 0 slices
    response = context_client.GetContext(ADMIN_CONTEXT_ID)
    assert len(response.service_ids) == 1
    assert len(response.slice_ids) == 0

    # Check there are no slices
    response = context_client.ListSlices(ADMIN_CONTEXT_ID)
    LOGGER.warning('Slices[{:d}] = {:s}'.format(len(response.slices), grpc_message_to_json_string(response)))
    assert len(response.slices) == 0

    # Check there is 1 service
    response = context_client.ListServices(ADMIN_CONTEXT_ID)
    LOGGER.warning('Services[{:d}] = {:s}'.format(len(response.services), grpc_message_to_json_string(response)))
    assert len(response.services) == 1

    context_service_uuids : Set[Tuple[str, str]] = set()
    for service in response.services:
        service_id = service.service_id
        assert service.service_status.service_status == ServiceStatusEnum.SERVICESTATUS_ACTIVE

        response = context_client.ListConnections(service_id)
        LOGGER.warning('  ServiceId[{:s}] => Connections[{:d}] = {:s}'.format(
            grpc_message_to_json_string(service_id), len(response.connections), grpc_message_to_json_string(response)))
    ip_context_client = tfs_clients[PROFILE_IP].context
    ip_device_client = tfs_clients[PROFILE_IP].device
    e2e_context_client = tfs_clients[PROFILE_E2E].context
    opt_context_client = tfs_clients[PROFILE_OPT].context

        if service.service_type == ServiceTypeEnum.SERVICETYPE_OPTICAL_CONNECTIVITY:
            assert len(response.connections) == 2
            context_uuid = service_id.context_id.context_uuid.uuid
            service_uuid = service_id.service_uuid.uuid
            context_service_uuids.add((context_uuid, service_uuid))
        else:
            str_service = grpc_message_to_json_string(service)
            raise Exception('Unexpected ServiceType: {:s}'.format(str_service))

    # Identify service to delete
    assert len(context_service_uuids) == 1
    context_uuid, service_uuid = set(context_service_uuids).pop()

    # Delete Service
    # service_client.DeleteService(ServiceId(**json_service_id(service_uuid, json_context_id(context_uuid))))
    expected_virtual_link_ids = {link_id for _, link_id in VIRTUAL_LINK_DESCRIPTORS}
    _wait_for_state_or_raise(
        ip_context_client=ip_context_client,
        e2e_context_client=e2e_context_client,
        opt_context_client=opt_context_client,
        expected_virtual_link_ids=expected_virtual_link_ids,
        expected_e2e_services=3,
        expected_opt_connections=1,
    )

    import delete_service
    for remaining, (descriptor_name, virtual_link_id) in zip(
        [2, 1, 0], reversed(VIRTUAL_LINK_DESCRIPTORS)
    ):
        descriptor_file = os.path.join(DESCRIPTORS_DIR, descriptor_name)
        descriptor_loader = DescriptorLoader(
            descriptors_file=descriptor_file,
            context_client=ip_context_client,
            device_client=ip_device_client,
        )
        descriptor_loader.unload()

    # 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
        expected_virtual_link_ids.remove(virtual_link_id)
        _wait_for_state_or_raise(
            ip_context_client=ip_context_client,
            e2e_context_client=e2e_context_client,
            opt_context_client=opt_context_client,
            expected_virtual_link_ids=expected_virtual_link_ids,
            expected_e2e_services=remaining,
            expected_opt_connections=(1 if remaining > 0 else 0),
        )