Add Device problems, privacy, working; Add event ErasePhysical

This commit is contained in:
Xavier Bustamante Talavera 2018-11-09 11:22:13 +01:00
parent e009bf4bc1
commit bd0eb3aad3
9 changed files with 158 additions and 70 deletions

View File

@ -6,13 +6,15 @@ skinparam ranksep 1
[*] -> Registered [*] -> Registered
state Attributes { state Attributes {
state Broken : cannot turn on state Broken : cannot turn on
state Owners state Owners
state Usufructuarees state Usufructuarees
state Reservees state Reservees
state "Physical\nPossessor" state "Physical\nPossessor"
state "Waste\n\Product" state "Waste\n\Product"
state problems : List of current events \nwith Warn/Error
state privacy : Set of\ncurrent erasures
state working : List of current events\naffecting working
} }
state Physical { state Physical {
@ -44,10 +46,4 @@ state Trading {
Renting --> Cancelled : Cancel Renting --> Cancelled : Cancel
} }
state DataStoragePrivacyCompliance {
state Erased
state Destroyed
}
@enduml @enduml

View File

@ -8,6 +8,7 @@ from typing import Dict, List, Set
from boltons import urlutils from boltons import urlutils
from citext import CIText from citext import CIText
from ereuse_utils.naming import Naming from ereuse_utils.naming import Naming
from more_itertools import unique_everseen
from sqlalchemy import BigInteger, Boolean, Column, Enum as DBEnum, Float, ForeignKey, Integer, \ from sqlalchemy import BigInteger, Boolean, Column, Enum as DBEnum, Float, ForeignKey, Integer, \
Sequence, SmallInteger, Unicode, inspect, text Sequence, SmallInteger, Unicode, inspect, text
from sqlalchemy.ext.declarative import declared_attr from sqlalchemy.ext.declarative import declared_attr
@ -22,8 +23,8 @@ from teal.marshmallow import ValidationError
from teal.resource import url_for_resource from teal.resource import url_for_resource
from ereuse_devicehub.db import db from ereuse_devicehub.db import db
from ereuse_devicehub.resources.enums import ComputerChassis, DataStorageInterface, \ from ereuse_devicehub.resources.enums import ComputerChassis, DataStorageInterface, DisplayTech, \
DataStoragePrivacyCompliance, DisplayTech, PrinterTechnology, RamFormat, RamInterface PrinterTechnology, RamFormat, RamInterface, Severity
from ereuse_devicehub.resources.models import STR_SM_SIZE, Thing from ereuse_devicehub.resources.models import STR_SM_SIZE, Thing
@ -31,6 +32,7 @@ class Device(Thing):
""" """
Base class for any type of physical object that can be identified. Base class for any type of physical object that can be identified.
""" """
EVENT_SORT_KEY = attrgetter('created')
id = Column(BigInteger, Sequence('device_seq'), primary_key=True) id = Column(BigInteger, Sequence('device_seq'), primary_key=True)
id.comment = """ id.comment = """
@ -77,6 +79,11 @@ class Device(Thing):
'color' 'color'
} }
def __init__(self, **kw) -> None:
super().__init__(**kw)
with suppress(TypeError):
self.hid = Naming.hid(self.manufacturer, self.serial_number, self.model)
@property @property
def events(self) -> list: def events(self) -> list:
""" """
@ -86,12 +93,25 @@ class Device(Thing):
Events are returned by ascending creation time. Events are returned by ascending creation time.
""" """
return sorted(chain(self.events_multiple, self.events_one), key=attrgetter('created')) return sorted(chain(self.events_multiple, self.events_one), key=self.EVENT_SORT_KEY)
def __init__(self, **kw) -> None: @property
super().__init__(**kw) def problems(self):
with suppress(TypeError): """Current events with severity.Warning or higher.
self.hid = Naming.hid(self.manufacturer, self.serial_number, self.model)
There can be up to 3 events: current Snapshot,
current Physical event, current Trading event.
"""
from ereuse_devicehub.resources.device import states
from ereuse_devicehub.resources.event.models import Snapshot
events = set()
with suppress(LookupError, ValueError):
events.add(self.last_event_of(Snapshot))
with suppress(LookupError, ValueError):
events.add(self.last_event_of(*states.Physical.events()))
with suppress(LookupError, ValueError):
events.add(self.last_event_of(*states.Trading.events()))
return self._warning_events(events)
@property @property
def physical_properties(self) -> Dict[str, object or None]: def physical_properties(self) -> Dict[str, object or None]:
@ -164,6 +184,20 @@ class Device(Thing):
event = self.last_event_of(Receive) event = self.last_event_of(Receive)
return event.agent return event.agent
@property
def working(self):
"""A list of the current tests with warning or errors. A
device is working if the list is empty.
This property returns, for the last test performed of each type,
the one with the worst severity of them, or None if no
test has been executed.
"""
from ereuse_devicehub.resources.event.models import Test
current_tests = unique_everseen((e for e in reversed(self.events) if isinstance(e, Test)),
key=attrgetter('type')) # last test of each type
return self._warning_events(current_tests)
@declared_attr @declared_attr
def __mapper_args__(cls): def __mapper_args__(cls):
""" """
@ -188,6 +222,10 @@ class Device(Thing):
except StopIteration: except StopIteration:
raise LookupError('{!r} does not contain events of types {}.'.format(self, types)) raise LookupError('{!r} does not contain events of types {}.'.format(self, types))
def _warning_events(self, events):
return sorted((ev for ev in events if ev.severity >= Severity.Warning),
key=self.EVENT_SORT_KEY)
def __lt__(self, other): def __lt__(self, other):
return self.id < other.id return self.id < other.id
@ -255,7 +293,7 @@ class Computer(Device):
@property @property
def events(self) -> list: def events(self) -> list:
return sorted(chain(super().events, self.events_parent), key=attrgetter('created')) return sorted(chain(super().events, self.events_parent), key=self.EVENT_SORT_KEY)
@property @property
def ram_size(self) -> int: def ram_size(self) -> int:
@ -294,6 +332,17 @@ class Computer(Device):
speeds[net.wireless] = max(net.speed or 0, speeds[net.wireless] or 0) speeds[net.wireless] = max(net.speed or 0, speeds[net.wireless] or 0)
return speeds return speeds
@property
def privacy(self):
"""Returns the privacy of all DataStorage components when
it is None.
"""
return set(
privacy for privacy in
(hdd.privacy for hdd in self.components if isinstance(hdd, DataStorage))
if privacy
)
def __format__(self, format_spec): def __format__(self, format_spec):
if not format_spec: if not format_spec:
return super().__format__(format_spec) return super().__format__(format_spec)
@ -405,7 +454,7 @@ class Component(Device):
@property @property
def events(self) -> list: def events(self) -> list:
return sorted(chain(super().events, self.events_components), key=attrgetter('created')) return sorted(chain(super().events, self.events_components), key=self.EVENT_SORT_KEY)
class JoinedComponentTableMixin: class JoinedComponentTableMixin:
@ -431,11 +480,12 @@ class DataStorage(JoinedComponentTableMixin, Component):
@property @property
def privacy(self): def privacy(self):
"""Returns the privacy compliance state of the data storage.""" """Returns the privacy compliance state of the data storage."""
# todo add physical destruction event
from ereuse_devicehub.resources.event.models import EraseBasic from ereuse_devicehub.resources.event.models import EraseBasic
with suppress(LookupError): try:
erase = self.last_event_of(EraseBasic) ev = self.last_event_of(EraseBasic)
return DataStoragePrivacyCompliance.from_erase(erase) except LookupError:
ev = None
return ev
def __format__(self, format_spec): def __format__(self, format_spec):
v = super().__format__(format_spec) v = super().__format__(format_spec)

