From e9e22817d42b579bb16398cac71e75730adbe534 Mon Sep 17 00:00:00 2001 From: gkatsikas Date: Thu, 6 Oct 2022 14:24:16 +0300 Subject: [PATCH 1/3] feat (service): skeleton P4 service handler Signed-off-by: Georgios P. Katsikas --- proto/context.proto | 1 + src/context/service/database/ServiceModel.py | 1 + .../service_handler_api/FilterFields.py | 1 + .../service/service_handlers/__init__.py | 9 +- .../service/service_handlers/p4/__init__.py | 13 + .../service_handlers/p4/p4_service_handler.py | 224 ++++++++++++++++++ src/service/tests/test_unitary.py | 3 +- 7 files changed, 249 insertions(+), 3 deletions(-) create mode 100644 src/service/service/service_handlers/p4/__init__.py create mode 100644 src/service/service/service_handlers/p4/p4_service_handler.py diff --git a/proto/context.proto b/proto/context.proto index 5b49bd288..cc231c101 100644 --- a/proto/context.proto +++ b/proto/context.proto @@ -240,6 +240,7 @@ enum ServiceTypeEnum { SERVICETYPE_L3NM = 1; SERVICETYPE_L2NM = 2; SERVICETYPE_TAPI_CONNECTIVITY_SERVICE = 3; + SERVICETYPE_P4 = 4; } enum ServiceStatusEnum { diff --git a/src/context/service/database/ServiceModel.py b/src/context/service/database/ServiceModel.py index 8b32d1cc9..88773a25c 100644 --- a/src/context/service/database/ServiceModel.py +++ b/src/context/service/database/ServiceModel.py @@ -34,6 +34,7 @@ class ORM_ServiceTypeEnum(Enum): L3NM = ServiceTypeEnum.SERVICETYPE_L3NM L2NM = ServiceTypeEnum.SERVICETYPE_L2NM TAPI_CONNECTIVITY_SERVICE = ServiceTypeEnum.SERVICETYPE_TAPI_CONNECTIVITY_SERVICE + P4_SERVICE = ServiceTypeEnum.SERVICETYPE_P4 grpc_to_enum__service_type = functools.partial( grpc_to_enum, ServiceTypeEnum, ORM_ServiceTypeEnum) diff --git a/src/service/service/service_handler_api/FilterFields.py b/src/service/service/service_handler_api/FilterFields.py index 0f2181208..3b87ab98f 100644 --- a/src/service/service/service_handler_api/FilterFields.py +++ b/src/service/service/service_handler_api/FilterFields.py @@ -24,6 +24,7 @@ SERVICE_TYPE_VALUES = { ServiceTypeEnum.SERVICETYPE_L3NM, ServiceTypeEnum.SERVICETYPE_L2NM, ServiceTypeEnum.SERVICETYPE_TAPI_CONNECTIVITY_SERVICE, + ServiceTypeEnum.SERVICETYPE_P4 } DEVICE_DRIVER_VALUES = { diff --git a/src/service/service/service_handlers/__init__.py b/src/service/service/service_handlers/__init__.py index 34689ca11..90c02f593 100644 --- a/src/service/service/service_handlers/__init__.py +++ b/src/service/service/service_handlers/__init__.py @@ -17,6 +17,7 @@ from ..service_handler_api.FilterFields import FilterFieldEnum from .l2nm_emulated.L2NMEmulatedServiceHandler import L2NMEmulatedServiceHandler from .l3nm_emulated.L3NMEmulatedServiceHandler import L3NMEmulatedServiceHandler from .l3nm_openconfig.L3NMOpenConfigServiceHandler import L3NMOpenConfigServiceHandler +from .p4.p4_service_handler import P4ServiceHandler from .tapi_tapi.TapiServiceHandler import TapiServiceHandler from .microwave.MicrowaveServiceHandler import MicrowaveServiceHandler @@ -51,4 +52,10 @@ SERVICE_HANDLERS = [ FilterFieldEnum.DEVICE_DRIVER : DeviceDriverEnum.DEVICEDRIVER_IETF_NETWORK_TOPOLOGY, } ]), -] \ No newline at end of file + (P4ServiceHandler, [ + { + FilterFieldEnum.SERVICE_TYPE: ServiceTypeEnum.SERVICETYPE_P4, + FilterFieldEnum.DEVICE_DRIVER: DeviceDriverEnum.DEVICEDRIVER_P4, + } + ]), +] diff --git a/src/service/service/service_handlers/p4/__init__.py b/src/service/service/service_handlers/p4/__init__.py new file mode 100644 index 000000000..9953c8205 --- /dev/null +++ b/src/service/service/service_handlers/p4/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2021-2023 H2020 TeraFlow (https://www.teraflow-h2020.eu/) +# +# 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. diff --git a/src/service/service/service_handlers/p4/p4_service_handler.py b/src/service/service/service_handlers/p4/p4_service_handler.py new file mode 100644 index 000000000..690c6adbc --- /dev/null +++ b/src/service/service/service_handlers/p4/p4_service_handler.py @@ -0,0 +1,224 @@ +# Copyright 2021-2023 H2020 TeraFlow (https://www.teraflow-h2020.eu/) +# +# 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. + +""" +P4 service handler for the TeraFlow SDN controller. +""" + +import json +import logging +from typing import Any, List, Optional, Tuple, Union +from common.orm.Database import Database +from common.orm.HighLevel import get_object +from common.orm.backend.Tools import key_to_str +# from common.proto.context_pb2 import Device +# from common.tools.object_factory.ConfigRule import json_config_rule_delete,\ +# json_config_rule_set +from common.type_checkers.Checkers import chk_type +from context.client.ContextClient import ContextClient +from device.client.DeviceClient import DeviceClient +from service.service.database.ConfigModel import get_config_rules +from service.service.database.ContextModel import ContextModel +# from service.service.database.DeviceModel import DeviceModel +from service.service.database.ServiceModel import ServiceModel +from service.service.service_handler_api._ServiceHandler import _ServiceHandler + +LOGGER = logging.getLogger(__name__) + + +class P4ServiceHandler(_ServiceHandler): + """ + P4ServiceHandler class inherits the abstract _ServiceHandler class + to provision network connectivity upon P4 devices. + + Attributes + ---------- + db_service: ServiceModel + The service instance from the local in-memory database. + database: Database + The instance of the local in-memory database. + context_client: ContextClient + gRPC client to get Context services + device_client : DeviceClient + gRPC client to get Device services + settings: map + Extra settings required by the service handler. + """ + def __init__(self, # pylint: disable=super-init-not-called + db_service: ServiceModel, + database: Database, + context_client: ContextClient, + device_client : DeviceClient, + **settings) -> None: + self.__db_service = db_service + self.__database = database + self.__context_client = context_client + self.__device_client = device_client + + self.__db_context: ContextModel = get_object( + self.__database, ContextModel, self.__db_service.context_fk) + str_service_key = key_to_str( + [self.__db_context.context_uuid, self.__db_service.service_uuid]) + db_config = get_config_rules( + self.__database, str_service_key, "running") + self.__resolver = None + self.__config = None + + def SetEndpoint(self, endpoints: List[Tuple[str, str, Optional[str]]]) \ + -> List[Union[bool, Exception]]: + """ + Create/Update service endpoints. + + :param endpoints: list of tuples, each containing a device_uuid, + endpoint_uuid and, optionally, the topology_uuid of the endpoint to add. + :return: list of results for endpoint changes requested. + Return values must be in the same order as the requested endpoints. + If an endpoint is properly added, True must be returned; + otherwise, the Exception that is raised during processing + must be returned. + """ + chk_type("endpoints", endpoints, list) + if len(endpoints) == 0: + return [] + return [] + + def DeleteEndpoint(self, endpoints: List[Tuple[str, str, Optional[str]]]) \ + -> List[Union[bool, Exception]]: + """ + Delete service endpoints. + + :param endpoints: list of tuples, each containing a device_uuid, + endpoint_uuid and, optionally, the topology_uuid of the endpoint to + delete. + :return: list of results for endpoint deletions requested. + Return values must be in the same order as the requested endpoints. + If an endpoint is properly deleted, True must be returned; + otherwise, the Exception that is raised during processing + must be returned. + """ + chk_type("endpoints", endpoints, list) + if len(endpoints) == 0: + return [] + return [] + + def SetConstraint(self, constraints: List[Tuple[str, Any]]) \ + -> List[Union[bool, Exception]]: + """ + Create/Update service constraints. + + :param constraints: List of tuples, each containing a constraint_type + and the new constraint_value to be set. + :return: List of results for constraint changes requested. + Return values must be in the same order as the requested constraints. + If a constraint is properly set, True must be returned; + otherwise, the Exception that is raised during the processing + must be returned. + """ + chk_type("constraints", constraints, list) + if len(constraints) == 0: + return [] + + msg = f"SetConstraint() not yet implemented by the" \ + f" P4 service handler. \nThe following constraints " \ + f"are being ignored: {str(constraints)}" + LOGGER.warning(msg) + return [Exception(msg) for _ in range(len(constraints))] + + def DeleteConstraint(self, constraints: List[Tuple[str, Any]]) \ + -> List[Union[bool, Exception]]: + """ + Delete service constraints. + + :param constraints: List of tuples, each containing a constraint_type + pointing to the constraint to be deleted, and a constraint_value + containing possible additionally required values to locate the + constraint to be removed. + :return: List of results for constraint deletions requested. + Return values must be in the same order as the requested constraints. + If a constraint is properly deleted, True must be returned; + otherwise, the Exception that is raised during the processing + must be returned. + """ + chk_type("constraints", constraints, list) + if len(constraints) == 0: + return [] + + msg = f"DeleteConstraint() not yet implemented by the" \ + f" P4 service handler. \nThe following constraints " \ + f"are being ignored: {str(constraints)}" + LOGGER.warning(msg) + return [Exception(msg) for _ in range(len(constraints))] + + def SetConfig(self, resources: List[Tuple[str, Any]]) \ + -> List[Union[bool, Exception]]: + """ + Create/Update configuration for a list of service resources. + + :param resources: List of tuples, each containing a resource_key + pointing to the resource to be modified, and a resource_value + containing the new value to be set. + :return: List of results for resource key changes requested. + Return values must be in the same order as the requested resource keys. + If a resource is properly set, True must be returned; + otherwise, the Exception that is raised during the processing + must be returned. + """ + chk_type("resources", resources, list) + if len(resources) == 0: + return [] + + results = [] + for resource in resources: + try: + resource_key, resource_value = resource + resource_value = json.loads(resource_value) + # Do the job + # results.append(True) + except Exception as ex: # pylint: disable=broad-except + LOGGER.exception( + "Failed to execute SetConfig(%s)", str(resource)) + results.append(ex) + + return results + + def DeleteConfig(self, resources: List[Tuple[str, Any]]) \ + -> List[Union[bool, Exception]]: + """ + Delete configuration for a list of service resources. + + :param resources: List of tuples, each containing a resource_key + pointing to the resource to be modified, and a resource_value containing + possible additionally required values to locate the value to be removed. + :return: List of results for resource key deletions requested. + Return values must be in the same order as the requested resource keys. + If a resource is properly deleted, True must be returned; + otherwise, the Exception that is raised during the processing + must be returned. + """ + chk_type("resources", resources, list) + if len(resources) == 0: + return [] + + results = [] + for resource in resources: + try: + resource_key, _ = resource + # Do the job + # results.append(True) + except Exception as ex: # pylint: disable=broad-except + LOGGER.exception( + "Failed to execute DeleteConfig(%s)", str(resource)) + results.append(ex) + + return results diff --git a/src/service/tests/test_unitary.py b/src/service/tests/test_unitary.py index e12ec2bc4..bc25dbfbb 100644 --- a/src/service/tests/test_unitary.py +++ b/src/service/tests/test_unitary.py @@ -15,12 +15,11 @@ import copy, grpc, logging, pytest from common.proto.context_pb2 import ( Context, ContextId, Device, DeviceId, Link, LinkId, Service, ServiceId, Topology, TopologyId) -from common.tests.PytestGenerateTests import pytest_generate_tests # (required) pylint: disable=unused-import from common.tools.grpc.Tools import grpc_message_to_json_string from context.client.ContextClient import ContextClient from device.client.DeviceClient import DeviceClient from service.client.ServiceClient import ServiceClient -from .PrepareTestScenario import ( # pylint: disable=unused-import +from .PrepareTestScenario import ( # pylint: disable=unused-import # be careful, order of symbols is important here! mock_service, service_service, context_client, device_client, service_client) -- GitLab From 93a17d1f7b41743b0157390792de348483303cb4 Mon Sep 17 00:00:00 2001 From: Panagiotis Famelis Date: Mon, 12 Dec 2022 15:22:11 +0200 Subject: [PATCH 2/3] feat: implement p4-service-handler --- proto/context.proto | 1 - src/common/tools/object_factory/Service.py | 10 + src/context/service/database/ServiceModel.py | 1 - .../service_handler_api/FilterFields.py | 3 +- .../service/service_handlers/__init__.py | 2 +- .../service_handlers/p4/p4_service_handler.py | 433 +++++++++++------- 6 files changed, 269 insertions(+), 181 deletions(-) diff --git a/proto/context.proto b/proto/context.proto index cc231c101..5b49bd288 100644 --- a/proto/context.proto +++ b/proto/context.proto @@ -240,7 +240,6 @@ enum ServiceTypeEnum { SERVICETYPE_L3NM = 1; SERVICETYPE_L2NM = 2; SERVICETYPE_TAPI_CONNECTIVITY_SERVICE = 3; - SERVICETYPE_P4 = 4; } enum ServiceStatusEnum { diff --git a/src/common/tools/object_factory/Service.py b/src/common/tools/object_factory/Service.py index 62f3dcbda..be8eefe5b 100644 --- a/src/common/tools/object_factory/Service.py +++ b/src/common/tools/object_factory/Service.py @@ -61,3 +61,13 @@ def json_service_tapi_planned( service_uuid, ServiceTypeEnum.SERVICETYPE_TAPI_CONNECTIVITY_SERVICE, context_id=json_context_id(context_uuid), status=ServiceStatusEnum.SERVICESTATUS_PLANNED, endpoint_ids=endpoint_ids, constraints=constraints, config_rules=config_rules) + +def json_service_p4_planned( + service_uuid : str, endpoint_ids : List[Dict] = [], constraints : List[Dict] = [], + config_rules : List[Dict] = [], context_uuid : str = DEFAULT_CONTEXT_UUID + ): + + return json_service( + service_uuid, ServiceTypeEnum.SERVICETYPE_L2NM, context_id=json_context_id(context_uuid), + status=ServiceStatusEnum.SERVICESTATUS_PLANNED, endpoint_ids=endpoint_ids, constraints=constraints, + config_rules=config_rules) \ No newline at end of file diff --git a/src/context/service/database/ServiceModel.py b/src/context/service/database/ServiceModel.py index 88773a25c..8b32d1cc9 100644 --- a/src/context/service/database/ServiceModel.py +++ b/src/context/service/database/ServiceModel.py @@ -34,7 +34,6 @@ class ORM_ServiceTypeEnum(Enum): L3NM = ServiceTypeEnum.SERVICETYPE_L3NM L2NM = ServiceTypeEnum.SERVICETYPE_L2NM TAPI_CONNECTIVITY_SERVICE = ServiceTypeEnum.SERVICETYPE_TAPI_CONNECTIVITY_SERVICE - P4_SERVICE = ServiceTypeEnum.SERVICETYPE_P4 grpc_to_enum__service_type = functools.partial( grpc_to_enum, ServiceTypeEnum, ORM_ServiceTypeEnum) diff --git a/src/service/service/service_handler_api/FilterFields.py b/src/service/service/service_handler_api/FilterFields.py index 3b87ab98f..afc15795c 100644 --- a/src/service/service/service_handler_api/FilterFields.py +++ b/src/service/service/service_handler_api/FilterFields.py @@ -23,8 +23,7 @@ SERVICE_TYPE_VALUES = { ServiceTypeEnum.SERVICETYPE_UNKNOWN, ServiceTypeEnum.SERVICETYPE_L3NM, ServiceTypeEnum.SERVICETYPE_L2NM, - ServiceTypeEnum.SERVICETYPE_TAPI_CONNECTIVITY_SERVICE, - ServiceTypeEnum.SERVICETYPE_P4 + ServiceTypeEnum.SERVICETYPE_TAPI_CONNECTIVITY_SERVICE } DEVICE_DRIVER_VALUES = { diff --git a/src/service/service/service_handlers/__init__.py b/src/service/service/service_handlers/__init__.py index 90c02f593..4310a17d7 100644 --- a/src/service/service/service_handlers/__init__.py +++ b/src/service/service/service_handlers/__init__.py @@ -54,7 +54,7 @@ SERVICE_HANDLERS = [ ]), (P4ServiceHandler, [ { - FilterFieldEnum.SERVICE_TYPE: ServiceTypeEnum.SERVICETYPE_P4, + FilterFieldEnum.SERVICE_TYPE: ServiceTypeEnum.SERVICETYPE_L2NM, FilterFieldEnum.DEVICE_DRIVER: DeviceDriverEnum.DEVICEDRIVER_P4, } ]), diff --git a/src/service/service/service_handlers/p4/p4_service_handler.py b/src/service/service/service_handlers/p4/p4_service_handler.py index 690c6adbc..48b9715b7 100644 --- a/src/service/service/service_handlers/p4/p4_service_handler.py +++ b/src/service/service/service_handlers/p4/p4_service_handler.py @@ -13,212 +13,293 @@ # limitations under the License. """ -P4 service handler for the TeraFlow SDN controller. +P4 service handler for the TeraFlowSDN controller. """ -import json -import logging -from typing import Any, List, Optional, Tuple, Union -from common.orm.Database import Database -from common.orm.HighLevel import get_object -from common.orm.backend.Tools import key_to_str -# from common.proto.context_pb2 import Device -# from common.tools.object_factory.ConfigRule import json_config_rule_delete,\ -# json_config_rule_set -from common.type_checkers.Checkers import chk_type -from context.client.ContextClient import ContextClient -from device.client.DeviceClient import DeviceClient -from service.service.database.ConfigModel import get_config_rules -from service.service.database.ContextModel import ContextModel -# from service.service.database.DeviceModel import DeviceModel -from service.service.database.ServiceModel import ServiceModel +import anytree, json, logging +from typing import Any, Dict, List, Optional, Tuple, Union +from common.proto.context_pb2 import ConfigActionEnum, ConfigRule, DeviceId, Service +from common.tools.object_factory.ConfigRule import json_config_rule, json_config_rule_delete, json_config_rule_set +from common.tools.object_factory.Device import json_device_id +from common.type_checkers.Checkers import chk_type, chk_length from service.service.service_handler_api._ServiceHandler import _ServiceHandler +from service.service.service_handler_api.AnyTreeTools import TreeNode, delete_subnode, get_subnode, set_subnode_value +from service.service.task_scheduler.TaskExecutor import TaskExecutor LOGGER = logging.getLogger(__name__) +def create_rule_set(endpoint_a, endpoint_b): + return json_config_rule_set( + 'table', + { + 'table-name': 'IngressPipeImpl.l2_exact_table', + 'match-fields': [ + { + 'match-field': 'standard_metadata.ingress_port', + 'match-value': endpoint_a + } + ], + 'action-name': 'IngressPipeImpl.set_egress_port', + 'action-params': [ + { + 'action-param': 'port', + 'action-value': endpoint_b + } + ] + } +) + +def create_rule_del(endpoint_a, endpoint_b): + return json_config_rule_delete( + 'table', + { + 'table-name': 'IngressPipeImpl.l2_exact_table', + 'match-fields': [ + { + 'match-field': 'standard_metadata.ingress_port', + 'match-value': endpoint_a + } + ], + 'action-name': 'IngressPipeImpl.set_egress_port', + 'action-params': [ + { + 'action-param': 'port', + 'action-value': endpoint_b + } + ] + } +) class P4ServiceHandler(_ServiceHandler): - """ - P4ServiceHandler class inherits the abstract _ServiceHandler class - to provision network connectivity upon P4 devices. - - Attributes - ---------- - db_service: ServiceModel - The service instance from the local in-memory database. - database: Database - The instance of the local in-memory database. - context_client: ContextClient - gRPC client to get Context services - device_client : DeviceClient - gRPC client to get Device services - settings: map - Extra settings required by the service handler. - """ - def __init__(self, # pylint: disable=super-init-not-called - db_service: ServiceModel, - database: Database, - context_client: ContextClient, - device_client : DeviceClient, + def __init__(self, + service: Service, + task_executor : TaskExecutor, **settings) -> None: - self.__db_service = db_service - self.__database = database - self.__context_client = context_client - self.__device_client = device_client - - self.__db_context: ContextModel = get_object( - self.__database, ContextModel, self.__db_service.context_fk) - str_service_key = key_to_str( - [self.__db_context.context_uuid, self.__db_service.service_uuid]) - db_config = get_config_rules( - self.__database, str_service_key, "running") - self.__resolver = None - self.__config = None - - def SetEndpoint(self, endpoints: List[Tuple[str, str, Optional[str]]]) \ - -> List[Union[bool, Exception]]: + """ Initialize Driver. + Parameters: + service + The service instance (gRPC message) to be managed. + task_executor + An instance of Task Executor providing access to the + service handlers factory, the context and device clients, + and an internal cache of already-loaded gRPC entities. + **settings + Extra settings required by the service handler. """ - Create/Update service endpoints. - - :param endpoints: list of tuples, each containing a device_uuid, - endpoint_uuid and, optionally, the topology_uuid of the endpoint to add. - :return: list of results for endpoint changes requested. - Return values must be in the same order as the requested endpoints. - If an endpoint is properly added, True must be returned; - otherwise, the Exception that is raised during processing - must be returned. - """ - chk_type("endpoints", endpoints, list) - if len(endpoints) == 0: - return [] - return [] + self.__service = service + self.__task_executor = task_executor # pylint: disable=unused-private-member - def DeleteEndpoint(self, endpoints: List[Tuple[str, str, Optional[str]]]) \ - -> List[Union[bool, Exception]]: + def SetEndpoint( + self, endpoints : List[Tuple[str, str, Optional[str]]], + connection_uuid : Optional[str] = None + ) -> List[Union[bool, Exception]]: + """ Create/Update service endpoints form a list. + Parameters: + endpoints: List[Tuple[str, str, Optional[str]]] + List of tuples, each containing a device_uuid, + endpoint_uuid and, optionally, the topology_uuid + of the endpoint to be added. + connection_uuid : Optional[str] + If specified, is the UUID of the connection this endpoint is associated to. + Returns: + results: List[Union[bool, Exception]] + List of results for endpoint changes requested. + Return values must be in the same order as the requested + endpoints. If an endpoint is properly added, True must be + returned; otherwise, the Exception that is raised during + the processing must be returned. """ - Delete service endpoints. - - :param endpoints: list of tuples, each containing a device_uuid, - endpoint_uuid and, optionally, the topology_uuid of the endpoint to - delete. - :return: list of results for endpoint deletions requested. - Return values must be in the same order as the requested endpoints. - If an endpoint is properly deleted, True must be returned; - otherwise, the Exception that is raised during processing - must be returned. + chk_type('endpoints', endpoints, list) + if len(endpoints) == 0: return [] + + service_uuid = self.__service.service_id.service_uuid.uuid + + history = {} + + results = [] + index = {} + i = 0 + for endpoint in endpoints: + device_uuid, endpoint_uuid = endpoint[0:2] # ignore topology_uuid by now + if device_uuid in history: + try: + matched_endpoint_uuid = history.pop(device_uuid) + device = self.__task_executor.get_device(DeviceId(**json_device_id(device_uuid))) + + del device.device_config.config_rules[:] + + # One way + rule = create_rule_set(matched_endpoint_uuid, endpoint_uuid) + device.device_config.config_rules.append(ConfigRule(**rule)) + # The other way + rule = create_rule_set(endpoint_uuid, matched_endpoint_uuid) + device.device_config.config_rules.append(ConfigRule(**rule)) + + self.__task_executor.configure_device(device) + + results.append(True) + results[index[device_uuid]] = True + except Exception as e: + LOGGER.exception('Unable to SetEndpoint({:s})'.format(str(endpoint))) + results.append(e) + else: + history[device_uuid] = endpoint_uuid + index[device_uuid] = i + results.append(False) + i = i+1 + + return results + + def DeleteEndpoint( + self, endpoints : List[Tuple[str, str, Optional[str]]], + connection_uuid : Optional[str] = None + ) -> List[Union[bool, Exception]]: + """ Delete service endpoints form a list. + Parameters: + endpoints: List[Tuple[str, str, Optional[str]]] + List of tuples, each containing a device_uuid, + endpoint_uuid, and the topology_uuid of the endpoint + to be removed. + connection_uuid : Optional[str] + If specified, is the UUID of the connection this endpoint is associated to. + Returns: + results: List[Union[bool, Exception]] + List of results for endpoint deletions requested. + Return values must be in the same order as the requested + endpoints. If an endpoint is properly deleted, True must be + returned; otherwise, the Exception that is raised during + the processing must be returned. """ - chk_type("endpoints", endpoints, list) - if len(endpoints) == 0: - return [] - return [] + chk_type('endpoints', endpoints, list) + if len(endpoints) == 0: return [] + + service_uuid = self.__service.service_id.service_uuid.uuid + + history = {} + + results = [] + index = {} + i = 0 + for endpoint in endpoints: + device_uuid, endpoint_uuid = endpoint[0:2] # ignore topology_uuid by now + if device_uuid in history: + try: + matched_endpoint_uuid = history.pop(device_uuid) + device = self.__task_executor.get_device(DeviceId(**json_device_id(device_uuid))) + + del device.device_config.config_rules[:] + + # One way + rule = create_rule_del(matched_endpoint_uuid, endpoint_uuid) + device.device_config.config_rules.append(ConfigRule(**rule)) + # The other way + rule = create_rule_del(endpoint_uuid, matched_endpoint_uuid) + device.device_config.config_rules.append(ConfigRule(**rule)) + + self.__task_executor.configure_device(device) + + results.append(True) + results[index[device_uuid]] = True + except Exception as e: + LOGGER.exception('Unable to SetEndpoint({:s})'.format(str(endpoint))) + results.append(e) + else: + history[device_uuid] = endpoint_uuid + index[device_uuid] = i + results.append(False) + i = i+1 + + return results def SetConstraint(self, constraints: List[Tuple[str, Any]]) \ -> List[Union[bool, Exception]]: + """ Create/Update service constraints. + Parameters: + constraints: List[Tuple[str, Any]] + List of tuples, each containing a constraint_type and the + new constraint_value to be set. + Returns: + results: List[Union[bool, Exception]] + List of results for constraint changes requested. + Return values must be in the same order as the requested + constraints. If a constraint is properly set, True must be + returned; otherwise, the Exception that is raised during + the processing must be returned. """ - Create/Update service constraints. - - :param constraints: List of tuples, each containing a constraint_type - and the new constraint_value to be set. - :return: List of results for constraint changes requested. - Return values must be in the same order as the requested constraints. - If a constraint is properly set, True must be returned; - otherwise, the Exception that is raised during the processing - must be returned. - """ - chk_type("constraints", constraints, list) - if len(constraints) == 0: - return [] + chk_type('constraints', constraints, list) + if len(constraints) == 0: return [] - msg = f"SetConstraint() not yet implemented by the" \ - f" P4 service handler. \nThe following constraints " \ - f"are being ignored: {str(constraints)}" - LOGGER.warning(msg) - return [Exception(msg) for _ in range(len(constraints))] + msg = '[SetConstraint] Method not implemented. Constraints({:s}) are being ignored.' + LOGGER.warning(msg.format(str(constraints))) + return [True for _ in range(len(constraints))] def DeleteConstraint(self, constraints: List[Tuple[str, Any]]) \ -> List[Union[bool, Exception]]: + """ Delete service constraints. + Parameters: + constraints: List[Tuple[str, Any]] + List of tuples, each containing a constraint_type pointing + to the constraint to be deleted, and a constraint_value + containing possible additionally required values to locate + the constraint to be removed. + Returns: + results: List[Union[bool, Exception]] + List of results for constraint deletions requested. + Return values must be in the same order as the requested + constraints. If a constraint is properly deleted, True must + be returned; otherwise, the Exception that is raised during + the processing must be returned. """ - Delete service constraints. - - :param constraints: List of tuples, each containing a constraint_type - pointing to the constraint to be deleted, and a constraint_value - containing possible additionally required values to locate the - constraint to be removed. - :return: List of results for constraint deletions requested. - Return values must be in the same order as the requested constraints. - If a constraint is properly deleted, True must be returned; - otherwise, the Exception that is raised during the processing - must be returned. - """ - chk_type("constraints", constraints, list) - if len(constraints) == 0: - return [] + chk_type('constraints', constraints, list) + if len(constraints) == 0: return [] - msg = f"DeleteConstraint() not yet implemented by the" \ - f" P4 service handler. \nThe following constraints " \ - f"are being ignored: {str(constraints)}" - LOGGER.warning(msg) - return [Exception(msg) for _ in range(len(constraints))] + msg = '[DeleteConstraint] Method not implemented. Constraints({:s}) are being ignored.' + LOGGER.warning(msg.format(str(constraints))) + return [True for _ in range(len(constraints))] def SetConfig(self, resources: List[Tuple[str, Any]]) \ -> List[Union[bool, Exception]]: + """ Create/Update configuration for a list of service resources. + Parameters: + resources: List[Tuple[str, Any]] + List of tuples, each containing a resource_key pointing to + the resource to be modified, and a resource_value + containing the new value to be set. + Returns: + results: List[Union[bool, Exception]] + List of results for resource key changes requested. + Return values must be in the same order as the requested + resource keys. If a resource is properly set, True must be + returned; otherwise, the Exception that is raised during + the processing must be returned. """ - Create/Update configuration for a list of service resources. - - :param resources: List of tuples, each containing a resource_key - pointing to the resource to be modified, and a resource_value - containing the new value to be set. - :return: List of results for resource key changes requested. - Return values must be in the same order as the requested resource keys. - If a resource is properly set, True must be returned; - otherwise, the Exception that is raised during the processing - must be returned. - """ - chk_type("resources", resources, list) - if len(resources) == 0: - return [] + chk_type('resources', resources, list) + if len(resources) == 0: return [] - results = [] - for resource in resources: - try: - resource_key, resource_value = resource - resource_value = json.loads(resource_value) - # Do the job - # results.append(True) - except Exception as ex: # pylint: disable=broad-except - LOGGER.exception( - "Failed to execute SetConfig(%s)", str(resource)) - results.append(ex) - - return results + msg = '[SetConfig] Method not implemented. Resources({:s}) are being ignored.' + LOGGER.warning(msg.format(str(resources))) + return [True for _ in range(len(resources))] def DeleteConfig(self, resources: List[Tuple[str, Any]]) \ -> List[Union[bool, Exception]]: + """ Delete configuration for a list of service resources. + Parameters: + resources: List[Tuple[str, Any]] + List of tuples, each containing a resource_key pointing to + the resource to be modified, and a resource_value containing + possible additionally required values to locate the value + to be removed. + Returns: + results: List[Union[bool, Exception]] + List of results for resource key deletions requested. + Return values must be in the same order as the requested + resource keys. If a resource is properly deleted, True must + be returned; otherwise, the Exception that is raised during + the processing must be returned. """ - Delete configuration for a list of service resources. - - :param resources: List of tuples, each containing a resource_key - pointing to the resource to be modified, and a resource_value containing - possible additionally required values to locate the value to be removed. - :return: List of results for resource key deletions requested. - Return values must be in the same order as the requested resource keys. - If a resource is properly deleted, True must be returned; - otherwise, the Exception that is raised during the processing - must be returned. - """ - chk_type("resources", resources, list) - if len(resources) == 0: - return [] + chk_type('resources', resources, list) + if len(resources) == 0: return [] - results = [] - for resource in resources: - try: - resource_key, _ = resource - # Do the job - # results.append(True) - except Exception as ex: # pylint: disable=broad-except - LOGGER.exception( - "Failed to execute DeleteConfig(%s)", str(resource)) - results.append(ex) - - return results + msg = '[SetConfig] Method not implemented. Resources({:s}) are being ignored.' + LOGGER.warning(msg.format(str(resources))) + return [True for _ in range(len(resources))] \ No newline at end of file -- GitLab From 7a2a4850aa648316f79280ed92765e12de5d9cbe Mon Sep 17 00:00:00 2001 From: Panagiotis Famelis Date: Mon, 12 Dec 2022 18:12:14 +0200 Subject: [PATCH 3/3] test: functional test for p4 --- src/tests/p4/__init__.py | 14 + src/tests/p4/deploy_specs.sh | 17 + src/tests/p4/mininet/1switch1path.py | 99 +++++ src/tests/p4/mininet/2switch1path.py | 99 +++++ src/tests/p4/mininet/4switch2path.py | 110 +++++ src/tests/p4/mininet/6switch2path.py | 118 ++++++ src/tests/p4/p4/bmv2.json | 381 ++++++++++++++++++ src/tests/p4/p4/main.p4 | 144 +++++++ src/tests/p4/p4/p4info.txt | 62 +++ src/tests/p4/run_test_01_bootstrap.sh | 22 + src/tests/p4/run_test_02_create_service.sh | 17 + src/tests/p4/run_test_03_delete_service.sh | 17 + src/tests/p4/run_test_04_cleanup.sh | 17 + src/tests/p4/setup.sh | 8 + src/tests/p4/tests/.gitignore | 2 + src/tests/p4/tests/BuildDescriptors.py | 35 ++ src/tests/p4/tests/LoadDescriptors.py | 40 ++ src/tests/p4/tests/Objects.py | 345 ++++++++++++++++ src/tests/p4/tests/__init__.py | 14 + .../p4/tests/test_functional_bootstrap.py | 107 +++++ src/tests/p4/tests/test_functional_cleanup.py | 83 ++++ .../tests/test_functional_create_service.py | 93 +++++ .../tests/test_functional_delete_service.py | 69 ++++ 23 files changed, 1913 insertions(+) create mode 100644 src/tests/p4/__init__.py create mode 100644 src/tests/p4/deploy_specs.sh create mode 100755 src/tests/p4/mininet/1switch1path.py create mode 100755 src/tests/p4/mininet/2switch1path.py create mode 100755 src/tests/p4/mininet/4switch2path.py create mode 100755 src/tests/p4/mininet/6switch2path.py create mode 100644 src/tests/p4/p4/bmv2.json create mode 100644 src/tests/p4/p4/main.p4 create mode 100644 src/tests/p4/p4/p4info.txt create mode 100755 src/tests/p4/run_test_01_bootstrap.sh create mode 100755 src/tests/p4/run_test_02_create_service.sh create mode 100755 src/tests/p4/run_test_03_delete_service.sh create mode 100755 src/tests/p4/run_test_04_cleanup.sh create mode 100755 src/tests/p4/setup.sh create mode 100644 src/tests/p4/tests/.gitignore create mode 100644 src/tests/p4/tests/BuildDescriptors.py create mode 100644 src/tests/p4/tests/LoadDescriptors.py create mode 100644 src/tests/p4/tests/Objects.py create mode 100644 src/tests/p4/tests/__init__.py create mode 100644 src/tests/p4/tests/test_functional_bootstrap.py create mode 100644 src/tests/p4/tests/test_functional_cleanup.py create mode 100644 src/tests/p4/tests/test_functional_create_service.py create mode 100644 src/tests/p4/tests/test_functional_delete_service.py diff --git a/src/tests/p4/__init__.py b/src/tests/p4/__init__.py new file mode 100644 index 000000000..70a332512 --- /dev/null +++ b/src/tests/p4/__init__.py @@ -0,0 +1,14 @@ +# Copyright 2021-2023 H2020 TeraFlow (https://www.teraflow-h2020.eu/) +# +# 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. + diff --git a/src/tests/p4/deploy_specs.sh b/src/tests/p4/deploy_specs.sh new file mode 100644 index 000000000..b486474e2 --- /dev/null +++ b/src/tests/p4/deploy_specs.sh @@ -0,0 +1,17 @@ +# Set the URL of your local Docker registry where the images will be uploaded to. +export TFS_REGISTRY_IMAGE="http://localhost:32000/tfs/" + +# Set the list of components, separated by spaces, you want to build images for, and deploy. +export TFS_COMPONENTS="context device automation service compute monitoring webui" + +# Set the tag you want to use for your images. +export TFS_IMAGE_TAG="dev" + +# Set the name of the Kubernetes namespace to deploy to. +export TFS_K8S_NAMESPACE="tfs" + +# Set additional manifest files to be applied after the deployment +export TFS_EXTRA_MANIFESTS="manifests/nginx_ingress_http.yaml" + +# Set the neew Grafana admin password +export TFS_GRAFANA_PASSWORD="admin123+" diff --git a/src/tests/p4/mininet/1switch1path.py b/src/tests/p4/mininet/1switch1path.py new file mode 100755 index 000000000..466fb6a06 --- /dev/null +++ b/src/tests/p4/mininet/1switch1path.py @@ -0,0 +1,99 @@ +#!/usr/bin/python + +# Copyright 2019-present Open Networking Foundation +# +# 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 argparse + +from mininet.cli import CLI +from mininet.log import setLogLevel +from mininet.net import Mininet +from mininet.node import Host +from mininet.topo import Topo +from stratum import StratumBmv2Switch + +CPU_PORT = 255 + +class IPv4Host(Host): + """Host that can be configured with an IPv4 gateway (default route). + """ + + def config(self, mac=None, ip=None, defaultRoute=None, lo='up', gw=None, + **_params): + super(IPv4Host, self).config(mac, ip, defaultRoute, lo, **_params) + self.cmd('ip -4 addr flush dev %s' % self.defaultIntf()) + self.cmd('ip -6 addr flush dev %s' % self.defaultIntf()) + self.cmd('ip -4 link set up %s' % self.defaultIntf()) + self.cmd('ip -4 addr add %s dev %s' % (ip, self.defaultIntf())) + if gw: + self.cmd('ip -4 route add default via %s' % gw) + # Disable offload + for attr in ["rx", "tx", "sg"]: + cmd = "/sbin/ethtool --offload %s %s off" % ( + self.defaultIntf(), attr) + self.cmd(cmd) + + def updateIP(): + return ip.split('/')[0] + + self.defaultIntf().updateIP = updateIP + +class TutorialTopo(Topo): + """Basic Server-Client topology with IPv4 hosts""" + + def __init__(self, *args, **kwargs): + Topo.__init__(self, *args, **kwargs) + + # Spines + # gRPC port 50001 + switch1 = self.addSwitch('switch1', cls=StratumBmv2Switch, cpuport=CPU_PORT) + + # IPv4 hosts attached to switch 1 + client = self.addHost('client', cls=IPv4Host, mac="aa:bb:cc:dd:ee:11", + ip='10.0.0.1/24', gw='10.0.0.100') +# client.sendCmd('arp -s 10.0.0.2 aa:bb:cc:dd:ee:22') +# client.setARP('10.0.0.2', 'aa:bb:cc:dd:ee:22') + server = self.addHost('server', cls=IPv4Host, mac="aa:bb:cc:dd:ee:22", + ip='10.0.0.2/24', gw='10.0.0.100') +# server.sendCmd('arp -s 10.0.0.1 aa:bb:cc:dd:ee:11') +# server.setARP('10.0.0.1', 'aa:bb:cc:dd:ee:11') + self.addLink(client, switch1) # port 1 + self.addLink(server, switch1) # port 2 + + +def main(): + net = Mininet(topo=TutorialTopo(), controller=None) + net.start() + client = net.hosts[0] + server = net.hosts[1] + client.setARP('10.0.0.2', 'aa:bb:cc:dd:ee:22') + server.setARP('10.0.0.1', 'aa:bb:cc:dd:ee:11') + CLI(net) + net.stop() + print '#' * 80 + print 'ATTENTION: Mininet was stopped! Perhaps accidentally?' + print 'No worries, it will restart automatically in a few seconds...' + print 'To access again the Mininet CLI, use `make mn-cli`' + print 'To detach from the CLI (without stopping), press Ctrl-D' + print 'To permanently quit Mininet, use `make stop`' + print '#' * 80 + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description='Mininet topology script for 2x2 fabric with stratum_bmv2 and IPv4 hosts') + args = parser.parse_args() + setLogLevel('info') + + main() diff --git a/src/tests/p4/mininet/2switch1path.py b/src/tests/p4/mininet/2switch1path.py new file mode 100755 index 000000000..91db70052 --- /dev/null +++ b/src/tests/p4/mininet/2switch1path.py @@ -0,0 +1,99 @@ +#!/usr/bin/python + +# Copyright 2019-present Open Networking Foundation +# +# 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 argparse + +from mininet.cli import CLI +from mininet.log import setLogLevel +from mininet.net import Mininet +from mininet.node import Host +from mininet.topo import Topo +from stratum import StratumBmv2Switch + +CPU_PORT = 255 + +class IPv4Host(Host): + """Host that can be configured with an IPv4 gateway (default route). + """ + + def config(self, mac=None, ip=None, defaultRoute=None, lo='up', gw=None, + **_params): + super(IPv4Host, self).config(mac, ip, defaultRoute, lo, **_params) + self.cmd('ip -4 addr flush dev %s' % self.defaultIntf()) + self.cmd('ip -6 addr flush dev %s' % self.defaultIntf()) + self.cmd('ip -4 link set up %s' % self.defaultIntf()) + self.cmd('ip -4 addr add %s dev %s' % (ip, self.defaultIntf())) + if gw: + self.cmd('ip -4 route add default via %s' % gw) + # Disable offload + for attr in ["rx", "tx", "sg"]: + cmd = "/sbin/ethtool --offload %s %s off" % ( + self.defaultIntf(), attr) + self.cmd(cmd) + + def updateIP(): + return ip.split('/')[0] + + self.defaultIntf().updateIP = updateIP + +class TutorialTopo(Topo): + """Basic Server-Client topology with IPv4 hosts""" + + def __init__(self, *args, **kwargs): + Topo.__init__(self, *args, **kwargs) + + # Spines + # gRPC port 50001 + switch1 = self.addSwitch('switch1', cls=StratumBmv2Switch, cpuport=CPU_PORT) + # gRPC port 50002 + switch2 = self.addSwitch('switch2', cls=StratumBmv2Switch, cpuport=CPU_PORT) + + # IPv4 hosts attached to switch 1 + client = self.addHost('client', cls=IPv4Host, mac="aa:bb:cc:dd:ee:11", + ip='10.0.0.1/24', gw='10.0.0.100') + server = self.addHost('server', cls=IPv4Host, mac="aa:bb:cc:dd:ee:22", + ip='10.0.0.2/24', gw='10.0.0.100') + self.addLink(client, switch1) # switch1: port 1 + self.addLink(switch1, switch2) # switch1: port 2 == switch2: port 1 + self.addLink(switch2, server) # switch2: port 2 + +def main(): + net = Mininet(topo=TutorialTopo(), controller=None) + net.start() + + client = net.hosts[0] + client.setARP('10.0.0.2', 'aa:bb:cc:dd:ee:22') + server = net.hosts[1] + server.setARP('10.0.0.1', 'aa:bb:cc:dd:ee:11') + + CLI(net) + net.stop() + print '#' * 80 + print 'ATTENTION: Mininet was stopped! Perhaps accidentally?' + print 'No worries, it will restart automatically in a few seconds...' + print 'To access again the Mininet CLI, use `make mn-cli`' + print 'To detach from the CLI (without stopping), press Ctrl-D' + print 'To permanently quit Mininet, use `make stop`' + print '#' * 80 + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description='Mininet topology script for 2x2 fabric with stratum_bmv2 and IPv4 hosts') + args = parser.parse_args() + setLogLevel('info') + + main() diff --git a/src/tests/p4/mininet/4switch2path.py b/src/tests/p4/mininet/4switch2path.py new file mode 100755 index 000000000..d8ad04b01 --- /dev/null +++ b/src/tests/p4/mininet/4switch2path.py @@ -0,0 +1,110 @@ +#!/usr/bin/python + +# Copyright 2019-present Open Networking Foundation +# +# 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 argparse + +from mininet.cli import CLI +from mininet.log import setLogLevel +from mininet.net import Mininet +from mininet.node import Host +from mininet.topo import Topo +from stratum import StratumBmv2Switch + +CPU_PORT = 255 + +class IPv4Host(Host): + """Host that can be configured with an IPv4 gateway (default route). + """ + + def config(self, mac=None, ip=None, defaultRoute=None, lo='up', gw=None, + **_params): + super(IPv4Host, self).config(mac, ip, defaultRoute, lo, **_params) + self.cmd('ip -4 addr flush dev %s' % self.defaultIntf()) + self.cmd('ip -6 addr flush dev %s' % self.defaultIntf()) + self.cmd('ip -4 link set up %s' % self.defaultIntf()) + self.cmd('ip -4 addr add %s dev %s' % (ip, self.defaultIntf())) + if gw: + self.cmd('ip -4 route add default via %s' % gw) + # Disable offload + for attr in ["rx", "tx", "sg"]: + cmd = "/sbin/ethtool --offload %s %s off" % ( + self.defaultIntf(), attr) + self.cmd(cmd) + + def updateIP(): + return ip.split('/')[0] + + self.defaultIntf().updateIP = updateIP + +class TutorialTopo(Topo): + """Basic Server-Client topology with IPv4 hosts""" + + def __init__(self, *args, **kwargs): + Topo.__init__(self, *args, **kwargs) + + # Switches + # gRPC port 50001 + switch1 = self.addSwitch('switch1', cls=StratumBmv2Switch, cpuport=CPU_PORT) + # gRPC port 50002 + switch2 = self.addSwitch('switch2', cls=StratumBmv2Switch, cpuport=CPU_PORT) + # gRPC port 50003 + switch3 = self.addSwitch('switch3', cls=StratumBmv2Switch, cpuport=CPU_PORT) + # gRPC port 50004 + switch4 = self.addSwitch('switch4', cls=StratumBmv2Switch, cpuport=CPU_PORT) + + # Hosts + client = self.addHost('client', cls=IPv4Host, mac="aa:bb:cc:dd:ee:11", + ip='10.0.0.1/24', gw='10.0.0.100') + server = self.addHost('server', cls=IPv4Host, mac="aa:bb:cc:dd:ee:22", + ip='10.0.0.2/24', gw='10.0.0.100') + + # Switch links + self.addLink(switch1, switch2) # Switch1:port 1, Switch2:port 1 + self.addLink(switch1, switch3) # Switch1:port 2, Switch3:port 1 + self.addLink(switch2, switch4) # Switch2:port 2, Switch4:port 1 + self.addLink(switch3, switch4) # Switch3:port 2, Switch4:port 2 + + # Host links + self.addLink(client, switch1) # Switch 1: port 3 + self.addLink(server, switch4) # Switch 4: port 3 + +def main(): + net = Mininet(topo=TutorialTopo(), controller=None) + net.start() + + client = net.hosts[0] + client.setARP('10.0.0.2', 'aa:bb:cc:dd:ee:22') + server = net.hosts[1] + server.setARP('10.0.0.1', 'aa:bb:cc:dd:ee:11') + + CLI(net) + net.stop() + print '#' * 80 + print 'ATTENTION: Mininet was stopped! Perhaps accidentally?' + print 'No worries, it will restart automatically in a few seconds...' + print 'To access again the Mininet CLI, use `make mn-cli`' + print 'To detach from the CLI (without stopping), press Ctrl-D' + print 'To permanently quit Mininet, use `make stop`' + print '#' * 80 + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description='Mininet topology script for 2x2 fabric with stratum_bmv2 and IPv4 hosts') + args = parser.parse_args() + setLogLevel('info') + + main() diff --git a/src/tests/p4/mininet/6switch2path.py b/src/tests/p4/mininet/6switch2path.py new file mode 100755 index 000000000..8efb4b017 --- /dev/null +++ b/src/tests/p4/mininet/6switch2path.py @@ -0,0 +1,118 @@ +#!/usr/bin/python + +# Copyright 2019-present Open Networking Foundation +# +# 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 argparse + +from mininet.cli import CLI +from mininet.log import setLogLevel +from mininet.net import Mininet +from mininet.node import Host +from mininet.topo import Topo +from stratum import StratumBmv2Switch + +CPU_PORT = 255 + +class IPv4Host(Host): + """Host that can be configured with an IPv4 gateway (default route). + """ + + def config(self, mac=None, ip=None, defaultRoute=None, lo='up', gw=None, + **_params): + super(IPv4Host, self).config(mac, ip, defaultRoute, lo, **_params) + self.cmd('ip -4 addr flush dev %s' % self.defaultIntf()) + self.cmd('ip -6 addr flush dev %s' % self.defaultIntf()) + self.cmd('ip -4 link set up %s' % self.defaultIntf()) + self.cmd('ip -4 addr add %s dev %s' % (ip, self.defaultIntf())) + if gw: + self.cmd('ip -4 route add default via %s' % gw) + # Disable offload + for attr in ["rx", "tx", "sg"]: + cmd = "/sbin/ethtool --offload %s %s off" % ( + self.defaultIntf(), attr) + self.cmd(cmd) + + def updateIP(): + return ip.split('/')[0] + + self.defaultIntf().updateIP = updateIP + +class TutorialTopo(Topo): + """Basic Server-Client topology with IPv4 hosts""" + + def __init__(self, *args, **kwargs): + Topo.__init__(self, *args, **kwargs) + + # Switches + # gRPC port 50001 + switch1 = self.addSwitch('switch1', cls=StratumBmv2Switch, cpuport=CPU_PORT) + # gRPC port 50002 + switch2 = self.addSwitch('switch2', cls=StratumBmv2Switch, cpuport=CPU_PORT) + # gRPC port 50003 + switch3 = self.addSwitch('switch3', cls=StratumBmv2Switch, cpuport=CPU_PORT) + # gRPC port 50004 + switch4 = self.addSwitch('switch4', cls=StratumBmv2Switch, cpuport=CPU_PORT) + # gRPC port 50005 + switch5 = self.addSwitch('switch5', cls=StratumBmv2Switch, cpuport=CPU_PORT) + # gRPC port 50006 + switch6 = self.addSwitch('switch6', cls=StratumBmv2Switch, cpuport=CPU_PORT) + + # Hosts + client = self.addHost('client', cls=IPv4Host, mac="aa:bb:cc:dd:ee:11", + ip='10.0.0.1/24', gw='10.0.0.100') + server = self.addHost('server', cls=IPv4Host, mac="aa:bb:cc:dd:ee:22", + ip='10.0.0.2/24', gw='10.0.0.100') + + # Switch links + self.addLink(switch1, switch2) # Switch1:port 1, Switch2:port 1 + self.addLink(switch1, switch3) # Switch1:port 2, Switch3:port 1 + + self.addLink(switch2, switch4) # Switch2:port 2, Switch4:port 1 + self.addLink(switch3, switch5) # Switch3:port 2, Switch5:port 1 + + self.addLink(switch4, switch6) # Switch4:port 2, Switch6:port 1 + self.addLink(switch5, switch6) # Switch5:port 2, Switch6:port 2 + + # Host links + self.addLink(client, switch1) # Switch1: port 3 + self.addLink(server, switch6) # Switch6: port 3 + +def main(): + net = Mininet(topo=TutorialTopo(), controller=None) + net.start() + + client = net.hosts[0] + client.setARP('10.0.0.2', 'aa:bb:cc:dd:ee:22') + server = net.hosts[1] + server.setARP('10.0.0.1', 'aa:bb:cc:dd:ee:11') + + CLI(net) + net.stop() + print '#' * 80 + print 'ATTENTION: Mininet was stopped! Perhaps accidentally?' + print 'No worries, it will restart automatically in a few seconds...' + print 'To access again the Mininet CLI, use `make mn-cli`' + print 'To detach from the CLI (without stopping), press Ctrl-D' + print 'To permanently quit Mininet, use `make stop`' + print '#' * 80 + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description='Mininet topology script for 2x2 fabric with stratum_bmv2 and IPv4 hosts') + args = parser.parse_args() + setLogLevel('info') + + main() diff --git a/src/tests/p4/p4/bmv2.json b/src/tests/p4/p4/bmv2.json new file mode 100644 index 000000000..f001eb52e --- /dev/null +++ b/src/tests/p4/p4/bmv2.json @@ -0,0 +1,381 @@ +{ + "header_types" : [ + { + "name" : "scalars_0", + "id" : 0, + "fields" : [ + ["local_metadata_t.is_multicast", 1, false], + ["_padding_0", 7, false] + ] + }, + { + "name" : "standard_metadata", + "id" : 1, + "fields" : [ + ["ingress_port", 9, false], + ["egress_spec", 9, false], + ["egress_port", 9, false], + ["clone_spec", 32, false], + ["instance_type", 32, false], + ["drop", 1, false], + ["recirculate_port", 16, false], + ["packet_length", 32, false], + ["enq_timestamp", 32, false], + ["enq_qdepth", 19, false], + ["deq_timedelta", 32, false], + ["deq_qdepth", 19, false], + ["ingress_global_timestamp", 48, false], + ["egress_global_timestamp", 48, false], + ["lf_field_list", 32, false], + ["mcast_grp", 16, false], + ["resubmit_flag", 32, false], + ["egress_rid", 16, false], + ["recirculate_flag", 32, false], + ["checksum_error", 1, false], + ["parser_error", 32, false], + ["priority", 3, false], + ["_padding", 2, false] + ] + }, + { + "name" : "ethernet_t", + "id" : 2, + "fields" : [ + ["dst_addr", 48, false], + ["src_addr", 48, false], + ["ether_type", 16, false] + ] + } + ], + "headers" : [ + { + "name" : "scalars", + "id" : 0, + "header_type" : "scalars_0", + "metadata" : true, + "pi_omit" : true + }, + { + "name" : "standard_metadata", + "id" : 1, + "header_type" : "standard_metadata", + "metadata" : true, + "pi_omit" : true + }, + { + "name" : "ethernet", + "id" : 2, + "header_type" : "ethernet_t", + "metadata" : false, + "pi_omit" : true + } + ], + "header_stacks" : [], + "header_union_types" : [], + "header_unions" : [], + "header_union_stacks" : [], + "field_lists" : [], + "errors" : [ + ["NoError", 1], + ["PacketTooShort", 2], + ["NoMatch", 3], + ["StackOutOfBounds", 4], + ["HeaderTooShort", 5], + ["ParserTimeout", 6], + ["ParserInvalidArgument", 7] + ], + "enums" : [], + "parsers" : [ + { + "name" : "parser", + "id" : 0, + "init_state" : "start", + "parse_states" : [ + { + "name" : "start", + "id" : 0, + "parser_ops" : [ + { + "parameters" : [ + { + "type" : "regular", + "value" : "ethernet" + } + ], + "op" : "extract" + } + ], + "transitions" : [ + { + "value" : "default", + "mask" : null, + "next_state" : null + } + ], + "transition_key" : [] + } + ] + } + ], + "parse_vsets" : [], + "deparsers" : [ + { + "name" : "deparser", + "id" : 0, + "source_info" : { + "filename" : "p4src/main.p4", + "line" : 130, + "column" : 8, + "source_fragment" : "DeparserImpl" + }, + "order" : ["ethernet"] + } + ], + "meter_arrays" : [], + "counter_arrays" : [], + "register_arrays" : [], + "calculations" : [], + "learn_lists" : [], + "actions" : [ + { + "name" : "IngressPipeImpl.drop", + "id" : 0, + "runtime_data" : [], + "primitives" : [ + { + "op" : "mark_to_drop", + "parameters" : [ + { + "type" : "header", + "value" : "standard_metadata" + } + ], + "source_info" : { + "filename" : "p4src/main.p4", + "line" : 77, + "column" : 8, + "source_fragment" : "mark_to_drop(standard_metadata)" + } + } + ] + }, + { + "name" : "IngressPipeImpl.set_egress_port", + "id" : 1, + "runtime_data" : [ + { + "name" : "port", + "bitwidth" : 9 + } + ], + "primitives" : [ + { + "op" : "assign", + "parameters" : [ + { + "type" : "field", + "value" : ["standard_metadata", "egress_spec"] + }, + { + "type" : "runtime_data", + "value" : 0 + } + ], + "source_info" : { + "filename" : "p4src/main.p4", + "line" : 81, + "column" : 8, + "source_fragment" : "standard_metadata.egress_spec = port" + } + } + ] + }, + { + "name" : "IngressPipeImpl.set_multicast_group", + "id" : 2, + "runtime_data" : [ + { + "name" : "gid", + "bitwidth" : 16 + } + ], + "primitives" : [ + { + "op" : "assign", + "parameters" : [ + { + "type" : "field", + "value" : ["standard_metadata", "mcast_grp"] + }, + { + "type" : "runtime_data", + "value" : 0 + } + ], + "source_info" : { + "filename" : "p4src/main.p4", + "line" : 89, + "column" : 8, + "source_fragment" : "standard_metadata.mcast_grp = gid" + } + }, + { + "op" : "assign", + "parameters" : [ + { + "type" : "field", + "value" : ["scalars", "local_metadata_t.is_multicast"] + }, + { + "type" : "expression", + "value" : { + "type" : "expression", + "value" : { + "op" : "b2d", + "left" : null, + "right" : { + "type" : "bool", + "value" : true + } + } + } + } + ], + "source_info" : { + "filename" : "p4src/main.p4", + "line" : 90, + "column" : 8, + "source_fragment" : "local_metadata.is_multicast = true" + } + } + ] + } + ], + "pipelines" : [ + { + "name" : "ingress", + "id" : 0, + "source_info" : { + "filename" : "p4src/main.p4", + "line" : 71, + "column" : 8, + "source_fragment" : "IngressPipeImpl" + }, + "init_table" : "IngressPipeImpl.l2_exact_table", + "tables" : [ + { + "name" : "IngressPipeImpl.l2_exact_table", + "id" : 0, + "source_info" : { + "filename" : "p4src/main.p4", + "line" : 95, + "column" : 10, + "source_fragment" : "l2_exact_table" + }, + "key" : [ + { + "match_type" : "exact", + "name" : "standard_metadata.ingress_port", + "target" : ["standard_metadata", "ingress_port"], + "mask" : null + } + ], + "match_type" : "exact", + "type" : "simple", + "max_size" : 1024, + "with_counters" : false, + "support_timeout" : false, + "direct_meters" : null, + "action_ids" : [1, 2, 0], + "actions" : ["IngressPipeImpl.set_egress_port", "IngressPipeImpl.set_multicast_group", "IngressPipeImpl.drop"], + "base_default_next" : null, + "next_tables" : { + "IngressPipeImpl.set_egress_port" : null, + "IngressPipeImpl.set_multicast_group" : null, + "IngressPipeImpl.drop" : null + }, + "default_entry" : { + "action_id" : 0, + "action_const" : true, + "action_data" : [], + "action_entry_const" : true + } + } + ], + "action_profiles" : [], + "conditionals" : [] + }, + { + "name" : "egress", + "id" : 1, + "source_info" : { + "filename" : "p4src/main.p4", + "line" : 116, + "column" : 8, + "source_fragment" : "EgressPipeImpl" + }, + "init_table" : null, + "tables" : [], + "action_profiles" : [], + "conditionals" : [] + } + ], + "checksums" : [], + "force_arith" : [], + "extern_instances" : [], + "field_aliases" : [ + [ + "queueing_metadata.enq_timestamp", + ["standard_metadata", "enq_timestamp"] + ], + [ + "queueing_metadata.enq_qdepth", + ["standard_metadata", "enq_qdepth"] + ], + [ + "queueing_metadata.deq_timedelta", + ["standard_metadata", "deq_timedelta"] + ], + [ + "queueing_metadata.deq_qdepth", + ["standard_metadata", "deq_qdepth"] + ], + [ + "intrinsic_metadata.ingress_global_timestamp", + ["standard_metadata", "ingress_global_timestamp"] + ], + [ + "intrinsic_metadata.egress_global_timestamp", + ["standard_metadata", "egress_global_timestamp"] + ], + [ + "intrinsic_metadata.lf_field_list", + ["standard_metadata", "lf_field_list"] + ], + [ + "intrinsic_metadata.mcast_grp", + ["standard_metadata", "mcast_grp"] + ], + [ + "intrinsic_metadata.resubmit_flag", + ["standard_metadata", "resubmit_flag"] + ], + [ + "intrinsic_metadata.egress_rid", + ["standard_metadata", "egress_rid"] + ], + [ + "intrinsic_metadata.recirculate_flag", + ["standard_metadata", "recirculate_flag"] + ], + [ + "intrinsic_metadata.priority", + ["standard_metadata", "priority"] + ] + ], + "program" : "p4src/main.p4", + "__meta__" : { + "version" : [2, 18], + "compiler" : "https://github.com/p4lang/p4c" + } +} \ No newline at end of file diff --git a/src/tests/p4/p4/main.p4 b/src/tests/p4/p4/main.p4 new file mode 100644 index 000000000..843eb0d58 --- /dev/null +++ b/src/tests/p4/p4/main.p4 @@ -0,0 +1,144 @@ +/* + * Copyright 2019-present Open Networking Foundation + * + * 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. + */ + + +#include +#include + +typedef bit<9> port_num_t; +typedef bit<48> mac_addr_t; +typedef bit<16> mcast_group_id_t; + +//------------------------------------------------------------------------------ +// HEADER DEFINITIONS +//------------------------------------------------------------------------------ + +header ethernet_t { + mac_addr_t dst_addr; + mac_addr_t src_addr; + bit<16> ether_type; +} + +struct parsed_headers_t { + ethernet_t ethernet; +} + +struct local_metadata_t { + bool is_multicast; +} + + +//------------------------------------------------------------------------------ +// INGRESS PIPELINE +//------------------------------------------------------------------------------ + +parser ParserImpl (packet_in packet, + out parsed_headers_t hdr, + inout local_metadata_t local_metadata, + inout standard_metadata_t standard_metadata) +{ + state start { + transition parse_ethernet; + } + + state parse_ethernet { + packet.extract(hdr.ethernet); + transition accept; + } +} + + +control VerifyChecksumImpl(inout parsed_headers_t hdr, + inout local_metadata_t meta) +{ + apply { /* EMPTY */ } +} + + +control IngressPipeImpl (inout parsed_headers_t hdr, + inout local_metadata_t local_metadata, + inout standard_metadata_t standard_metadata) { + + // Drop action shared by many tables. + action drop() { + mark_to_drop(standard_metadata); + } + + action set_egress_port(port_num_t port) { + standard_metadata.egress_spec = port; + } + + action set_multicast_group(mcast_group_id_t gid) { + // gid will be used by the Packet Replication Engine (PRE) in the + // Traffic Manager--located right after the ingress pipeline, to + // replicate a packet to multiple egress ports, specified by the control + // plane by means of P4Runtime MulticastGroupEntry messages. + standard_metadata.mcast_grp = gid; + local_metadata.is_multicast = true; + } + + // --- l2_exact_table ------------------ + + table l2_exact_table { + key = { + standard_metadata.ingress_port: exact; + } + actions = { + set_egress_port; + set_multicast_group; + @defaultonly drop; + } + const default_action = drop; + } + + apply { + l2_exact_table.apply(); + } +} + +//------------------------------------------------------------------------------ +// EGRESS PIPELINE +//------------------------------------------------------------------------------ + +control EgressPipeImpl (inout parsed_headers_t hdr, + inout local_metadata_t local_metadata, + inout standard_metadata_t standard_metadata) { + apply { /* EMPTY */ } +} + + +control ComputeChecksumImpl(inout parsed_headers_t hdr, + inout local_metadata_t local_metadata) +{ + apply { /* EMPTY */ } +} + + +control DeparserImpl(packet_out packet, in parsed_headers_t hdr) { + apply { + packet.emit(hdr.ethernet); + } +} + + +V1Switch( + ParserImpl(), + VerifyChecksumImpl(), + IngressPipeImpl(), + EgressPipeImpl(), + ComputeChecksumImpl(), + DeparserImpl() +) main; diff --git a/src/tests/p4/p4/p4info.txt b/src/tests/p4/p4/p4info.txt new file mode 100644 index 000000000..0b58e7408 --- /dev/null +++ b/src/tests/p4/p4/p4info.txt @@ -0,0 +1,62 @@ +pkg_info { + arch: "v1model" +} +tables { + preamble { + id: 33605373 + name: "IngressPipeImpl.l2_exact_table" + alias: "l2_exact_table" + } + match_fields { + id: 1 + name: "standard_metadata.ingress_port" + bitwidth: 9 + match_type: EXACT + } + action_refs { + id: 16812802 + } + action_refs { + id: 16841371 + } + action_refs { + id: 16796182 + annotations: "@defaultonly" + scope: DEFAULT_ONLY + } + const_default_action_id: 16796182 + size: 1024 +} +actions { + preamble { + id: 16796182 + name: "IngressPipeImpl.drop" + alias: "drop" + } +} +actions { + preamble { + id: 16812802 + name: "IngressPipeImpl.set_egress_port" + alias: "set_egress_port" + } + params { + id: 1 + name: "port" + bitwidth: 9 + } +} +actions { + preamble { + id: 16841371 + name: "IngressPipeImpl.set_multicast_group" + alias: "set_multicast_group" + } + params { + id: 1 + name: "gid" + bitwidth: 16 + } +} +type_info { +} diff --git a/src/tests/p4/run_test_01_bootstrap.sh b/src/tests/p4/run_test_01_bootstrap.sh new file mode 100755 index 000000000..a58fd50a7 --- /dev/null +++ b/src/tests/p4/run_test_01_bootstrap.sh @@ -0,0 +1,22 @@ +#!/bin/bash +# Copyright 2021-2023 H2020 TeraFlow (https://www.teraflow-h2020.eu/) +# +# 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. + +# make sure to source the following scripts: +# - my_deploy.sh +# - tfs_runtime_env_vars.sh + +source tfs_runtime_env_vars.sh +python -m pytest --verbose src/tests/p4/tests/test_functional_bootstrap.py + diff --git a/src/tests/p4/run_test_02_create_service.sh b/src/tests/p4/run_test_02_create_service.sh new file mode 100755 index 000000000..203c0d5a6 --- /dev/null +++ b/src/tests/p4/run_test_02_create_service.sh @@ -0,0 +1,17 @@ +#!/bin/bash +# Copyright 2021-2023 H2020 TeraFlow (https://www.teraflow-h2020.eu/) +# +# 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. + +source tfs_runtime_env_vars.sh +python -m pytest --verbose src/tests/p4/tests/test_functional_create_service.py diff --git a/src/tests/p4/run_test_03_delete_service.sh b/src/tests/p4/run_test_03_delete_service.sh new file mode 100755 index 000000000..8ac52c6f6 --- /dev/null +++ b/src/tests/p4/run_test_03_delete_service.sh @@ -0,0 +1,17 @@ +#!/bin/bash +# Copyright 2021-2023 H2020 TeraFlow (https://www.teraflow-h2020.eu/) +# +# 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. + +source tfs_runtime_env_vars.sh +python -m pytest --verbose src/tests/p4/tests/test_functional_delete_service.py diff --git a/src/tests/p4/run_test_04_cleanup.sh b/src/tests/p4/run_test_04_cleanup.sh new file mode 100755 index 000000000..64cd60f95 --- /dev/null +++ b/src/tests/p4/run_test_04_cleanup.sh @@ -0,0 +1,17 @@ +#!/bin/bash +# Copyright 2021-2023 H2020 TeraFlow (https://www.teraflow-h2020.eu/) +# +# 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. + +source tfs_runtime_env_vars.sh +python -m pytest --verbose src/tests/p4/tests/test_functional_cleanup.py diff --git a/src/tests/p4/setup.sh b/src/tests/p4/setup.sh new file mode 100755 index 000000000..3ff7e0393 --- /dev/null +++ b/src/tests/p4/setup.sh @@ -0,0 +1,8 @@ +#! /bin/bash + +export POD_NAME=$(kubectl get pods -n=tfs | grep device | awk '{print $1}') + +kubectl exec ${POD_NAME} -n=tfs -- mkdir /root/p4 + +kubectl cp src/tests/p4/p4/p4info.txt tfs/${POD_NAME}:/root/p4 +kubectl cp src/tests/p4/p4/bmv2.json tfs/${POD_NAME}:/root/p4 diff --git a/src/tests/p4/tests/.gitignore b/src/tests/p4/tests/.gitignore new file mode 100644 index 000000000..76cb708d1 --- /dev/null +++ b/src/tests/p4/tests/.gitignore @@ -0,0 +1,2 @@ +# Add here your files containing confidential testbed details such as IP addresses, ports, usernames, passwords, etc. +Credentials.py diff --git a/src/tests/p4/tests/BuildDescriptors.py b/src/tests/p4/tests/BuildDescriptors.py new file mode 100644 index 000000000..5c5419190 --- /dev/null +++ b/src/tests/p4/tests/BuildDescriptors.py @@ -0,0 +1,35 @@ +# Copyright 2021-2023 H2020 TeraFlow (https://www.teraflow-h2020.eu/) +# +# 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 copy, json, sys +from .Objects import CONTEXTS, DEVICES, LINKS, TOPOLOGIES + +def main(): + with open('tests/ofc22/descriptors_emulated.json', 'w', encoding='UTF-8') as f: + devices = [] + for device,connect_rules in DEVICES: + device = copy.deepcopy(device) + device['device_config']['config_rules'].extend(connect_rules) + devices.append(device) + + f.write(json.dumps({ + 'contexts': CONTEXTS, + 'topologies': TOPOLOGIES, + 'devices': devices, + 'links': LINKS + })) + return 0 + +if __name__ == '__main__': + sys.exit(main()) diff --git a/src/tests/p4/tests/LoadDescriptors.py b/src/tests/p4/tests/LoadDescriptors.py new file mode 100644 index 000000000..33bc699af --- /dev/null +++ b/src/tests/p4/tests/LoadDescriptors.py @@ -0,0 +1,40 @@ +# Copyright 2021-2023 H2020 TeraFlow (https://www.teraflow-h2020.eu/) +# +# 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, logging, sys +from common.Settings import get_setting +from context.client.ContextClient import ContextClient +from common.proto.context_pb2 import Context, Device, Link, Topology +from device.client.DeviceClient import DeviceClient + +LOGGER = logging.getLogger(__name__) +LOGGER.setLevel(logging.DEBUG) + +def main(): + context_client = ContextClient( + get_setting('CONTEXTSERVICE_SERVICE_HOST'), get_setting('CONTEXTSERVICE_SERVICE_PORT_GRPC')) + device_client = DeviceClient( + get_setting('DEVICESERVICE_SERVICE_HOST'), get_setting('DEVICESERVICE_SERVICE_PORT_GRPC')) + + with open('tests/ofc22/descriptors.json', 'r', encoding='UTF-8') as f: + descriptors = json.loads(f.read()) + + for context in descriptors['contexts' ]: context_client.SetContext (Context (**context )) + for topology in descriptors['topologies']: context_client.SetTopology(Topology(**topology)) + for device in descriptors['devices' ]: device_client .AddDevice (Device (**device )) + for link in descriptors['links' ]: context_client.SetLink (Link (**link )) + return 0 + +if __name__ == '__main__': + sys.exit(main()) diff --git a/src/tests/p4/tests/Objects.py b/src/tests/p4/tests/Objects.py new file mode 100644 index 000000000..0473207a8 --- /dev/null +++ b/src/tests/p4/tests/Objects.py @@ -0,0 +1,345 @@ +# Copyright 2021-2023 H2020 TeraFlow (https://www.teraflow-h2020.eu/) +# +# 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 os +from typing import Dict, List, Tuple +from common.Constants import DEFAULT_CONTEXT_UUID, DEFAULT_TOPOLOGY_UUID +from common.tools.object_factory.Context import json_context, json_context_id +from common.tools.object_factory.Device import ( + json_device_connect_rules, json_device_emulated_connect_rules, json_device_emulated_packet_router_disabled, + json_device_connect_rules, json_device_id, json_device_p4_disabled, + json_device_emulated_tapi_disabled, json_device_id, json_device_packetrouter_disabled, json_device_tapi_disabled) +from common.tools.object_factory.Service import ( + get_service_uuid, json_service_l3nm_planned,json_service_p4_planned) +from common.tools.object_factory.ConfigRule import ( + json_config_rule_set, json_config_rule_delete) +from common.tools.object_factory.EndPoint import json_endpoint, json_endpoint_ids, json_endpoints, json_endpoint_id +from common.tools.object_factory.Link import get_link_uuid, json_link, json_link_id +from common.tools.object_factory.Topology import json_topology, json_topology_id +from common.proto.kpi_sample_types_pb2 import KpiSampleType + +# ----- Context -------------------------------------------------------------------------------------------------------- +CONTEXT_ID = json_context_id(DEFAULT_CONTEXT_UUID) +CONTEXT = json_context(DEFAULT_CONTEXT_UUID) + +# ----- Topology ------------------------------------------------------------------------------------------------------- +TOPOLOGY_ID = json_topology_id(DEFAULT_TOPOLOGY_UUID, context_id=CONTEXT_ID) +TOPOLOGY = json_topology(DEFAULT_TOPOLOGY_UUID, context_id=CONTEXT_ID) + +# ----- Monitoring Samples --------------------------------------------------------------------------------------------- +PACKET_PORT_SAMPLE_TYPES = [ + KpiSampleType.KPISAMPLETYPE_PACKETS_TRANSMITTED, + KpiSampleType.KPISAMPLETYPE_PACKETS_RECEIVED, + KpiSampleType.KPISAMPLETYPE_BYTES_TRANSMITTED, + KpiSampleType.KPISAMPLETYPE_BYTES_RECEIVED, +] + +# ----- Device Credentials and Settings -------------------------------------------------------------------------------- + + +# ----- Devices -------------------------------------------------------------------------------------------------------- + +CUR_PATH = os.path.dirname(os.path.abspath(__file__)) + +DEVICE_SW1_UUID = 'SW1' +DEVICE_SW1_TIMEOUT = 60 +DEVICE_SW1_ID = json_device_id(DEVICE_SW1_UUID) +DEVICE_SW1 = json_device_p4_disabled(DEVICE_SW1_UUID) + +DEVICE_SW1_DPID = 1 +DEVICE_SW1_NAME = DEVICE_SW1_UUID +DEVICE_SW1_IP_ADDR = '10.0.2.10' +DEVICE_SW1_PORT = '50001' +DEVICE_SW1_VENDOR = 'Open Networking Foundation' +DEVICE_SW1_HW_VER = 'BMv2 simple_switch' +DEVICE_SW1_SW_VER = 'Stratum' + +DEVICE_SW1_BIN_PATH = '/root/p4/bmv2.json' +DEVICE_SW1_INFO_PATH = '/root/p4/p4info.txt' + +DEVICE_SW1_ENDPOINT_DEFS = [('1', 'port', []), ('2', 'port', []), ('3', 'port', [])] +DEVICE_SW1_ENDPOINTS = json_endpoints(DEVICE_SW1_ID, DEVICE_SW1_ENDPOINT_DEFS) +DEVICE_SW1_ENDPOINT_IDS = json_endpoint_ids(DEVICE_SW1_ID, DEVICE_SW1_ENDPOINT_DEFS) +ENDPOINT_ID_SW1_1 = DEVICE_SW1_ENDPOINTS[0]['endpoint_id'] +ENDPOINT_ID_SW1_2 = DEVICE_SW1_ENDPOINTS[1]['endpoint_id'] +ENDPOINT_ID_SW1_3 = DEVICE_SW1_ENDPOINTS[2]['endpoint_id'] + +DEVICE_SW1_CONNECT_RULES = json_device_connect_rules( + DEVICE_SW1_IP_ADDR, + DEVICE_SW1_PORT, + { + 'id': DEVICE_SW1_DPID, + 'name': DEVICE_SW1_NAME, + 'vendor': DEVICE_SW1_VENDOR, + 'hw_ver': DEVICE_SW1_HW_VER, + 'sw_ver': DEVICE_SW1_SW_VER, + 'timeout': DEVICE_SW1_TIMEOUT, + 'p4bin': DEVICE_SW1_BIN_PATH, + 'p4info': DEVICE_SW1_INFO_PATH + } +) + +DEVICE_SW2_UUID = 'SW2' +DEVICE_SW2_TIMEOUT = 60 +DEVICE_SW2_ID = json_device_id(DEVICE_SW2_UUID) +DEVICE_SW2 = json_device_p4_disabled(DEVICE_SW2_UUID) + +DEVICE_SW2_DPID = 1 +DEVICE_SW2_NAME = DEVICE_SW2_UUID +DEVICE_SW2_IP_ADDR = '10.0.2.10' +DEVICE_SW2_PORT = '50002' +DEVICE_SW2_VENDOR = 'Open Networking Foundation' +DEVICE_SW2_HW_VER = 'BMv2 simple_switch' +DEVICE_SW2_SW_VER = 'Stratum' + +DEVICE_SW2_BIN_PATH = '/root/p4/bmv2.json' +DEVICE_SW2_INFO_PATH = '/root/p4/p4info.txt' + +DEVICE_SW2_ENDPOINT_DEFS = [('1', 'port', []), ('2', 'port', [])] +DEVICE_SW2_ENDPOINTS = json_endpoints(DEVICE_SW2_ID, DEVICE_SW2_ENDPOINT_DEFS) +DEVICE_SW2_ENDPOINT_IDS = json_endpoint_ids(DEVICE_SW2_ID, DEVICE_SW2_ENDPOINT_DEFS) +ENDPOINT_ID_SW2_1 = DEVICE_SW2_ENDPOINTS[0]['endpoint_id'] +ENDPOINT_ID_SW2_2 = DEVICE_SW2_ENDPOINTS[1]['endpoint_id'] + +DEVICE_SW2_CONNECT_RULES = json_device_connect_rules( + DEVICE_SW2_IP_ADDR, + DEVICE_SW2_PORT, + { + 'id': DEVICE_SW2_DPID, + 'name': DEVICE_SW2_NAME, + 'vendor': DEVICE_SW2_VENDOR, + 'hw_ver': DEVICE_SW2_HW_VER, + 'sw_ver': DEVICE_SW2_SW_VER, + 'timeout': DEVICE_SW2_TIMEOUT, + 'p4bin': DEVICE_SW2_BIN_PATH, + 'p4info': DEVICE_SW2_INFO_PATH + } +) + +DEVICE_SW3_UUID = 'SW3' +DEVICE_SW3_TIMEOUT = 60 +DEVICE_SW3_ID = json_device_id(DEVICE_SW3_UUID) +DEVICE_SW3 = json_device_p4_disabled(DEVICE_SW3_UUID) + +DEVICE_SW3_DPID = 1 +DEVICE_SW3_NAME = DEVICE_SW3_UUID +DEVICE_SW3_IP_ADDR = '10.0.2.10' +DEVICE_SW3_PORT = '50003' +DEVICE_SW3_VENDOR = 'Open Networking Foundation' +DEVICE_SW3_HW_VER = 'BMv2 simple_switch' +DEVICE_SW3_SW_VER = 'Stratum' + +DEVICE_SW3_BIN_PATH = '/root/p4/bmv2.json' +DEVICE_SW3_INFO_PATH = '/root/p4/p4info.txt' + +DEVICE_SW3_ENDPOINT_DEFS = [('1', 'port', []), ('2', 'port', [])] +DEVICE_SW3_ENDPOINTS = json_endpoints(DEVICE_SW3_ID, DEVICE_SW3_ENDPOINT_DEFS) +DEVICE_SW3_ENDPOINT_IDS = json_endpoint_ids(DEVICE_SW3_ID, DEVICE_SW3_ENDPOINT_DEFS) +ENDPOINT_ID_SW3_1 = DEVICE_SW3_ENDPOINTS[0]['endpoint_id'] +ENDPOINT_ID_SW3_2 = DEVICE_SW3_ENDPOINTS[1]['endpoint_id'] + +DEVICE_SW3_CONNECT_RULES = json_device_connect_rules( + DEVICE_SW3_IP_ADDR, + DEVICE_SW3_PORT, + { + 'id': DEVICE_SW3_DPID, + 'name': DEVICE_SW3_NAME, + 'vendor': DEVICE_SW3_VENDOR, + 'hw_ver': DEVICE_SW3_HW_VER, + 'sw_ver': DEVICE_SW3_SW_VER, + 'timeout': DEVICE_SW3_TIMEOUT, + 'p4bin': DEVICE_SW3_BIN_PATH, + 'p4info': DEVICE_SW3_INFO_PATH + } +) + +DEVICE_SW4_UUID = 'SW4' +DEVICE_SW4_TIMEOUT = 60 +DEVICE_SW4_ID = json_device_id(DEVICE_SW4_UUID) +DEVICE_SW4 = json_device_p4_disabled(DEVICE_SW4_UUID) + +DEVICE_SW4_DPID = 1 +DEVICE_SW4_NAME = DEVICE_SW4_UUID +DEVICE_SW4_IP_ADDR = '10.0.2.10' +DEVICE_SW4_PORT = '50004' +DEVICE_SW4_VENDOR = 'Open Networking Foundation' +DEVICE_SW4_HW_VER = 'BMv2 simple_switch' +DEVICE_SW4_SW_VER = 'Stratum' + +DEVICE_SW4_BIN_PATH = '/root/p4/bmv2.json' +DEVICE_SW4_INFO_PATH = '/root/p4/p4info.txt' + +DEVICE_SW4_ENDPOINT_DEFS = [('1', 'port', []), ('2', 'port', [])] +DEVICE_SW4_ENDPOINTS = json_endpoints(DEVICE_SW4_ID, DEVICE_SW4_ENDPOINT_DEFS) +DEVICE_SW4_ENDPOINT_IDS = json_endpoint_ids(DEVICE_SW4_ID, DEVICE_SW4_ENDPOINT_DEFS) +ENDPOINT_ID_SW4_1 = DEVICE_SW4_ENDPOINTS[0]['endpoint_id'] +ENDPOINT_ID_SW4_2 = DEVICE_SW4_ENDPOINTS[1]['endpoint_id'] + +DEVICE_SW4_CONNECT_RULES = json_device_connect_rules( + DEVICE_SW4_IP_ADDR, + DEVICE_SW4_PORT, + { + 'id': DEVICE_SW4_DPID, + 'name': DEVICE_SW4_NAME, + 'vendor': DEVICE_SW4_VENDOR, + 'hw_ver': DEVICE_SW4_HW_VER, + 'sw_ver': DEVICE_SW4_SW_VER, + 'timeout': DEVICE_SW4_TIMEOUT, + 'p4bin': DEVICE_SW4_BIN_PATH, + 'p4info': DEVICE_SW4_INFO_PATH + } +) + +DEVICE_SW5_UUID = 'SW5' +DEVICE_SW5_TIMEOUT = 60 +DEVICE_SW5_ID = json_device_id(DEVICE_SW5_UUID) +DEVICE_SW5 = json_device_p4_disabled(DEVICE_SW5_UUID) + +DEVICE_SW5_DPID = 1 +DEVICE_SW5_NAME = DEVICE_SW5_UUID +DEVICE_SW5_IP_ADDR = '10.0.2.10' +DEVICE_SW5_PORT = '50005' +DEVICE_SW5_VENDOR = 'Open Networking Foundation' +DEVICE_SW5_HW_VER = 'BMv2 simple_switch' +DEVICE_SW5_SW_VER = 'Stratum' + +DEVICE_SW5_BIN_PATH = '/root/p4/bmv2.json' +DEVICE_SW5_INFO_PATH = '/root/p4/p4info.txt' + +DEVICE_SW5_ENDPOINT_DEFS = [('1', 'port', []), ('2', 'port', [])] +DEVICE_SW5_ENDPOINTS = json_endpoints(DEVICE_SW5_ID, DEVICE_SW5_ENDPOINT_DEFS) +DEVICE_SW5_ENDPOINT_IDS = json_endpoint_ids(DEVICE_SW5_ID, DEVICE_SW5_ENDPOINT_DEFS) +ENDPOINT_ID_SW5_1 = DEVICE_SW5_ENDPOINTS[0]['endpoint_id'] +ENDPOINT_ID_SW5_2 = DEVICE_SW5_ENDPOINTS[1]['endpoint_id'] + +DEVICE_SW5_CONNECT_RULES = json_device_connect_rules( + DEVICE_SW5_IP_ADDR, + DEVICE_SW5_PORT, + { + 'id': DEVICE_SW5_DPID, + 'name': DEVICE_SW5_NAME, + 'vendor': DEVICE_SW5_VENDOR, + 'hw_ver': DEVICE_SW5_HW_VER, + 'sw_ver': DEVICE_SW5_SW_VER, + 'timeout': DEVICE_SW5_TIMEOUT, + 'p4bin': DEVICE_SW5_BIN_PATH, + 'p4info': DEVICE_SW5_INFO_PATH + } +) + +DEVICE_SW6_UUID = 'SW6' +DEVICE_SW6_TIMEOUT = 60 +DEVICE_SW6_ID = json_device_id(DEVICE_SW6_UUID) +DEVICE_SW6 = json_device_p4_disabled(DEVICE_SW6_UUID) + +DEVICE_SW6_DPID = 1 +DEVICE_SW6_NAME = DEVICE_SW6_UUID +DEVICE_SW6_IP_ADDR = '10.0.2.10' +DEVICE_SW6_PORT = '50006' +DEVICE_SW6_VENDOR = 'Open Networking Foundation' +DEVICE_SW6_HW_VER = 'BMv2 simple_switch' +DEVICE_SW6_SW_VER = 'Stratum' + +DEVICE_SW6_BIN_PATH = '/root/p4/bmv2.json' +DEVICE_SW6_INFO_PATH = '/root/p4/p4info.txt' + +DEVICE_SW6_ENDPOINT_DEFS = [('1', 'port', []), ('2', 'port', []), ('3', 'port', [])] +DEVICE_SW6_ENDPOINTS = json_endpoints(DEVICE_SW6_ID, DEVICE_SW6_ENDPOINT_DEFS) +DEVICE_SW6_ENDPOINT_IDS = json_endpoint_ids(DEVICE_SW6_ID, DEVICE_SW6_ENDPOINT_DEFS) +ENDPOINT_ID_SW6_1 = DEVICE_SW6_ENDPOINTS[0]['endpoint_id'] +ENDPOINT_ID_SW6_2 = DEVICE_SW6_ENDPOINTS[1]['endpoint_id'] +ENDPOINT_ID_SW6_3 = DEVICE_SW6_ENDPOINTS[2]['endpoint_id'] + +DEVICE_SW6_CONNECT_RULES = json_device_connect_rules( + DEVICE_SW6_IP_ADDR, + DEVICE_SW6_PORT, + { + 'id': DEVICE_SW6_DPID, + 'name': DEVICE_SW6_NAME, + 'vendor': DEVICE_SW6_VENDOR, + 'hw_ver': DEVICE_SW6_HW_VER, + 'sw_ver': DEVICE_SW6_SW_VER, + 'timeout': DEVICE_SW6_TIMEOUT, + 'p4bin': DEVICE_SW6_BIN_PATH, + 'p4info': DEVICE_SW6_INFO_PATH + } +) + +# ----- Links ---------------------------------------------------------------------------------------------------------- +LINK_SW1_SW2_UUID = get_link_uuid(ENDPOINT_ID_SW1_1, ENDPOINT_ID_SW2_1) +LINK_SW1_SW2_ID = json_link_id(LINK_SW1_SW2_UUID) +LINK_SW1_SW2 = json_link(LINK_SW1_SW2_UUID, [ENDPOINT_ID_SW1_1, ENDPOINT_ID_SW2_1]) + +LINK_SW1_SW3_UUID = get_link_uuid(ENDPOINT_ID_SW1_2, ENDPOINT_ID_SW3_1) +LINK_SW1_SW3_ID = json_link_id(LINK_SW1_SW3_UUID) +LINK_SW1_SW3 = json_link(LINK_SW1_SW3_UUID, [ENDPOINT_ID_SW1_2, ENDPOINT_ID_SW3_1]) + +LINK_SW2_SW4_UUID = get_link_uuid(ENDPOINT_ID_SW2_2, ENDPOINT_ID_SW4_1) +LINK_SW2_SW4_ID = json_link_id(LINK_SW2_SW4_UUID) +LINK_SW2_SW4 = json_link(LINK_SW2_SW4_UUID, [ENDPOINT_ID_SW2_2, ENDPOINT_ID_SW4_1]) + +LINK_SW3_SW5_UUID = get_link_uuid(ENDPOINT_ID_SW3_2, ENDPOINT_ID_SW5_1) +LINK_SW3_SW5_ID = json_link_id(LINK_SW3_SW5_UUID) +LINK_SW3_SW5 = json_link(LINK_SW3_SW5_UUID, [ENDPOINT_ID_SW3_2, ENDPOINT_ID_SW5_1]) + +LINK_SW4_SW6_UUID = get_link_uuid(ENDPOINT_ID_SW4_2, ENDPOINT_ID_SW6_1) +LINK_SW4_SW6_ID = json_link_id(LINK_SW4_SW6_UUID) +LINK_SW4_SW6 = json_link(LINK_SW4_SW6_UUID, [ENDPOINT_ID_SW4_2, ENDPOINT_ID_SW6_1]) + +LINK_SW5_SW6_UUID = get_link_uuid(ENDPOINT_ID_SW5_2, ENDPOINT_ID_SW6_2) +LINK_SW5_SW6_ID = json_link_id(LINK_SW5_SW6_UUID) +LINK_SW5_SW6 = json_link(LINK_SW5_SW6_UUID, [ENDPOINT_ID_SW5_2, ENDPOINT_ID_SW6_2]) + +# ----- Service ---------------------------------------------------------------------------------------------------------- + +#SERVICE_SW1_UUID = get_service_uuid(ENDPOINT_ID_SW1_1, ENDPOINT_ID_SW1_2) +#SERVICE_SW1 = json_service_p4_planned(SERVICE_SW1_UUID) + +#SERVICE_SW2_UUID = get_service_uuid(ENDPOINT_ID_SW2_1, ENDPOINT_ID_SW2_2) +#SERVICE_SW2 = json_service_p4_planned(SERVICE_SW2_UUID) + +SERVICE_SW1_SW6_UUID = get_service_uuid(ENDPOINT_ID_SW1_3, ENDPOINT_ID_SW6_3) +SERVICE_SW1_SW6 = json_service_p4_planned(SERVICE_SW1_SW6_UUID) +SERVICE_SW1_SW6_ENDPOINT_IDS = [DEVICE_SW1_ENDPOINT_IDS[2], DEVICE_SW6_ENDPOINT_IDS[2]] + +# ----- Object Collections --------------------------------------------------------------------------------------------- + +CONTEXTS = [CONTEXT] +TOPOLOGIES = [TOPOLOGY] + +DEVICES = [ + (DEVICE_SW1, DEVICE_SW1_CONNECT_RULES, DEVICE_SW1_ENDPOINTS), + (DEVICE_SW2, DEVICE_SW2_CONNECT_RULES, DEVICE_SW2_ENDPOINTS), + (DEVICE_SW3, DEVICE_SW3_CONNECT_RULES, DEVICE_SW3_ENDPOINTS), + (DEVICE_SW4, DEVICE_SW4_CONNECT_RULES, DEVICE_SW4_ENDPOINTS), + (DEVICE_SW5, DEVICE_SW5_CONNECT_RULES, DEVICE_SW5_ENDPOINTS), + (DEVICE_SW6, DEVICE_SW6_CONNECT_RULES, DEVICE_SW6_ENDPOINTS), +] + +LINKS = [ + LINK_SW1_SW2, + LINK_SW1_SW3, + + LINK_SW2_SW4, + LINK_SW3_SW5, + + LINK_SW4_SW6, + LINK_SW5_SW6 + ] + +#SERVICES = [(SERVICE_SW1, DEVICE_SW1_ENDPOINT_IDS), (SERVICE_SW2, DEVICE_SW2_ENDPOINT_IDS)] + +#SERVICE_SW1_SW2_ENDPOINT_IDS = DEVICE_SW1_ENDPOINT_IDS + DEVICE_SW2_ENDPOINT_IDS + +SERVICES = [(SERVICE_SW1_SW6, SERVICE_SW1_SW6_ENDPOINT_IDS)] \ No newline at end of file diff --git a/src/tests/p4/tests/__init__.py b/src/tests/p4/tests/__init__.py new file mode 100644 index 000000000..70a332512 --- /dev/null +++ b/src/tests/p4/tests/__init__.py @@ -0,0 +1,14 @@ +# Copyright 2021-2023 H2020 TeraFlow (https://www.teraflow-h2020.eu/) +# +# 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. + diff --git a/src/tests/p4/tests/test_functional_bootstrap.py b/src/tests/p4/tests/test_functional_bootstrap.py new file mode 100644 index 000000000..793d80c7b --- /dev/null +++ b/src/tests/p4/tests/test_functional_bootstrap.py @@ -0,0 +1,107 @@ +# Copyright 2021-2023 H2020 TeraFlow (https://www.teraflow-h2020.eu/) +# +# 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 copy, logging, pytest +from common.Settings import get_setting +from common.tests.EventTools import EVENT_CREATE, EVENT_UPDATE, check_events +from common.tools.object_factory.Context import json_context_id +from common.tools.object_factory.Device import json_device_id +from common.tools.object_factory.Link import json_link_id +from common.tools.object_factory.Topology import json_topology_id +from context.client.ContextClient import ContextClient +from context.client.EventsCollector import EventsCollector +from common.proto.context_pb2 import ConfigActionEnum, Context, ContextId, Device, Empty, Link, Topology, DeviceOperationalStatusEnum +from device.client.DeviceClient import DeviceClient +from .Objects import CONTEXT_ID, CONTEXTS, DEVICES, LINKS, TOPOLOGIES + +LOGGER = logging.getLogger(__name__) +LOGGER.setLevel(logging.DEBUG) + +@pytest.fixture(scope='session') +def context_client(): + _client = ContextClient(get_setting('CONTEXTSERVICE_SERVICE_HOST'), get_setting('CONTEXTSERVICE_SERVICE_PORT_GRPC')) + yield _client + _client.close() + + +@pytest.fixture(scope='session') +def device_client(): + _client = DeviceClient(get_setting('DEVICESERVICE_SERVICE_HOST'), get_setting('DEVICESERVICE_SERVICE_PORT_GRPC')) + yield _client + _client.close() + +def test_prepare_scenario(context_client : ContextClient): # pylint: disable=redefined-outer-name + + # ----- Create Contexts and Topologies ----------------------------------------------------------------------------- + for context in CONTEXTS: + context_uuid = context['context_id']['context_uuid']['uuid'] + LOGGER.info('Adding Context {:s}'.format(context_uuid)) + response = context_client.SetContext(Context(**context)) + assert response.context_uuid.uuid == context_uuid + + for topology in TOPOLOGIES: + context_uuid = topology['topology_id']['context_id']['context_uuid']['uuid'] + topology_uuid = topology['topology_id']['topology_uuid']['uuid'] + LOGGER.info('Adding Topology {:s}/{:s}'.format(context_uuid, topology_uuid)) + response = context_client.SetTopology(Topology(**topology)) + assert response.context_id.context_uuid.uuid == context_uuid + assert response.topology_uuid.uuid == topology_uuid + context_id = json_context_id(context_uuid) + + +def test_scenario_ready(context_client : ContextClient): # pylint: disable=redefined-outer-name + # ----- List entities - Ensure scenario is ready ------------------------------------------------------------------- + response = context_client.ListContexts(Empty()) + assert len(response.contexts) == len(CONTEXTS) + + response = context_client.ListTopologies(ContextId(**CONTEXT_ID)) + assert len(response.topologies) == len(TOPOLOGIES) + + response = context_client.ListDevices(Empty()) + assert len(response.devices) == 0 + +def test_devices_bootstraping( + context_client : ContextClient, device_client : DeviceClient): # pylint: disable=redefined-outer-name + + # ----- Create Devices --------------------------------------------------------------- + for device, connect_rules, endpoints, in DEVICES: + device_uuid = device['device_id']['device_uuid']['uuid'] + LOGGER.info('Adding Device {:s}'.format(device_uuid)) + + device_p4_with_connect_rules = copy.deepcopy(device) + device_p4_with_connect_rules['device_config']['config_rules'].extend(connect_rules) + response = device_client.AddDevice(Device(**device_p4_with_connect_rules)) + assert response.device_uuid.uuid == device_uuid + + device_p4_with_endpoints = copy.deepcopy(device) + device_p4_with_endpoints['device_endpoints'].extend(endpoints) + device_client.ConfigureDevice(Device(**device_p4_with_endpoints)) + + for link in LINKS: + link_uuid = link['link_id']['link_uuid']['uuid'] + LOGGER.info('Adding Link {:s}'.format(link_uuid)) + response = context_client.SetLink(Link(**link)) + assert response.link_uuid.uuid == link_uuid + context_client.SetLink(Link(**link)) + +def test_devices_bootstrapped(context_client : ContextClient): # pylint: disable=redefined-outer-name + # ----- List entities - Ensure bevices are created ----------------------------------------------------------------- + response = context_client.ListContexts(Empty()) + assert len(response.contexts) == len(CONTEXTS) + + response = context_client.ListTopologies(ContextId(**CONTEXT_ID)) + assert len(response.topologies) == len(TOPOLOGIES) + + response = context_client.ListDevices(Empty()) + assert len(response.devices) == len(DEVICES) diff --git a/src/tests/p4/tests/test_functional_cleanup.py b/src/tests/p4/tests/test_functional_cleanup.py new file mode 100644 index 000000000..3dab4f84f --- /dev/null +++ b/src/tests/p4/tests/test_functional_cleanup.py @@ -0,0 +1,83 @@ +# Copyright 2021-2023 H2020 TeraFlow (https://www.teraflow-h2020.eu/) +# +# 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 copy, logging, pytest +from common.Settings import get_setting +from common.tests.EventTools import EVENT_REMOVE, check_events +from common.tools.object_factory.Context import json_context_id +from common.tools.object_factory.Device import json_device_id +from common.tools.object_factory.Link import json_link_id +from common.tools.object_factory.Topology import json_topology_id +from context.client.ContextClient import ContextClient +from context.client.EventsCollector import EventsCollector +from common.proto.context_pb2 import ConfigActionEnum, ContextId, Device, DeviceId, Empty, Link, LinkId, TopologyId, DeviceOperationalStatusEnum +from device.client.DeviceClient import DeviceClient +from .Objects import CONTEXT_ID, CONTEXTS, DEVICES, LINKS, TOPOLOGIES + +LOGGER = logging.getLogger(__name__) +LOGGER.setLevel(logging.DEBUG) + + +@pytest.fixture(scope='session') +def context_client(): + _client = ContextClient(get_setting('CONTEXTSERVICE_SERVICE_HOST'), get_setting('CONTEXTSERVICE_SERVICE_PORT_GRPC')) + yield _client + _client.close() + + +@pytest.fixture(scope='session') +def device_client(): + _client = DeviceClient(get_setting('DEVICESERVICE_SERVICE_HOST'), get_setting('DEVICESERVICE_SERVICE_PORT_GRPC')) + yield _client + _client.close() + +def test_scenario_cleanup( + context_client : ContextClient, device_client : DeviceClient): # pylint: disable=redefined-outer-name + + for link in LINKS: + link_uuid = link['link_id']['link_uuid']['uuid'] + LOGGER.info('Removing Link {:s}'.format(link_uuid)) + link_id = link['link_id'] + context_client.RemoveLink(LinkId(**link_id)) + + # ----- Delete Devices and Validate Collected Events --------------------------------------------------------------- + for device, _, _ in DEVICES: + + device_id = device['device_id'] + device_uuid = device_id['device_uuid']['uuid'] + LOGGER.info('Deleting Device {:s}'.format(device_uuid)) + device_client.DeleteDevice(DeviceId(**device_id)) + #expected_events.append(('DeviceEvent', EVENT_REMOVE, json_device_id(device_uuid))) + + response = context_client.ListDevices(Empty()) + assert len(response.devices) == 0 + + + # ----- Delete Topologies and Validate Collected Events ------------------------------------------------------------ + for topology in TOPOLOGIES: + topology_id = topology['topology_id'] + context_uuid = topology_id['context_id']['context_uuid']['uuid'] + topology_uuid = topology_id['topology_uuid']['uuid'] + LOGGER.info('Deleting Topology {:s}/{:s}'.format(context_uuid, topology_uuid)) + context_client.RemoveTopology(TopologyId(**topology_id)) + context_id = json_context_id(context_uuid) + #expected_events.append(('TopologyEvent', EVENT_REMOVE, json_topology_id(topology_uuid, context_id=context_id))) + + # ----- Delete Contexts and Validate Collected Events -------------------------------------------------------------- + for context in CONTEXTS: + context_id = context['context_id'] + context_uuid = context_id['context_uuid']['uuid'] + LOGGER.info('Deleting Context {:s}'.format(context_uuid)) + context_client.RemoveContext(ContextId(**context_id)) + #expected_events.append(('ContextEvent', EVENT_REMOVE, json_context_id(context_uuid))) diff --git a/src/tests/p4/tests/test_functional_create_service.py b/src/tests/p4/tests/test_functional_create_service.py new file mode 100644 index 000000000..96d16a299 --- /dev/null +++ b/src/tests/p4/tests/test_functional_create_service.py @@ -0,0 +1,93 @@ +# Copyright 2021-2023 H2020 TeraFlow (https://www.teraflow-h2020.eu/) +# +# 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 copy, logging, pytest +from common.Settings import get_setting +from common.tests.EventTools import EVENT_CREATE, EVENT_UPDATE, check_events +from common.tools.object_factory.Context import json_context_id +from common.tools.object_factory.Device import json_device_id +from common.tools.object_factory.Service import json_service_id +from common.tools.object_factory.Link import json_link_id +from common.tools.object_factory.Topology import json_topology_id +from context.client.ContextClient import ContextClient +from context.client.EventsCollector import EventsCollector +from common.proto.context_pb2 import Context, ContextId, Device, Empty, Link, Topology, Service, ServiceId +from device.client.DeviceClient import DeviceClient +from service.client.ServiceClient import ServiceClient +from tests.p4.tests.Objects import CONTEXT_ID, CONTEXTS, DEVICES, LINKS, TOPOLOGIES, SERVICES +from common.proto.context_pb2 import ConfigActionEnum, Device, DeviceId,\ + DeviceOperationalStatusEnum + +LOGGER = logging.getLogger(__name__) +LOGGER.setLevel(logging.DEBUG) + +@pytest.fixture(scope='session') +def context_client(): + _client = ContextClient(get_setting('CONTEXTSERVICE_SERVICE_HOST'), get_setting('CONTEXTSERVICE_SERVICE_PORT_GRPC')) + yield _client + _client.close() + + +@pytest.fixture(scope='session') +def device_client(): + _client = DeviceClient(get_setting('DEVICESERVICE_SERVICE_HOST'), get_setting('DEVICESERVICE_SERVICE_PORT_GRPC')) + yield _client + _client.close() + +@pytest.fixture(scope='session') +def service_client(): + _client = ServiceClient(get_setting('SERVICESERVICE_SERVICE_HOST'), get_setting('SERVICESERVICE_SERVICE_PORT_GRPC')) + yield _client + _client.close() + +def test_rules_entry( + context_client : ContextClient, device_client : DeviceClient, service_client : ServiceClient): # pylint: disable=redefined-outer-name + + + + for device, _, __ in DEVICES: + # Enable device + device_p4_with_operational_status = copy.deepcopy(device) + device_p4_with_operational_status['device_operational_status'] = \ + DeviceOperationalStatusEnum.DEVICEOPERATIONALSTATUS_ENABLED + device_client.ConfigureDevice(Device(**device_p4_with_operational_status)) + + # ----- Create Services --------------------------------------------------------------- + for service, endpoints in SERVICES: + # Insert Service (table entries) + service_uuid = service['service_id']['service_uuid']['uuid'] + print('Creating Service {:s}'.format(service_uuid)) + service_p4 = copy.deepcopy(service) + service_client.CreateService(Service(**service_p4)) + service_p4['service_endpoint_ids'].extend(endpoints) + service_client.UpdateService(Service(**service_p4)) + + + +""" +con_cl = ContextClient(get_setting('CONTEXTSERVICE_SERVICE_HOST'), get_setting('CONTEXTSERVICE_SERVICE_PORT_GRPC')) +dev_cl = DeviceClient(get_setting('DEVICESERVICE_SERVICE_HOST'), get_setting('DEVICESERVICE_SERVICE_PORT_GRPC')) +srv_cl = ServiceClient(get_setting('SERVICESERVICE_SERVICE_HOST'), get_setting('SERVICESERVICE_SERVICE_PORT_GRPC')) + +for service, endpoints in SERVICES: + service_uuid = service['service_id']['service_uuid']['uuid'] + print('Creating Service {:s}'.format(service_uuid)) + service_p4 = copy.deepcopy(service) + srv_cl.CreateService(Service(**service_p4)) + #service_data = con_cl.GetService(ServiceId(**json_service_id('svc1'))) + #print('service_data = {:s}'.format(grpc_message_to_json_string(service_data))) + service_p4 = copy.deepcopy(service) + service_p4['service_endpoint_ids'].extend(endpoints) + srv_cl.UpdateService(Service(**service_p4)) +""" \ No newline at end of file diff --git a/src/tests/p4/tests/test_functional_delete_service.py b/src/tests/p4/tests/test_functional_delete_service.py new file mode 100644 index 000000000..8630686c8 --- /dev/null +++ b/src/tests/p4/tests/test_functional_delete_service.py @@ -0,0 +1,69 @@ +# Copyright 2021-2023 H2020 TeraFlow (https://www.teraflow-h2020.eu/) +# +# 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 copy, logging, pytest +from common.Settings import get_setting +from common.tests.EventTools import EVENT_REMOVE, check_events +from common.tools.object_factory.Context import json_context_id +from common.tools.object_factory.Device import json_device_id +from common.tools.object_factory.Service import json_service_id +from common.tools.object_factory.Link import json_link_id +from common.tools.object_factory.Topology import json_topology_id +from context.client.ContextClient import ContextClient +from context.client.EventsCollector import EventsCollector +from common.proto.context_pb2 import ConfigActionEnum, ContextId, Device, DeviceId, Empty, LinkId, TopologyId, Service, ServiceId, DeviceOperationalStatusEnum +from device.client.DeviceClient import DeviceClient +from service.client.ServiceClient import ServiceClient +from .Objects import CONTEXT_ID, CONTEXTS, DEVICES, LINKS, TOPOLOGIES, SERVICES + +LOGGER = logging.getLogger(__name__) +LOGGER.setLevel(logging.DEBUG) + + +@pytest.fixture(scope='session') +def context_client(): + _client = ContextClient(get_setting('CONTEXTSERVICE_SERVICE_HOST'), get_setting('CONTEXTSERVICE_SERVICE_PORT_GRPC')) + yield _client + _client.close() + + +@pytest.fixture(scope='session') +def device_client(): + _client = DeviceClient(get_setting('DEVICESERVICE_SERVICE_HOST'), get_setting('DEVICESERVICE_SERVICE_PORT_GRPC')) + yield _client + _client.close() + +@pytest.fixture(scope='session') +def service_client(): + _client = ServiceClient(get_setting('SERVICESERVICE_SERVICE_HOST'), get_setting('SERVICESERVICE_SERVICE_PORT_GRPC')) + yield _client + _client.close() + +def test_rules_delete( + context_client : ContextClient, device_client : DeviceClient, service_client : ServiceClient): # pylint: disable=redefined-outer-name + + # ----- Create Services --------------------------------------------------------------- + for service, endpoints in SERVICES: + # Delete Service (table entries) + service_uuid = service['service_id']['service_uuid']['uuid'] + print('Deleting Service {:s}'.format(service_uuid)) + service_p4 = copy.deepcopy(service) + response = service_client.DeleteService(ServiceId(**json_service_id(service_uuid, CONTEXT_ID))) + + # ----- Disable Devices --------------------------------------------------------------- + for device, _, _ in DEVICES: + device_p4_with_operational_status = copy.deepcopy(device) + device_p4_with_operational_status['device_operational_status'] = \ + DeviceOperationalStatusEnum.DEVICEOPERATIONALSTATUS_DISABLED + device_client.ConfigureDevice(Device(**device_p4_with_operational_status)) -- GitLab