2022-07-12 09:23:55 +00:00
|
|
|
|
import copy
|
2018-11-17 19:21:11 +00:00
|
|
|
|
import difflib
|
2018-04-27 17:16:43 +00:00
|
|
|
|
from itertools import groupby
|
2018-05-30 10:49:40 +00:00
|
|
|
|
from typing import Iterable, Set
|
2018-04-27 17:16:43 +00:00
|
|
|
|
|
2018-11-17 19:21:11 +00:00
|
|
|
|
import yaml
|
2020-11-06 16:10:32 +00:00
|
|
|
|
from flask import g
|
2018-07-14 14:41:22 +00:00
|
|
|
|
from sqlalchemy import inspect
|
|
|
|
|
from sqlalchemy.exc import IntegrityError
|
|
|
|
|
from sqlalchemy.util import OrderedSet
|
2018-09-07 10:38:02 +00:00
|
|
|
|
from teal.db import ResourceNotFound
|
|
|
|
|
from teal.marshmallow import ValidationError
|
2018-07-14 14:41:22 +00:00
|
|
|
|
|
2018-04-27 17:16:43 +00:00
|
|
|
|
from ereuse_devicehub.db import db
|
2019-05-11 14:27:22 +00:00
|
|
|
|
from ereuse_devicehub.resources.action.models import Remove
|
2022-06-13 15:33:22 +00:00
|
|
|
|
from ereuse_devicehub.resources.device.models import (
|
|
|
|
|
Component,
|
2022-12-13 13:25:44 +00:00
|
|
|
|
Computer,
|
2022-06-13 15:33:22 +00:00
|
|
|
|
Device,
|
2022-06-28 15:40:00 +00:00
|
|
|
|
Placeholder,
|
2022-06-13 15:33:22 +00:00
|
|
|
|
)
|
2018-05-30 10:49:40 +00:00
|
|
|
|
from ereuse_devicehub.resources.tag.model import Tag
|
2018-04-27 17:16:43 +00:00
|
|
|
|
|
2021-11-26 13:08:11 +00:00
|
|
|
|
DEVICES_ALLOW_DUPLICITY = [
|
|
|
|
|
'RamModule',
|
|
|
|
|
'Display',
|
|
|
|
|
'SoundCard',
|
|
|
|
|
'Battery',
|
|
|
|
|
'Camera',
|
|
|
|
|
'GraphicCard',
|
|
|
|
|
]
|
|
|
|
|
|
2022-10-24 15:58:25 +00:00
|
|
|
|
|
2018-04-27 17:16:43 +00:00
|
|
|
|
class Sync:
|
|
|
|
|
"""Synchronizes the device and components with the database."""
|
|
|
|
|
|
2022-06-13 15:33:22 +00:00
|
|
|
|
def run(
|
|
|
|
|
self, device: Device, components: Iterable[Component] or None
|
|
|
|
|
) -> (Device, OrderedSet):
|
2019-06-19 11:35:26 +00:00
|
|
|
|
"""Synchronizes the device and components with the database.
|
2018-04-27 17:16:43 +00:00
|
|
|
|
|
|
|
|
|
Identifies if the device and components exist in the database
|
|
|
|
|
and updates / inserts them as necessary.
|
|
|
|
|
|
2018-05-30 10:49:40 +00:00
|
|
|
|
Passed-in parameters have to be transient, or said differently,
|
|
|
|
|
not-db-synced objects, or otherwise they would end-up being
|
|
|
|
|
added in the session. `Learn more... <http://docs.sqlalchemy.org/
|
|
|
|
|
en/latest/orm/session_state_management.html#quickie-intro-to
|
|
|
|
|
-object-states>`_.
|
|
|
|
|
|
2018-04-27 17:16:43 +00:00
|
|
|
|
This performs Add / Remove as necessary.
|
2018-05-30 10:49:40 +00:00
|
|
|
|
|
2018-04-27 17:16:43 +00:00
|
|
|
|
:param device: The device to add / update to the database.
|
|
|
|
|
:param components: Components that are inside of the device.
|
2019-05-11 14:27:22 +00:00
|
|
|
|
This method performs Add and Remove actions
|
2018-04-27 17:16:43 +00:00
|
|
|
|
so the device ends up with these components.
|
|
|
|
|
Components are added / updated accordingly.
|
|
|
|
|
If this is empty, all components are removed.
|
2018-05-30 10:49:40 +00:00
|
|
|
|
If this is None, it means that we are not
|
|
|
|
|
providing info about the components, in which
|
|
|
|
|
case we keep the already existing components
|
|
|
|
|
of the device –we don't touch them.
|
2018-04-27 17:16:43 +00:00
|
|
|
|
:return: A tuple of:
|
2018-05-30 10:49:40 +00:00
|
|
|
|
|
2018-04-30 17:58:19 +00:00
|
|
|
|
1. The device from the database (with an ID) whose
|
2018-07-14 14:41:22 +00:00
|
|
|
|
``components`` field contain the db version
|
2018-04-30 17:58:19 +00:00
|
|
|
|
of the passed-in components.
|
|
|
|
|
2. A list of Add / Remove (not yet added to session).
|
2018-04-27 17:16:43 +00:00
|
|
|
|
"""
|
2022-12-14 10:42:11 +00:00
|
|
|
|
if components:
|
|
|
|
|
device.components = OrderedSet(components)
|
|
|
|
|
device.set_hid()
|
|
|
|
|
device.components = OrderedSet()
|
2018-05-30 10:49:40 +00:00
|
|
|
|
db_device = self.execute_register(device)
|
2022-10-24 15:46:57 +00:00
|
|
|
|
|
2019-05-11 14:27:22 +00:00
|
|
|
|
db_components, actions = OrderedSet(), OrderedSet()
|
2018-04-30 17:58:19 +00:00
|
|
|
|
if components is not None: # We have component info (see above)
|
2018-09-21 08:43:15 +00:00
|
|
|
|
if not isinstance(db_device, Computer):
|
|
|
|
|
# Until a good reason is given, we synthetically forbid
|
|
|
|
|
# non-computers with components
|
|
|
|
|
raise ValidationError('Only computers can have components.')
|
2018-04-30 17:58:19 +00:00
|
|
|
|
not_new_components = set()
|
|
|
|
|
for component in components:
|
2022-12-13 19:40:28 +00:00
|
|
|
|
db_component, is_new = self.execute_register_component(component)
|
2018-05-30 10:49:40 +00:00
|
|
|
|
db_components.add(db_component)
|
2018-04-30 17:58:19 +00:00
|
|
|
|
if not is_new:
|
|
|
|
|
not_new_components.add(db_component)
|
|
|
|
|
# We only want to perform Add/Remove to not new components
|
2019-05-11 14:27:22 +00:00
|
|
|
|
actions = self.add_remove(db_device, not_new_components)
|
2018-04-30 17:58:19 +00:00
|
|
|
|
db_device.components = db_components
|
2022-09-08 12:04:49 +00:00
|
|
|
|
|
|
|
|
|
self.create_placeholder(db_device)
|
2019-05-11 14:27:22 +00:00
|
|
|
|
return db_device, actions
|
2018-04-30 17:58:19 +00:00
|
|
|
|
|
2022-12-13 19:40:28 +00:00
|
|
|
|
def execute_register_component(self, component: Component):
|
2019-06-19 11:35:26 +00:00
|
|
|
|
"""Synchronizes one component to the DB.
|
2018-04-27 17:16:43 +00:00
|
|
|
|
|
2018-05-30 10:49:40 +00:00
|
|
|
|
This method is a specialization of :meth:`.execute_register`
|
|
|
|
|
but for components that are inside parents.
|
2018-04-30 17:58:19 +00:00
|
|
|
|
|
2018-05-30 10:49:40 +00:00
|
|
|
|
This method assumes components don't have tags, and it tries
|
|
|
|
|
to identify a non-hid component by finding a
|
|
|
|
|
:meth:`ereuse_devicehub.resources.device.models.Component.
|
|
|
|
|
similar_one`.
|
2018-04-27 17:16:43 +00:00
|
|
|
|
|
2018-05-30 10:49:40 +00:00
|
|
|
|
:param component: The component to sync.
|
2018-04-27 17:16:43 +00:00
|
|
|
|
:param blacklist: A set of components already found by
|
|
|
|
|
Component.similar_one(). Pass-in an empty Set.
|
|
|
|
|
:param parent: For components, the computer that contains them.
|
|
|
|
|
Helper used by Component.similar_one().
|
2018-04-30 17:58:19 +00:00
|
|
|
|
:return: A tuple with:
|
2018-05-30 10:49:40 +00:00
|
|
|
|
- The synced component. See :meth:`.execute_register`
|
|
|
|
|
for more info.
|
|
|
|
|
- A flag stating if the device is new or it already
|
|
|
|
|
existed in the DB.
|
|
|
|
|
"""
|
|
|
|
|
assert inspect(component).transient, 'Component should not be synced from DB'
|
2021-11-26 13:08:11 +00:00
|
|
|
|
# if not is a DataStorage, then need build a new one
|
|
|
|
|
if component.t in DEVICES_ALLOW_DUPLICITY:
|
|
|
|
|
db.session.add(component)
|
|
|
|
|
is_new = True
|
|
|
|
|
return component, is_new
|
|
|
|
|
|
2022-12-14 10:42:11 +00:00
|
|
|
|
db_component = None
|
|
|
|
|
|
2022-12-13 19:40:28 +00:00
|
|
|
|
if component.hid:
|
|
|
|
|
db_component = Device.query.filter_by(
|
|
|
|
|
hid=component.hid, owner_id=g.user.id, placeholder=None
|
|
|
|
|
).first()
|
|
|
|
|
is_new = False
|
2022-12-14 10:42:11 +00:00
|
|
|
|
if not db_component:
|
2018-05-30 10:49:40 +00:00
|
|
|
|
db.session.add(component)
|
|
|
|
|
db_component = component
|
|
|
|
|
is_new = True
|
|
|
|
|
return db_component, is_new
|
|
|
|
|
|
|
|
|
|
def execute_register(self, device: Device) -> Device:
|
2019-06-19 11:35:26 +00:00
|
|
|
|
"""Synchronizes one device to the DB.
|
2018-05-30 10:49:40 +00:00
|
|
|
|
|
|
|
|
|
This method tries to get an existing device using the HID
|
|
|
|
|
or one of the tags, and...
|
|
|
|
|
|
2018-07-14 14:41:22 +00:00
|
|
|
|
- if it already exists it returns a "local synced version"
|
2018-05-30 10:49:40 +00:00
|
|
|
|
–the same ``device`` you passed-in but with updated values
|
|
|
|
|
from the database. In this case we do not
|
|
|
|
|
"touch" any of its values on the DB.
|
|
|
|
|
- If it did not exist, a new device is created in the db.
|
|
|
|
|
|
|
|
|
|
This method validates that all passed-in tags (``device.tags``),
|
|
|
|
|
if linked, are linked to the same device, ditto for the hid.
|
|
|
|
|
Finally it links the tags with the device.
|
|
|
|
|
|
|
|
|
|
If you pass-in a component that is inside a parent, use
|
|
|
|
|
:meth:`.execute_register_component` as it has more specialized
|
|
|
|
|
methods to handle them.
|
|
|
|
|
|
|
|
|
|
:param device: The device to synchronize to the DB.
|
2018-04-27 17:16:43 +00:00
|
|
|
|
:raise NeedsId: The device has not any identifier we can use.
|
|
|
|
|
To still create the device use
|
|
|
|
|
``force_creation``.
|
|
|
|
|
:raise DatabaseError: Any other error from the DB.
|
2018-05-30 10:49:40 +00:00
|
|
|
|
:return: The synced device from the db with the tags linked.
|
2018-04-27 17:16:43 +00:00
|
|
|
|
"""
|
2022-12-13 13:02:38 +00:00
|
|
|
|
db_device = device.get_from_db()
|
2022-06-28 15:40:00 +00:00
|
|
|
|
|
2020-12-04 15:58:53 +00:00
|
|
|
|
if db_device and db_device.allocated:
|
|
|
|
|
raise ResourceNotFound('device is actually allocated {}'.format(device))
|
2022-06-24 09:27:42 +00:00
|
|
|
|
|
2022-12-13 19:40:28 +00:00
|
|
|
|
if not db_device:
|
2018-05-30 10:49:40 +00:00
|
|
|
|
device.tags.clear() # We don't want to add the transient dummy tags
|
|
|
|
|
db.session.add(device)
|
|
|
|
|
db_device = device
|
2018-06-19 16:38:42 +00:00
|
|
|
|
try:
|
|
|
|
|
db.session.flush()
|
|
|
|
|
except IntegrityError as e:
|
|
|
|
|
# Manage 'one tag per organization' unique constraint
|
|
|
|
|
if 'One tag per organization' in e.args[0]:
|
|
|
|
|
# todo test for this
|
2022-12-14 10:42:11 +00:00
|
|
|
|
id = int(e.args[0][135 : e.args[0].index(',', 135)]) # noqa: E203
|
2022-06-13 15:33:22 +00:00
|
|
|
|
raise ValidationError(
|
|
|
|
|
'The device is already linked to tag {} '
|
|
|
|
|
'from the same organization.'.format(id),
|
|
|
|
|
field_names=['device.tags'],
|
|
|
|
|
)
|
2018-06-19 16:38:42 +00:00
|
|
|
|
else:
|
|
|
|
|
raise
|
2018-05-30 10:49:40 +00:00
|
|
|
|
assert db_device is not None
|
|
|
|
|
return db_device
|
2018-04-27 17:16:43 +00:00
|
|
|
|
|
2022-07-12 09:23:55 +00:00
|
|
|
|
@staticmethod
|
|
|
|
|
def create_placeholder(device: Device):
|
|
|
|
|
"""If the device is new, we need create automaticaly a new placeholder"""
|
2022-07-13 09:23:18 +00:00
|
|
|
|
if device.binding:
|
2022-11-03 17:19:56 +00:00
|
|
|
|
for c in device.components:
|
|
|
|
|
if c.phid():
|
|
|
|
|
continue
|
|
|
|
|
c_dict = copy.copy(c.__dict__)
|
|
|
|
|
c_dict.pop('_sa_instance_state')
|
|
|
|
|
c_dict.pop('id', None)
|
|
|
|
|
c_dict.pop('devicehub_id', None)
|
|
|
|
|
c_dict.pop('actions_multiple', None)
|
|
|
|
|
c_dict.pop('actions_one', None)
|
|
|
|
|
c_placeholder = c.__class__(**c_dict)
|
|
|
|
|
c_placeholder.parent = c.parent.binding.device
|
|
|
|
|
c.parent = device
|
|
|
|
|
component_placeholder = Placeholder(
|
|
|
|
|
device=c_placeholder, binding=c, is_abstract=True
|
|
|
|
|
)
|
|
|
|
|
db.session.add(c_placeholder)
|
|
|
|
|
db.session.add(component_placeholder)
|
2022-07-13 09:23:18 +00:00
|
|
|
|
return
|
2022-11-03 17:19:56 +00:00
|
|
|
|
|
2022-07-12 09:23:55 +00:00
|
|
|
|
dict_device = copy.copy(device.__dict__)
|
|
|
|
|
dict_device.pop('_sa_instance_state')
|
2022-07-13 09:23:18 +00:00
|
|
|
|
dict_device.pop('id', None)
|
|
|
|
|
dict_device.pop('devicehub_id', None)
|
|
|
|
|
dict_device.pop('actions_multiple', None)
|
|
|
|
|
dict_device.pop('actions_one', None)
|
|
|
|
|
dict_device.pop('components', None)
|
2022-07-12 09:23:55 +00:00
|
|
|
|
dev_placeholder = device.__class__(**dict_device)
|
2022-09-08 12:04:49 +00:00
|
|
|
|
if hasattr(device, 'components'):
|
|
|
|
|
for c in device.components:
|
|
|
|
|
c_dict = copy.copy(c.__dict__)
|
|
|
|
|
c_dict.pop('_sa_instance_state')
|
|
|
|
|
c_dict.pop('id', None)
|
|
|
|
|
c_dict.pop('devicehub_id', None)
|
|
|
|
|
c_dict.pop('actions_multiple', None)
|
|
|
|
|
c_dict.pop('actions_one', None)
|
|
|
|
|
c_placeholder = c.__class__(**c_dict)
|
|
|
|
|
c_placeholder.parent = dev_placeholder
|
|
|
|
|
c.parent = device
|
|
|
|
|
component_placeholder = Placeholder(
|
|
|
|
|
device=c_placeholder, binding=c, is_abstract=True
|
|
|
|
|
)
|
|
|
|
|
db.session.add(c_placeholder)
|
|
|
|
|
db.session.add(component_placeholder)
|
2022-07-12 09:23:55 +00:00
|
|
|
|
|
2022-07-28 15:48:14 +00:00
|
|
|
|
placeholder = Placeholder(
|
|
|
|
|
device=dev_placeholder, binding=device, is_abstract=True
|
|
|
|
|
)
|
2022-07-12 09:23:55 +00:00
|
|
|
|
db.session.add(dev_placeholder)
|
|
|
|
|
db.session.add(placeholder)
|
2022-06-28 15:40:00 +00:00
|
|
|
|
|
2018-05-30 10:49:40 +00:00
|
|
|
|
@staticmethod
|
2022-06-13 15:33:22 +00:00
|
|
|
|
def add_remove(device: Computer, components: Set[Component]) -> OrderedSet:
|
2019-06-19 11:35:26 +00:00
|
|
|
|
"""Generates the Add and Remove actions (but doesn't add them to
|
2018-04-30 17:58:19 +00:00
|
|
|
|
session).
|
|
|
|
|
|
|
|
|
|
:param device: A device which ``components`` attribute contains
|
|
|
|
|
the old list of components. The components that
|
|
|
|
|
are not in ``components`` will be Removed.
|
|
|
|
|
:param components: List of components that are potentially to
|
|
|
|
|
be Added. Some of them can already exist
|
|
|
|
|
on the device, in which case they won't
|
|
|
|
|
be re-added.
|
2019-05-11 14:27:22 +00:00
|
|
|
|
:return: A list of Add / Remove actions.
|
2018-04-27 17:16:43 +00:00
|
|
|
|
"""
|
2019-05-11 14:27:22 +00:00
|
|
|
|
# Note that we create the Remove actions before the Add ones
|
|
|
|
|
actions = OrderedSet()
|
2018-04-27 17:16:43 +00:00
|
|
|
|
old_components = set(device.components)
|
2018-05-13 13:13:12 +00:00
|
|
|
|
|
2018-04-30 17:58:19 +00:00
|
|
|
|
adding = components - old_components
|
|
|
|
|
if adding:
|
|
|
|
|
# For the components we are adding, let's remove them from their old parents
|
2018-06-10 16:47:49 +00:00
|
|
|
|
def g_parent(component: Component) -> Device:
|
2022-06-13 15:33:22 +00:00
|
|
|
|
return component.parent or Device(
|
|
|
|
|
id=0
|
|
|
|
|
) # Computer with id 0 is our Identity
|
2018-04-30 17:58:19 +00:00
|
|
|
|
|
2022-06-13 15:33:22 +00:00
|
|
|
|
for parent, _components in groupby(
|
|
|
|
|
sorted(adding, key=g_parent), key=g_parent
|
|
|
|
|
):
|
2020-11-06 16:10:32 +00:00
|
|
|
|
set_components = OrderedSet(_components)
|
|
|
|
|
check_owners = (x.owner_id == g.user.id for x in set_components)
|
|
|
|
|
# Is not Computer Identity and all components have the correct owner
|
|
|
|
|
if parent.id != 0 and all(check_owners):
|
|
|
|
|
actions.add(Remove(device=parent, components=set_components))
|
2019-05-11 14:27:22 +00:00
|
|
|
|
return actions
|
2018-05-30 10:49:40 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class MismatchBetweenTags(ValidationError):
|
2022-06-13 15:33:22 +00:00
|
|
|
|
def __init__(self, tag: Tag, other_tag: Tag, field_names={'device.tags'}):
|
|
|
|
|
message = '{!r} and {!r} are linked to different devices.'.format(
|
|
|
|
|
tag, other_tag
|
|
|
|
|
)
|
2018-05-30 10:49:40 +00:00
|
|
|
|
super().__init__(message, field_names)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class MismatchBetweenTagsAndHid(ValidationError):
|
2022-06-13 15:33:22 +00:00
|
|
|
|
def __init__(self, device_id: int, hid: str, field_names={'device.hid'}):
|
|
|
|
|
message = 'Tags are linked to device {} but hid refers to device {}.'.format(
|
|
|
|
|
device_id, hid
|
|
|
|
|
)
|
2018-05-30 10:49:40 +00:00
|
|
|
|
super().__init__(message, field_names)
|
2018-11-17 19:21:11 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class MismatchBetweenProperties(ValidationError):
|
|
|
|
|
def __init__(self, props1, props2, field_names={'device'}):
|
|
|
|
|
message = 'The device from the tag and the passed-in differ the following way:'
|
|
|
|
|
message += '\n'.join(
|
2022-06-13 15:33:22 +00:00
|
|
|
|
difflib.ndiff(
|
|
|
|
|
yaml.dump(props1).splitlines(), yaml.dump(props2).splitlines()
|
|
|
|
|
)
|
2018-11-17 19:21:11 +00:00
|
|
|
|
)
|
|
|
|
|
super().__init__(message, field_names)
|