diff --git a/report_coverage.sh b/report_coverage_all.sh similarity index 100% rename from report_coverage.sh rename to report_coverage_all.sh diff --git a/report_coverage_context.sh b/report_coverage_context.sh index 15a7712a0724bd0b86f7ccd2a3490abe85994f05..f2f71fa744b5d8209589b283c7a375b4f25be0c8 100755 --- a/report_coverage_context.sh +++ b/report_coverage_context.sh @@ -1,3 +1,3 @@ #!/bin/bash -./report_coverage.sh | grep --color -E -i "^.*context.*$|$" +./report_coverage_all.sh | grep --color -E -i "^.*context.*$|$" diff --git a/report_coverage_device.sh b/report_coverage_device.sh index f884fb1c7806069412e29fcb11ba278974520c35..b4215cd30141bb524a7d99717841de127d7cda15 100755 --- a/report_coverage_device.sh +++ b/report_coverage_device.sh @@ -1,3 +1,3 @@ #!/bin/bash -./report_coverage.sh | grep --color -E -i "^.*device.*$|$" +./report_coverage_all.sh | grep --color -E -i "^.*device.*$|$" diff --git a/run_unitary_tests.sh b/run_unitary_tests.sh index 2f18b7285c44456a400c9b229c2e4b442256af86..84b8010341b9fa70275cd2c1039c8c0cca3d2fdc 100755 --- a/run_unitary_tests.sh +++ b/run_unitary_tests.sh @@ -5,7 +5,8 @@ RCFILE=~/teraflow/controller/coverage/.coveragerc # Run unitary tests and analyze coverage of code at same time coverage run --rcfile=$RCFILE -m pytest --log-level=DEBUG --verbose \ - common/database/tests/test_unitary_inmemory.py \ + common/database/tests/test_unitary.py \ + common/database/tests/test_engine_inmemory.py \ context/tests/test_unitary.py \ device/tests/test_unitary.py diff --git a/src/common/database/api/Database.py b/src/common/database/api/Database.py index 3ce5d8dfbaf7a46e77ce14a5e2165e68023d067e..c3aeaf628339f8ba58e3c616b7eb6a501cad9278 100644 --- a/src/common/database/api/Database.py +++ b/src/common/database/api/Database.py @@ -2,20 +2,21 @@ import logging from typing import List from ..engines._DatabaseEngine import _DatabaseEngine from .context.Context import Context +from .Exceptions import WrongDatabaseEngine, MutexException LOGGER = logging.getLogger(__name__) class Database: def __init__(self, database_engine : _DatabaseEngine): - if not isinstance(database_engine, _DatabaseEngine): - raise Exception('database_engine must inherit from _DatabaseEngine') + if not isinstance(database_engine, _DatabaseEngine): + raise WrongDatabaseEngine('database_engine must inherit from _DatabaseEngine') self._database_engine = database_engine self._acquired = False self._owner_key = None def __enter__(self) -> '_DatabaseEngine': self._acquired, self._owner_key = self._database_engine.lock() - if not self._acquired: raise Exception('Unable to acquire database lock') + if not self._acquired: raise MutexException('Unable to acquire database lock') return self def __exit__(self, exc_type, exc_val, exc_tb): diff --git a/src/common/database/api/Exceptions.py b/src/common/database/api/Exceptions.py new file mode 100644 index 0000000000000000000000000000000000000000..ef60ac5f5ab9c3ec3de6cbf4c1af54dcc4389470 --- /dev/null +++ b/src/common/database/api/Exceptions.py @@ -0,0 +1,5 @@ +class WrongDatabaseEngine(Exception): + pass + +class MutexException(Exception): + pass diff --git a/src/common/database/api/context/Context.py b/src/common/database/api/context/Context.py index f913becbf01f6efb04c77bf4300db0b7c1a206bc..32991cc5ad2b29fe8492d42539edb7cccad7c5f1 100644 --- a/src/common/database/api/context/Context.py +++ b/src/common/database/api/context/Context.py @@ -1,23 +1,18 @@ from typing import Dict from ...engines._DatabaseEngine import _DatabaseEngine -from ..entity._Entity import _Entity -from ..entity.EntityAttributes import EntityAttributes +from ..entity._RootEntity import _RootEntity from ..entity.EntityCollection import EntityCollection from .Keys import KEY_CONTEXT, KEY_TOPOLOGIES from .Topology import Topology -VALIDATORS = {} +VALIDATORS = {} # no attributes accepted +TRANSCODERS = {} # no transcoding applied to attributes -class Context(_Entity): +class Context(_RootEntity): def __init__(self, context_uuid : str, database_engine : _DatabaseEngine): - self._database_engine = database_engine - super().__init__(context_uuid, parent=self) - self._attributes = EntityAttributes(self, KEY_CONTEXT, validators=VALIDATORS) + super().__init__(database_engine, context_uuid, KEY_CONTEXT, VALIDATORS, TRANSCODERS) self._topologies = EntityCollection(self, KEY_TOPOLOGIES) - @property - def database_engine(self) -> _DatabaseEngine: return self._database_engine - @property def parent(self) -> 'Context': return self @@ -27,9 +22,6 @@ class Context(_Entity): @property def context_uuid(self) -> str: return self._entity_uuid - @property - def attributes(self) -> EntityAttributes: return self._attributes - @property def topologies(self) -> EntityCollection: return self._topologies diff --git a/src/common/database/api/context/Device.py b/src/common/database/api/context/Device.py index 54ba02f6bbd385916c36410928eae7f35dba5653..fb4b5becb6de1f158447b3e7630ff6f87fbbdf7d 100644 --- a/src/common/database/api/context/Device.py +++ b/src/common/database/api/context/Device.py @@ -1,7 +1,6 @@ from __future__ import annotations from typing import TYPE_CHECKING, Dict from ..entity._Entity import _Entity -from ..entity.EntityAttributes import EntityAttributes from ..entity.EntityCollection import EntityCollection from .Endpoint import Endpoint from .Keys import KEY_DEVICE, KEY_DEVICE_ENDPOINTS @@ -12,9 +11,9 @@ if TYPE_CHECKING: from .Topology import Topology VALIDATORS = { - 'device_type': lambda v: v is None or isinstance(v, str), - 'device_config': lambda v: v is None or isinstance(v, str), - 'device_operational_status': lambda v: v is None or isinstance(v, OperationalStatus), + 'device_type': lambda v: v is not None and isinstance(v, str) and (len(v) > 0), + 'device_config': lambda v: v is not None and isinstance(v, str) and (len(v) > 0), + 'device_operational_status': lambda v: v is not None and isinstance(v, OperationalStatus), } TRANSCODERS = { @@ -27,8 +26,7 @@ TRANSCODERS = { class Device(_Entity): def __init__(self, device_uuid : str, parent : 'Topology'): - super().__init__(device_uuid, parent=parent) - self._attributes = EntityAttributes(self, KEY_DEVICE, VALIDATORS, transcoders=TRANSCODERS) + super().__init__(parent, device_uuid, KEY_DEVICE, VALIDATORS, TRANSCODERS) self._endpoints = EntityCollection(self, KEY_DEVICE_ENDPOINTS) @property @@ -49,9 +47,6 @@ class Device(_Entity): @property def device_uuid(self) -> str: return self._entity_uuid - @property - def attributes(self) -> EntityAttributes: return self._attributes - @property def endpoints(self) -> EntityCollection: return self._endpoints diff --git a/src/common/database/api/context/Endpoint.py b/src/common/database/api/context/Endpoint.py index c9364847c86a281bfe4c90ebcf8fdbdf60f30ace..413a680a8e8cdb13e8df120a2514c46497d7a071 100644 --- a/src/common/database/api/context/Endpoint.py +++ b/src/common/database/api/context/Endpoint.py @@ -1,7 +1,6 @@ from __future__ import annotations from typing import TYPE_CHECKING, Dict from ..entity._Entity import _Entity -from ..entity.EntityAttributes import EntityAttributes from .Keys import KEY_ENDPOINT if TYPE_CHECKING: @@ -10,13 +9,14 @@ if TYPE_CHECKING: from .Device import Device VALIDATORS = { - 'port_type': lambda v: v is None or isinstance(v, str), + 'port_type': lambda v: v is not None and isinstance(v, str) and (len(v) > 0), } +TRANSCODERS = {} # no transcoding applied to attributes + class Endpoint(_Entity): def __init__(self, endpoint_uuid : str, parent : 'Device'): - super().__init__(endpoint_uuid, parent=parent) - self._attributes = EntityAttributes(self, KEY_ENDPOINT, VALIDATORS) + super().__init__(parent, endpoint_uuid, KEY_ENDPOINT, VALIDATORS, TRANSCODERS) @property def parent(self) -> 'Device': return self._parent @@ -42,9 +42,6 @@ class Endpoint(_Entity): @property def endpoint_uuid(self) -> str: return self._entity_uuid - @property - def attributes(self) -> EntityAttributes: return self._attributes - def create(self, port_type : str) -> 'Endpoint': self.update(update_attributes={ 'port_type': port_type diff --git a/src/common/database/api/context/Link.py b/src/common/database/api/context/Link.py index 8dfffe3d29c63718bd75a15ede52112bea6497d5..bf661dbb2897822a45071c157619b97c1ebca1d9 100644 --- a/src/common/database/api/context/Link.py +++ b/src/common/database/api/context/Link.py @@ -3,15 +3,18 @@ from typing import TYPE_CHECKING, Dict from ..entity._Entity import _Entity from ..entity.EntityCollection import EntityCollection from .LinkEndpoint import LinkEndpoint -from .Keys import KEY_LINK_ENDPOINTS +from .Keys import KEY_LINK, KEY_LINK_ENDPOINTS if TYPE_CHECKING: from .Context import Context from .Topology import Topology +VALIDATORS = {} # no attributes accepted +TRANSCODERS = {} # no transcoding applied to attributes + class Link(_Entity): def __init__(self, link_uuid : str, parent : 'Topology'): - super().__init__(link_uuid, parent=parent) + super().__init__(parent, link_uuid, KEY_LINK, VALIDATORS, TRANSCODERS) self._endpoints = EntityCollection(self, KEY_LINK_ENDPOINTS) @property @@ -43,6 +46,7 @@ class Link(_Entity): def delete(self) -> None: for endpoint_uuid in self.endpoints.get(): self.endpoint(endpoint_uuid).delete() + self.attributes.delete() self.parent.links.delete(self.link_uuid) def dump_id(self) -> Dict: diff --git a/src/common/database/api/context/LinkEndpoint.py b/src/common/database/api/context/LinkEndpoint.py index 741946ba36428930dc0408694b6e52f416065c57..4acb62fdb36fe78f65710d361370f41962312859 100644 --- a/src/common/database/api/context/LinkEndpoint.py +++ b/src/common/database/api/context/LinkEndpoint.py @@ -1,7 +1,6 @@ from __future__ import annotations from typing import TYPE_CHECKING, Dict from ..entity._Entity import _Entity -from ..entity.EntityAttributes import EntityAttributes from .Endpoint import Endpoint from .Keys import KEY_LINK_ENDPOINT @@ -11,14 +10,15 @@ if TYPE_CHECKING: from .Link import Link VALIDATORS = { - 'device_uuid': lambda v: v is None or isinstance(v, str), - 'endpoint_uuid': lambda v: v is None or isinstance(v, str), + 'device_uuid': lambda v: v is not None and isinstance(v, str) and (len(v) > 0), + 'endpoint_uuid': lambda v: v is not None and isinstance(v, str) and (len(v) > 0), } +TRANSCODERS = {} # no transcoding applied to attributes + class LinkEndpoint(_Entity): def __init__(self, link_endpoint_uuid : str, parent : 'Link'): - super().__init__(link_endpoint_uuid, parent=parent) - self._attributes = EntityAttributes(self, KEY_LINK_ENDPOINT, VALIDATORS) + super().__init__(parent, link_endpoint_uuid, KEY_LINK_ENDPOINT, VALIDATORS, TRANSCODERS) @property def parent(self) -> 'Link': return self._parent @@ -44,9 +44,6 @@ class LinkEndpoint(_Entity): @property def link_endpoint_uuid(self) -> str: return self._entity_uuid - @property - def attributes(self) -> EntityAttributes: return self._attributes - def create(self, endpoint : Endpoint) -> 'LinkEndpoint': self.update(update_attributes={ 'device_uuid': endpoint.device_uuid, diff --git a/src/common/database/api/context/Topology.py b/src/common/database/api/context/Topology.py index 82680f8b20f908a403c07ae09e95e19cd00220bc..2fc36ed3c46a64dfb92337c2f62608529d2e65d9 100644 --- a/src/common/database/api/context/Topology.py +++ b/src/common/database/api/context/Topology.py @@ -1,7 +1,6 @@ from __future__ import annotations from typing import TYPE_CHECKING, Dict from ..entity._Entity import _Entity -from ..entity.EntityAttributes import EntityAttributes from ..entity.EntityCollection import EntityCollection from .Keys import KEY_TOPOLOGY, KEY_DEVICES, KEY_LINKS from .Device import Device @@ -10,12 +9,12 @@ from .Link import Link if TYPE_CHECKING: from .Context import Context -VALIDATORS = {} +VALIDATORS = {} # no attributes accepted +TRANSCODERS = {} # no transcoding applied to attributes class Topology(_Entity): def __init__(self, topology_uuid : str, parent : 'Context'): - super().__init__(topology_uuid, parent=parent) - self._attributes = EntityAttributes(self, KEY_TOPOLOGY, validators=VALIDATORS) + super().__init__(parent, topology_uuid, KEY_TOPOLOGY, VALIDATORS, TRANSCODERS) self._devices = EntityCollection(self, KEY_DEVICES) self._links = EntityCollection(self, KEY_LINKS) @@ -31,17 +30,14 @@ class Topology(_Entity): @property def topology_uuid(self) -> str: return self._entity_uuid - @property - def attributes(self) -> EntityAttributes: return self._attributes - @property def devices(self) -> EntityCollection: return self._devices - def device(self, device_uuid : str) -> Device: return Device(device_uuid, self) - @property def links(self) -> EntityCollection: return self._links + def device(self, device_uuid : str) -> Device: return Device(device_uuid, self) + def link(self, link_uuid : str) -> Link: return Link(link_uuid, self) def create(self) -> 'Topology': diff --git a/src/common/database/api/entity/EntityAttributes.py b/src/common/database/api/entity/EntityAttributes.py index 244b43bc711cfa67067de47866cee302cfc9138e..47642b823de3164db18667aa474a95aed84e730c 100644 --- a/src/common/database/api/entity/EntityAttributes.py +++ b/src/common/database/api/entity/EntityAttributes.py @@ -8,7 +8,7 @@ if TYPE_CHECKING: from ._Entity import _Entity class EntityAttributes: - def __init__(self, parent : '_Entity', entity_key : str, validators : Dict, transcoders : Dict={}): + def __init__(self, parent : '_Entity', entity_key : str, validators : Dict, transcoders : Dict = {}): self._parent = parent self._database_engine : _DatabaseEngine = self._parent.database_engine self._entity_key = format_key(entity_key, self._parent) @@ -18,6 +18,7 @@ class EntityAttributes: def validate(self, update_attributes, remove_attributes, attribute_name): remove_attributes.discard(attribute_name) value = update_attributes.pop(attribute_name, None) + if value is None: return validator = self._validators.get(attribute_name) if validator is None: return if not validator(value): raise AttributeError('{} is invalid'.format(attribute_name)) diff --git a/src/common/database/api/entity/_Entity.py b/src/common/database/api/entity/_Entity.py index 2d8fbdfcd0500648eec80ba0ad20a20e489ff654..9d0b3dfb05f58e9af1a595e7527f3e48b5bb7a5d 100644 --- a/src/common/database/api/entity/_Entity.py +++ b/src/common/database/api/entity/_Entity.py @@ -1,19 +1,35 @@ -from typing import Dict +from typing import Any, Callable, Dict +from ...engines._DatabaseEngine import _DatabaseEngine +from .EntityAttributes import EntityAttributes class _Entity: - def __init__(self, entity_uuid : str, parent=None): - if entity_uuid is None: - raise AttributeError('entity_uuid is None') - if (parent is None) or (not isinstance(parent, _Entity)): + def __init__(self, parent, entity_uuid : str, attributes_key : str, + attribute_validators : Dict[str, Callable[[Any], bool]], + attribute_transcoders : Dict[str, Dict[Any, Callable[[Any], Any]]]): + if not isinstance(parent, _Entity): raise AttributeError('parent must be an instance of _Entity') + if (not isinstance(entity_uuid, str)) or (len(entity_uuid) == 0): + raise AttributeError('entity_uuid must be a non-empty instance of str') + if (not isinstance(attributes_key, str)) or (len(attributes_key) == 0): + raise AttributeError('attributes_key must be a non-empty instance of str') + if not isinstance(attribute_validators, dict): + raise AttributeError('attribute_validators must be an instance of dict') + if not isinstance(attribute_transcoders, dict): + raise AttributeError('attribute_transcoders must be an instance of dict') + self._entity_uuid = entity_uuid self._parent = parent + self._attributes = EntityAttributes(self, attributes_key, attribute_validators, + transcoders=attribute_transcoders) @property def parent(self) -> '_Entity': return self._parent @property - def database_engine(self) -> object: return self._parent.database_engine + def database_engine(self) -> _DatabaseEngine: return self._parent.database_engine + + @property + def attributes(self) -> EntityAttributes: return self._attributes def load(self): raise NotImplementedError() diff --git a/src/common/database/api/entity/_RootEntity.py b/src/common/database/api/entity/_RootEntity.py new file mode 100644 index 0000000000000000000000000000000000000000..6047bec8b268d4e412161d4515b2c3c7c56d800a --- /dev/null +++ b/src/common/database/api/entity/_RootEntity.py @@ -0,0 +1,16 @@ +from typing import Any, Callable, Dict +from ._Entity import _Entity +from ...engines._DatabaseEngine import _DatabaseEngine + +class _RootEntity(_Entity): + def __init__(self, database_engine : _DatabaseEngine, entity_uuid: str, attributes_key: str, + attributes_validators: Dict[str, Callable[[Any], bool]], + attribute_transcoders: Dict[str, Dict[Any, Callable[[Any], Any]]]): + self._database_engine = database_engine + super().__init__(self, entity_uuid, attributes_key, attributes_validators, attribute_transcoders) + + @property + def parent(self) -> '_RootEntity': return self + + @property + def database_engine(self) -> _DatabaseEngine: return self._database_engine diff --git a/src/common/database/tests/test_unitary_inmemory.py b/src/common/database/tests/test_engine_inmemory.py similarity index 100% rename from src/common/database/tests/test_unitary_inmemory.py rename to src/common/database/tests/test_engine_inmemory.py diff --git a/src/common/database/tests/test_integration_redis.py b/src/common/database/tests/test_engine_redis.py similarity index 100% rename from src/common/database/tests/test_integration_redis.py rename to src/common/database/tests/test_engine_redis.py diff --git a/src/common/database/tests/test_unitary.py b/src/common/database/tests/test_unitary.py new file mode 100644 index 0000000000000000000000000000000000000000..c00e2f7c9165ee84bccf4a34f06c0915bf4726c9 --- /dev/null +++ b/src/common/database/tests/test_unitary.py @@ -0,0 +1,84 @@ +import logging, pytest +from ..api.Database import Database +from ..api.entity._Entity import _Entity +from ..api.entity._RootEntity import _RootEntity +from ..api.entity.EntityAttributes import EntityAttributes +from ..api.Exceptions import WrongDatabaseEngine +from ..engines._DatabaseEngine import _DatabaseEngine +from ..engines.inmemory.InMemoryDatabaseEngine import InMemoryDatabaseEngine + +logging.basicConfig(level=logging.INFO) + +def test_database_gets_none_database_engine(): + # should fail with invalid database engine + with pytest.raises(WrongDatabaseEngine) as e: + Database(None) + assert str(e.value) == 'database_engine must inherit from _DatabaseEngine' + +def test_database_gets_correct_database_engine(): + # should work + assert Database(InMemoryDatabaseEngine()) is not None + +def test_entity_gets_invalid_parameters(): + + class RootMockEntity(_RootEntity): + def __init__(self, database_engine : _DatabaseEngine): + super().__init__(database_engine, 'valid-uuid', 'valid-key', {}, {}) + + # should fail with invalid parent + with pytest.raises(AttributeError) as e: + _Entity(None, 'valid-uuid', 'valid-attributes-key', {}, {}) + assert str(e.value) == 'parent must be an instance of _Entity' + + # should fail with invalid entity uuid + with pytest.raises(AttributeError) as e: + _Entity(RootMockEntity(InMemoryDatabaseEngine()), None, 'valid-attributes-key', {}, {}) + assert str(e.value) == 'entity_uuid must be a non-empty instance of str' + + # should fail with invalid entity uuid + with pytest.raises(AttributeError) as e: + _Entity(RootMockEntity(InMemoryDatabaseEngine()), '', 'valid-attributes-key', {}, {}) + assert str(e.value) == 'entity_uuid must be a non-empty instance of str' + + # should fail with invalid attribute key + with pytest.raises(AttributeError) as e: + _Entity(RootMockEntity(InMemoryDatabaseEngine()), 'valid-uuid', None, {}, {}) + assert str(e.value) == 'attributes_key must be a non-empty instance of str' + + # should fail with invalid attribute key + with pytest.raises(AttributeError) as e: + _Entity(RootMockEntity(InMemoryDatabaseEngine()), 'valid-uuid', '', {}, {}) + assert str(e.value) == 'attributes_key must be a non-empty instance of str' + + # should fail with invalid attribute validators + with pytest.raises(AttributeError) as e: + _Entity(RootMockEntity(InMemoryDatabaseEngine()), 'valid-uuid', 'valid-attributes-key', [], {}) + assert str(e.value) == 'attribute_validators must be an instance of dict' + + # should fail with invalid attribute transcoders + with pytest.raises(AttributeError) as e: + _Entity(RootMockEntity(InMemoryDatabaseEngine()), 'valid-uuid', 'valid-attributes-key', {}, []) + assert str(e.value) == 'attribute_transcoders must be an instance of dict' + + # should work + assert _Entity(RootMockEntity(InMemoryDatabaseEngine()), 'valid-uuid', 'valid-attributes-key', {}, {}) is not None + +def test_entity_attributes_gets_invalid_parameters(): + + class RootMockEntity(_RootEntity): + def __init__(self, database_engine : _DatabaseEngine): + super().__init__(database_engine, 'valid-uuid', 'valid-key', {}, {}) + + # should work + root_entity = RootMockEntity(InMemoryDatabaseEngine()) + validators = {'attr': lambda v: True} + entity_attrs = EntityAttributes(root_entity, 'valid-attributes-key', validators, {}) + assert entity_attrs is not None + + with pytest.raises(AttributeError) as e: + entity_attrs.update(update_attributes={'non-defined-attr': 'random-value'}) + assert str(e.value) == "Unexpected update_attributes: {'non-defined-attr': 'random-value'}" + + with pytest.raises(AttributeError) as e: + entity_attrs.update(remove_attributes=['non-defined-attr']) + assert str(e.value) == "Unexpected remove_attributes: {'non-defined-attr'}" diff --git a/src/common/tests/Assertions.py b/src/common/tests/Assertions.py index f4f88e4aa2aac645f8cd84a7ee3912071cd2606b..6cf2f757bb87174882344ff4c762716e217accb3 100644 --- a/src/common/tests/Assertions.py +++ b/src/common/tests/Assertions.py @@ -16,6 +16,12 @@ def validate_device_id(message): assert 'device_id' in message validate_uuid(message['device_id']) +def validate_link_id(message): + assert type(message) is dict + assert len(message.keys()) == 1 + assert 'link_id' in message + validate_uuid(message['link_id']) + def validate_topology(message): assert type(message) is dict assert len(message.keys()) > 0 diff --git a/src/context/client/ContextClient.py b/src/context/client/ContextClient.py index d868b5694abf69e076ca9d8f314cbde16fc0d42d..64bd0010b3d1d57a90023b5473d8d89b8a69823a 100644 --- a/src/context/client/ContextClient.py +++ b/src/context/client/ContextClient.py @@ -1,5 +1,6 @@ import grpc, logging from common.tools.RetryDecorator import retry, delay_exponential +from context.proto.context_pb2 import Link, LinkId, Empty, Topology from context.proto.context_pb2_grpc import ContextServiceStub LOGGER = logging.getLogger(__name__) @@ -25,8 +26,22 @@ class ContextClient: self.stub = None @retry(exceptions=set(), max_retries=MAX_RETRIES, delay_function=DELAY_FUNCTION, prepare_method_name='connect') - def GetTopology(self, request): + def GetTopology(self, request : Empty) -> Topology: LOGGER.debug('GetTopology request: {}'.format(request)) response = self.stub.GetTopology(request) LOGGER.debug('GetTopology result: {}'.format(response)) return response + + @retry(exceptions=set(), max_retries=MAX_RETRIES, delay_function=DELAY_FUNCTION, prepare_method_name='connect') + def AddLink(self, request : Link) -> LinkId: + LOGGER.debug('AddLink request: {}'.format(request)) + response = self.stub.AddLink(request) + LOGGER.debug('AddLink result: {}'.format(response)) + return response + + @retry(exceptions=set(), max_retries=MAX_RETRIES, delay_function=DELAY_FUNCTION, prepare_method_name='connect') + def DeleteLink(self, request : LinkId) -> Empty: + LOGGER.debug('DeleteLink request: {}'.format(request)) + response = self.stub.DeleteLink(request) + LOGGER.debug('DeleteLink result: {}'.format(response)) + return response diff --git a/src/context/service/ContextServiceServicerImpl.py b/src/context/service/ContextServiceServicerImpl.py index 543adf7dfac9356e91d43ea38ef47f6fe8d26bfc..765a825081985f8f973ef9e71f6a5639780f444b 100644 --- a/src/context/service/ContextServiceServicerImpl.py +++ b/src/context/service/ContextServiceServicerImpl.py @@ -1,8 +1,10 @@ +from typing import Dict, List, Set, Tuple import grpc, logging from prometheus_client import Counter, Histogram +from common.Checkers import chk_string from common.database.api.Database import Database from common.exceptions.ServiceException import ServiceException -from context.proto.context_pb2 import Empty, Topology +from context.proto.context_pb2 import Empty, Link, LinkId, Topology from context.proto.context_pb2_grpc import ContextServiceServicer LOGGER = logging.getLogger(__name__) @@ -19,6 +21,24 @@ GETTOPOLOGY_COUNTER_FAILED = Counter ('context_gettopology_counter_failed', GETTOPOLOGY_HISTOGRAM_DURATION = Histogram('context_gettopology_histogram_duration', 'Context:GetTopology histogram of request duration') +ADDLINK_COUNTER_STARTED = Counter ('context_addlink_counter_started', + 'Context:AddLink counter of requests started' ) +ADDLINK_COUNTER_COMPLETED = Counter ('context_addlink_counter_completed', + 'Context:AddLink counter of requests completed') +ADDLINK_COUNTER_FAILED = Counter ('context_addlink_counter_failed', + 'Context:AddLink counter of requests failed' ) +ADDLINK_HISTOGRAM_DURATION = Histogram('context_addlink_histogram_duration', + 'Context:AddLink histogram of request duration') + +DELETELINK_COUNTER_STARTED = Counter ('context_deletelink_counter_started', + 'Context:DeleteLink counter of requests started' ) +DELETELINK_COUNTER_COMPLETED = Counter ('context_deletelink_counter_completed', + 'Context:DeleteLink counter of requests completed') +DELETELINK_COUNTER_FAILED = Counter ('context_deletelink_counter_failed', + 'Context:DeleteLink counter of requests failed' ) +DELETELINK_HISTOGRAM_DURATION = Histogram('context_deletelink_histogram_duration', + 'Context:DeleteLink histogram of request duration') + class ContextServiceServicerImpl(ContextServiceServicer): def __init__(self, database : Database): LOGGER.debug('Creating Servicer...') @@ -43,9 +63,157 @@ class ContextServiceServicerImpl(ContextServiceServicer): LOGGER.debug('GetTopology reply: {}'.format(str(reply))) GETTOPOLOGY_COUNTER_COMPLETED.inc() return reply - except ServiceException as e: - grpc_context.abort(e.code, e.details) + except ServiceException as e: # pragma: no cover (ServiceException not thrown) + grpc_context.abort(e.code, e.details) # pragma: no cover (ServiceException not thrown) except Exception as e: # pragma: no cover LOGGER.exception('GetTopology exception') # pragma: no cover GETTOPOLOGY_COUNTER_FAILED.inc() # pragma: no cover grpc_context.abort(grpc.StatusCode.INTERNAL, str(e)) # pragma: no cover + + @ADDLINK_HISTOGRAM_DURATION.time() + def AddLink(self, request : Link, grpc_context : grpc.ServicerContext) -> LinkId: + ADDLINK_COUNTER_STARTED.inc() + try: + LOGGER.debug('AddLink request: {}'.format(str(request))) + + # ----- Validate request data and pre-conditions ----------------------------------------------------------- + try: + link_id = chk_string('link.link_id.link_id.uuid', + request.link_id.link_id.uuid, + allow_empty=False) + except Exception as e: + LOGGER.exception('Invalid arguments:') + raise ServiceException(grpc.StatusCode.INVALID_ARGUMENT, str(e)) + + db_context = self.database.context(DEFAULT_CONTEXT_ID).create() + db_topology = db_context.topology(DEFAULT_TOPOLOGY_ID).create() + + if db_topology.links.contains(link_id): + msg = 'Link({}) already exists in the database.' + msg = msg.format(link_id) + raise ServiceException(grpc.StatusCode.ALREADY_EXISTS, msg) + + added_devices_and_endpoints : Dict[str, Set[str]] = {} + device_endpoint_pairs : List[Tuple[str, str]] = [] + for i,endpoint in enumerate(request.endpointList): + try: + ep_context_id = chk_string('endpoint[#{}].topoId.contextId.contextUuid.uuid'.format(i), + endpoint.topoId.contextId.contextUuid.uuid, + allow_empty=True) + ep_topology_id = chk_string('endpoint[#{}].topoId.topoId.uuid'.format(i), + endpoint.topoId.topoId.uuid, + allow_empty=True) + ep_device_id = chk_string('endpoint[#{}].dev_id.device_id.uuid'.format(i), + endpoint.dev_id.device_id.uuid, + allow_empty=False) + ep_port_id = chk_string('endpoint[#{}].port_id.uuid'.format(i), + endpoint.port_id.uuid, + allow_empty=False) + except Exception as e: + LOGGER.exception('Invalid arguments:') + raise ServiceException(grpc.StatusCode.INVALID_ARGUMENT, str(e)) + + if (len(ep_context_id) > 0) and (ep_context_id != DEFAULT_CONTEXT_ID): + msg = ' '.join([ + 'Unsupported Context({}) in Endpoint(#{}) of Link({}).', + 'Only default Context({}) is currently supported.', + 'Optionally, leave field empty to use default Context.', + ]) + msg = msg.format(ep_context_id, i, link_id, DEFAULT_CONTEXT_ID) + raise ServiceException(grpc.StatusCode.INVALID_ARGUMENT, msg) + elif len(ep_context_id) == 0: + ep_context_id = DEFAULT_CONTEXT_ID + + if (len(ep_topology_id) > 0) and (ep_topology_id != DEFAULT_TOPOLOGY_ID): + msg = ' '.join([ + 'Unsupported Topology({}) in Endpoint(#{}) of Link({}).', + 'Only default Topology({}) is currently supported.', + 'Optionally, leave field empty to use default Topology.', + ]) + msg = msg.format(ep_topology_id, i, link_id, DEFAULT_TOPOLOGY_ID) + raise ServiceException(grpc.StatusCode.INVALID_ARGUMENT, msg) + elif len(ep_topology_id) == 0: + ep_topology_id = DEFAULT_TOPOLOGY_ID + + if ep_device_id in added_devices_and_endpoints: + msg = 'Duplicated Device({}) in Endpoint(#{}) of Link({}).' + msg = msg.format(ep_device_id, i, link_id) + raise ServiceException(grpc.StatusCode.INVALID_ARGUMENT, msg) + + if not db_topology.devices.contains(ep_device_id): + msg = 'Device({}) in Endpoint(#{}) of Link({}) does not exist in the database.' + msg = msg.format(ep_device_id, i, link_id) + raise ServiceException(grpc.StatusCode.NOT_FOUND, msg) + + added_device_and_endpoints = added_devices_and_endpoints.setdefault(ep_device_id, set()) + + # should never happen since same device cannot appear 2 times in the link + if ep_port_id in added_device_and_endpoints: # pragma: no cover + msg = 'Duplicated Device({})/Port({}) in Endpoint(#{}) of Link({}).' # pragma: no cover + msg = msg.format(ep_device_id, ep_port_id, i, link_id) # pragma: no cover + raise ServiceException(grpc.StatusCode.INVALID_ARGUMENT, msg) # pragma: no cover + + if not db_topology.device(ep_device_id).endpoints.contains(ep_port_id): + msg = 'Device({})/Port({}) in Endpoint(#{}) of Link({}) does not exist in the database.' + msg = msg.format(ep_device_id, ep_port_id, i, link_id) + raise ServiceException(grpc.StatusCode.NOT_FOUND, msg) + + added_device_and_endpoints.add(ep_port_id) + device_endpoint_pairs.append((ep_device_id, ep_port_id)) + + # ----- Implement changes in the database ------------------------------------------------------------------ + db_link = db_topology.link(link_id).create() + for device_id,endpoint_id in device_endpoint_pairs: + link_endpoint_id = '{}/{}'.format(device_id, endpoint_id) + db_endpoint = db_topology.device(ep_device_id).endpoint(ep_port_id) + db_link.endpoint(link_endpoint_id).create(db_endpoint) + + # ----- Compose reply -------------------------------------------------------------------------------------- + reply = LinkId(**db_link.dump_id()) + LOGGER.debug('AddLink reply: {}'.format(str(reply))) + ADDLINK_COUNTER_COMPLETED.inc() + return reply + except ServiceException as e: + grpc_context.abort(e.code, e.details) + except Exception as e: # pragma: no cover + LOGGER.exception('AddLink exception') # pragma: no cover + ADDLINK_COUNTER_FAILED.inc() # pragma: no cover + grpc_context.abort(grpc.StatusCode.INTERNAL, str(e)) # pragma: no cover + + @DELETELINK_HISTOGRAM_DURATION.time() + def DeleteLink(self, request : LinkId, grpc_context : grpc.ServicerContext) -> Empty: + DELETELINK_COUNTER_STARTED.inc() + try: + LOGGER.debug('DeleteLink request: {}'.format(str(request))) + + # ----- Validate request data and pre-conditions ----------------------------------------------------------- + try: + link_id = chk_string('link_id.link_id.uuid', + request.link_id.uuid, + allow_empty=False) + except Exception as e: + LOGGER.exception('Invalid arguments:') + raise ServiceException(grpc.StatusCode.INVALID_ARGUMENT, str(e)) + + db_context = self.database.context(DEFAULT_CONTEXT_ID).create() + db_topology = db_context.topology(DEFAULT_TOPOLOGY_ID).create() + + if not db_topology.links.contains(link_id): + msg = 'Link({}) does not exist in the database.' + msg = msg.format(link_id) + raise ServiceException(grpc.StatusCode.NOT_FOUND, msg) + + # ----- Implement changes in the database ------------------------------------------------------------------ + db_topology.link(link_id).delete() + + # ----- Compose reply -------------------------------------------------------------------------------------- + reply = Empty() + LOGGER.debug('DeleteLink reply: {}'.format(str(reply))) + DELETELINK_COUNTER_COMPLETED.inc() + return reply + except ServiceException as e: + grpc_context.abort(e.code, e.details) + except Exception as e: # pragma: no cover + LOGGER.exception('DeleteLink exception') # pragma: no cover + DELETELINK_COUNTER_FAILED.inc() # pragma: no cover + grpc_context.abort(grpc.StatusCode.INTERNAL, str(e)) # pragma: no cover diff --git a/src/context/tests/test_integration.py b/src/context/tests/test_integration.py index eab068b493a06754ec335ea118fa60e671fddec7..b8d5de420bb9ead65b9fdf7dab3cd5147765b8cc 100644 --- a/src/context/tests/test_integration.py +++ b/src/context/tests/test_integration.py @@ -1,24 +1,24 @@ -import logging, os, pytest, sys - -from pathlib import Path -sys.path.append(__file__.split('src')[0] + 'src') -print(sys.path) - -from context.client.ContextClient import ContextClient -from context.proto.context_pb2 import Empty -from .tools.ValidateTopology import validate_topology_dict - -LOGGER = logging.getLogger(__name__) -LOGGER.setLevel(logging.DEBUG) - -@pytest.fixture(scope='session') -def remote_context_client(): - address = os.environ.get('TEST_TARGET_ADDRESS') - if(address is None): raise Exception('EnvironmentVariable(TEST_TARGET_ADDRESS) not specified') - port = os.environ.get('TEST_TARGET_PORT') - if(port is None): raise Exception('EnvironmentVariable(TEST_TARGET_PORT) not specified') - return ContextClient(address=address, port=port) - -def test_remote_get_topology(remote_context_client): - response = remote_context_client.GetTopology(Empty()) - validate_topology_dict(response) +#import logging, os, pytest, sys +# +#from pathlib import Path +#sys.path.append(__file__.split('src')[0] + 'src') +#print(sys.path) +# +#from context.client.ContextClient import ContextClient +#from context.proto.context_pb2 import Empty +#from .tools.ValidateTopology import validate_topology_dict +# +#LOGGER = logging.getLogger(__name__) +#LOGGER.setLevel(logging.DEBUG) +# +#@pytest.fixture(scope='session') +#def remote_context_client(): +# address = os.environ.get('TEST_TARGET_ADDRESS') +# if(address is None): raise Exception('EnvironmentVariable(TEST_TARGET_ADDRESS) not specified') +# port = os.environ.get('TEST_TARGET_PORT') +# if(port is None): raise Exception('EnvironmentVariable(TEST_TARGET_PORT) not specified') +# return ContextClient(address=address, port=port) +# +#def test_remote_get_topology(remote_context_client): +# response = remote_context_client.GetTopology(Empty()) +# validate_topology_dict(response) diff --git a/src/context/tests/test_unitary.py b/src/context/tests/test_unitary.py index c05aa930c9c5a713b7ba246f7b0a342c38c13611..104736af195f0eb0643563f816bdaeefa5b0743b 100644 --- a/src/context/tests/test_unitary.py +++ b/src/context/tests/test_unitary.py @@ -1,11 +1,11 @@ -import logging, pytest +import copy, grpc, logging, pytest from google.protobuf.json_format import MessageToDict from common.database.Factory import get_database, DatabaseEngineEnum from common.database.api.Database import Database from common.database.tests.script import populate_example -from common.tests.Assertions import validate_topology +from common.tests.Assertions import validate_empty, validate_link_id, validate_topology from context.client.ContextClient import ContextClient -from context.proto.context_pb2 import Empty +from context.proto.context_pb2 import Empty, Link, LinkId from context.service.ContextService import ContextService from context.Config import GRPC_SERVICE_PORT, GRPC_MAX_WORKERS, GRPC_GRACE_PERIOD @@ -14,35 +14,206 @@ port = 10000 + GRPC_SERVICE_PORT # avoid first 1024 privileged ports to avoid ev LOGGER = logging.getLogger(__name__) LOGGER.setLevel(logging.DEBUG) +LINK_ID = {'link_id': {'uuid': 'dev1/to-dev2 ==> dev2/to-dev1'}} +LINK = { + 'link_id': {'link_id': {'uuid': 'dev1/to-dev2 ==> dev2/to-dev1'}}, + 'endpointList' : [ + { + 'topoId': { + 'contextId': {'contextUuid': {'uuid': 'admin'}}, + 'topoId': {'uuid': 'admin'} + }, + 'dev_id': {'device_id': {'uuid': 'dev1'}}, + 'port_id': {'uuid' : 'to-dev2'} + }, + { + 'topoId': { + 'contextId': {'contextUuid': {'uuid': 'admin'}}, + 'topoId': {'uuid': 'admin'} + }, + 'dev_id': {'device_id': {'uuid': 'dev2'}}, + 'port_id': {'uuid' : 'to-dev1'} + }, + ] +} + @pytest.fixture(scope='session') -def database(): +def context_database(): _database = get_database(engine=DatabaseEngineEnum.INMEMORY) return _database @pytest.fixture(scope='session') -def service(database : Database): +def context_service(context_database : Database): _service = ContextService( - database, port=port, max_workers=GRPC_MAX_WORKERS, grace_period=GRPC_GRACE_PERIOD) + context_database, port=port, max_workers=GRPC_MAX_WORKERS, grace_period=GRPC_GRACE_PERIOD) _service.start() yield _service _service.stop() @pytest.fixture(scope='session') -def client(service): +def context_client(context_service): _client = ContextClient(address='127.0.0.1', port=port) yield _client _client.close() -def test_get_topology_empty(client : ContextClient, database : Database): - database.clear_all() +def test_get_topology_empty(context_client : ContextClient, context_database : Database): + # should work + context_database.clear_all() validate_topology(MessageToDict( - client.GetTopology(Empty()), + context_client.GetTopology(Empty()), + including_default_value_fields=True, preserving_proto_field_name=True, + use_integers_for_enums=False)) + +def test_get_topology_completed(context_client : ContextClient, context_database : Database): + # should work + populate_example(context_database) + validate_topology(MessageToDict( + context_client.GetTopology(Empty()), + including_default_value_fields=True, preserving_proto_field_name=True, + use_integers_for_enums=False)) + +def test_delete_link_empty_uuid(context_client : ContextClient): + # should fail with link not found + with pytest.raises(grpc._channel._InactiveRpcError) as e: + copy_link_id = copy.deepcopy(LINK_ID) + 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.' + +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 + assert e.value.details() == 'Link(dev1/to-dev2 ==> dev2/to-dev1) already exists in the database.' + +def test_delete_link(context_client : ContextClient): + # should work + validate_empty(MessageToDict( + context_client.DeleteLink(LinkId(**LINK_ID)), + including_default_value_fields=True, preserving_proto_field_name=True, + use_integers_for_enums=False)) + +def test_delete_link_not_existing(context_client : ContextClient): + # should fail with link not found + with pytest.raises(grpc._channel._InactiveRpcError) as e: + context_client.DeleteLink(LinkId(**LINK_ID)) + assert e.value.code() == grpc.StatusCode.NOT_FOUND + assert e.value.details() == 'Link(dev1/to-dev2 ==> dev2/to-dev1) does not exist in the database.' + +def test_add_link_uuid_empty(context_client : ContextClient): + # should fail with link uuid empty + with pytest.raises(grpc._channel._InactiveRpcError) as e: + copy_link = copy.deepcopy(LINK) + 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.' + +def test_add_link_endpoint_wrong_context(context_client : ContextClient): + # should fail with unsupported context + with pytest.raises(grpc._channel._InactiveRpcError) as e: + copy_link = copy.deepcopy(LINK) + 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([ + 'Unsupported Context(wrong-context) in Endpoint(#0) of Link(dev1/to-dev2 ==> dev2/to-dev1).', + 'Only default Context(admin) is currently supported.', + 'Optionally, leave field empty to use default Context.', + ]) + assert e.value.details() == msg + +def test_add_link_endpoint_wrong_topology(context_client : ContextClient): + # should fail with unsupported topology + with pytest.raises(grpc._channel._InactiveRpcError) as e: + copy_link = copy.deepcopy(LINK) + 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([ + 'Unsupported Topology(wrong-topo) in Endpoint(#0) of Link(dev1/to-dev2 ==> dev2/to-dev1).', + 'Only default Topology(admin) is currently supported.', + 'Optionally, leave field empty to use default Topology.', + ]) + assert e.value.details() == msg + +def test_add_link_empty_device_uuid(context_client : ContextClient): + # should fail with port uuid is empty + with pytest.raises(grpc._channel._InactiveRpcError) as e: + copy_link = copy.deepcopy(LINK) + 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[#0].dev_id.device_id.uuid() string is empty.' + +def test_add_link_endpoint_wrong_device(context_client : ContextClient): + # should fail with wrong endpoint device + with pytest.raises(grpc._channel._InactiveRpcError) as e: + copy_link = copy.deepcopy(LINK) + 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 = 'Device(wrong-device) in Endpoint(#0) of Link(dev1/to-dev2 ==> dev2/to-dev1) does not exist in the database.' + assert e.value.details() == msg + +def test_add_link_endpoint_wrong_port(context_client : ContextClient): + # should fail with wrong endpoint port + with pytest.raises(grpc._channel._InactiveRpcError) as e: + copy_link = copy.deepcopy(LINK) + copy_link['endpointList'][0]['port_id']['uuid'] = 'wrong-port' + context_client.AddLink(Link(**copy_link)) + assert e.value.code() == grpc.StatusCode.NOT_FOUND + msg = 'Device(dev1)/Port(wrong-port) in Endpoint(#0) of Link(dev1/to-dev2 ==> dev2/to-dev1) does not exist in the database.' + assert e.value.details() == msg + +def test_add_link_endpoint_duplicated_device(context_client : ContextClient): + # should fail with duplicated endpoint device + with pytest.raises(grpc._channel._InactiveRpcError) as e: + copy_link = copy.deepcopy(LINK) + 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 = 'Duplicated Device(dev1) in Endpoint(#1) of Link(dev1/to-dev2 ==> dev2/to-dev1).' + assert e.value.details() == msg + +def test_add_link_empty_port_uuid(context_client : ContextClient): + # should fail with port uuid is empty + with pytest.raises(grpc._channel._InactiveRpcError) as e: + copy_link = copy.deepcopy(LINK) + 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[#0].port_id.uuid() string is empty.' + +def test_add_link(context_client : ContextClient): + # should work + validate_link_id(MessageToDict( + context_client.AddLink(Link(**LINK)), + including_default_value_fields=True, preserving_proto_field_name=True, + use_integers_for_enums=False)) + +def test_delete_link_2(context_client : ContextClient): + # should work + validate_empty(MessageToDict( + context_client.DeleteLink(LinkId(**LINK_ID)), including_default_value_fields=True, preserving_proto_field_name=True, use_integers_for_enums=False)) -def test_get_topology_completed(client : ContextClient, database : Database): - populate_example(database) +def test_add_link_default_endpoint_context_topology(context_client : ContextClient): + # should work + copy_link = copy.deepcopy(LINK) + copy_link['endpointList'][0]['topoId']['contextId']['contextUuid']['uuid'] = '' + copy_link['endpointList'][0]['topoId']['topoId']['uuid'] = '' + validate_link_id(MessageToDict( + context_client.AddLink(Link(**copy_link)), + including_default_value_fields=True, preserving_proto_field_name=True, + use_integers_for_enums=False)) + +def test_get_topology_completed_2(context_client : ContextClient): + # should work validate_topology(MessageToDict( - client.GetTopology(Empty()), + context_client.GetTopology(Empty()), including_default_value_fields=True, preserving_proto_field_name=True, use_integers_for_enums=False)) diff --git a/src/context/tests/tools/ValidateTopology.py b/src/context/tests/tools/ValidateTopology.py deleted file mode 100644 index b52546e39c27292bec4f11755dade987929e5e71..0000000000000000000000000000000000000000 --- a/src/context/tests/tools/ValidateTopology.py +++ /dev/null @@ -1,6 +0,0 @@ -def validate_topology_dict(topology): - assert type(topology) is dict - assert len(topology.keys()) > 0 - assert 'topoId' in topology - assert 'device' in topology - assert 'link' in topology diff --git a/src/context/tests/tools/__init__.py b/src/context/tests/tools/__init__.py deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/src/device/client/DeviceClient.py b/src/device/client/DeviceClient.py index 3e9f83f3f5459a738f7863955b78164c81ec21a3..a517ebdd1551465f9404714ec07bd7326cad7c2d 100644 --- a/src/device/client/DeviceClient.py +++ b/src/device/client/DeviceClient.py @@ -1,5 +1,6 @@ import grpc, logging from common.tools.RetryDecorator import retry, delay_exponential +from device.proto.context_pb2 import Device, DeviceId, Empty from device.proto.device_pb2_grpc import DeviceServiceStub LOGGER = logging.getLogger(__name__) @@ -25,21 +26,21 @@ class DeviceClient: self.stub = None @retry(exceptions=set(), max_retries=MAX_RETRIES, delay_function=DELAY_FUNCTION, prepare_method_name='connect') - def AddDevice(self, request): + def AddDevice(self, request : Device) -> DeviceId: LOGGER.debug('AddDevice request: {}'.format(request)) response = self.stub.AddDevice(request) LOGGER.debug('AddDevice result: {}'.format(response)) return response @retry(exceptions=set(), max_retries=MAX_RETRIES, delay_function=DELAY_FUNCTION, prepare_method_name='connect') - def ConfigureDevice(self, request): + def ConfigureDevice(self, request : Device) -> DeviceId: LOGGER.debug('ConfigureDevice request: {}'.format(request)) response = self.stub.ConfigureDevice(request) LOGGER.debug('ConfigureDevice result: {}'.format(response)) return response @retry(exceptions=set(), max_retries=MAX_RETRIES, delay_function=DELAY_FUNCTION, prepare_method_name='connect') - def DeleteDevice(self, request): + def DeleteDevice(self, request : DeviceId) -> Empty: LOGGER.debug('DeleteDevice request: {}'.format(request)) response = self.stub.DeleteDevice(request) LOGGER.debug('DeleteDevice result: {}'.format(response)) diff --git a/src/device/service/DeviceServiceServicerImpl.py b/src/device/service/DeviceServiceServicerImpl.py index afacfc27881fcad524e8218e88d8b36a3ec4ed11..2aa135c287e153031b2b5651af19f41112416d4e 100644 --- a/src/device/service/DeviceServiceServicerImpl.py +++ b/src/device/service/DeviceServiceServicerImpl.py @@ -55,16 +55,29 @@ class DeviceServiceServicerImpl(DeviceServiceServicer): # ----- Validate request data and pre-conditions ----------------------------------------------------------- try: - device_uuid = chk_string('device_uuid', request.device_id.device_id.uuid, allow_empty=False) - device_type = chk_string('device_type', request.device_type, allow_empty=False) - device_config = chk_string('device_config', request.device_config.device_config, allow_empty=True) - device_opstat = chk_options('devOperationalStatus', request.devOperationalStatus, + device_id = chk_string ('device.device_id.device_id.uuid', + request.device_id.device_id.uuid, + allow_empty=False) + device_type = chk_string ('device.device_type', + request.device_type, + allow_empty=False) + device_config = chk_string ('device.device_config.device_config', + request.device_config.device_config, + allow_empty=True) + device_opstat = chk_options('device.devOperationalStatus', + request.devOperationalStatus, operationalstatus_enum_values()) except Exception as e: LOGGER.exception('Invalid arguments:') raise ServiceException(grpc.StatusCode.INVALID_ARGUMENT, str(e)) device_opstat = to_operationalstatus_enum(device_opstat) + # should not happen because gRPC limits accepted values in enums + if device_opstat is None: # pragma: no cover + msg = 'Unsupported OperationalStatus({}).' # pragma: no cover + msg = msg.format(request.devOperationalStatus) # pragma: no cover + raise ServiceException(grpc.StatusCode.INVALID_ARGUMENT, msg) # pragma: no cover + if device_opstat == OperationalStatus.KEEP_STATE: msg = ' '.join([ 'Device has to be created with either ENABLED/DISABLED Operational State.', @@ -75,71 +88,81 @@ class DeviceServiceServicerImpl(DeviceServiceServicer): db_context = self.database.context(DEFAULT_CONTEXT_ID).create() db_topology = db_context.topology(DEFAULT_TOPOLOGY_ID).create() - if db_topology.devices.contains(device_uuid): - msg = 'device_uuid({}) already exists.' - msg = msg.format(device_uuid) + if db_topology.devices.contains(device_id): + msg = 'Device({}) already exists in the database.' + msg = msg.format(device_id) raise ServiceException(grpc.StatusCode.ALREADY_EXISTS, msg) added_endpoint_uuids = set() endpoint_pairs : List[Tuple[str, str]] = [] for i,endpoint in enumerate(request.endpointList): - contextId = endpoint.port_id.topoId.contextId.contextUuid.uuid - if (len(contextId) > 0) and (contextId != DEFAULT_CONTEXT_ID): + try: + ep_context_id = chk_string('endpoint[#{}].port_id.topoId.contextId.contextUuid.uuid'.format(i), + endpoint.port_id.topoId.contextId.contextUuid.uuid, + allow_empty=True) + ep_topology_id = chk_string('endpoint[#{}].port_id.topoId.topoId.uuid'.format(i), + endpoint.port_id.topoId.topoId.uuid, + allow_empty=True) + ep_device_id = chk_string('endpoint[#{}].port_id.dev_id.device_id.uuid'.format(i), + endpoint.port_id.dev_id.device_id.uuid, + allow_empty=True) + ep_port_id = chk_string('endpoint[#{}].port_id.port_id.uuid'.format(i), + endpoint.port_id.port_id.uuid, + allow_empty=False) + ep_port_type = chk_string('endpoint[#{}].port_type'.format(i), + endpoint.port_type, + allow_empty=False) + except Exception as e: + LOGGER.exception('Invalid arguments:') + raise ServiceException(grpc.StatusCode.INVALID_ARGUMENT, str(e)) + + if (len(ep_context_id) > 0) and (ep_context_id != DEFAULT_CONTEXT_ID): msg = ' '.join([ - 'Unsupported context_id({}) in endpoint #{}.', - 'Only default context_id({}) is currently supported.', - 'Optionally, leave field empty to use default context_id.', + 'Unsupported Context({}) in Endpoint(#{}) of Device({}).', + 'Only default Context({}) is currently supported.', + 'Optionally, leave field empty to use default Context.', ]) - msg = msg.format(contextId, i, DEFAULT_CONTEXT_ID) + msg = msg.format(ep_context_id, i, device_id, DEFAULT_CONTEXT_ID) raise ServiceException(grpc.StatusCode.INVALID_ARGUMENT, msg) - elif len(contextId) == 0: - contextId = DEFAULT_CONTEXT_ID + elif len(ep_context_id) == 0: + ep_context_id = DEFAULT_CONTEXT_ID - topoId = endpoint.port_id.topoId.topoId.uuid - if (len(topoId) > 0) and (topoId != DEFAULT_TOPOLOGY_ID): + if (len(ep_topology_id) > 0) and (ep_topology_id != DEFAULT_TOPOLOGY_ID): msg = ' '.join([ - 'Unsupported topology_id({}) in endpoint #{}.', - 'Only default topology_id({}) is currently supported.', - 'Optionally, leave field empty to use default topology_id.', + 'Unsupported Topology({}) in Endpoint(#{}) of Device({}).', + 'Only default Topology({}) is currently supported.', + 'Optionally, leave field empty to use default Topology.', ]) - msg = msg.format(topoId, i, DEFAULT_TOPOLOGY_ID) + msg = msg.format(ep_topology_id, i, device_id, DEFAULT_TOPOLOGY_ID) raise ServiceException(grpc.StatusCode.INVALID_ARGUMENT, msg) - elif len(topoId) == 0: - topoId = DEFAULT_TOPOLOGY_ID + elif len(ep_topology_id) == 0: + ep_topology_id = DEFAULT_TOPOLOGY_ID - dev_id = endpoint.port_id.dev_id.device_id.uuid - if (len(dev_id) > 0) and (dev_id != device_uuid): + if (len(ep_device_id) > 0) and (ep_device_id != device_id): msg = ' '.join([ - 'Wrong device_id({}) in endpoint #{}.', - 'Parent specified in message is device_id({}).', - 'Optionally, leave field empty to use parent device_id.', + 'Wrong Device({}) in Endpoint(#{}).', + 'Parent specified in message is Device({}).', + 'Optionally, leave field empty to use parent Device.', ]) - msg = msg.format(dev_id, i, device_uuid) + msg = msg.format(ep_device_id, i, device_id) raise ServiceException(grpc.StatusCode.INVALID_ARGUMENT, msg) - elif len(dev_id) == 0: - dev_id = device_uuid + elif len(ep_device_id) == 0: + ep_device_id = device_id - try: - port_id = chk_string('port_uuid', endpoint.port_id.port_id.uuid, allow_empty=False) - port_type = chk_string('port_type', endpoint.port_type, allow_empty=False) - except Exception as e: - LOGGER.exception('Invalid arguments:') - raise ServiceException(grpc.StatusCode.INVALID_ARGUMENT, str(e)) - - if port_id in added_endpoint_uuids: - msg = 'Duplicated port_id({}) in device_id({}).' - msg = msg.format(port_id, device_uuid) + if ep_port_id in added_endpoint_uuids: + msg = 'Duplicated Port({}) in Endpoint(#{}) of Device({}).' + msg = msg.format(ep_port_id, i, device_id) raise ServiceException(grpc.StatusCode.ALREADY_EXISTS, msg) - added_endpoint_uuids.add(port_id) - endpoint_pairs.append((port_id, port_type)) + added_endpoint_uuids.add(ep_port_id) + endpoint_pairs.append((ep_port_id, ep_port_type)) - # ----- Implement changes in database ---------------------------------------------------------------------- - db_device = db_topology.device(device_uuid).create(device_type, device_config, device_opstat) + # ----- Implement changes in the database ------------------------------------------------------------------ + db_device = db_topology.device(device_id).create(device_type, device_config, device_opstat) for port_id,port_type in endpoint_pairs: db_device.endpoint(port_id).create(port_type) # ----- Compose reply -------------------------------------------------------------------------------------- - reply = DeviceId(device_id=dict(uuid=device_uuid)) + reply = DeviceId(**db_device.dump_id()) LOGGER.debug('AddDevice reply: {}'.format(str(reply))) ADDDEVICE_COUNTER_COMPLETED.inc() return reply @@ -154,54 +177,64 @@ class DeviceServiceServicerImpl(DeviceServiceServicer): def ConfigureDevice(self, request : Device, grpc_context : grpc.ServicerContext) -> DeviceId: CONFIGUREDEVICE_COUNTER_STARTED.inc() try: - LOGGER.info('ConfigureDevice request: {}'.format(str(request))) + LOGGER.debug('ConfigureDevice request: {}'.format(str(request))) # ----- Validate request data and pre-conditions ----------------------------------------------------------- try: - device_uuid = chk_string('device_uuid', request.device_id.device_id.uuid, allow_empty=False) - device_type = chk_string('device_type', request.device_type, allow_empty=True) - device_config = chk_string('device_config', request.device_config.device_config, allow_empty=True) - device_opstat = chk_options('devOperationalStatus', request.devOperationalStatus, + device_id = chk_string ('device.device_id.device_id.uuid', + request.device_id.device_id.uuid, + allow_empty=False) + device_type = chk_string ('device.device_type', + request.device_type, + allow_empty=True) + device_config = chk_string ('device.device_config.device_config', + request.device_config.device_config, + allow_empty=True) + device_opstat = chk_options('device.devOperationalStatus', + request.devOperationalStatus, operationalstatus_enum_values()) except Exception as e: LOGGER.exception('Invalid arguments:') raise ServiceException(grpc.StatusCode.INVALID_ARGUMENT, str(e)) device_opstat = to_operationalstatus_enum(device_opstat) - if device_opstat is None: - msg = 'Unsupported OperationalStatus({}).' - msg = msg.format(request.devOperationalStatus) - raise ServiceException(grpc.StatusCode.INVALID_ARGUMENT, msg) + # should not happen because gRPC limits accepted values in enums + if device_opstat is None: # pragma: no cover + msg = 'Unsupported OperationalStatus({}).' # pragma: no cover + msg = msg.format(request.devOperationalStatus) # pragma: no cover + raise ServiceException(grpc.StatusCode.INVALID_ARGUMENT, msg) # pragma: no cover db_context = self.database.context(DEFAULT_CONTEXT_ID).create() db_topology = db_context.topology(DEFAULT_TOPOLOGY_ID).create() - if not db_topology.devices.contains(device_uuid): - msg = 'device_uuid({}) does not exist.' - msg = msg.format(device_uuid) + if not db_topology.devices.contains(device_id): + msg = 'Device({}) does not exist in the database.' + msg = msg.format(device_id) raise ServiceException(grpc.StatusCode.NOT_FOUND, msg) - db_device = db_topology.device(device_uuid) + db_device = db_topology.device(device_id) db_device_attributes = db_device.attributes.get(attributes=['device_type']) - if len(db_device_attributes) == 0: - msg = 'attribute device_type for device_uuid({}) does not exist.' - msg = msg.format(device_uuid) - raise ServiceException(grpc.StatusCode.FAILED_PRECONDITION, msg) + # should not happen, device creation through Database API ensures all fields are always present + if len(db_device_attributes) == 0: # pragma: no cover + msg = 'Attribute device_type for Device({}) does not exist in the database.' # pragma: no cover + msg = msg.format(device_id) # pragma: no cover + raise ServiceException(grpc.StatusCode.FAILED_PRECONDITION, msg) # pragma: no cover db_device_type = db_device_attributes.get('device_type') - if len(db_device_type) == 0: - msg = 'attribute device_type for device_uuid({}) is empty.' - msg = msg.format(device_uuid) - raise ServiceException(grpc.StatusCode.FAILED_PRECONDITION, msg) + # should not happen, device creation through Database API ensures all fields are always present + if len(db_device_type) == 0: # pragma: no cover + msg = 'Attribute device_type for Device({}) is empty in the database.' # pragma: no cover + msg = msg.format(device_id) # pragma: no cover + raise ServiceException(grpc.StatusCode.FAILED_PRECONDITION, msg) # pragma: no cover if db_device_type != device_type: - msg = 'Device({}) has Type({}). Cannot be changed to Type({}).' - msg = msg.format(device_uuid, db_device_type, device_type) + msg = 'Device({}) has Type({}) in the database. Cannot be changed to Type({}).' + msg = msg.format(device_id, db_device_type, device_type) raise ServiceException(grpc.StatusCode.INVALID_ARGUMENT, msg) if len(request.endpointList) > 0: msg = 'Endpoints belonging to Device({}) cannot be modified.' - msg = msg.format(device_uuid) + msg = msg.format(device_id) raise ServiceException(grpc.StatusCode.INVALID_ARGUMENT, msg) update_attributes = {} @@ -212,22 +245,20 @@ class DeviceServiceServicerImpl(DeviceServiceServicer): if device_opstat != OperationalStatus.KEEP_STATE: update_attributes['device_operational_status'] = device_opstat - LOGGER.info('update_attributes={}'.format(str(update_attributes))) - if len(update_attributes) == 0: msg = ' '.join([ 'Any change has been requested for Device({}).', - 'Either specify a new configuration or a new device state.', + 'Either specify a new configuration or a new device operational status.', ]) - msg = msg.format(device_uuid) + msg = msg.format(device_id) raise ServiceException(grpc.StatusCode.ABORTED, msg) - # ----- Implement changes in database ---------------------------------------------------------------------- + # ----- Implement changes in the database ------------------------------------------------------------------ db_device.update(update_attributes=update_attributes) # ----- Compose reply -------------------------------------------------------------------------------------- - reply = DeviceId(device_id=dict(uuid=device_uuid)) - LOGGER.info('ConfigureDevice reply: {}'.format(str(reply))) + reply = DeviceId(**db_device.dump_id()) + LOGGER.debug('ConfigureDevice reply: {}'.format(str(reply))) CONFIGUREDEVICE_COUNTER_COMPLETED.inc() return reply except ServiceException as e: @@ -245,7 +276,9 @@ class DeviceServiceServicerImpl(DeviceServiceServicer): # ----- Validate request data and pre-conditions ----------------------------------------------------------- try: - device_uuid = chk_string('device_uuid', request.device_id.uuid, allow_empty=False) + device_id = chk_string('device_id.device_id.uuid', + request.device_id.uuid, + allow_empty=False) except Exception as e: LOGGER.exception('Invalid arguments:') raise ServiceException(grpc.StatusCode.INVALID_ARGUMENT, str(e)) @@ -253,13 +286,13 @@ class DeviceServiceServicerImpl(DeviceServiceServicer): db_context = self.database.context(DEFAULT_CONTEXT_ID).create() db_topology = db_context.topology(DEFAULT_TOPOLOGY_ID).create() - if not db_topology.devices.contains(device_uuid): - msg = 'device_uuid({}) does not exist.' - msg = msg.format(device_uuid) + if not db_topology.devices.contains(device_id): + msg = 'Device({}) does not exist in the database.' + msg = msg.format(device_id) raise ServiceException(grpc.StatusCode.NOT_FOUND, msg) - # ----- Implement changes in database ---------------------------------------------------------------------- - db_topology.device(device_uuid).delete() + # ----- Implement changes in the database ------------------------------------------------------------------ + db_topology.device(device_id).delete() # ----- Compose reply -------------------------------------------------------------------------------------- reply = Empty() diff --git a/src/device/tests/test_unitary.py b/src/device/tests/test_unitary.py index 43e1c45aec638b93dbf0a9bcd5888c7ccf3d9302..9834c5c39761997b336ed9feda11d6899dde19c0 100644 --- a/src/device/tests/test_unitary.py +++ b/src/device/tests/test_unitary.py @@ -11,12 +11,12 @@ from device.Config import GRPC_SERVICE_PORT, GRPC_MAX_WORKERS, GRPC_GRACE_PERIOD LOGGER = logging.getLogger(__name__) LOGGER.setLevel(logging.DEBUG) -DEVICE_ID = {'device_id': {'uuid': 'test-device-001'}} +DEVICE_ID = {'device_id': {'uuid': 'dev1'}} DEVICE = { - 'device_id': {'device_id': {'uuid': 'test-device-001'}}, + 'device_id': {'device_id': {'uuid': 'dev1'}}, 'device_type': 'ROADM', - 'device_config': {'device_config': ''}, - 'devOperationalStatus': 1, + 'device_config': {'device_config': '<config/>'}, + 'devOperationalStatus': OperationalStatus.ENABLED.value, 'endpointList' : [ { 'port_id': { @@ -24,10 +24,10 @@ DEVICE = { 'contextId': {'contextUuid': {'uuid': 'admin'}}, 'topoId': {'uuid': 'admin'} }, - 'dev_id': {'device_id': {'uuid': 'test-device-001'}}, - 'port_id': {'uuid' : 'port-101'} + 'dev_id': {'device_id': {'uuid': 'dev1'}}, + 'port_id': {'uuid' : 'to-dev2'} }, - 'port_type': 'LINE' + 'port_type': 'WDM' }, { 'port_id': { @@ -35,53 +35,68 @@ DEVICE = { 'contextId': {'contextUuid': {'uuid': 'admin'}}, 'topoId': {'uuid': 'admin'} }, - 'dev_id': {'device_id': {'uuid': 'test-device-001'}}, - 'port_id': {'uuid' : 'port-102'} + 'dev_id': {'device_id': {'uuid': 'dev1'}}, + 'port_id': {'uuid' : 'to-dev3'} }, - 'port_type': 'LINE' + 'port_type': 'WDM' + }, + { + 'port_id': { + 'topoId': { + 'contextId': {'contextUuid': {'uuid': 'admin'}}, + 'topoId': {'uuid': 'admin'} + }, + 'dev_id': {'device_id': {'uuid': 'dev1'}}, + 'port_id': {'uuid' : 'to-dev4'} + }, + 'port_type': 'WDM' }, ] } @pytest.fixture(scope='session') -def service(): - database = get_database(engine=DatabaseEngineEnum.INMEMORY) +def device_database(): + _database = get_database(engine=DatabaseEngineEnum.INMEMORY) + return _database + +@pytest.fixture(scope='session') +def device_service(device_database): _service = DeviceService( - database, port=GRPC_SERVICE_PORT, max_workers=GRPC_MAX_WORKERS, grace_period=GRPC_GRACE_PERIOD) + device_database, port=GRPC_SERVICE_PORT, max_workers=GRPC_MAX_WORKERS, grace_period=GRPC_GRACE_PERIOD) _service.start() yield _service _service.stop() @pytest.fixture(scope='session') -def client(service): +def device_client(device_service): _client = DeviceClient(address='127.0.0.1', port=GRPC_SERVICE_PORT) yield _client _client.close() -def test_create_empty_device_uuid(client : DeviceClient): +def test_add_device_empty_device_uuid(device_client : DeviceClient): # should fail with device uuid is empty with pytest.raises(grpc._channel._InactiveRpcError) as e: copy_device = copy.deepcopy(DEVICE) copy_device['device_id']['device_id']['uuid'] = '' - client.AddDevice(Device(**copy_device)) + device_client.AddDevice(Device(**copy_device)) assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT - assert e.value.details() == 'device_uuid() string is empty.' + assert e.value.details() == 'device.device_id.device_id.uuid() string is empty.' -def test_create_empty_device_type(client : DeviceClient): +def test_add_device_empty_device_type(device_client : DeviceClient): # should fail with device type is empty with pytest.raises(grpc._channel._InactiveRpcError) as e: copy_device = copy.deepcopy(DEVICE) copy_device['device_type'] = '' - client.AddDevice(Device(**copy_device)) + device_client.AddDevice(Device(**copy_device)) assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT - assert e.value.details() == 'device_type() string is empty.' + assert e.value.details() == 'device.device_type() string is empty.' -def test_create_wrong_device_operational_status(client : DeviceClient): +def test_add_device_wrong_device_operational_status(device_client : DeviceClient): # should fail with wrong device operational status with pytest.raises(grpc._channel._InactiveRpcError) as e: copy_device = copy.deepcopy(DEVICE) copy_device['devOperationalStatus'] = OperationalStatus.KEEP_STATE.value - client.AddDevice(Device(**copy_device)) + device_client.AddDevice(Device(**copy_device)) assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT msg = ' '.join([ 'Device has to be created with either ENABLED/DISABLED Operational State.', @@ -89,183 +104,183 @@ def test_create_wrong_device_operational_status(client : DeviceClient): ]) assert e.value.details() == msg -def test_create_endpoint_wrong_context(client : DeviceClient): +def test_add_device_endpoint_wrong_context(device_client : DeviceClient): # should fail with unsupported context with pytest.raises(grpc._channel._InactiveRpcError) as e: copy_device = copy.deepcopy(DEVICE) copy_device['endpointList'][0]['port_id']['topoId']['contextId']['contextUuid']['uuid'] = 'wrong-context' request = Device(**copy_device) - LOGGER.warning('request = {}'.format(request)) - client.AddDevice(request) + device_client.AddDevice(request) assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT msg = ' '.join([ - 'Unsupported context_id(wrong-context) in endpoint #0.', - 'Only default context_id(admin) is currently supported.', - 'Optionally, leave field empty to use default context_id.', + 'Unsupported Context(wrong-context) in Endpoint(#0) of Device(dev1).', + 'Only default Context(admin) is currently supported.', + 'Optionally, leave field empty to use default Context.', ]) assert e.value.details() == msg -def test_create_endpoint_wrong_topology(client : DeviceClient): +def test_add_device_endpoint_wrong_topology(device_client : DeviceClient): # should fail with unsupported topology with pytest.raises(grpc._channel._InactiveRpcError) as e: copy_device = copy.deepcopy(DEVICE) copy_device['endpointList'][0]['port_id']['topoId']['topoId']['uuid'] = 'wrong-topo' - client.AddDevice(Device(**copy_device)) + device_client.AddDevice(Device(**copy_device)) assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT msg = ' '.join([ - 'Unsupported topology_id(wrong-topo) in endpoint #0.', - 'Only default topology_id(admin) is currently supported.', - 'Optionally, leave field empty to use default topology_id.', + 'Unsupported Topology(wrong-topo) in Endpoint(#0) of Device(dev1).', + 'Only default Topology(admin) is currently supported.', + 'Optionally, leave field empty to use default Topology.', ]) assert e.value.details() == msg -def test_create_endpoint_wrong_device(client : DeviceClient): +def test_add_device_endpoint_wrong_device(device_client : DeviceClient): # should fail with wrong endpoint device with pytest.raises(grpc._channel._InactiveRpcError) as e: copy_device = copy.deepcopy(DEVICE) copy_device['endpointList'][0]['port_id']['dev_id']['device_id']['uuid'] = 'wrong-device' - client.AddDevice(Device(**copy_device)) + device_client.AddDevice(Device(**copy_device)) assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT msg = ' '.join([ - 'Wrong device_id(wrong-device) in endpoint #0.', - 'Parent specified in message is device_id(test-device-001).', - 'Optionally, leave field empty to use parent device_id.', + 'Wrong Device(wrong-device) in Endpoint(#0).', + 'Parent specified in message is Device(dev1).', + 'Optionally, leave field empty to use parent Device.', ]) assert e.value.details() == msg -def test_create_empty_port_uuid(client : DeviceClient): +def test_add_device_empty_port_uuid(device_client : DeviceClient): # should fail with port uuid is empty with pytest.raises(grpc._channel._InactiveRpcError) as e: copy_device = copy.deepcopy(DEVICE) copy_device['endpointList'][0]['port_id']['port_id']['uuid'] = '' - client.AddDevice(Device(**copy_device)) + device_client.AddDevice(Device(**copy_device)) assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT - assert e.value.details() == 'port_uuid() string is empty.' + assert e.value.details() == 'endpoint[#0].port_id.port_id.uuid() string is empty.' -def test_create_empty_port_type(client : DeviceClient): +def test_add_device_empty_port_type(device_client : DeviceClient): # should fail with port type is empty with pytest.raises(grpc._channel._InactiveRpcError) as e: copy_device = copy.deepcopy(DEVICE) copy_device['endpointList'][0]['port_type'] = '' - client.AddDevice(Device(**copy_device)) + device_client.AddDevice(Device(**copy_device)) assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT - assert e.value.details() == 'port_type() string is empty.' + assert e.value.details() == 'endpoint[#0].port_type() string is empty.' -def test_create_duplicate_port(client : DeviceClient): +def test_add_device_duplicate_port(device_client : DeviceClient): # should fail with uplicate port in device with pytest.raises(grpc._channel._InactiveRpcError) as e: copy_device = copy.deepcopy(DEVICE) - copy_device['endpointList'][1]['port_id']['port_id']['uuid'] = 'port-101' - client.AddDevice(Device(**copy_device)) + copy_device['endpointList'][1]['port_id']['port_id']['uuid'] = 'to-dev2' + device_client.AddDevice(Device(**copy_device)) assert e.value.code() == grpc.StatusCode.ALREADY_EXISTS - assert e.value.details() == 'Duplicated port_id(port-101) in device_id(test-device-001).' + assert e.value.details() == 'Duplicated Port(to-dev2) in Endpoint(#1) of Device(dev1).' -def test_create(client : DeviceClient): +def test_add_device(device_client : DeviceClient): # should work validate_device_id(MessageToDict( - client.AddDevice(Device(**DEVICE)), + device_client.AddDevice(Device(**DEVICE)), including_default_value_fields=True, preserving_proto_field_name=True, use_integers_for_enums=False)) -def test_create_duplicate(client : DeviceClient): +def test_add_device_duplicate(device_client : DeviceClient): # should fail with device already exists with pytest.raises(grpc._channel._InactiveRpcError) as e: - client.AddDevice(Device(**DEVICE)) + device_client.AddDevice(Device(**DEVICE)) assert e.value.code() == grpc.StatusCode.ALREADY_EXISTS - assert e.value.details() == 'device_uuid(test-device-001) already exists.' + assert e.value.details() == 'Device(dev1) already exists in the database.' -def test_delete_empty_uuid(client : DeviceClient): +def test_delete_device_empty_uuid(device_client : DeviceClient): # should fail with device uuid is empty with pytest.raises(grpc._channel._InactiveRpcError) as e: copy_device_id = copy.deepcopy(DEVICE_ID) copy_device_id['device_id']['uuid'] = '' - client.DeleteDevice(DeviceId(**copy_device_id)) + device_client.DeleteDevice(DeviceId(**copy_device_id)) assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT - assert e.value.details() == 'device_uuid() string is empty.' + assert e.value.details() == 'device_id.device_id.uuid() string is empty.' -def test_delete_device_not_found(client : DeviceClient): +def test_delete_device_not_found(device_client : DeviceClient): # should fail with device not found with pytest.raises(grpc._channel._InactiveRpcError) as e: copy_device_id = copy.deepcopy(DEVICE_ID) copy_device_id['device_id']['uuid'] = 'wrong-device-id' - client.DeleteDevice(DeviceId(**copy_device_id)) + device_client.DeleteDevice(DeviceId(**copy_device_id)) assert e.value.code() == grpc.StatusCode.NOT_FOUND - assert e.value.details() == 'device_uuid(wrong-device-id) does not exist.' + assert e.value.details() == 'Device(wrong-device-id) does not exist in the database.' -def test_delete(client : DeviceClient): +def test_delete_device(device_client : DeviceClient): # should work validate_empty(MessageToDict( - client.DeleteDevice(DeviceId(**DEVICE_ID)), + device_client.DeleteDevice(DeviceId(**DEVICE_ID)), including_default_value_fields=True, preserving_proto_field_name=True, use_integers_for_enums=False)) -def test_configure_empty_device_uuid(client : DeviceClient): +def test_configure_device_empty_device_uuid(device_client : DeviceClient): # should fail with device uuid is empty with pytest.raises(grpc._channel._InactiveRpcError) as e: copy_device = copy.deepcopy(DEVICE) copy_device['device_id']['device_id']['uuid'] = '' - client.ConfigureDevice(Device(**copy_device)) + device_client.ConfigureDevice(Device(**copy_device)) assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT - assert e.value.details() == 'device_uuid() string is empty.' + assert e.value.details() == 'device.device_id.device_id.uuid() string is empty.' -def test_configure_device_not_found(client : DeviceClient): +def test_configure_device_not_found(device_client : DeviceClient): # should fail with device not found with pytest.raises(grpc._channel._InactiveRpcError) as e: copy_device = copy.deepcopy(DEVICE) copy_device['device_id']['device_id']['uuid'] = 'wrong-device-id' - client.ConfigureDevice(Device(**copy_device)) + device_client.ConfigureDevice(Device(**copy_device)) assert e.value.code() == grpc.StatusCode.NOT_FOUND - assert e.value.details() == 'device_uuid(wrong-device-id) does not exist.' + assert e.value.details() == 'Device(wrong-device-id) does not exist in the database.' -def test_create_device_default_endpoint_context_topology(client : DeviceClient): +def test_add_device_default_endpoint_context_topology_device(device_client : DeviceClient): # should work copy_device = copy.deepcopy(DEVICE) copy_device['endpointList'][0]['port_id']['topoId']['contextId']['contextUuid']['uuid'] = '' copy_device['endpointList'][0]['port_id']['topoId']['topoId']['uuid'] = '' copy_device['endpointList'][0]['port_id']['dev_id']['device_id']['uuid'] = '' validate_device_id(MessageToDict( - client.AddDevice(Device(**copy_device)), + device_client.AddDevice(Device(**copy_device)), including_default_value_fields=True, preserving_proto_field_name=True, use_integers_for_enums=False)) -def test_configure_wrong_device_type(client : DeviceClient): +def test_configure_device_wrong_device_type(device_client : DeviceClient): # should fail with device type is wrong with pytest.raises(grpc._channel._InactiveRpcError) as e: copy_device = copy.deepcopy(DEVICE) copy_device['device_type'] = 'wrong-type' - client.ConfigureDevice(Device(**copy_device)) + device_client.ConfigureDevice(Device(**copy_device)) assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT - assert e.value.details() == 'Device(test-device-001) has Type(ROADM). Cannot be changed to Type(wrong-type).' + assert e.value.details() == 'Device(dev1) has Type(ROADM) in the database. Cannot be changed to Type(wrong-type).' -def test_configure_with_endpoints(client : DeviceClient): +def test_configure_device_with_endpoints(device_client : DeviceClient): # should fail with endpoints cannot be modified with pytest.raises(grpc._channel._InactiveRpcError) as e: copy_device = copy.deepcopy(DEVICE) - client.ConfigureDevice(Device(**copy_device)) + device_client.ConfigureDevice(Device(**copy_device)) assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT - assert e.value.details() == 'Endpoints belonging to Device(test-device-001) cannot be modified.' + assert e.value.details() == 'Endpoints belonging to Device(dev1) cannot be modified.' -def test_configure_no_change(client : DeviceClient): +def test_configure_device_no_change(device_client : DeviceClient): # should fail with any change detected with pytest.raises(grpc._channel._InactiveRpcError) as e: copy_device = copy.deepcopy(DEVICE) + copy_device['device_config']['device_config'] = '' copy_device['devOperationalStatus'] = OperationalStatus.KEEP_STATE.value copy_device['endpointList'].clear() - client.ConfigureDevice(Device(**copy_device)) + device_client.ConfigureDevice(Device(**copy_device)) assert e.value.code() == grpc.StatusCode.ABORTED msg = ' '.join([ - 'Any change has been requested for Device(test-device-001).', - 'Either specify a new configuration or a new device state.', + '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(client : DeviceClient): +def test_configure_device(device_client : DeviceClient): # should work copy_device = copy.deepcopy(DEVICE) copy_device['device_config']['device_config'] = '<new_config/>' copy_device['devOperationalStatus'] = OperationalStatus.DISABLED.value copy_device['endpointList'].clear() validate_device_id(MessageToDict( - client.ConfigureDevice(Device(**copy_device)), + device_client.ConfigureDevice(Device(**copy_device)), including_default_value_fields=True, preserving_proto_field_name=True, use_integers_for_enums=False))