From 199fbb2c8ff40815b84ca709ff983efb79e01516 Mon Sep 17 00:00:00 2001 From: Ville Hallivuori Date: Tue, 21 Feb 2023 10:53:13 +0200 Subject: [PATCH 1/2] Support creating XR service without using multi-layer path computation --- src/device/service/drivers/xr/service-cli.py | 140 ++++++++++++++++-- .../service/drivers/xr/setup_test_env.sh | 4 + 2 files changed, 133 insertions(+), 11 deletions(-) diff --git a/src/device/service/drivers/xr/service-cli.py b/src/device/service/drivers/xr/service-cli.py index 01bd2aaa1..11cdc90fd 100755 --- a/src/device/service/drivers/xr/service-cli.py +++ b/src/device/service/drivers/xr/service-cli.py @@ -19,21 +19,31 @@ # # Run in this directory with PYTHONPATH=../../../../ # E.g.: +# Multi-layer: # PYTHONPATH=../../../../ ./service-cli.py create 1 R1-EMU 13/1/2 500 2 R3-EMU 13/1/2 500 +# Single-layer: +# PYTHONPATH=../../../../ ./service-cli.py create-xr mydirectservice7 X1-XR-CONSTELLATION "XR HUB 1|XR-T1" "XR LEAF 1|XR-T1" +# # PYTHONPATH=../../../../ ./service-cli.py list # PYTHONPATH=../../../../ ./service-cli.py delete 43a8046a-5dec-463d-82f7-7cc3442dbf4f - import argparse import logging -import traceback +#import traceback from contextlib import contextmanager from common.Settings import get_setting from context.client.ContextClient import ContextClient +from service.client.ServiceClient import ServiceClient from tests.tools.mock_osm.MockOSM import MockOSM -from common.proto.context_pb2 import ContextId, ServiceTypeEnum, ServiceStatusEnum +from common.proto.context_pb2 import ContextId, ServiceTypeEnum, ServiceStatusEnum, Service, Context, Empty from common.tools.grpc.Tools import grpc_message_to_json_string +from common.tools.object_factory.Context import json_context, json_context_id +from common.tools.object_factory.Topology import json_topology_id +from common.tools.object_factory.ConfigRule import json_config_rule_set +from copy import deepcopy +from dataclasses import dataclass, field +from typing import Dict LOGGER = logging.getLogger(__name__) @@ -48,11 +58,44 @@ def make_context_client(): finally: _client.close() +@contextmanager +def make_service_client(): + try: + _client = ServiceClient(get_setting('SERVICESERVICE_SERVICE_HOST'), get_setting('SERVICESERVICE_SERVICE_PORT_GRPC')) + yield _client + finally: + _client.close() + def make_osm_wim(): wim_url = 'http://{:s}:{:s}'.format( get_setting('COMPUTESERVICE_SERVICE_HOST'), str(get_setting('COMPUTESERVICE_SERVICE_PORT_HTTP'))) return MockOSM(wim_url, WIM_MAPPING, WIM_USERNAME, WIM_PASSWORD) +@dataclass +class DevInfo: + name: str + uuid: str + # endpoints name --> uuid + endpoints: Dict[str, str] = field(default_factory= dict) + + def get_endpoint_uuid_or_exit(self, ep_name: str) -> str: + if ep_name not in self.endpoints: + print(f"Endpoint {ep_name} does not exist in device {self.name}. See \"service-cli.py list-endpoints\"") + exit(-1) + return self.endpoints[ep_name] + +def get_devices(cc: ContextClient) -> Dict[str, DevInfo]: + r = cc.ListDevices(Empty()) + # print(grpc_message_to_json_string(r)) + + devices = dict() + for dev in r.devices: + di = DevInfo(dev.name, dev.device_id.device_uuid.uuid) + for ep in dev.device_endpoints: + di.endpoints[ep.name] = ep.endpoint_id.endpoint_uuid.uuid + devices[dev.name] = di + return devices + logging.basicConfig(level=logging.ERROR) parser = argparse.ArgumentParser(description='TF Service Management Utility') @@ -74,6 +117,13 @@ delete_parser = subparsers.add_parser('delete') delete_parser.add_argument('service_uuid', type=str, help='UUID of the service to be deleted') list_parser = subparsers.add_parser('list') +list_parser = subparsers.add_parser('list-endpoints') + +create_xr_parser = subparsers.add_parser('create-xr') +create_xr_parser.add_argument('service_name', type=str, help='Service Name') +create_xr_parser.add_argument('constellation', type=str, help='XR Constellation') +create_xr_parser.add_argument('interface1', type=str, help='One endpoint of the service') +create_xr_parser.add_argument('interface2', type=str, help='Second endpoint of the service') args = parser.parse_args() @@ -103,12 +153,12 @@ else: WIM_SERVICE_CONNECTION_POINTS = [] #print(str(args)) -print(f"=== WIM_SERVICE_TYPE: {WIM_SERVICE_TYPE}") -print(f"=== WIM_SERVICE_CONNECTION_POINTS: {WIM_SERVICE_CONNECTION_POINTS}") -print(f"=== WIM_MAPPING: {WIM_MAPPING}") +#print(f"=== WIM_SERVICE_TYPE: {WIM_SERVICE_TYPE}") +#print(f"=== WIM_SERVICE_CONNECTION_POINTS: {WIM_SERVICE_CONNECTION_POINTS}") +#print(f"=== WIM_MAPPING: {WIM_MAPPING}") with make_context_client() as client: - osm_wim = make_osm_wim(); + osm_wim = make_osm_wim() if args.command == "create": service_uuid = osm_wim.create_connectivity_service(WIM_SERVICE_TYPE, WIM_SERVICE_CONNECTION_POINTS) @@ -123,22 +173,90 @@ with make_context_client() as client: print(f"*** Service {args.service_uuid} is no longer present (delete was successfull or service did not exist)") except Exception as e: print(f"*** Failed to delete service {args.service_uuid}, {e}") + + elif args.command == "create-xr": + CONTEXT_NAME = 'admin' + CONTEXT_ID = json_context_id(CONTEXT_NAME) + CONTEXT = json_context(CONTEXT_NAME, name=CONTEXT_NAME) + + json_tapi_settings = { + 'capacity_value' : 50.0, + 'capacity_unit' : 'GHz', + 'layer_proto_name': 'PHOTONIC_MEDIA', + 'layer_proto_qual': 'tapi-photonic-media:PHOTONIC_LAYER_QUALIFIER_NMC', + 'direction' : 'UNIDIRECTIONAL', + } + config_rule = json_config_rule_set('/settings', json_tapi_settings) + + response = client.ListContextIds(Empty()) + assert len(response.context_ids) == 1 + context_uuid=json_context_id(response.context_ids[0].context_uuid.uuid) + + devices = get_devices(client) + if args.constellation not in devices: + print(f"Constellation {args.constellation} does not exist as a device. See \"service-cli.py list-endpoints\"") + exit(-1) + else: + dev_info = devices[args.constellation] + constellation_uuid = dev_info.uuid + + interface1_uuid = dev_info.get_endpoint_uuid_or_exit(args.interface1) + interface2_uuid = dev_info.get_endpoint_uuid_or_exit(args.interface2) + + print(f"Constellation {args.constellation:40}: {constellation_uuid:36}") + print(f"Interface 1 {args.interface1:40}: {interface1_uuid:36}") + print(f"Interface 2 {args.interface2:40}: {interface2_uuid:36}") + + service_request = { + "name": args.service_name, + "service_id": { + "context_id": {"context_uuid": {"uuid": response.context_ids[0].context_uuid.uuid}}, + "service_uuid": {"uuid": args.service_name} + }, + 'service_type' : ServiceTypeEnum.SERVICETYPE_TAPI_CONNECTIVITY_SERVICE, + "service_endpoint_ids": [ + {'device_id': {'device_uuid': {'uuid': constellation_uuid}}, 'endpoint_uuid': {'uuid': interface1_uuid}, 'topology_id': json_topology_id("admin", context_id=context_uuid)}, + {'device_id': {'device_uuid': {'uuid': constellation_uuid}}, 'endpoint_uuid': {'uuid': interface2_uuid}, 'topology_id': json_topology_id("admin", context_id=context_uuid)} + ], + 'service_status' : {'service_status': ServiceStatusEnum.SERVICESTATUS_PLANNED}, + 'service_constraints' : [], + } + + with make_service_client() as service_client: + sr = deepcopy(service_request) + endpoints, sr['service_endpoint_ids'] = sr['service_endpoint_ids'], [] + create_response = service_client.CreateService(Service(**sr)) + print(f'CreateService: {grpc_message_to_json_string(create_response)}') + + sr['service_endpoint_ids'] = endpoints + #sr['service_id']['service_uuid'] = create_response + sr['service_config'] = {'config_rules': [config_rule]} + + update_response = service_client.UpdateService(Service(**sr)) + print(f'UpdateService: {grpc_message_to_json_string(update_response)}') + elif args.command == "list": response = client.ListServices(ContextId(**CONTEXT_ID)) #print('Services[{:d}] = {:s}'.format(len(response.services), grpc_message_to_json_string(response))) for service in response.services: scs = "" - + # See if there are endpoint constraints that might be regognizable by the user. # Keys do not necessarily exist, so catch exceptions and ignore those constraints # that we cannot easily represent. for sc in service.service_constraints: try: scs += f"{sc.endpoint_location.endpoint_id.device_id.device_uuid.uuid}:{sc.endpoint_location.endpoint_id.endpoint_uuid.uuid} " - except Exception: + except Exception: # pylint: disable=bare-except pass - print(f"{service.service_id.service_uuid.uuid:36} {ServiceTypeEnum.Name(service.service_type):40} {ServiceStatusEnum.Name(service.service_status.service_status)} {scs}") - + print(f"{service.service_id.service_uuid.uuid:36} {ServiceTypeEnum.Name(service.service_type):40} {service.name:40} {ServiceStatusEnum.Name(service.service_status.service_status)} {scs}") + elif args.command == "list-endpoints": + devices = get_devices(client) + for name in sorted(devices.keys()): + dev = devices[name] + print(f"{name:40} {dev.uuid:36}") + for ep_name in sorted(dev.endpoints.keys()): + print(f" {ep_name:40} {dev.endpoints[ep_name]:36}") diff --git a/src/device/service/drivers/xr/setup_test_env.sh b/src/device/service/drivers/xr/setup_test_env.sh index 92ff4a031..bd5463cd4 100755 --- a/src/device/service/drivers/xr/setup_test_env.sh +++ b/src/device/service/drivers/xr/setup_test_env.sh @@ -17,7 +17,11 @@ export CONTEXTSERVICE_SERVICE_HOST=$(kubectl get service/contextservice --namesp export CONTEXTSERVICE_SERVICE_PORT_GRPC=$(kubectl get service/contextservice --namespace tfs -o jsonpath='{.spec.ports[?(@.name=="grpc")].port}') export COMPUTESERVICE_SERVICE_HOST=$(kubectl get service/computeservice --namespace tfs --template '{{.spec.clusterIP}}') export COMPUTESERVICE_SERVICE_PORT_HTTP=$(kubectl get service/computeservice --namespace tfs -o jsonpath='{.spec.ports[?(@.name=="http")].port}') +export SERVICESERVICE_SERVICE_HOST=$(kubectl get service/serviceservice --namespace tfs --template '{{.spec.clusterIP}}') +export SERVICESERVICE_SERVICE_PORT_GRPC=$(kubectl get service/serviceservice --namespace tfs -o jsonpath='{.spec.ports[?(@.name=="grpc")].port}') echo "CONTEXTSERVICE_SERVICE_HOST=$CONTEXTSERVICE_SERVICE_HOST" echo "CONTEXTSERVICE_SERVICE_PORT_GRPC=$CONTEXTSERVICE_SERVICE_PORT_GRPC" echo "COMPUTESERVICE_SERVICE_HOST=$COMPUTESERVICE_SERVICE_HOST" echo "COMPUTESERVICE_SERVICE_PORT_HTTP=$COMPUTESERVICE_SERVICE_PORT_HTTP" +echo "SERVICESERVICE_SERVICE_HOST=$SERVICESERVICE_SERVICE_HOST" +echo "SERVICESERVICE_SERVICE_PORT_GRPC=$SERVICESERVICE_SERVICE_PORT_GRPC" -- GitLab From 8b3a0373fe8167bef380a1f75680f832d6d51d94 Mon Sep 17 00:00:00 2001 From: Ville Hallivuori Date: Tue, 21 Feb 2023 12:25:13 +0200 Subject: [PATCH 2/2] Allow deletion of low level services with service-cli.py --- src/device/service/drivers/xr/README_XR.md | 12 +++ src/device/service/drivers/xr/service-cli.py | 101 +++++++++++++------ 2 files changed, 83 insertions(+), 30 deletions(-) diff --git a/src/device/service/drivers/xr/README_XR.md b/src/device/service/drivers/xr/README_XR.md index c741c3e80..fa1bc9440 100644 --- a/src/device/service/drivers/xr/README_XR.md +++ b/src/device/service/drivers/xr/README_XR.md @@ -146,6 +146,18 @@ arbitrary endpoints in the topology (with consequent underlying XR service insta PYTHONPATH=../../../../ ./service-cli.py list PYTHONPATH=../../../../ ./service-cli.py delete 43a8046a-5dec-463d-82f7-7cc3442dbf4f ``` + +It is also possible to create direct XR services without multi-layer services. E.g.: +``` + PYTHONPATH=../../../../ ./service-cli.py create-xr FooService X1-XR-CONSTELLATION "XR HUB 1|XR-T1" "XR LEAF 2|XR-T1" +``` + +Additionally it is possible to list services and endpoints: +``` + PYTHONPATH=../../../../ ./service-cli.py list-endpoints + PYTHONPATH=../../../../ ./service-cli.py delete 43a8046a-5dec-463d-82f7-7cc3442dbf4f +``` + The PYTHONPATH is mandatory. Suitable topology JSON must have been loaded before. With the CocroachDB persistence, it is sufficient to load the topology once and it will persist. diff --git a/src/device/service/drivers/xr/service-cli.py b/src/device/service/drivers/xr/service-cli.py index 11cdc90fd..7ab9606ce 100755 --- a/src/device/service/drivers/xr/service-cli.py +++ b/src/device/service/drivers/xr/service-cli.py @@ -19,31 +19,33 @@ # # Run in this directory with PYTHONPATH=../../../../ # E.g.: -# Multi-layer: +# Create multi-layer service (L2 VPN over XR): # PYTHONPATH=../../../../ ./service-cli.py create 1 R1-EMU 13/1/2 500 2 R3-EMU 13/1/2 500 -# Single-layer: -# PYTHONPATH=../../../../ ./service-cli.py create-xr mydirectservice7 X1-XR-CONSTELLATION "XR HUB 1|XR-T1" "XR LEAF 1|XR-T1" -# +# Single-layer (XR without services on top of it): +# PYTHONPATH=../../../../ ./service-cli.py create-xr FooService X1-XR-CONSTELLATION "XR HUB 1|XR-T1" "XR LEAF 2|XR-T1" +# List services: # PYTHONPATH=../../../../ ./service-cli.py list +# List possible endpoints: +# PYTHONPATH=../../../../ ./service-cli.py list-endpoints +# Delete service (if multi-layer, always deleter highest layer!) # PYTHONPATH=../../../../ ./service-cli.py delete 43a8046a-5dec-463d-82f7-7cc3442dbf4f import argparse import logging -#import traceback +from copy import deepcopy +from dataclasses import dataclass, field +from typing import Dict from contextlib import contextmanager from common.Settings import get_setting from context.client.ContextClient import ContextClient from service.client.ServiceClient import ServiceClient from tests.tools.mock_osm.MockOSM import MockOSM -from common.proto.context_pb2 import ContextId, ServiceTypeEnum, ServiceStatusEnum, Service, Context, Empty +from common.proto.context_pb2 import ContextId, ServiceTypeEnum, ServiceStatusEnum, Service, Empty, ServiceId from common.tools.grpc.Tools import grpc_message_to_json_string from common.tools.object_factory.Context import json_context, json_context_id from common.tools.object_factory.Topology import json_topology_id from common.tools.object_factory.ConfigRule import json_config_rule_set -from copy import deepcopy -from dataclasses import dataclass, field -from typing import Dict LOGGER = logging.getLogger(__name__) @@ -75,8 +77,8 @@ def make_osm_wim(): class DevInfo: name: str uuid: str - # endpoints name --> uuid endpoints: Dict[str, str] = field(default_factory= dict) + endpoints_by_uuid: Dict[str, str] = field(default_factory= dict) def get_endpoint_uuid_or_exit(self, ep_name: str) -> str: if ep_name not in self.endpoints: @@ -93,9 +95,17 @@ def get_devices(cc: ContextClient) -> Dict[str, DevInfo]: di = DevInfo(dev.name, dev.device_id.device_uuid.uuid) for ep in dev.device_endpoints: di.endpoints[ep.name] = ep.endpoint_id.endpoint_uuid.uuid + di.endpoints_by_uuid[ep.endpoint_id.endpoint_uuid.uuid] = ep.name devices[dev.name] = di return devices +def get_endpoint_map(devices: Dict[str, DevInfo]): + ep_map = dict() + for dev in devices.values(): + for ep_name, ep_uuid in dev.endpoints.items(): + ep_map[ep_uuid] = (dev.name, ep_name) + return ep_map + logging.basicConfig(level=logging.ERROR) parser = argparse.ArgumentParser(description='TF Service Management Utility') @@ -158,6 +168,11 @@ else: #print(f"=== WIM_MAPPING: {WIM_MAPPING}") with make_context_client() as client: + # We only permit one context on our demos/testing + response = client.ListContextIds(Empty()) + assert len(response.context_ids) == 1 + context_uuid=json_context_id(response.context_ids[0].context_uuid.uuid) + osm_wim = make_osm_wim() if args.command == "create": @@ -167,12 +182,38 @@ with make_context_client() as client: print(f"*** Get created service status --> {str(status)}") elif args.command == "delete": - osm_wim.wim.check_credentials() + service_id = { + "context_id": context_uuid, + "service_uuid": { + "uuid": args.service_uuid + } + } + try: - osm_wim.wim.delete_connectivity_service(args.service_uuid) - print(f"*** Service {args.service_uuid} is no longer present (delete was successfull or service did not exist)") - except Exception as e: - print(f"*** Failed to delete service {args.service_uuid}, {e}") + response = client.GetService(ServiceId(**service_id)) + #print(grpc_message_to_json_string(response)) + + high_level_delete = response.service_type == ServiceTypeEnum.SERVICETYPE_L2NM or response.service_type == ServiceTypeEnum.SERVICETYPE_L3NM + print(f"Deleting service {response.name}, type {ServiceTypeEnum.Name(response.service_type)}, {high_level_delete=}") + + except: + print(f"No service with uuid {args.service_uuid} ({service_id})") + exit(-1) + + if high_level_delete: + osm_wim.wim.check_credentials() + try: + osm_wim.wim.delete_connectivity_service(args.service_uuid) + print(f"*** Service {args.service_uuid} deleted (L2SM/L3SM layer)") + except Exception as e: + print(f"*** Failed to delete service {args.service_uuid}, {e}") + else: + with make_service_client() as service_client: + try: + service_client.DeleteService(ServiceId(**service_id)) + print(f"*** Service {args.service_uuid} deleted (low level)") + except Exception as e: + print(f"*** Failed to delete service {args.service_uuid}, {e}") elif args.command == "create-xr": CONTEXT_NAME = 'admin' @@ -188,10 +229,6 @@ with make_context_client() as client: } config_rule = json_config_rule_set('/settings', json_tapi_settings) - response = client.ListContextIds(Empty()) - assert len(response.context_ids) == 1 - context_uuid=json_context_id(response.context_ids[0].context_uuid.uuid) - devices = get_devices(client) if args.constellation not in devices: print(f"Constellation {args.constellation} does not exist as a device. See \"service-cli.py list-endpoints\"") @@ -236,22 +273,26 @@ with make_context_client() as client: print(f'UpdateService: {grpc_message_to_json_string(update_response)}') elif args.command == "list": + devices = get_devices(client) + ep_map = get_endpoint_map(devices) + response = client.ListServices(ContextId(**CONTEXT_ID)) - #print('Services[{:d}] = {:s}'.format(len(response.services), grpc_message_to_json_string(response))) + # print('Services[{:d}] = {:s}'.format(len(response.services), grpc_message_to_json_string(response))) for service in response.services: scs = "" - # See if there are endpoint constraints that might be regognizable by the user. - # Keys do not necessarily exist, so catch exceptions and ignore those constraints - # that we cannot easily represent. - for sc in service.service_constraints: - try: - scs += f"{sc.endpoint_location.endpoint_id.device_id.device_uuid.uuid}:{sc.endpoint_location.endpoint_id.endpoint_uuid.uuid} " - except Exception: # pylint: disable=bare-except - pass - - print(f"{service.service_id.service_uuid.uuid:36} {ServiceTypeEnum.Name(service.service_type):40} {service.name:40} {ServiceStatusEnum.Name(service.service_status.service_status)} {scs}") + ep_list = [] + for ep in service.service_endpoint_ids: + ep_uuid = ep.endpoint_uuid.uuid + if ep_uuid in ep_map: + dev_name, ep_name = ep_map[ep_uuid] + ep_list.append(f"{dev_name}:{ep_name}") + ep_list.sort() + eps = ", ".join(ep_list) + + #print(f"{service.service_id.service_uuid.uuid:36} {ServiceTypeEnum.Name(service.service_type):40} {service.name:40} {ServiceStatusEnum.Name(service.service_status.service_status)} {scs}") + print(f"{service.service_id.service_uuid.uuid:36} {ServiceTypeEnum.Name(service.service_type):40} {service.name:40} {ServiceStatusEnum.Name(service.service_status.service_status):28} {eps}") elif args.command == "list-endpoints": devices = get_devices(client) -- GitLab