diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index d627359e93ea57b865c91f211f5683ee7b5a8a07..f72a4a8aa62369b978523a429881d679815955cd 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,10 +1,10 @@ # stages of the cicd pipeline stages: + - dependencies - build - test - unit_test - integ_test - - dependencies - deploy - funct_test diff --git a/run_local_tests.sh b/run_local_tests.sh index c817a792ddfbb203cb72d4aa9dcc1cbd56456196..71ff06a54638f1392b07dac9c4d1323e3a2ec3da 100755 --- a/run_local_tests.sh +++ b/run_local_tests.sh @@ -17,7 +17,8 @@ coverage run --rcfile=$RCFILE --append -m pytest --log-level=INFO --verbose \ context/tests/test_unitary.py coverage run --rcfile=$RCFILE --append -m pytest --log-level=INFO --verbose \ - device/tests/test_unitary.py + device/tests/test_unitary_driverapi.py \ + device/tests/test_unitary_service.py coverage run --rcfile=$RCFILE --append -m pytest --log-level=INFO --verbose \ service/tests/test_unitary.py diff --git a/src/common/Checkers.py b/src/common/Checkers.py index 4ac047a1c6d9078b66d70e6a97755cd3ac6349cf..b9e0f3067357912ef8e7404768b234f3c58d40e5 100644 --- a/src/common/Checkers.py +++ b/src/common/Checkers.py @@ -1,4 +1,4 @@ -from typing import Any, Set, Union +from typing import Any, List, Set, Sized, Tuple, Union def chk_none(name : str, value : Any) -> Any: if value is not None: @@ -18,13 +18,60 @@ def chk_type(name : str, value : Any, type_or_types : Union[type, Set[type]] = s raise AttributeError(msg.format(str(name), str(value), type(value).__name__, str(type_or_types))) return value -def chk_string(name, value, allow_empty=False) -> str: - chk_not_none(name, value) +def chk_length( + name : str, value : Sized, allow_empty : bool = False, + min_length : Union[int, None] = None, max_length : Union[int, None] = None, + allowed_lengths : Union[None, int, Set[int], List[int], Tuple[int]] = None) -> Any: + + length = len(chk_type(name, value, Sized)) + + allow_empty = chk_type('allow_empty for {}'.format(name), allow_empty, bool) + if not allow_empty and length == 0: + msg = '{}({}) is out of range: allow_empty({}) min_length({}) max_length({}) allowed_lengths({}).' + raise AttributeError(msg.format( + str(name), str(value), str(allow_empty), str(min_length), str(max_length), str(allowed_lengths))) + + + if min_length is not None: + min_length = chk_type('min_length for {}'.format(name), min_length, int) + if length < min_length: + msg = '{}({}) is out of range: allow_empty({}) min_length({}) max_length({}) allowed_lengths({}).' + raise AttributeError(msg.format( + str(name), str(value), str(allow_empty), str(min_length), str(max_length), str(allowed_lengths))) + + if max_length is not None: + max_length = chk_type('max_length for {}'.format(name), max_length, int) + if length > max_length: + msg = '{}({}) is out of range: allow_empty({}) min_value({}) max_value({}) allowed_lengths({}).' + raise AttributeError(msg.format( + str(name), str(value), str(allow_empty), str(max_length), str(max_length), str(allowed_lengths))) + + if allowed_lengths is not None: + chk_type('allowed_lengths for {}'.format(name), allowed_lengths, (int, set, list, tuple)) + if isinstance(allowed_lengths, int): + fixed_length = allowed_lengths + if length != fixed_length: + msg = '{}({}) is out of range: allow_empty({}) min_value({}) max_value({}) allowed_lengths({}).' + raise AttributeError(msg.format( + str(name), str(value), str(allow_empty), str(max_length), str(max_length), str(allowed_lengths))) + else: + for i in allowed_lengths: chk_type('allowed_lengths[#{}] for {}'.format(i, name), i, int) + if length not in allowed_lengths: + msg = '{}({}) is out of range: allow_empty({}) min_value({}) max_value({}) allowed_lengths({}).' + raise AttributeError(msg.format( + str(name), str(value), str(allow_empty), str(max_length), str(max_length), str(allowed_lengths))) + + return value + +def chk_string( + name : str, value : Any, allow_empty : bool = False, + min_length : Union[int, None] = None, max_length : Union[int, None] = None, + allowed_lengths : Union[None, int, Set[int], List[int], Tuple[int]] = None) -> str: + chk_type(name, value, str) - if allow_empty: return value - if len(value) == 0: - msg = '{}({}) string is empty.' - raise AttributeError(msg.format(str(name), str(value))) + chk_length( + name, value, allow_empty=allow_empty, min_length=min_length, max_length=max_length, + allowed_lengths=allowed_lengths) return value def chk_float(name, value, type_or_types=(int, float), min_value=None, max_value=None) -> float: diff --git a/src/context/tests/test_unitary.py b/src/context/tests/test_unitary.py index b3a22c60d1aad2e7ad70db42ecfd1d0641a5749a..bba2a346adc2554e81b9048394fb964c4144a982 100644 --- a/src/context/tests/test_unitary.py +++ b/src/context/tests/test_unitary.py @@ -88,17 +88,16 @@ def test_delete_link_empty_uuid(context_client : ContextClient): copy_link_id['link_id']['uuid'] = '' context_client.DeleteLink(LinkId(**copy_link_id)) assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT - assert e.value.details() == 'link_id.link_id.uuid() string is empty.' + msg = 'link_id.link_id.uuid() is out of range: '\ + 'allow_empty(False) min_length(None) max_length(None) allowed_lengths(None).' + assert e.value.details() == msg def test_add_link_already_exists(context_client : ContextClient): # should fail with link already exists with pytest.raises(grpc._channel._InactiveRpcError) as e: context_client.AddLink(Link(**LINK)) assert e.value.code() == grpc.StatusCode.ALREADY_EXISTS - msg = ' '.join([ - 'Context(admin)/Topology(admin)/Link(DEV1/EP2 ==> DEV2/EP1)', - 'already exists in the database.', - ]) + msg = 'Context(admin)/Topology(admin)/Link(DEV1/EP2 ==> DEV2/EP1) already exists in the database.' assert e.value.details() == msg def test_delete_link(context_client : ContextClient): @@ -113,10 +112,7 @@ def test_delete_link_not_existing(context_client : ContextClient): with pytest.raises(grpc._channel._InactiveRpcError) as e: context_client.DeleteLink(LinkId(**LINK_ID)) assert e.value.code() == grpc.StatusCode.NOT_FOUND - msg = ' '.join([ - 'Context(admin)/Topology(admin)/Link(DEV1/EP2 ==> DEV2/EP1)', - 'does not exist in the database.' - ]) + msg = 'Context(admin)/Topology(admin)/Link(DEV1/EP2 ==> DEV2/EP1) does not exist in the database.' assert e.value.details() == msg def test_add_link_uuid_empty(context_client : ContextClient): @@ -126,7 +122,9 @@ def test_add_link_uuid_empty(context_client : ContextClient): copy_link['link_id']['link_id']['uuid'] = '' context_client.AddLink(Link(**copy_link)) assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT - assert e.value.details() == 'link.link_id.link_id.uuid() string is empty.' + msg = 'link.link_id.link_id.uuid() is out of range: '\ + 'allow_empty(False) min_length(None) max_length(None) allowed_lengths(None).' + assert e.value.details() == msg def test_add_link_wrong_endpoint(context_client : ContextClient): # should fail with wrong endpoint context @@ -135,11 +133,9 @@ def test_add_link_wrong_endpoint(context_client : ContextClient): copy_link['endpointList'][0]['topoId']['contextId']['contextUuid']['uuid'] = 'wrong-context' context_client.AddLink(Link(**copy_link)) assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT - msg = ' '.join([ - 'Context(wrong-context) in Endpoint(#0) of Context(admin)/Topology(admin)/Link(DEV1/EP2 ==> DEV2/EP1)', - 'mismatches acceptable Contexts({\'admin\'}).', - 'Optionally, leave field empty to use predefined Context(admin).', - ]) + msg = 'Context(wrong-context) in Endpoint(#0) of '\ + 'Context(admin)/Topology(admin)/Link(DEV1/EP2 ==> DEV2/EP1) mismatches acceptable Contexts({\'admin\'}). '\ + 'Optionally, leave field empty to use predefined Context(admin).' assert e.value.details() == msg # should fail with wrong endpoint topology @@ -148,12 +144,9 @@ def test_add_link_wrong_endpoint(context_client : ContextClient): copy_link['endpointList'][0]['topoId']['topoId']['uuid'] = 'wrong-topo' context_client.AddLink(Link(**copy_link)) assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT - msg = ' '.join([ - 'Context(admin)/Topology(wrong-topo)', - 'in Endpoint(#0) of Context(admin)/Topology(admin)/Link(DEV1/EP2 ==> DEV2/EP1)', - 'mismatches acceptable Topologies({\'admin\'}).', - 'Optionally, leave field empty to use predefined Topology(admin).', - ]) + msg = 'Context(admin)/Topology(wrong-topo) in Endpoint(#0) of '\ + 'Context(admin)/Topology(admin)/Link(DEV1/EP2 ==> DEV2/EP1) mismatches acceptable Topologies({\'admin\'}). '\ + 'Optionally, leave field empty to use predefined Topology(admin).' assert e.value.details() == msg # should fail with device uuid is empty @@ -162,7 +155,9 @@ def test_add_link_wrong_endpoint(context_client : ContextClient): copy_link['endpointList'][0]['dev_id']['device_id']['uuid'] = '' context_client.AddLink(Link(**copy_link)) assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT - assert e.value.details() == 'endpoint_id[#0].dev_id.device_id.uuid() string is empty.' + msg = 'endpoint_id[#0].dev_id.device_id.uuid() is out of range: '\ + 'allow_empty(False) min_length(None) max_length(None) allowed_lengths(None).' + assert e.value.details() == msg # should fail with wrong endpoint device with pytest.raises(grpc._channel._InactiveRpcError) as e: @@ -170,11 +165,8 @@ def test_add_link_wrong_endpoint(context_client : ContextClient): copy_link['endpointList'][0]['dev_id']['device_id']['uuid'] = 'wrong-device' context_client.AddLink(Link(**copy_link)) assert e.value.code() == grpc.StatusCode.NOT_FOUND - msg = ' '.join([ - 'Context(admin)/Topology(admin)/Device(wrong-device)', - 'in Endpoint(#0) of Context(admin)/Topology(admin)/Link(DEV1/EP2 ==> DEV2/EP1)', - 'does not exist in the database.', - ]) + msg = 'Context(admin)/Topology(admin)/Device(wrong-device) in Endpoint(#0) of '\ + 'Context(admin)/Topology(admin)/Link(DEV1/EP2 ==> DEV2/EP1) does not exist in the database.' assert e.value.details() == msg # should fail with endpoint uuid is empty @@ -183,7 +175,9 @@ def test_add_link_wrong_endpoint(context_client : ContextClient): copy_link['endpointList'][0]['port_id']['uuid'] = '' context_client.AddLink(Link(**copy_link)) assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT - assert e.value.details() == 'endpoint_id[#0].port_id.uuid() string is empty.' + msg = 'endpoint_id[#0].port_id.uuid() is out of range: '\ + 'allow_empty(False) min_length(None) max_length(None) allowed_lengths(None).' + assert e.value.details() == msg # should fail with wrong endpoint port with pytest.raises(grpc._channel._InactiveRpcError) as e: @@ -191,11 +185,8 @@ def test_add_link_wrong_endpoint(context_client : ContextClient): copy_link['endpointList'][0]['port_id']['uuid'] = 'wrong-port' context_client.AddLink(Link(**copy_link)) assert e.value.code() == grpc.StatusCode.NOT_FOUND - msg = ' '.join([ - 'Context(admin)/Topology(admin)/Device(DEV1)/Port(wrong-port)', - 'in Endpoint(#0) of Context(admin)/Topology(admin)/Link(DEV1/EP2 ==> DEV2/EP1)', - 'does not exist in the database.', - ]) + msg = 'Context(admin)/Topology(admin)/Device(DEV1)/Port(wrong-port) in Endpoint(#0) of '\ + 'Context(admin)/Topology(admin)/Link(DEV1/EP2 ==> DEV2/EP1) does not exist in the database.' assert e.value.details() == msg # should fail with endpoint device duplicated @@ -204,10 +195,8 @@ def test_add_link_wrong_endpoint(context_client : ContextClient): copy_link['endpointList'][1]['dev_id']['device_id']['uuid'] = 'DEV1' context_client.AddLink(Link(**copy_link)) assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT - msg = ' '.join([ - 'Duplicated Context(admin)/Topology(admin)/Device(DEV1)', - 'in Endpoint(#1) of Context(admin)/Topology(admin)/Link(DEV1/EP2 ==> DEV2/EP1).', - ]) + msg = 'Duplicated Context(admin)/Topology(admin)/Device(DEV1) in Endpoint(#1) of '\ + 'Context(admin)/Topology(admin)/Link(DEV1/EP2 ==> DEV2/EP1).' assert e.value.details() == msg def test_add_link(context_client : ContextClient): diff --git a/src/device/.gitlab-ci.yml b/src/device/.gitlab-ci.yml index 0d538ad47c4e7b95c7e012ef0750039c89ac4a06..fca94bf712606f590c13c3e90f8fc686b9238471 100644 --- a/src/device/.gitlab-ci.yml +++ b/src/device/.gitlab-ci.yml @@ -35,7 +35,7 @@ unit_test device: - sleep 5 - docker ps -a - docker logs $IMAGE_NAME - - docker exec -i $IMAGE_NAME bash -c "pytest --log-level=DEBUG --verbose $IMAGE_NAME/tests/test_unitary.py" + - docker exec -i $IMAGE_NAME bash -c "pytest --log-level=DEBUG --verbose $IMAGE_NAME/tests/test_unitary_service.py $IMAGE_NAME/tests/test_unitary_driverapi.py" after_script: - docker stop $IMAGE_NAME - docker rm $IMAGE_NAME diff --git a/src/device/requirements.in b/src/device/requirements.in index 25abdad1b5767117956a88b816399635348884c7..5a05d7efa6ef6964717635123756da0aead4ceff 100644 --- a/src/device/requirements.in +++ b/src/device/requirements.in @@ -1,6 +1,10 @@ +anytree +apscheduler +fastcache grpcio-health-checking grpcio prometheus-client pytest pytest-benchmark +pytz redis diff --git a/src/device/service/DeviceServiceServicerImpl.py b/src/device/service/DeviceServiceServicerImpl.py index 84ff22b3cad32252160596a457ea8358bcad953b..c5893b7387b46d06262a808aea09e870178e7648 100644 --- a/src/device/service/DeviceServiceServicerImpl.py +++ b/src/device/service/DeviceServiceServicerImpl.py @@ -1,11 +1,8 @@ -from typing import List, Tuple import grpc, logging from prometheus_client import Counter, Histogram -from common.Checkers import chk_options, chk_string from common.database.api.context.Constants import DEFAULT_CONTEXT_ID, DEFAULT_TOPOLOGY_ID from common.database.api.Database import Database -from common.database.api.context.topology.device.OperationalStatus import OperationalStatus, \ - operationalstatus_enum_values, to_operationalstatus_enum +from common.database.api.context.topology.device.OperationalStatus import OperationalStatus from common.exceptions.ServiceException import ServiceException from device.proto.context_pb2 import DeviceId, Device, Empty from device.proto.device_pb2_grpc import DeviceServiceServicer diff --git a/src/device/service/driver_api/DriverFactory.py b/src/device/service/driver_api/DriverFactory.py new file mode 100644 index 0000000000000000000000000000000000000000..c226e434c75286d99c2051098e0d18d9cf483caa --- /dev/null +++ b/src/device/service/driver_api/DriverFactory.py @@ -0,0 +1,50 @@ +from typing import Dict, Set +from device.service.driver_api.QueryFields import QUERY_FIELDS +from device.service.driver_api._Driver import _Driver +from device.service.driver_api.Exceptions import MultipleResultsForQueryException, UnsatisfiedQueryException, \ + UnsupportedDriverClassException, UnsupportedQueryFieldException, UnsupportedQueryFieldValueException + +class DriverFactory: + def __init__(self) -> None: + self.__indices : Dict[str, Dict[str, Set[_Driver]]] = {} # Dict{field_name => Dict{field_value => Set{Driver}}} + + def register_driver_class(self, driver_class, **query_fields): + if not issubclass(driver_class, _Driver): raise UnsupportedDriverClassException(str(driver_class)) + + driver_name = driver_class.__name__ + unsupported_query_fields = set(query_fields.keys()).difference(set(QUERY_FIELDS.keys())) + if len(unsupported_query_fields) > 0: + raise UnsupportedQueryFieldException(unsupported_query_fields, driver_class_name=driver_name) + + for field_name, field_value in query_fields.items(): + field_indice = self.__indices.setdefault(field_name, dict()) + field_enum_values = QUERY_FIELDS.get(field_name) + if field_enum_values is not None and field_value not in field_enum_values: + raise UnsupportedQueryFieldValueException( + field_name, field_value, field_enum_values, driver_class_name=driver_name) + field_indice_drivers = field_indice.setdefault(field_name, set()) + field_indice_drivers.add(driver_class) + + def get_driver_class(self, **query_fields) -> _Driver: + unsupported_query_fields = set(query_fields.keys()).difference(set(QUERY_FIELDS.keys())) + if len(unsupported_query_fields) > 0: raise UnsupportedQueryFieldException(unsupported_query_fields) + + candidate_driver_classes = None + + for field_name, field_value in query_fields.items(): + field_indice = self.__indices.get(field_name) + if field_indice is None: continue + field_enum_values = QUERY_FIELDS.get(field_name) + if field_enum_values is not None and field_value not in field_enum_values: + raise UnsupportedQueryFieldValueException(field_name, field_value, field_enum_values) + field_indice_drivers = field_indice.get(field_name) + if field_indice_drivers is None: continue + + candidate_driver_classes = set().union(field_indice_drivers) if candidate_driver_classes is None else \ + candidate_driver_classes.intersection(field_indice_drivers) + + if len(candidate_driver_classes) == 0: raise UnsatisfiedQueryException(query_fields) + if len(candidate_driver_classes) > 1: + # TODO: Consider choosing driver with more query fields being satisfied (i.e., the most restrictive one) + raise MultipleResultsForQueryException(query_fields, {d.__name__ for d in candidate_driver_classes}) + return candidate_driver_classes.pop() diff --git a/src/device/service/driver_api/Exceptions.py b/src/device/service/driver_api/Exceptions.py new file mode 100644 index 0000000000000000000000000000000000000000..2275bb04cbf9334a6823544bc48e8ad345d2dc20 --- /dev/null +++ b/src/device/service/driver_api/Exceptions.py @@ -0,0 +1,30 @@ +class MultipleResultsForQueryException(Exception): + def __init__(self, query_fields, driver_names): + super().__init__('Multiple Drivers({}) satisfy QueryFields({})'.format(str(driver_names), str(query_fields))) + +class UnsatisfiedQueryException(Exception): + def __init__(self, query_fields): + super().__init__('No Driver satisfies QueryFields({})'.format(str(query_fields))) + +class UnsupportedDriverClassException(Exception): + def __init__(self, driver_class_name): + super().__init__('Class({}) is not a subclass of _Driver'.format(str(driver_class_name))) + +class UnsupportedQueryFieldException(Exception): + def __init__(self, unsupported_query_fields, driver_class_name=None): + if driver_class_name: + msg = 'QueryFields({}) specified by Driver({}) are not supported'.format( + str(unsupported_query_fields), str(driver_class_name)) + else: + msg = 'QueryFields({}) specified in query are not supported'.format(str(unsupported_query_fields)) + super().__init__(msg) + +class UnsupportedQueryFieldValueException(Exception): + def __init__(self, query_field_name, query_field_value, allowed_query_field_values, driver_class_name=None): + if driver_class_name: + msg = 'QueryField({}={}) specified by Driver({}) is not supported. Allowed values are {}'.format( + str(query_field_name), str(query_field_value), str(driver_class_name), str(allowed_query_field_values)) + else: + msg = 'QueryField({}={}) specified in query is not supported. Allowed values are {}'.format( + str(query_field_name), str(query_field_value), str(allowed_query_field_values)) + super().__init__(msg) diff --git a/src/device/service/driver_api/QueryFields.py b/src/device/service/driver_api/QueryFields.py new file mode 100644 index 0000000000000000000000000000000000000000..15b3f5b7582283083c9ea1080dc4f3d6f5390501 --- /dev/null +++ b/src/device/service/driver_api/QueryFields.py @@ -0,0 +1,33 @@ +from enum import Enum + +class DeviceTypeQueryFieldEnum(Enum): + OPTICAL_ROADM = 'optical-roadm' + OPTICAL_TRANDPONDER = 'optical-trandponder' + PACKET_ROUTER = 'packet-router' + PACKET_SWITCH = 'packet-switch' + +class ProtocolQueryFieldEnum(Enum): + SOFTWARE = 'software' + GRPC = 'grpc' + RESTAPI = 'restapi' + NETCONF = 'netconf' + GNMI = 'gnmi' + RESTCONF = 'restconf' + +class DataModelQueryFieldEnum(Enum): + EMULATED = 'emu' + OPENCONFIG = 'oc' + P4 = 'p4' + TRANSPORT_API = 'tapi' + IETF_NETWORK_TOPOLOGY = 'ietf-netw-topo' + ONF_TR_352 = 'onf-tr-352' + +# Map allowed query fields to allowed values per query field. If no restriction (free text) None is specified +QUERY_FIELDS = { + 'device_type' : {i.value for i in DeviceTypeQueryFieldEnum}, + 'protocol' : {i.value for i in ProtocolQueryFieldEnum}, + 'data_model' : {i.value for i in DataModelQueryFieldEnum}, + 'vendor' : None, + 'model' : None, + 'serial_number': None, +} diff --git a/src/device/service/driver_api/_Driver.py b/src/device/service/driver_api/_Driver.py new file mode 100644 index 0000000000000000000000000000000000000000..69ac93cb6a071a40aca25b28f930e390bfbfe6b0 --- /dev/null +++ b/src/device/service/driver_api/_Driver.py @@ -0,0 +1,138 @@ +from typing import Any, Iterator, List, Tuple, Union + +class _Driver: + def __init__(self, address : str, port : int, **kwargs) -> None: + """ Initialize Driver. + Parameters: + address : str + The address of the device + port : int + The port of the device + **kwargs + Extra attributes can be configured using kwargs. + """ + raise NotImplementedError() + + def Connect(self) -> bool: + """ Connect to the Device. + Returns: + succeeded : bool + Boolean variable indicating if connection succeeded. + """ + raise NotImplementedError() + + def Disconnect(self) -> bool: + """ Disconnect from the Device. + Returns: + succeeded : bool + Boolean variable indicating if disconnection succeeded. + """ + raise NotImplementedError() + + def GetConfig(self, resource_keys : List[str]) -> List[Union[Any, None, Exception]]: + """ Retrieve running configuration of entire device, or selected resource keys. + Parameters: + resource_keys : List[str] + List of keys pointing to the resources to be retrieved. + Returns: + values : List[Union[Any, None, Exception]] + List of values for resource keys requested. Return values must be in the same order than resource + keys requested. If a resource is found, the appropriate value type must be retrieved, if a resource + is not found, None must be retrieved in the List for that resource. In case of Exception processing + a resource, the Exception must be retrieved. + """ + raise NotImplementedError() + + def SetConfig(self, resources : List[Tuple[str, Any]]) -> List[Union[bool, Exception]]: + """ Create/Update configuration for a list of resources. + Parameters: + resources : List[Tuple[str, Any]] + List of tuples, each containing a resource_key pointing 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 than + resource keys requested. If a resource is properly set, True must be retrieved; otherwise, the + Exception that is raised during the processing must be retrieved. + """ + raise NotImplementedError() + + def DeleteConfig(self, resource_keys : List[str]) -> List[Union[bool, Exception]]: + """ Delete configuration for a list of resource keys. + Parameters: + resource_keys : List[str] + List of keys pointing to the resources to be deleted. + Returns: + results : List[bool] + List of results for resource key deletions requested. Return values must be in the same order than + resource keys requested. If a resource is properly deleted, True must be retrieved; otherwise, the + Exception that is raised during the processing must be retrieved. + """ + raise NotImplementedError() + + def SubscribeState(self, subscriptions : List[Tuple[str, float, float]]) -> List[Union[bool, Exception]]: + """ Subscribe to state information of entire device, or selected resources. Subscriptions are incremental. + Driver should keep track of requested resources. + Parameters: + subscriptions : List[Tuple[str, float, float]] + List of tuples, each containing a resource_key pointing the resource to be subscribed, a + sampling_duration, and a sampling_interval (both in seconds with float representation) defining, + respectively, for how long monitoring should last, and the desired monitoring interval for the + resource specified. + Returns: + results : List[bool] + List of results for resource key subscriptions requested. Return values must be in the same order + than resource keys requested. If a resource is properly subscribed, True must be retrieved; + otherwise, the Exception that is raised during the processing must be retrieved. + """ + raise NotImplementedError() + + def UnsubscribeState(self, subscriptions : List[Tuple[str, float, float]]) -> List[Union[bool, Exception]]: + """ Unsubscribe from state information of entire device, or selected resources. Subscriptions are incremental. + Driver should keep track of requested resources. + Parameters: + subscriptions : List[str] + List of tuples, each containing a resource_key pointing the resource to be subscribed, a + sampling_duration, and a sampling_interval (both in seconds with float representation) defining, + respectively, for how long monitoring should last, and the desired monitoring interval for the + resource specified. + Returns: + results : List[Union[bool, Exception]] + List of results for resource key unsubscriptions requested. Return values must be in the same order + than resource keys requested. If a resource is properly unsubscribed, True must be retrieved; + otherwise, the Exception that is raised during the processing must be retrieved. + """ + raise NotImplementedError() + + def GetState(self, blocking=False) -> Iterator[Tuple[float, str, Any]]: + """ Retrieve last collected values for subscribed resources. Operates as a generator, so this method should be + called once and will block until values are available. When values are available, it should yield each of + them and block again until new values are available. When the driver is destroyed, GetState() can return + instead of yield to terminate the loop. + Examples: + # keep looping waiting for extra samples (generator loop) + for timestamp,resource_key,resource_value in my_driver.GetState(blocking=True): + process(timestamp, resource_key, resource_value) + + # just retrieve accumulated samples + samples = my_driver.GetState(blocking=False) + # or (as classical loop) + for timestamp,resource_key,resource_value in my_driver.GetState(blocking=False): + process(timestamp, resource_key, resource_value) + Parameters: + blocking : bool + Select the driver behaviour. In both cases, the driver will first retrieve the samples accumulated + and available in the internal queue. Then, if blocking, the driver does not terminate the loop and + waits for additional samples to come, thus behaving as a generator. If non-blocking, the driver + terminates the loop and returns. Non-blocking behaviour can be used for periodically polling the + driver, while blocking can be used when a separate thread is in charge of collecting the samples + produced by the driver. + Returns: + results : Iterator[Tuple[float, str, Any]] + Sequences of state sample. Each State sample contains a float Unix-like timestamps of the samples in + seconds with up to microsecond resolution, the resource_key of the sample, and its resource_value. + Only resources with an active subscription must be retrieved. Interval and duration of the sampling + process are specified when creating the subscription using method SubscribeState(). Order of values + yielded is arbitrary. + """ + raise NotImplementedError() diff --git a/src/device/service/driver_api/__init__.py b/src/device/service/driver_api/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/device/service/drivers/__init__.py b/src/device/service/drivers/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/device/service/drivers/emulated/AnyTreeTools.py b/src/device/service/drivers/emulated/AnyTreeTools.py new file mode 100644 index 0000000000000000000000000000000000000000..e7817b78921e3b91fb84ca660f022c869ac88220 --- /dev/null +++ b/src/device/service/drivers/emulated/AnyTreeTools.py @@ -0,0 +1,62 @@ +import anytree +from typing import Any, List, Optional + +from anytree.render import Row + +class TreeNode(anytree.node.Node): + def __init__(self, name, parent=None, children=None, **kwargs) -> None: + super().__init__(name, parent=parent, children=children, **kwargs) + self.value : Optional[Any] = None + + def get_full_path(self): + return self.separator.join([''] + [str(node.name) for node in self.path]) + +class RawStyle(anytree.render.AbstractStyle): + def __init__(self): + """ + Raw style. + + >>> from anytree import Node, RenderTree + >>> root = Node("root") + >>> s0 = Node("sub0", parent=root) + >>> s0b = Node("sub0B", parent=s0) + >>> s0a = Node("sub0A", parent=s0) + >>> s1 = Node("sub1", parent=root) + >>> print(RenderTree(root, style=RawStyle())) + Node('/root') + Node('/root/sub0') + Node('/root/sub0/sub0B') + Node('/root/sub0/sub0A') + Node('/root/sub1') + """ + super(RawStyle, self).__init__(u'', u'', u'') + +def get_subnode(resolver : anytree.Resolver, root : TreeNode, path : List[str], default : Optional[Any] = None): + node = root + for path_item in path: + try: + node = resolver.get(node, path_item) + except anytree.ChildResolverError: + return default + return node + +def set_subnode_value(resolver : anytree.Resolver, root : TreeNode, path : List[str], value : Any): + node = root + for path_item in path: + try: + node = resolver.get(node, path_item) + except anytree.ChildResolverError: + node = TreeNode(path_item, parent=node) + node.value = value + +def dump_subtree(root : TreeNode): + if not isinstance(root, TreeNode): raise Exception('root must be a TreeNode') + results = [] + for row in anytree.RenderTree(root, style=RawStyle()): + node : TreeNode = row.node + path = node.get_full_path()[2:] # get full path except the heading root placeholder "/." + if len(path) == 0: continue + value = node.value + if value is None: continue + results.append((path, value)) + return results diff --git a/src/device/service/drivers/emulated/EmulatedDriver.py b/src/device/service/drivers/emulated/EmulatedDriver.py new file mode 100644 index 0000000000000000000000000000000000000000..7dc5757f7afb928bd56e64b5a87d4c414171cd6b --- /dev/null +++ b/src/device/service/drivers/emulated/EmulatedDriver.py @@ -0,0 +1,201 @@ +import anytree, logging, pytz, queue, random, threading +from datetime import datetime, timedelta +from typing import Any, Iterator, List, Tuple, Union +from apscheduler.executors.pool import ThreadPoolExecutor +from apscheduler.job import Job +from apscheduler.jobstores.memory import MemoryJobStore +from apscheduler.schedulers.background import BackgroundScheduler +from common.Checkers import chk_float, chk_length, chk_string, chk_type +from device.service.driver_api._Driver import _Driver +from .AnyTreeTools import TreeNode, dump_subtree, get_subnode, set_subnode_value + +LOGGER = logging.getLogger(__name__) + +def sample(resource_key : str, out_samples : queue.Queue): + out_samples.put_nowait((datetime.timestamp(datetime.utcnow()), resource_key, random.random())) + +class EmulatedDriver(_Driver): + def __init__(self, address : str, port : int, **kwargs) -> None: + self.__lock = threading.Lock() + self.__root = TreeNode('.') + self.__terminate = threading.Event() + self.__scheduler = BackgroundScheduler(daemon=True) # scheduler used to emulate sampling events + self.__scheduler.configure( + jobstores = {'default': MemoryJobStore()}, + executors = {'default': ThreadPoolExecutor(max_workers=1)}, + job_defaults = {'coalesce': False, 'max_instances': 3}, + timezone=pytz.utc) + self.__out_samples = queue.Queue() + + def Connect(self) -> bool: + # Connect triggers activation of sampling events that will be scheduled based on subscriptions + self.__scheduler.start() + return True + + def Disconnect(self) -> bool: + # Trigger termination of loops and processes + self.__terminate.set() + # Disconnect triggers deactivation of sampling events + self.__scheduler.shutdown() + return True + + def GetConfig(self, resource_keys : List[str] = []) -> List[Union[Any, None, Exception]]: + chk_type('resources', resource_keys, list) + with self.__lock: + if len(resource_keys) == 0: return dump_subtree(self.__root) + results = [] + resolver = anytree.Resolver(pathattr='name') + for i,resource_key in enumerate(resource_keys): + str_resource_name = 'resource_key[#{:d}]'.format(i) + try: + chk_string(str_resource_name, resource_key, allow_empty=False) + resource_path = resource_key.split('/') + except Exception as e: + LOGGER.exception('Exception validating {}: {}'.format(str_resource_name, str(resource_key))) + results.append(e) # if validation fails, store the exception + continue + + resource_node = get_subnode(resolver, self.__root, resource_path, default=None) + # if not found, resource_node is None + results.append(None if resource_node is None else dump_subtree(resource_node)) + return results + + def SetConfig(self, resources : List[Tuple[str, Any]]) -> List[Union[bool, Exception]]: + chk_type('resources', resources, list) + if len(resources) == 0: return [] + results = [] + resolver = anytree.Resolver(pathattr='name') + with self.__lock: + for i,resource in enumerate(resources): + str_resource_name = 'resources[#{:d}]'.format(i) + try: + chk_type(str_resource_name, resource, (list, tuple)) + chk_length(str_resource_name, resource, allowed_lengths=2) + resource_key,resource_value = resource + resource_path = resource_key.split('/') + except Exception as e: + LOGGER.exception('Exception validating {}: {}'.format(str_resource_name, str(resource_key))) + results.append(e) # if validation fails, store the exception + continue + + set_subnode_value(resolver, self.__root, resource_path, resource_value) + results.append(True) + return results + + def DeleteConfig(self, resource_keys : List[str]) -> List[Union[bool, Exception]]: + chk_type('resources', resource_keys, list) + if len(resource_keys) == 0: return [] + results = [] + resolver = anytree.Resolver(pathattr='name') + with self.__lock: + for i,resource_key in enumerate(resource_keys): + str_resource_name = 'resource_key[#{:d}]'.format(i) + try: + chk_string(str_resource_name, resource_key, allow_empty=False) + resource_path = resource_key.split('/') + except Exception as e: + LOGGER.exception('Exception validating {}: {}'.format(str_resource_name, str(resource_key))) + results.append(e) # if validation fails, store the exception + continue + + resource_node = get_subnode(resolver, self.__root, resource_path, default=None) + # if not found, resource_node is None + if resource_node is None: + results.append(False) + continue + + parent = resource_node.parent + children = list(parent.children) + children.remove(resource_node) + parent.children = tuple(children) + results.append(True) + return results + + def SubscribeState(self, resources : List[Tuple[str, float, float]]) -> List[Union[bool, Exception]]: + chk_type('resources', resources, list) + if len(resources) == 0: return [] + results = [] + resolver = anytree.Resolver(pathattr='name') + with self.__lock: + for i,resource in enumerate(resources): + str_resource_name = 'resources[#{:d}]'.format(i) + try: + chk_type(str_resource_name, resource, (list, tuple)) + chk_length(str_resource_name, resource, allowed_lengths=3) + resource_key,sampling_duration,sampling_interval = resource + chk_string(str_resource_name + '.resource_key', resource_key, allow_empty=False) + resource_path = resource_key.split('/') + chk_float(str_resource_name + '.sampling_duration', sampling_duration, min_value=0) + chk_float(str_resource_name + '.sampling_interval', sampling_interval, min_value=0) + except Exception as e: + LOGGER.exception('Exception validating {}: {}'.format(str_resource_name, str(resource_key))) + results.append(e) # if validation fails, store the exception + continue + + start_date,end_date = None,None + if sampling_duration <= 1.e-12: + start_date = datetime.utcnow() + end_date = start_date + timedelta(seconds=sampling_duration) + + job_id = 'k={:s}/d={:f}/i={:f}'.format(resource_key, sampling_duration, sampling_interval) + job = self.__scheduler.add_job( + sample, args=(resource_key, self.__out_samples), kwargs={}, + id=job_id, trigger='interval', seconds=sampling_interval, + start_date=start_date, end_date=end_date, timezone=pytz.utc) + + subscription_path = resource_path + ['{:.3f}:{:.3f}'.format(sampling_duration, sampling_interval)] + set_subnode_value(resolver, self.__root, subscription_path, job) + results.append(True) + return results + + def UnsubscribeState(self, resources : List[Tuple[str, float, float]]) -> List[Union[bool, Exception]]: + chk_type('resources', resources, list) + if len(resources) == 0: return [] + results = [] + resolver = anytree.Resolver(pathattr='name') + with self.__lock: + for i,resource in enumerate(resources): + str_resource_name = 'resources[#{:d}]'.format(i) + try: + chk_type(str_resource_name, resource, (list, tuple)) + chk_length(str_resource_name, resource, allowed_lengths=3) + resource_key,sampling_duration,sampling_interval = resource + chk_string(str_resource_name + '.resource_key', resource_key, allow_empty=False) + resource_path = resource_key.split('/') + chk_float(str_resource_name + '.sampling_duration', sampling_duration, min_value=0) + chk_float(str_resource_name + '.sampling_interval', sampling_interval, min_value=0) + except Exception as e: + LOGGER.exception('Exception validating {}: {}'.format(str_resource_name, str(resource_key))) + results.append(e) # if validation fails, store the exception + continue + + subscription_path = resource_path + ['{:.3f}:{:.3f}'.format(sampling_duration, sampling_interval)] + subscription_node = get_subnode(resolver, self.__root, subscription_path) + + # if not found, resource_node is None + if subscription_node is None: + results.append(False) + continue + + job : Job = getattr(subscription_node, 'value', None) + if job is None or not isinstance(job, Job): + raise Exception('Malformed subscription node or wrong resource key: {}'.format(str(resource))) + job.remove() + + parent = subscription_node.parent + children = list(parent.children) + children.remove(subscription_node) + parent.children = tuple(children) + + results.append(True) + return results + + def GetState(self, blocking=False) -> Iterator[Tuple[str, Any]]: + while not self.__terminate.is_set(): + try: + sample = self.__out_samples.get(block=blocking, timeout=0.1) + except queue.Empty: + if blocking: continue + return + if sample is None: continue + yield sample diff --git a/src/device/service/drivers/emulated/QueryFields.py b/src/device/service/drivers/emulated/QueryFields.py new file mode 100644 index 0000000000000000000000000000000000000000..6db43e5b5d4ffe1bbcc652d305981757bd960c3e --- /dev/null +++ b/src/device/service/drivers/emulated/QueryFields.py @@ -0,0 +1,8 @@ +from enum import Enum + +VENDOR_CTTC = 'cttc' + +DEVICE_MODEL_EMULATED_OPTICAL_ROADM = 'cttc_emu_opt_rdm' +DEVICE_MODEL_EMULATED_OPTICAL_TRANDPONDER = 'cttc_emu_opt_tp' +DEVICE_MODEL_EMULATED_PACKET_ROUTER = 'cttc_emu_pkt_rtr' +DEVICE_MODEL_EMULATED_PACKET_SWITCH = 'cttc_emu_pkt_swt' diff --git a/src/device/service/drivers/emulated/__init__.py b/src/device/service/drivers/emulated/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/device/service/drivers/openconfig/__init__.py b/src/device/service/drivers/openconfig/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/device/tests/test_unitary_driverapi.py b/src/device/tests/test_unitary_driverapi.py new file mode 100644 index 0000000000000000000000000000000000000000..ac7231cf76d4bcba0ea37da0cab781e21bc1c560 --- /dev/null +++ b/src/device/tests/test_unitary_driverapi.py @@ -0,0 +1,128 @@ +import copy, logging, math, pytest, time +from device.service.drivers.emulated.EmulatedDriver import EmulatedDriver + +LOGGER = logging.getLogger(__name__) +LOGGER.setLevel(logging.DEBUG) + +PATH_IF = '/interfaces/interface[name="{}"]' +PATH_SUBIF = PATH_IF + '/subinterfaces/subinterface[index="{}"]' +PATH_ADDRIPV4 = PATH_SUBIF + '/ipv4/address[ip="{}"]' + +DEVICE_CONFIG_IF1 = [] +DEVICE_CONFIG_IF1.append((PATH_IF .format('IF1' ) + '/config/name', "IF1" )) +DEVICE_CONFIG_IF1.append((PATH_IF .format('IF1' ) + '/config/enabled', True )) +DEVICE_CONFIG_IF1.append((PATH_SUBIF .format('IF1', 0 ) + '/config/index', 0 )) +DEVICE_CONFIG_IF1.append((PATH_ADDRIPV4.format('IF1', 0, '10.1.0.1') + '/config/ip', "10.1.0.1")) +DEVICE_CONFIG_IF1.append((PATH_ADDRIPV4.format('IF1', 0, '10.1.0.1') + '/config/prefix_length', 24 )) + +DEVICE_CONFIG_IF2 = [] +DEVICE_CONFIG_IF2.append((PATH_IF .format('IF2' ) + '/config/name', "IF2" )) +DEVICE_CONFIG_IF2.append((PATH_IF .format('IF2' ) + '/config/enabled', True )) +DEVICE_CONFIG_IF2.append((PATH_SUBIF .format('IF2', 0 ) + '/config/index', 0 )) +DEVICE_CONFIG_IF2.append((PATH_ADDRIPV4.format('IF2', 0, '10.2.0.1') + '/config/ip', "10.2.0.1")) +DEVICE_CONFIG_IF2.append((PATH_ADDRIPV4.format('IF2', 0, '10.2.0.1') + '/config/prefix_length', 24 )) + +PATH_IF_TX_PKTS = PATH_IF + 'state/tx_packets_per_second' +PATH_IF_RX_PKTS = PATH_IF + 'state/rx_packets_per_second' + +DEVICE_STATE_IF1_TX_PKTS = PATH_IF_TX_PKTS.format('IF1') +DEVICE_STATE_IF1_RX_PKTS = PATH_IF_RX_PKTS.format('IF1') +DEVICE_STATE_IF2_TX_PKTS = PATH_IF_TX_PKTS.format('IF2') +DEVICE_STATE_IF2_RX_PKTS = PATH_IF_RX_PKTS.format('IF2') + +@pytest.fixture(scope='session') +def device_driverapi_emulated(): + _driver = EmulatedDriver('127.0.0.1', 0) + _driver.Connect() + yield _driver + _driver.Disconnect() + +def test_device_driverapi_emulated_setconfig(device_driverapi_emulated : EmulatedDriver): + # should work + results = device_driverapi_emulated.SetConfig(DEVICE_CONFIG_IF1) + LOGGER.info('results:\n{}'.format('\n'.join(map(str, results)))) + assert len(results) == len(DEVICE_CONFIG_IF1) + for result in results: assert isinstance(result, bool) and result + + results = device_driverapi_emulated.SetConfig(DEVICE_CONFIG_IF2) + LOGGER.info('results:\n{}'.format('\n'.join(map(str, results)))) + assert len(results) == len(DEVICE_CONFIG_IF2) + for result in results: assert isinstance(result, bool) and result + +def test_device_driverapi_emulated_getconfig(device_driverapi_emulated : EmulatedDriver): + stored_config = device_driverapi_emulated.GetConfig() + LOGGER.info('stored_config:\n{}'.format('\n'.join(map(str, stored_config)))) + assert len(stored_config) == len(DEVICE_CONFIG_IF1) + len(DEVICE_CONFIG_IF2) + for config_row in stored_config: assert (config_row in DEVICE_CONFIG_IF1) or (config_row in DEVICE_CONFIG_IF2) + for config_row in DEVICE_CONFIG_IF1: assert config_row in stored_config + for config_row in DEVICE_CONFIG_IF2: assert config_row in stored_config + + # should work + stored_config = device_driverapi_emulated.GetConfig([PATH_IF.format('IF2')]) + LOGGER.info('stored_config:\n{}'.format('\n'.join(map(str, stored_config)))) + assert len(stored_config) == 1 + stored_config = stored_config[0] + LOGGER.info('stored_config[0]:\n{}'.format('\n'.join(map(str, stored_config)))) + assert len(stored_config) == len(DEVICE_CONFIG_IF2) + for config_row in stored_config: assert config_row in DEVICE_CONFIG_IF2 + for config_row in DEVICE_CONFIG_IF2: assert config_row in stored_config + +def test_device_driverapi_emulated_deleteconfig(device_driverapi_emulated : EmulatedDriver): + # should work + results = device_driverapi_emulated.DeleteConfig([PATH_ADDRIPV4.format('IF2', 0, '10.2.0.1')]) + LOGGER.info('results:\n{}'.format('\n'.join(map(str, results)))) + assert (len(results) == 1) and isinstance(results[0], bool) and results[0] + + stored_config = device_driverapi_emulated.GetConfig() + LOGGER.info('stored_config:\n{}'.format('\n'.join(map(str, stored_config)))) + + device_config_if2 = list(filter(lambda row: '10.2.0.1' not in row[0], copy.deepcopy(DEVICE_CONFIG_IF2))) + assert len(stored_config) == len(DEVICE_CONFIG_IF1) + len(device_config_if2) + for config_row in stored_config: assert (config_row in DEVICE_CONFIG_IF1) or (config_row in device_config_if2) + for config_row in DEVICE_CONFIG_IF1: assert config_row in stored_config + for config_row in device_config_if2: assert config_row in stored_config + +def test_device_driverapi_emulated_subscriptions(device_driverapi_emulated : EmulatedDriver): + # should work + duration = 10.0 + interval = 1.5 + results = device_driverapi_emulated.SubscribeState([ + (DEVICE_STATE_IF1_TX_PKTS, duration, interval), + (DEVICE_STATE_IF1_RX_PKTS, duration, interval), + (DEVICE_STATE_IF2_TX_PKTS, duration, interval), + (DEVICE_STATE_IF2_RX_PKTS, duration, interval), + ]) + LOGGER.info('results:\n{}'.format('\n'.join(map(str, results)))) + assert len(results) == 4 + for result in results: assert isinstance(result, bool) and result + + stored_config = device_driverapi_emulated.GetConfig() + LOGGER.info('stored_config:\n{}'.format('\n'.join(map(str, stored_config)))) + + time.sleep(duration + 1.0) # let time to generate samples, plus 1 second extra time + + samples = [] + for sample in device_driverapi_emulated.GetState(blocking=False): + LOGGER.info('sample: {}'.format(str(sample))) + timestamp,resource_key,resource_value = sample + samples.append((timestamp, resource_key, resource_value)) + LOGGER.info('samples:\n{}'.format('\n'.join(map(str, samples)))) + assert len(samples) == 4 * (math.floor(duration/interval) + 1) + + results = device_driverapi_emulated.UnsubscribeState([ + (DEVICE_STATE_IF1_TX_PKTS, 10.0, 1.5), + (DEVICE_STATE_IF1_RX_PKTS, 10.0, 1.5), + (DEVICE_STATE_IF2_TX_PKTS, 10.0, 1.5), + (DEVICE_STATE_IF2_RX_PKTS, 10.0, 1.5), + ]) + LOGGER.info('results:\n{}'.format('\n'.join(map(str, results)))) + assert len(results) == 4 + for result in results: assert isinstance(result, bool) and result + + stored_config = device_driverapi_emulated.GetConfig() + LOGGER.info('stored_config:\n{}'.format('\n'.join(map(str, stored_config)))) + device_config_if2 = list(filter(lambda row: '10.2.0.1' not in row[0], copy.deepcopy(DEVICE_CONFIG_IF2))) + assert len(stored_config) == len(DEVICE_CONFIG_IF1) + len(device_config_if2) + for config_row in stored_config: assert (config_row in DEVICE_CONFIG_IF1) or (config_row in device_config_if2) + for config_row in DEVICE_CONFIG_IF1: assert config_row in stored_config + for config_row in device_config_if2: assert config_row in stored_config diff --git a/src/device/tests/test_unitary.py b/src/device/tests/test_unitary_service.py similarity index 79% rename from src/device/tests/test_unitary.py rename to src/device/tests/test_unitary_service.py index 95eb0a1af27433ddd85d100160cd122aebc60b8a..8d9591dd6492395a44adc25e4a54eaebc8ff9121 100644 --- a/src/device/tests/test_unitary.py +++ b/src/device/tests/test_unitary_service.py @@ -65,7 +65,9 @@ def test_add_device_wrong_attributes(device_client : DeviceClient): copy_device['device_id']['device_id']['uuid'] = '' device_client.AddDevice(Device(**copy_device)) assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT - assert e.value.details() == 'device.device_id.device_id.uuid() string is empty.' + msg = 'device.device_id.device_id.uuid() is out of range: '\ + 'allow_empty(False) min_length(None) max_length(None) allowed_lengths(None).' + assert e.value.details() == msg # should fail with device type is empty with pytest.raises(grpc._channel._InactiveRpcError) as e: @@ -73,7 +75,9 @@ def test_add_device_wrong_attributes(device_client : DeviceClient): copy_device['device_type'] = '' device_client.AddDevice(Device(**copy_device)) assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT - assert e.value.details() == 'device.device_type() string is empty.' + msg = 'device.device_type() is out of range: '\ + 'allow_empty(False) min_length(None) max_length(None) allowed_lengths(None).' + assert e.value.details() == msg # should fail with wrong device operational status with pytest.raises(grpc._channel._InactiveRpcError) as e: @@ -81,10 +85,8 @@ def test_add_device_wrong_attributes(device_client : DeviceClient): copy_device['devOperationalStatus'] = OperationalStatus.KEEP_STATE.value device_client.AddDevice(Device(**copy_device)) assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT - msg = ' '.join([ - 'Method(AddDevice) does not accept OperationalStatus(KEEP_STATE).', - 'Permitted values for Method(AddDevice) are OperationalStatus([\'DISABLED\', \'ENABLED\']).', - ]) + msg = 'Method(AddDevice) does not accept OperationalStatus(KEEP_STATE). '\ + 'Permitted values for Method(AddDevice) are OperationalStatus([\'DISABLED\', \'ENABLED\']).' assert e.value.details() == msg def test_add_device_wrong_endpoint(device_client : DeviceClient): @@ -95,11 +97,9 @@ def test_add_device_wrong_endpoint(device_client : DeviceClient): request = Device(**copy_device) device_client.AddDevice(request) assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT - msg = ' '.join([ - 'Context(wrong-context)', - 'in Endpoint(#0) of Context(admin)/Topology(admin)/Device(DEV1) mismatches acceptable Contexts({\'admin\'}).', - 'Optionally, leave field empty to use predefined Context(admin).', - ]) + msg = 'Context(wrong-context) in Endpoint(#0) of '\ + 'Context(admin)/Topology(admin)/Device(DEV1) mismatches acceptable Contexts({\'admin\'}). '\ + 'Optionally, leave field empty to use predefined Context(admin).' assert e.value.details() == msg # should fail with unsupported endpoint topology @@ -108,11 +108,9 @@ def test_add_device_wrong_endpoint(device_client : DeviceClient): copy_device['endpointList'][0]['port_id']['topoId']['topoId']['uuid'] = 'wrong-topo' device_client.AddDevice(Device(**copy_device)) assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT - msg = ' '.join([ - 'Context(admin)/Topology(wrong-topo)', - 'in Endpoint(#0) of Context(admin)/Topology(admin)/Device(DEV1) mismatches acceptable Topologies({\'admin\'}).', - 'Optionally, leave field empty to use predefined Topology(admin).', - ]) + msg = 'Context(admin)/Topology(wrong-topo) in Endpoint(#0) of '\ + 'Context(admin)/Topology(admin)/Device(DEV1) mismatches acceptable Topologies({\'admin\'}). '\ + 'Optionally, leave field empty to use predefined Topology(admin).' assert e.value.details() == msg # should fail with wrong endpoint device @@ -121,11 +119,9 @@ def test_add_device_wrong_endpoint(device_client : DeviceClient): copy_device['endpointList'][0]['port_id']['dev_id']['device_id']['uuid'] = 'wrong-device' device_client.AddDevice(Device(**copy_device)) assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT - msg = ' '.join([ - 'Context(admin)/Topology(admin)/Device(wrong-device)', - 'in Endpoint(#0) of Context(admin)/Topology(admin)/Device(DEV1) mismatches acceptable Devices({\'DEV1\'}).', - 'Optionally, leave field empty to use predefined Device(DEV1).', - ]) + msg = 'Context(admin)/Topology(admin)/Device(wrong-device) in Endpoint(#0) of '\ + 'Context(admin)/Topology(admin)/Device(DEV1) mismatches acceptable Devices({\'DEV1\'}). '\ + 'Optionally, leave field empty to use predefined Device(DEV1).' assert e.value.details() == msg # should fail with endpoint port uuid is empty @@ -134,7 +130,9 @@ def test_add_device_wrong_endpoint(device_client : DeviceClient): copy_device['endpointList'][0]['port_id']['port_id']['uuid'] = '' device_client.AddDevice(Device(**copy_device)) assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT - assert e.value.details() == 'endpoint_id[#0].port_id.uuid() string is empty.' + msg = 'endpoint_id[#0].port_id.uuid() is out of range: '\ + 'allow_empty(False) min_length(None) max_length(None) allowed_lengths(None).' + assert e.value.details() == msg # should fail with endpoint port type is empty with pytest.raises(grpc._channel._InactiveRpcError) as e: @@ -142,7 +140,9 @@ def test_add_device_wrong_endpoint(device_client : DeviceClient): copy_device['endpointList'][0]['port_type'] = '' device_client.AddDevice(Device(**copy_device)) assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT - assert e.value.details() == 'endpoint[#0].port_type() string is empty.' + msg = 'endpoint[#0].port_type() is out of range: '\ + 'allow_empty(False) min_length(None) max_length(None) allowed_lengths(None).' + assert e.value.details() == msg # should fail with duplicate port in device with pytest.raises(grpc._channel._InactiveRpcError) as e: @@ -150,10 +150,8 @@ def test_add_device_wrong_endpoint(device_client : DeviceClient): copy_device['endpointList'][1]['port_id']['port_id']['uuid'] = 'EP2' device_client.AddDevice(Device(**copy_device)) assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT - msg = ' '.join([ - 'Duplicated Context(admin)/Topology(admin)/Device(DEV1)/Port(EP2)', - 'in Endpoint(#1) of Context(admin)/Topology(admin)/Device(DEV1).', - ]) + msg = 'Duplicated Context(admin)/Topology(admin)/Device(DEV1)/Port(EP2) in Endpoint(#1) of '\ + 'Context(admin)/Topology(admin)/Device(DEV1).' assert e.value.details() == msg def test_add_device(device_client : DeviceClient): @@ -168,7 +166,8 @@ def test_add_device_duplicate(device_client : DeviceClient): with pytest.raises(grpc._channel._InactiveRpcError) as e: device_client.AddDevice(Device(**DEVICE)) assert e.value.code() == grpc.StatusCode.ALREADY_EXISTS - assert e.value.details() == 'Context(admin)/Topology(admin)/Device(DEV1) already exists in the database.' + msg = 'Context(admin)/Topology(admin)/Device(DEV1) already exists in the database.' + assert e.value.details() == msg def test_delete_device_empty_uuid(device_client : DeviceClient): # should fail with device uuid is empty @@ -177,7 +176,9 @@ def test_delete_device_empty_uuid(device_client : DeviceClient): copy_device_id['device_id']['uuid'] = '' device_client.DeleteDevice(DeviceId(**copy_device_id)) assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT - assert e.value.details() == 'device_id.device_id.uuid() string is empty.' + msg = 'device_id.device_id.uuid() is out of range: '\ + 'allow_empty(False) min_length(None) max_length(None) allowed_lengths(None).' + assert e.value.details() == msg def test_delete_device_not_found(device_client : DeviceClient): # should fail with device not found @@ -186,7 +187,8 @@ def test_delete_device_not_found(device_client : DeviceClient): copy_device_id['device_id']['uuid'] = 'wrong-device-id' device_client.DeleteDevice(DeviceId(**copy_device_id)) assert e.value.code() == grpc.StatusCode.NOT_FOUND - assert e.value.details() == 'Context(admin)/Topology(admin)/Device(wrong-device-id) does not exist in the database.' + msg = 'Context(admin)/Topology(admin)/Device(wrong-device-id) does not exist in the database.' + assert e.value.details() == msg def test_delete_device(device_client : DeviceClient): # should work @@ -202,7 +204,9 @@ def test_configure_device_empty_device_uuid(device_client : DeviceClient): copy_device['device_id']['device_id']['uuid'] = '' device_client.ConfigureDevice(Device(**copy_device)) assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT - assert e.value.details() == 'device.device_id.device_id.uuid() string is empty.' + msg = 'device.device_id.device_id.uuid() is out of range: '\ + 'allow_empty(False) min_length(None) max_length(None) allowed_lengths(None).' + assert e.value.details() == msg def test_configure_device_not_found(device_client : DeviceClient): # should fail with device not found @@ -211,7 +215,8 @@ def test_configure_device_not_found(device_client : DeviceClient): copy_device['device_id']['device_id']['uuid'] = 'wrong-device-id' device_client.ConfigureDevice(Device(**copy_device)) assert e.value.code() == grpc.StatusCode.NOT_FOUND - assert e.value.details() == 'Context(admin)/Topology(admin)/Device(wrong-device-id) does not exist in the database.' + msg = 'Context(admin)/Topology(admin)/Device(wrong-device-id) does not exist in the database.' + assert e.value.details() == msg def test_add_device_default_endpoint_context_topology_device(device_client : DeviceClient): # should work @@ -231,7 +236,8 @@ def test_configure_device_wrong_attributes(device_client : DeviceClient): copy_device['device_type'] = 'wrong-type' device_client.ConfigureDevice(Device(**copy_device)) assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT - assert e.value.details() == 'Device(DEV1) has Type(ROADM) in the database. Cannot be changed to Type(wrong-type).' + msg = 'Device(DEV1) has Type(ROADM) in the database. Cannot be changed to Type(wrong-type).' + assert e.value.details() == msg # should fail with endpoints cannot be modified with pytest.raises(grpc._channel._InactiveRpcError) as e: @@ -248,10 +254,8 @@ def test_configure_device_wrong_attributes(device_client : DeviceClient): copy_device['endpointList'].clear() device_client.ConfigureDevice(Device(**copy_device)) assert e.value.code() == grpc.StatusCode.ABORTED - msg = ' '.join([ - 'Any change has been requested for Device(DEV1).', - 'Either specify a new configuration or a new device operational status.', - ]) + msg = 'Any change has been requested for Device(DEV1). '\ + 'Either specify a new configuration or a new device operational status.' assert e.value.details() == msg def test_configure_device(device_client : DeviceClient): diff --git a/src/service/tests/test_unitary.py b/src/service/tests/test_unitary.py index 48d56af038b226caa2ffe6a73b71509f4c011ad1..fb7d1465d3308261e0f2bdc5bc534f67d89fae1e 100644 --- a/src/service/tests/test_unitary.py +++ b/src/service/tests/test_unitary.py @@ -71,7 +71,8 @@ def test_create_service_wrong_service_attributes(service_client : ServiceClient) copy_service['cs_id']['contextId']['contextUuid']['uuid'] = '' service_client.CreateService(Service(**copy_service)) assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT - msg = 'service.cs_id.contextId.contextUuid.uuid() string is empty.' + msg = 'service.cs_id.contextId.contextUuid.uuid() is out of range: '\ + 'allow_empty(False) min_length(None) max_length(None) allowed_lengths(None).' assert e.value.details() == msg # should fail with service context does not exist @@ -89,7 +90,8 @@ def test_create_service_wrong_service_attributes(service_client : ServiceClient) copy_service['cs_id']['cs_id']['uuid'] = '' service_client.CreateService(Service(**copy_service)) assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT - msg = 'service.cs_id.cs_id.uuid() string is empty.' + msg = 'service.cs_id.cs_id.uuid() is out of range: '\ + 'allow_empty(False) min_length(None) max_length(None) allowed_lengths(None).' assert e.value.details() == msg # should fail with wrong service type @@ -98,11 +100,9 @@ def test_create_service_wrong_service_attributes(service_client : ServiceClient) copy_service['serviceType'] = ServiceType.UNKNOWN service_client.CreateService(Service(**copy_service)) assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT - msg = ' '.join([ - 'Method(CreateService) does not accept ServiceType(UNKNOWN).', - 'Permitted values for Method(CreateService) are', - 'ServiceType([\'L2NM\', \'L3NM\', \'TAPI_CONNECTIVITY_SERVICE\']).', - ]) + msg = 'Method(CreateService) does not accept ServiceType(UNKNOWN). '\ + 'Permitted values for Method(CreateService) are '\ + 'ServiceType([\'L2NM\', \'L3NM\', \'TAPI_CONNECTIVITY_SERVICE\']).' assert e.value.details() == msg # should fail with wrong service state @@ -111,11 +111,9 @@ def test_create_service_wrong_service_attributes(service_client : ServiceClient) copy_service['serviceState']['serviceState'] = ServiceStateEnum.PENDING_REMOVAL service_client.CreateService(Service(**copy_service)) assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT - msg = ' '.join([ - 'Method(CreateService) does not accept ServiceState(PENDING_REMOVAL).', - 'Permitted values for Method(CreateService) are', - 'ServiceState([\'PLANNED\']).', - ]) + msg = 'Method(CreateService) does not accept ServiceState(PENDING_REMOVAL). '\ + 'Permitted values for Method(CreateService) are '\ + 'ServiceState([\'PLANNED\']).' assert e.value.details() == msg def test_create_service_wrong_constraint(service_client : ServiceClient): @@ -125,7 +123,8 @@ def test_create_service_wrong_constraint(service_client : ServiceClient): copy_service['constraint'][0]['constraint_type'] = '' service_client.CreateService(Service(**copy_service)) assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT - msg = 'constraint[#0].constraint_type() string is empty.' + msg = 'constraint[#0].constraint_type() is out of range: '\ + 'allow_empty(False) min_length(None) max_length(None) allowed_lengths(None).' assert e.value.details() == msg # should fail with wrong constraint value @@ -134,7 +133,8 @@ def test_create_service_wrong_constraint(service_client : ServiceClient): copy_service['constraint'][0]['constraint_value'] = '' service_client.CreateService(Service(**copy_service)) assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT - msg = 'constraint[#0].constraint_value() string is empty.' + msg = 'constraint[#0].constraint_value() is out of range: '\ + 'allow_empty(False) min_length(None) max_length(None) allowed_lengths(None).' assert e.value.details() == msg # should fail with dupplicated constraint type @@ -154,11 +154,9 @@ def test_create_service_wrong_endpoint(service_client : ServiceClient, database print(copy_service) service_client.CreateService(Service(**copy_service)) assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT - msg = ' '.join([ - 'Context(wrong-context) in Endpoint(#0) of Context(admin)/Service(DEV1)', - 'mismatches acceptable Contexts({\'admin\'}).', - 'Optionally, leave field empty to use predefined Context(admin).' - ]) + msg = 'Context(wrong-context) in Endpoint(#0) of '\ + 'Context(admin)/Service(DEV1) mismatches acceptable Contexts({\'admin\'}). '\ + 'Optionally, leave field empty to use predefined Context(admin).' assert e.value.details() == msg # should fail with wrong endpoint topology @@ -167,11 +165,9 @@ def test_create_service_wrong_endpoint(service_client : ServiceClient, database copy_service['endpointList'][0]['topoId']['topoId']['uuid'] = 'wrong-topo' service_client.CreateService(Service(**copy_service)) assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT - msg = ' '.join([ - 'Context(admin)/Topology(wrong-topo) in Endpoint(#0) of Context(admin)/Service(DEV1)', - 'mismatches acceptable Topologies({\'admin\'}).', - 'Optionally, leave field empty to use predefined Topology(admin).', - ]) + msg = 'Context(admin)/Topology(wrong-topo) in Endpoint(#0) of '\ + 'Context(admin)/Service(DEV1) mismatches acceptable Topologies({\'admin\'}). '\ + 'Optionally, leave field empty to use predefined Topology(admin).' assert e.value.details() == msg # should fail with endpoint device is empty @@ -180,7 +176,8 @@ def test_create_service_wrong_endpoint(service_client : ServiceClient, database copy_service['endpointList'][0]['dev_id']['device_id']['uuid'] = '' service_client.CreateService(Service(**copy_service)) assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT - msg = 'endpoint_id[#0].dev_id.device_id.uuid() string is empty.' + msg = 'endpoint_id[#0].dev_id.device_id.uuid() is out of range: '\ + 'allow_empty(False) min_length(None) max_length(None) allowed_lengths(None).' assert e.value.details() == msg # should fail with endpoint device not found @@ -189,10 +186,8 @@ def test_create_service_wrong_endpoint(service_client : ServiceClient, database copy_service['endpointList'][0]['dev_id']['device_id']['uuid'] = 'wrong-device' service_client.CreateService(Service(**copy_service)) assert e.value.code() == grpc.StatusCode.NOT_FOUND - msg = ' '.join([ - 'Context(admin)/Topology(admin)/Device(wrong-device) in Endpoint(#0)', - 'of Context(admin)/Service(DEV1) does not exist in the database.', - ]) + msg = 'Context(admin)/Topology(admin)/Device(wrong-device) in Endpoint(#0) of '\ + 'Context(admin)/Service(DEV1) does not exist in the database.' assert e.value.details() == msg # should fail with endpoint device duplicated @@ -201,7 +196,8 @@ def test_create_service_wrong_endpoint(service_client : ServiceClient, database copy_service['endpointList'][1] = copy_service['endpointList'][0] service_client.CreateService(Service(**copy_service)) assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT - msg = 'Duplicated Context(admin)/Topology(admin)/Device(DEV1) in Endpoint(#1) of Context(admin)/Service(DEV1).' + msg = 'Duplicated Context(admin)/Topology(admin)/Device(DEV1) in Endpoint(#1) of '\ + 'Context(admin)/Service(DEV1).' assert e.value.details() == msg # should fail with endpoint port is empty @@ -210,7 +206,8 @@ def test_create_service_wrong_endpoint(service_client : ServiceClient, database copy_service['endpointList'][0]['port_id']['uuid'] = '' service_client.CreateService(Service(**copy_service)) assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT - msg = 'endpoint_id[#0].port_id.uuid() string is empty.' + msg = 'endpoint_id[#0].port_id.uuid() is out of range: '\ + 'allow_empty(False) min_length(None) max_length(None) allowed_lengths(None).' assert e.value.details() == msg # should fail with endpoint port not found @@ -219,10 +216,8 @@ def test_create_service_wrong_endpoint(service_client : ServiceClient, database copy_service['endpointList'][0]['port_id']['uuid'] = 'wrong-port' service_client.CreateService(Service(**copy_service)) assert e.value.code() == grpc.StatusCode.NOT_FOUND - msg = ' '.join([ - 'Context(admin)/Topology(admin)/Device(DEV1)/Port(wrong-port) in Endpoint(#0)', - 'of Context(admin)/Service(DEV1) does not exist in the database.', - ]) + msg = 'Context(admin)/Topology(admin)/Device(DEV1)/Port(wrong-port) in Endpoint(#0) of '\ + 'Context(admin)/Service(DEV1) does not exist in the database.' assert e.value.details() == msg def test_get_service_does_not_exist(service_client : ServiceClient): @@ -305,7 +300,8 @@ def test_delete_service_wrong_service_id(service_client : ServiceClient): copy_service_id['contextId']['contextUuid']['uuid'] = '' service_client.DeleteService(ServiceId(**copy_service_id)) assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT - msg = 'service_id.contextId.contextUuid.uuid() string is empty.' + msg = 'service_id.contextId.contextUuid.uuid() is out of range: '\ + 'allow_empty(False) min_length(None) max_length(None) allowed_lengths(None).' assert e.value.details() == msg # should fail with service context does not exist @@ -323,7 +319,8 @@ def test_delete_service_wrong_service_id(service_client : ServiceClient): copy_service_id['cs_id']['uuid'] = '' service_client.DeleteService(ServiceId(**copy_service_id)) assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT - msg = 'service_id.cs_id.uuid() string is empty.' + msg = 'service_id.cs_id.uuid() is out of range: '\ + 'allow_empty(False) min_length(None) max_length(None) allowed_lengths(None).' assert e.value.details() == msg # should fail with service id is empty diff --git a/src/tester_integration/.gitlab-ci.yml b/src/tester_integration/.gitlab-ci.yml index cf868b1a40c583481b292dfe2445b3e0fcfe4501..02eccbc9bf9fc1103b373b4d062333c42f52b87e 100644 --- a/src/tester_integration/.gitlab-ci.yml +++ b/src/tester_integration/.gitlab-ci.yml @@ -54,6 +54,7 @@ integ_test execute: - unit_test device - unit_test service - unit_test integ_test + - dependencies all before_script: - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY - if docker network list | grep teraflowbridge; then echo "teraflowbridge is already created"; else docker network create -d bridge teraflowbridge; fi