# Copyright 2017-present Open Networking Foundation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import datetime import time import calendar import json import pytz import inspect import sys import threading import django from django.db import models from django.utils.timezone import now from django.db.models import * from django.db import transaction from django.forms.models import model_to_dict from django.utils import timezone from django.core.exceptions import PermissionDenied from cgi import escape as html_escape from django.db.models.deletion import Collector from django.db import router from django.contrib.contenttypes.models import ContentType from django.core.validators import MaxValueValidator, MinValueValidator from xoskafka import XOSKafkaProducer from xosconfig import Config from multistructlog import create_logger log = create_logger(Config().get("logging")) XOS_GLOBAL_DEFAULT_SECURITY_POLICY = True def get_first_site(): # Hackish solution to Node.site needing a default from site import Site return Site.objects.first().id def json_handler(obj): if isinstance(obj, pytz.tzfile.DstTzInfo): # json can't serialize DstTzInfo return str(obj) elif hasattr(obj, "timetuple"): return calendar.timegm(obj.timetuple()) elif isinstance(obj, QuerySet): # django 1.11.0 - model_to_dict() turns reverse foreign relations into querysets return [x.id for x in obj] elif isinstance(obj, Model): # django 1.11.10 - model_to_dict() turns reverse foreign relations into lists of models return obj.id else: return obj class StrippedCharField(models.CharField): """ CharField that strips trailing and leading spaces.""" def clean(self, value, *args, **kwds): if value is not None: value = value.strip() return super(StrippedCharField, self).clean(value, *args, **kwds) # This manager will be inherited by all subclasses because # the core model is abstract. class XOSBaseDeletionManager(models.Manager): def get_queryset(self): parent = super(XOSBaseDeletionManager, self) if hasattr(parent, "get_queryset"): return parent.get_queryset().filter(deleted=True) else: return parent.get_query_set().filter(deleted=True) # deprecated in django 1.7 in favor of get_queryset(). def get_query_set(self): return self.get_queryset() # This manager will be inherited by all subclasses because # the core model is abstract. class XOSBaseManager(models.Manager): def get_queryset(self): parent = super(XOSBaseManager, self) if hasattr(parent, "get_queryset"): return parent.get_queryset().filter(deleted=False) else: return parent.get_query_set().filter(deleted=False) # deprecated in django 1.7 in favor of get_queryset(). def get_query_set(self): return self.get_queryset() class PlModelMixIn(object): # Provides useful methods for computing which objects in a model have # changed. Make sure to do self._initial = self._dict in the __init__ # method. # Also includes useful utility, like getValidators # This is broken out of XOSBase into a Mixin so the User model can # also make use of it. @property def _dict(self): return model_to_dict(self, fields=[field.name for field in self._meta.fields]) def fields_differ(self, f1, f2): if ( isinstance(f1, datetime.datetime) and isinstance(f2, datetime.datetime) and (timezone.is_aware(f1) != timezone.is_aware(f2)) ): return True else: return f1 != f2 @property def diff(self): d1 = self._initial d2 = self._dict diffs = [(k, (v, d2[k])) for k, v in d1.items() if self.fields_differ(v, d2[k])] return dict(diffs) @property def has_changed(self): return bool(self.diff) @property def changed_fields(self): if self.is_new: return self._dict.keys() return self.diff.keys() @property def is_new(self): return self.pk is None def has_field_changed(self, field_name): return field_name in self.diff.keys() def get_field_diff(self, field_name): return self.diff.get(field_name, None) @classmethod def get_model_class_by_name(cls, name): all_models = django.apps.apps.get_models(include_auto_created=False) all_models_by_name = {} for model in all_models: all_models_by_name[model.__name__] = model return all_models_by_name.get(name) @property def leaf_model(self): leaf_model_name = getattr(self, "leaf_model_name", None) if not leaf_model_name: return self if leaf_model_name == self.__class__.__name__: return self leaf_model_class = self.get_model_class_by_name(self.leaf_model_name) assert self.id if self.deleted: return leaf_model_class.deleted_objects.get(id=self.id) else: return leaf_model_class.objects.get(id=self.id) # classmethod def getValidators(cls): """ primarily for REST API, return a dictionary of field names mapped to lists of the type of validations that need to be applied to those fields. """ validators = {} for field in cls._meta.fields: l = [] if not field.blank: l.append("notBlank") if field.__class__.__name__ == "URLField": l.append("url") validators[field.name] = l return validators def get_backend_register(self, k, default=None): try: return json.loads(self.backend_register).get(k, default) except AttributeError: return default def set_backend_register(self, k, v): br = {} try: br = json.loads(self.backend_register) except AttributeError: br = {} br[k] = v self.backend_register = json.dumps(br) def get_backend_details(self): try: scratchpad = json.loads(self.backend_register) except AttributeError: return (None, None, None, None) try: exponent = scratchpad["exponent"] except KeyError: exponent = None try: last_success_time = scratchpad["last_success"] dt = datetime.datetime.fromtimestamp(last_success_time) last_success = dt.strftime("%Y-%m-%d %H:%M") except KeyError: last_success = None try: failures = scratchpad["failures"] except KeyError: failures = None try: last_failure_time = scratchpad["last_failure"] dt = datetime.datetime.fromtimestamp(last_failure_time) last_failure = dt.strftime("%Y-%m-%d %H:%M") except KeyError: last_failure = None return (exponent, last_success, last_failure, failures) def get_backend_icon(self): is_perfect = ( self.backend_status is not None ) and self.backend_status.startswith("1 -") is_good = (self.backend_status is not None) and ( self.backend_status.startswith("0 -") or self.backend_status.startswith("1 -") ) is_provisioning = ( self.backend_status is None or self.backend_status == "Provisioning in progress" or self.backend_status == "" ) # returns (icon_name, tooltip) if ( (self.enacted is not None) and (self.enacted >= self.updated and is_good) or is_perfect ): return ("success", "successfully enacted") else: if is_good or is_provisioning: return ( "clock", "Pending sync, last_status = " + html_escape(self.backend_status, quote=True), ) else: return ("error", html_escape(self.backend_status, quote=True)) def enforce_choices(self, field, choices): choices = [x[0] for x in choices] for choice in choices: if field == choice: return if (choice is None) and (field == ""): # allow "" and None to be equivalent return raise Exception("Field value %s is not in %s" % (field, str(choices))) def serialize_for_messagebus(self): """ Serialize the object for posting to messagebus. The API serializes ForeignKey fields by naming them <name>_id whereas model_to_dict leaves them with the original name. Modify the results of model_to_dict to provide the same fieldnames. """ field_types = {} for f in self._meta.fields: field_types[f.name] = f.get_internal_type() fields = model_to_dict(self) for k in fields.keys(): if field_types.get(k, None) == "ForeignKey": new_key_name = "%s_id" % k if (k in fields) and (new_key_name not in fields): fields[new_key_name] = fields[k] del fields[k] return fields def push_messagebus_event(self, deleted=False, pk=None): self.push_kafka_event(deleted, pk) def push_kafka_event(self, deleted=False, pk=None): # Transmit update via kafka model = self.serialize_for_messagebus() bases = inspect.getmro(self.__class__) class_names = ",".join([x.__name__ for x in bases]) model["class_names"] = class_names if not pk: pk = self.pk json_dict = {"pk": pk, "changed_fields": self.changed_fields, "object": model} if deleted: json_dict["deleted"] = True json_dict["object"]["id"] = pk topic = "xos.gui_events" key = self.__class__.__name__ json_value = json.dumps(json_dict, default=json_handler) XOSKafkaProducer.produce(topic, key, json_value) class AttributeMixin(object): # helper for extracting things from a json-encoded # service_specific_attribute def get_attribute(self, name, default=None): if self.service_specific_attribute: attributes = json.loads(self.service_specific_attribute) else: attributes = {} return attributes.get(name, default) def set_attribute(self, name, value): if self.service_specific_attribute: attributes = json.loads(self.service_specific_attribute) else: attributes = {} attributes[name] = value self.service_specific_attribute = json.dumps(attributes) def get_initial_attribute(self, name, default=None): if self._initial["service_specific_attribute"]: attributes = json.loads(self._initial["service_specific_attribute"]) else: attributes = {} return attributes.get(name, default) @classmethod def get_default_attribute(cls, name): for (attrname, default) in cls.simple_attributes: if attrname == name: return default if hasattr(cls, "default_attributes"): if name in cls.default_attributes: return cls.default_attributes[name] return None @classmethod def setup_simple_attributes(cls): for (attrname, default) in cls.simple_attributes: setattr( cls, attrname, property( lambda self, attrname=attrname, default=default: self.get_attribute( attrname, default ), lambda self, value, attrname=attrname: self.set_attribute( attrname, value ), None, attrname, ), ) # For cascading deletes, we need a Collector that doesn't do fastdelete, # so we get a full list of models. class XOSCollector(Collector): def can_fast_delete(self, *args, **kwargs): return False class ModelLink: def __init__(self, dest, via, into=None): self.dest = dest self.via = via self.into = into