View File

@ -1,5 +1,6 @@
from datetime import datetime from datetime import datetime
from typing import Dict, List, Set, Type, Union from operator import attrgetter
from typing import Dict, Generator, Iterable, List, Optional, Set, Type
from boltons import urlutils from boltons import urlutils
from boltons.urlutils import URL from boltons.urlutils import URL
@ -11,8 +12,8 @@ from teal.enums import Layouts
from ereuse_devicehub.resources.agent.models import Agent from ereuse_devicehub.resources.agent.models import Agent
from ereuse_devicehub.resources.device import states from ereuse_devicehub.resources.device import states
from ereuse_devicehub.resources.enums import ComputerChassis, DataStorageInterface, \ from ereuse_devicehub.resources.enums import ComputerChassis, DataStorageInterface, DisplayTech, \
DataStoragePrivacyCompliance, DisplayTech, PrinterTechnology, RamFormat, RamInterface PrinterTechnology, RamFormat, RamInterface
from ereuse_devicehub.resources.event import models as e from ereuse_devicehub.resources.event import models as e
from ereuse_devicehub.resources.image.models import ImageList from ereuse_devicehub.resources.image.models import ImageList
from ereuse_devicehub.resources.lot.models import Lot from ereuse_devicehub.resources.lot.models import Lot
@ -21,6 +22,8 @@ from ereuse_devicehub.resources.tag import Tag
class Device(Thing): class Device(Thing):
EVENT_SORT_KEY = attrgetter('created')
id = ... # type: Column id = ... # type: Column
type = ... # type: Column type = ... # type: Column
hid = ... # type: Column hid = ... # type: Column
@ -48,7 +51,6 @@ class Device(Thing):
self.height = ... # type: float self.height = ... # type: float
self.depth = ... # type: float self.depth = ... # type: float
self.color = ... # type: Color self.color = ... # type: Color
self.events = ... # type: List[e.Event]
self.physical_properties = ... # type: Dict[str, object or None] self.physical_properties = ... # type: Dict[str, object or None]
self.events_multiple = ... # type: Set[e.EventWithMultipleDevices] self.events_multiple = ... # type: Set[e.EventWithMultipleDevices]
self.events_one = ... # type: Set[e.EventWithOneDevice] self.events_one = ... # type: Set[e.EventWithOneDevice]
@ -57,33 +59,48 @@ class Device(Thing):
self.lots = ... # type: Set[Lot] self.lots = ... # type: Set[Lot]
self.production_date = ... # type: datetime self.production_date = ... # type: datetime
@property
def events(self) -> List[e.Event]:
pass
@property
def problems(self) -> List[e.Event]:
pass
@property @property
def url(self) -> urlutils.URL: def url(self) -> urlutils.URL:
pass pass
@property @property
def rate(self) -> Union[e.AggregateRate, None]: def rate(self) -> Optional[e.AggregateRate]:
pass pass
@property @property
def price(self) -> Union[e.Price, None]: def price(self) -> Optional[e.Price]:
pass pass
@property @property
def trading(self) -> Union[states.Trading, None]: def trading(self) -> Optional[states.Trading]:
pass pass
@property @property
def physical(self) -> Union[states.Physical, None]: def physical(self) -> Optional[states.Physical]:
pass pass
@property @property
def physical_possessor(self) -> Union[Agent, None]: def physical_possessor(self) -> Optional[Agent]:
pass
@property
def working(self) -> List[e.Test]:
pass pass
def last_event_of(self, *types: Type[e.Event]) -> e.Event: def last_event_of(self, *types: Type[e.Event]) -> e.Event:
pass pass
def _warning_events(self, events: Iterable[e.Event]) -> Generator[e.Event]:
pass
class DisplayMixin: class DisplayMixin:
technology = ... # type: Column technology = ... # type: Column
@ -139,6 +156,10 @@ class Computer(DisplayMixin, Device):
def network_speeds(self) -> List[int]: def network_speeds(self) -> List[int]:
pass pass
@property
def privacy(self) -> Set[e.EraseBasic]:
pass
class Desktop(Computer): class Desktop(Computer):
pass pass
@ -219,7 +240,7 @@ class DataStorage(Component):
self.interface = ... # type: DataStorageInterface self.interface = ... # type: DataStorageInterface
@property @property
def privacy(self) -> DataStoragePrivacyCompliance: def privacy(self) -> Optional[e.EraseBasic]:
pass pass

