Skip to content
Snippets Groups Projects
Commit a3e73f06 authored by Lluis Gifre Renom's avatar Lluis Gifre Renom
Browse files

Several changes:

Common:
- renamed report_coverage.sh script to report_coverage_all.sh

Common/DatabaseAPI:
- improved root class validations and test units
- added _RootEntity interface
- improved definition of Context database API entities

Context service:
- added AddLink/DeleteLink rpc methods
- temporarily disabled integration tests

Device service:
- applied renamings in service and test units
parent 76242121
No related branches found
No related tags found
1 merge request!54Release 2.0.0
Showing
with 188 additions and 62 deletions
File moved
#!/bin/bash
./report_coverage.sh | grep --color -E -i "^.*context.*$|$"
./report_coverage_all.sh | grep --color -E -i "^.*context.*$|$"
#!/bin/bash
./report_coverage.sh | grep --color -E -i "^.*device.*$|$"
./report_coverage_all.sh | grep --color -E -i "^.*device.*$|$"
......@@ -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
......
......@@ -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):
......
class WrongDatabaseEngine(Exception):
pass
class MutexException(Exception):
pass
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
......
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
......
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
......
......@@ -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:
......
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,
......
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':
......
......@@ -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))
......
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()
......
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
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'}"
......@@ -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
......
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
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment