import json import logging from copy import copy from django.core.exceptions import ObjectDoesNotExist from django.db import models from django.forms import model_to_dict from lib import FactoryMixin class RouterObjectCollection(set, FactoryMixin): _objectclass = None def _check_member_class(self, member): if self._objectclass: if not isinstance(member, self._objectclass): raise ValueError( f'Only {self._objectclass.__class__.__name__} can be added to {self.__class__.__name__}') else: if not isinstance(member, RouterObjectAbstract): raise ValueError( f'Only {RouterObjectAbstract.__class__.__name__} can be added to {self.__class__.__name__}') self._objectclass = member.__class__ def add(self, *args, **kwargs): """Override add() to check if object is of correct type we need a __hash__() function for this""" self._check_member_class(args[0]) return super().add(*args, **kwargs) def filter(self, *args, **kwargs): """This returns a new object filtered by some criteria """ _new = self.__class__() for obj in self: if obj.filtered(*args, **kwargs): _new.add(obj) return _new def all(self): """This returns all objects - a copy""" return self.copy() def first(self, *args, **kwargs): """This returns the first object from a copy of self""" if args or kwargs: objects = self.filter(*args, **kwargs) else: objects = self.all() return objects[0] if objects else None def remove(self, *args, **kwargs): """Override remove() to not throw KeyError (like discard())""" try: super().remove(*args, **kwargs) except KeyError: pass return self def values_list(self, keyset: list, flat=False): if flat: _ret = [] else: _ret = self.__class__() for obj in self: new_obj = obj.__class__(**{k: v for k, v in copy(obj).items() if k in keyset}) if flat: valuesdict = {} for k, v in new_obj.items(): valuesdict.setdefault(k, []).append(v) return list(valuesdict.items()) else: _ret.add(new_obj) return _ret class RoutedModelAbstract(models.Model): class Meta: abstract = True internal_id = models.BigAutoField(primary_key=True) @property def router_object(self): return self._router_object @router_object.setter def router_object(self, value): self._router_object = value def __init__(self, *args, **kwargs): self._router_object = kwargs.pop('router_object', None) super().__init__(*args, **kwargs) self._old_values = self.to_json @property def to_json(self): return model_to_dict(self) @property def unique_on_router(self): raise NotImplemented(f'{self.__class__.__name__} must implement unique_on_router') class RouterObjectAbstract(dict, FactoryMixin): _router_instance = None _model_class = None _model_instance = None def __init__(self, router=None, model=None, **kwargs): if router: self.set_router(router) if model: self.set_model(model) super().__init__(**kwargs) class DoesNotExistsOnRouter(ObjectDoesNotExist): pass @property def model_class(self): return self._model_class @property def get_from_db(self): raise NotImplemented(f"{self.__class__.__name__} must implement get_from_db") @model_class.setter def model_class(self, model): if isinstance(model, RoutedModelAbstract): self._model_class = model.__class__ elif issubclass(model, RoutedModelAbstract): self._model_class = model else: raise ValueError( f"model must be of type {RoutedModelAbstract.__class__.__name__}, not {model.__class__.__name__}") @property def model_instance(self): return self._model_instance @model_instance.setter def model_instance(self, model): if self._model_class and not issubclass(model.__class__, self._model_class): raise ValueError( f'Model {model.__class__.__name__} must be of type {self._model_class.__class__.__name__}') if not isinstance(model, RoutedModelAbstract): raise ValueError( f"model must be of type {RoutedModelAbstract.__class__.__name__}, not {model.__class__.__name__}") self._model_instance = model self._model_class = model.__class_ model.router_object = self def set_model(self, model): if isinstance(model, RoutedModelAbstract): self.model_instance = model else: self.model_class = model.__class__ if not self._model_instance: logging.debug(f'Creating new unsaved {self._model_class.__name__} from {self}') self._model_instance = self.model_class(**({'router_object': self} | self)) return self def set_router(self, router): assert isinstance(router, RouterAbstract), f"router must be of type {RouterAbstract.__class__.__name__}, not {router.__class__.__name__}" self._router_instance = router return self def to_db_object(self, raise_on_keyerror=False) -> models.Model: """This returns a dict representation of the object""" if raise_on_keyerror: _data = {k: self.get(k, '') for k in model_to_dict(self._model_class).keys()} else: _errors = [] _data = {} for k in model_to_dict(self._model_class): try: _data[k] = self[k] except KeyError as e: _errors.append(str(e)) if _errors: raise KeyError(f'Could not convert {self.__class__.__name__} to DB object - missing keys: {_errors}') return self._model_class(**_data) @classmethod def from_db_object(cls, db_object): return cls(**model_to_dict(db_object)) @property def router(self): return self._router_instance def __hash__(self): return len(self.to_json) def _filter_or(self, **kwargs): for k, v in kwargs.items(): if self.get(k) == v: return True return False def _filter_and(self, **kwargs): for k, v in kwargs.items(): if self.get(k) != v: return False return True def filtered(self, mode='or', raise_on_failure=False, **kwargs): """This returns objects filtered by some criteria Return self if criterias match, else None """ assert mode in ('or', 'and'), f"mode must be 'or' or 'and', not {mode}" if getattr(self, f'_filter_{mode}')(**kwargs): return self if raise_on_failure: raise self.DoesNotExists(f'Object {self} does not match criteria {kwargs}') return None def __getattr__(self, name): """This makes a 'magic' trick""" if name in self: return self[name] raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'") def to_json(self, **dump_params): """This returns a dict representation of the object""" if default_fun := dump_params.pop('default', None): pass else: default_fun = lambda o: str(o) return json.loads(json.dumps(self, default=default_fun, **dump_params)) @classmethod def router_get(cls, *args, **kwargs): """This returns a RouterObjectCollection of objects""" raise NotImplemented(f"{cls.__name__} must implement router_get()") def router_post(self): """This adds an object to the router""" raise NotImplemented(f"{self.__class__.__name__} must implement router_post()") def router_put(self, **kwargs): """This removes an object from the router""" raise NotImplemented(f"{self.__class__.__name__} must implement router_put()") def router_delete(self): """This removes an object from the router""" raise NotImplemented(f"{self.__class__.__name__} must implement router_delete()") class DNSStaticAbstract(RoutedModelAbstract): """The DNSStatic Object""" class Meta: abstract = True @property def get_name(self): raise NotImplemented(f'{self.__class__.__name__} must implement get_name') @property def get_regex(self): raise NotImplemented(f'{self.__class__.__name__} must implement get_name') @property def get_ip4(self): raise NotImplemented(f'{self.__class__.__name__} must implement get_ip4') @property def get_ip6(self): raise NotImplemented(f'{self.__class__.__name__} must implement get_ip6') class LeaseAbstract(RoutedModelAbstract): """The IP Lease Object""" class Meta: abstract = True @property def get_mac(self): raise NotImplemented(f'{self.__class__.__name__} must implement get_mac') @property def get_ip4(self): raise NotImplemented(f'{self.__class__.__name__} must implement get_ip4') @property def get_ip6(self): raise NotImplemented(f'{self.__class__.__name__} must implement get_ip6') @property def get_status(self): raise NotImplemented(f'{self.__class__.__name__} must implement get_status') class IPAddressAbstract(RoutedModelAbstract): """The Address Pool""" class Meta: abstract = True @property def get_address(self): raise NotImplemented(f'{self.__class__.__name__} must implement get_address') @property def get_network(self): raise NotImplemented(f'{self.__class__.__name__} must implement get_network') class RouterAbstract: def __init__(self, *args, **kwargs): self.initialize(*args, **kwargs) @property def api(self): raise NotImplemented(f"{self.__class__.__name__} must implement api or init with api parameter") def initialize(self, *args, **kwargs): """This initializes the connection to the router""" raise NotImplemented(f"{self.__class__.__name__} must implement initialize()") def get(self, **kwargs): raise NotImplemented(f"{self.__class__.__name__} must implement get()") def add(self, **kwargs): raise NotImplemented(f"{self.__class__.__name__} must implement post()") def set(self, **kwargs): raise NotImplemented(f"{self.__class__.__name__} must implement put()") def remove(self, **kwargs): raise NotImplemented(f"{self.__class__.__name__} must implement delete()")