View File

@ -8,9 +8,8 @@ from teal.marshmallow import EnumField, SanitizedStr, URL, ValidationError
from teal.resource import Schema from teal.resource import Schema
from ereuse_devicehub.marshmallow import NestedOn from ereuse_devicehub.marshmallow import NestedOn
from ereuse_devicehub.resources import enums
from ereuse_devicehub.resources.device import models as m, states from ereuse_devicehub.resources.device import models as m, states
from ereuse_devicehub.resources.enums import ComputerChassis, DataStorageInterface, \
DataStoragePrivacyCompliance, DisplayTech, PrinterTechnology, RamFormat, RamInterface
from ereuse_devicehub.resources.models import STR_BIG_SIZE, STR_SIZE from ereuse_devicehub.resources.models import STR_BIG_SIZE, STR_SIZE
from ereuse_devicehub.resources.schemas import Thing, UnitCodes from ereuse_devicehub.resources.schemas import Thing, UnitCodes
@ -31,6 +30,7 @@ class Device(Thing):
depth = Float(validate=Range(0.1, 5), unit=UnitCodes.m, description=m.Device.depth.comment) depth = Float(validate=Range(0.1, 5), unit=UnitCodes.m, description=m.Device.depth.comment)
events = NestedOn('Event', many=True, dump_only=True, description=m.Device.events.__doc__) events = NestedOn('Event', many=True, dump_only=True, description=m.Device.events.__doc__)
events_one = NestedOn('Event', many=True, load_only=True, collection_class=OrderedSet) events_one = NestedOn('Event', many=True, load_only=True, collection_class=OrderedSet)
problems = NestedOn('Event', many=True, dump_only=True, description=m.Device.problems.__doc__)
url = URL(dump_only=True, description=m.Device.url.__doc__) url = URL(dump_only=True, description=m.Device.url.__doc__)
lots = NestedOn('Lot', lots = NestedOn('Lot',
many=True, many=True,
@ -44,6 +44,10 @@ class Device(Thing):
production_date = DateTime('iso', production_date = DateTime('iso',
description=m.Device.updated.comment, description=m.Device.updated.comment,
data_key='productionDate') data_key='productionDate')
working = NestedOn('Event',
many=True,
dump_only=True,
description=m.Device.working.__doc__)
@pre_load @pre_load
def from_events_to_events_one(self, data: dict): def from_events_to_events_one(self, data: dict):
@ -72,12 +76,13 @@ class Device(Thing):
class Computer(Device): class Computer(Device):
components = NestedOn('Component', many=True, dump_only=True, collection_class=OrderedSet) components = NestedOn('Component', many=True, dump_only=True, collection_class=OrderedSet)
chassis = EnumField(ComputerChassis, required=True) chassis = EnumField(enums.ComputerChassis, required=True)
ram_size = Integer(dump_only=True, data_key='ramSize') ram_size = Integer(dump_only=True, data_key='ramSize')
data_storage_size = Integer(dump_only=True, data_key='dataStorageSize') data_storage_size = Integer(dump_only=True, data_key='dataStorageSize')
processor_model = Str(dump_only=True, data_key='processorModel') processor_model = Str(dump_only=True, data_key='processorModel')
graphic_card_model = Str(dump_only=True, data_key='graphicCardModel') graphic_card_model = Str(dump_only=True, data_key='graphicCardModel')
network_speeds = List(Integer(dump_only=True), dump_only=True, data_key='networkSpeeds') network_speeds = List(Integer(dump_only=True), dump_only=True, data_key='networkSpeeds')
privacy = NestedOn('Event', many=True, dump_only=True, collection_class=set)
class Desktop(Computer): class Desktop(Computer):
@ -94,7 +99,7 @@ class Server(Computer):
class DisplayMixin: class DisplayMixin:
size = Float(description=m.DisplayMixin.size.comment, validate=Range(2, 150)) size = Float(description=m.DisplayMixin.size.comment, validate=Range(2, 150))
technology = EnumField(DisplayTech, technology = EnumField(enums.DisplayTech,
description=m.DisplayMixin.technology.comment) description=m.DisplayMixin.technology.comment)
resolution_width = Integer(data_key='resolutionWidth', resolution_width = Integer(data_key='resolutionWidth',
validate=Range(10, 20000), validate=Range(10, 20000),
@ -168,8 +173,8 @@ class DataStorage(Component):
size = Integer(validate=Range(0, 10 ** 8), size = Integer(validate=Range(0, 10 ** 8),
unit=UnitCodes.mbyte, unit=UnitCodes.mbyte,
description=m.DataStorage.size.comment) description=m.DataStorage.size.comment)
interface = EnumField(DataStorageInterface) interface = EnumField(enums.DataStorageInterface)
privacy = EnumField(DataStoragePrivacyCompliance, dump_only=True) privacy = NestedOn('Event', dump_only=True)
class HardDrive(DataStorage): class HardDrive(DataStorage):
@ -203,8 +208,8 @@ class Processor(Component):
class RamModule(Component): class RamModule(Component):
size = Integer(validate=Range(min=128, max=17000), unit=UnitCodes.mbyte) size = Integer(validate=Range(min=128, max=17000), unit=UnitCodes.mbyte)
speed = Integer(validate=Range(min=100, max=10000), unit=UnitCodes.mhz) speed = Integer(validate=Range(min=100, max=10000), unit=UnitCodes.mhz)
interface = EnumField(RamInterface) interface = EnumField(enums.RamInterface)
format = EnumField(RamFormat) format = EnumField(enums.RamFormat)
class SoundCard(Component): class SoundCard(Component):
@ -264,7 +269,7 @@ class WirelessAccessPoint(Networking):
class Printer(Device): class Printer(Device):
wireless = Boolean(required=True, missing=False) wireless = Boolean(required=True, missing=False)
scanning = Boolean(required=True, missing=False) scanning = Boolean(required=True, missing=False)
technology = EnumField(PrinterTechnology, required=True) technology = EnumField(enums.PrinterTechnology, required=True)
monochrome = Boolean(required=True, missing=True) monochrome = Boolean(required=True, missing=True)

View File

@ -260,25 +260,6 @@ class ReceiverRole(Enum):
Transporter = 'An user that ships the devices to another one.' Transporter = 'An user that ships the devices to another one.'
class DataStoragePrivacyCompliance(Enum):
EraseBasic = 'EraseBasic'
EraseBasicError = 'EraseBasicError'
EraseSectors = 'EraseSectors'
EraseSectorsError = 'EraseSectorsError'
Destruction = 'Destruction'
DestructionError = 'DestructionError'
@classmethod
def from_erase(cls, erasure) -> 'DataStoragePrivacyCompliance':
"""Returns the correct enum depending of the passed-in erasure."""
from ereuse_devicehub.resources.event.models import EraseSectors
if isinstance(erasure, EraseSectors):
c = cls.EraseSectors if erasure.severity != Severity.Error else cls.EraseSectorsError
else:
c = cls.EraseBasic if erasure.severity == Severity.Error else cls.EraseBasicError
return c
class PrinterTechnology(Enum): class PrinterTechnology(Enum):
"""Technology of the printer.""" """Technology of the printer."""
Toner = 'Toner / Laser' Toner = 'Toner / Laser'

View File

@ -299,6 +299,12 @@ class EraseSectors(EraseBasic):
pass pass
class ErasePhysical(EraseBasic):
"""Physical destruction of a data storage unit."""
# todo add attributes
pass
class Step(db.Model): class Step(db.Model):
erasure_id = Column(UUID(as_uuid=True), ForeignKey(EraseBasic.id), primary_key=True) erasure_id = Column(UUID(as_uuid=True), ForeignKey(EraseBasic.id), primary_key=True)
type = Column(Unicode(STR_SM_SIZE), nullable=False) type = Column(Unicode(STR_SM_SIZE), nullable=False)

View File

@ -73,6 +73,11 @@ def test_device_model():
assert d.GraphicCard.query.first() is None, 'We should have deleted it it was inside the pc' assert d.GraphicCard.query.first() is None, 'We should have deleted it it was inside the pc'
@pytest.mark.xfail(reason='Test not developed')
def test_device_problems():
pass
@pytest.mark.usefixtures(conftest.app_context.__name__) @pytest.mark.usefixtures(conftest.app_context.__name__)
def test_device_schema(): def test_device_schema():
"""Ensures the user does not upload non-writable or extra fields.""" """Ensures the user does not upload non-writable or extra fields."""

View File

@ -86,18 +86,35 @@ def test_erase_sectors_steps():
@pytest.mark.usefixtures(conftest.auth_app_context.__name__) @pytest.mark.usefixtures(conftest.auth_app_context.__name__)
def test_test_data_storage(): def test_test_data_storage_working():
"""Tests TestDataStorage with the resulting properties in Device."""
hdd = HardDrive(serial_number='foo', manufacturer='bar', model='foo-bar')
test = models.TestDataStorage( test = models.TestDataStorage(
device=HardDrive(serial_number='foo', manufacturer='bar', model='foo-bar'), device=hdd,
severity=Severity.Info, severity=Severity.Error,
elapsed=timedelta(minutes=25), elapsed=timedelta(minutes=25),
length=TestDataStorageLength.Short, length=TestDataStorageLength.Short,
status='ok!', status=':-(',
lifetime=timedelta(days=120) lifetime=timedelta(days=120)
) )
db.session.add(test) db.session.add(test)
db.session.commit() db.session.flush()
assert models.TestDataStorage.query.one() assert hdd.working == [test]
assert not hdd.problems
# Add new test overriding the first test in the problems
# / working condition
test2 = models.TestDataStorage(
device=hdd,
severity=Severity.Warning,
elapsed=timedelta(minutes=25),
length=TestDataStorageLength.Short,
status=':-(',
lifetime=timedelta(days=120)
)
db.session.add(test2)
db.session.flush()
assert hdd.working == [test2]
assert hdd.problems == []
@pytest.mark.usefixtures(conftest.auth_app_context.__name__) @pytest.mark.usefixtures(conftest.auth_app_context.__name__)

View File

@ -289,8 +289,10 @@ def test_snapshot_component_containing_components(user: UserClient):
user.post(s, res=Snapshot, status=ValidationError) user.post(s, res=Snapshot, status=ValidationError)
def test_erase(user: UserClient): def test_erase_privacy(user: UserClient):
"""Tests a Snapshot with EraseSectors.""" """Tests a Snapshot with EraseSectors and the resulting
privacy properties.
"""
s = file('erase-sectors.snapshot') s = file('erase-sectors.snapshot')
snapshot = snapshot_and_check(user, s, (EraseSectors.t,), perform_second_snapshot=True) snapshot = snapshot_and_check(user, s, (EraseSectors.t,), perform_second_snapshot=True)
storage, *_ = snapshot['components'] storage, *_ = snapshot['components']
@ -312,14 +314,19 @@ def test_erase(user: UserClient):
assert step['type'] == 'StepZero' assert step['type'] == 'StepZero'
assert step['severity'] == 'Info' assert step['severity'] == 'Info'
assert 'num' not in step assert 'num' not in step
assert storage['privacy'] == erasure['device']['privacy'] == 'EraseSectors' assert storage['privacy']['type'] == 'EraseSectors'
pc, _ = user.get(res=m.Device, item=snapshot['device']['id'])
assert pc['privacy'] == [storage['privacy']]
# Let's try a second erasure with an error # Let's try a second erasure with an error
s['uuid'] = uuid4() s['uuid'] = uuid4()
s['components'][0]['events'][0]['severity'] = 'Error' s['components'][0]['events'][0]['severity'] = 'Error'
snapshot, _ = user.post(s, res=Snapshot) snapshot, _ = user.post(s, res=Snapshot)
assert snapshot['components'][0]['hid'] == 'c1mr-c1s-c1ml' storage, _ = user.get(res=m.Device, item=storage['id'])
assert snapshot['components'][0]['privacy'] == 'EraseSectorsError' assert storage['hid'] == 'c1mr-c1s-c1ml'
assert storage['privacy']['type'] == 'EraseSectors'
pc, _ = user.get(res=m.Device, item=snapshot['device']['id'])
assert pc['privacy'] == [storage['privacy']]
def test_test_data_storage(user: UserClient): def test_test_data_storage(user: UserClient):
@ -330,7 +337,7 @@ def test_test_data_storage(user: UserClient):
ev for ev in snapshot['events'] ev for ev in snapshot['events']
if ev.get('reallocatedSectorCount', None) == 15 if ev.get('reallocatedSectorCount', None) == 15
) )
incidence_test['severity'] == 'Error' assert incidence_test['severity'] == 'Error'
def test_snapshot_computer_monitor(user: UserClient): def test_snapshot_computer_monitor(user: UserClient):