<gh_stars>10-100
# 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.

from xos.exceptions import *
from xosbase_decl import *


class XOSBase(XOSBase_decl):
    objects = XOSBaseManager()
    deleted_objects = XOSBaseDeletionManager()

    class Meta:
        # Changing abstract to False would require the managers of subclasses of
        # XOSBase to be customized individually.
        abstract = True
        app_label = "core"

    def __init__(self, *args, **kwargs):
        super(XOSBase, self).__init__(*args, **kwargs)
        self._initial = self._dict  # for PlModelMixIn
        self.silent = False

    def get_controller(self):
        return self.controller

    def delete(self, *args, **kwds):
        # so we have something to give the observer
        purge = kwds.get("purge", False)
        if purge:
            del kwds["purge"]
        silent = kwds.get("silent", False)
        if silent:
            del kwds["silent"]
        try:
            purge = purge or observer_disabled
        except NameError:
            pass

        if purge:
            pk = self.pk
            super(XOSBase, self).delete(*args, **kwds)
            self.push_messagebus_event(deleted=True, pk=pk)
        else:
            if not self.write_protect:
                self.deleted = True
                self.enacted = None
                self.policed = None
                self.save(
                    update_fields=["enacted", "deleted", "policed"], silent=silent
                )

                collector = XOSCollector(
                    using=router.db_for_write(self.__class__, instance=self)
                )
                collector.collect([self])
                with transaction.atomic():
                    for (k, models) in collector.data.items():
                        for model in models:
                            if not hasattr(model, "deleted"):
                                # Automatically generated through relations from ManyToMany fields do not have soft-delete
                                # capability.
                                continue
                            if model.deleted:
                                # in case it's already been deleted, don't delete again
                                continue
                            model.deleted = True
                            model.enacted = None
                            model.policed = None
                            model.save(
                                update_fields=["enacted", "deleted", "policed"],
                                silent=silent,
                            )

    def verify_live_keys(self, update_fields):
        """ Check the fields to be updated, if they contain foreign keys, that the foreign keys only point
            to live objects in the database.

            This is to catch races between model policies where an object is being deleted while a model policy is
            still operating on it.
        """

        if getattr(self, "deleted", False):
            # If this model is already deleted, then no need to check anything. We only need to check for live
            # models that point to dead models. If a dead model points to other dead models, then we could
            # be updating something else in the dead model (backend_status, etc)
            return

        for field in self._meta.fields:
            try:
                f = getattr(self, field.name)
            except Exception as e:
                # Exception django.db.models.fields.related.RelatedObjectDoesNotExist
                # is thrown by django when you're creating an object that has a base and the base doesn't exist yet
                continue

            if f is None:
                # If field hold a null value, we don't care
                continue

            ftype = field.get_internal_type()
            if ftype != "ForeignKey":
                # If field isn't a foreign key, we don't care
                continue

            if (update_fields) and (field.name not in update_fields):
                # If update_fields is nonempty, and field is not to be updated, we don't care.
                continue

            if getattr(f, "deleted", False):
                raise Exception(
                    "Attempt to save object with deleted foreign key reference"
                )

    def has_important_changes(self):
        """ Determine whether the model has changes that should be reflected in one of the changed_by_* timestamps.
            Ignores various feedback and bookkeeping state set by synchronizers.
        """
        for field_name in self.changed_fields:
            if field_name in [
                "policed",
                "updated",
                "enacted",
                "changed_by_step",
                "changed_by_policy",
            ]:
                continue
            if field_name.startswith("backend_"):
                continue
            if field_name.startswith("policy_"):
                continue
            return True
        return False

    def save(self, *args, **kwargs):

        # let the user specify silence as either a kwarg or an instance varible
        silent = self.silent
        if "silent" in kwargs:
            silent = silent or kwargs.pop("silent")

        caller_kind = "unknown"

        if "caller_kind" in kwargs:
            caller_kind = kwargs.pop("caller_kind")

        update_fields = None
        if "update_fields" in kwargs:
            # NOTE(smbaker): modifying update_fields will cause kwargs["update_fields"] to be modified. This is
            # intended, as kwargs will be passed to save() below.
            update_fields = kwargs["update_fields"]

        # NOTE(smbaker): always_update_timestamp still has some relevance for event_steps and pull_steps that
        # want to cause an update. For model_policies or sync_steps it should no longer be required.
        always_update_timestamp = False
        if "always_update_timestamp" in kwargs:
            always_update_timestamp = always_update_timestamp or kwargs.pop(
                "always_update_timestamp"
            )

        is_sync_save = False
        if "is_sync_save" in kwargs:
            is_sync_save = kwargs.pop("is_sync_save")

        is_policy_save = False
        if "is_policy_save" in kwargs:
            is_policy_save = kwargs.pop("is_policy_save")

        # validate that only synchronizers can write feedback state

        # this for operator support via xossh in case if you want to force changes in feedback state
        allow_modify_feedback = False
        if "allow_modify_feedback" in kwargs:
            allow_modify_feedback = kwargs.pop("allow_modify_feedback")

        if (
            hasattr(self, "feedback_state_fields")
            and not allow_modify_feedback
            and not self.is_new
        ):
            feedback_changed = [
                field
                for field in self.changed_fields
                if field in self.feedback_state_fields
            ]

            if len(feedback_changed) > 0 and caller_kind != "synchronizer":
                log.error(
                    "A non Synchronizer is trying to update fields marked as feedback_state",
                    model=self._dict,
                    feedback_state_fields=self.feedback_state_fields,
                    caller_kind=caller_kind,
                    feedback_changed=feedback_changed,
                )
                raise XOSPermissionDenied(
                    "A non Synchronizer is trying to update fields marked as feedback_state: %s"
                    % feedback_changed
                )

        if (caller_kind != "synchronizer") or always_update_timestamp:
            # Non-synchronizers update the `updated` timestamp
            self.updated = timezone.now()
        else:
            # We're not auto-setting timestamp, but let's check to make sure that the caller hasn't tried to set our
            # timestamp backward...
            if (self.updated != self._initial["updated"]) and (
                (not update_fields) or ("updated" in update_fields)
            ):
                log.info(
                    "Synchronizer tried to change `updated` timestamp on model %s from %s to %s. Ignored."
                    % (self, self._initial["updated"], self.updated)
                )
                self.updated = self._initial["updated"]

        if is_sync_save and self.has_important_changes():
            self.changed_by_step = timezone.now()
            if update_fields:
                update_fields.append("changed_by_step")

        if is_policy_save and self.has_important_changes():
            self.changed_by_policy = timezone.now()
            if update_fields:
                update_fields.append("changed_by_policy")

        with transaction.atomic():
            self.verify_live_keys(update_fields=update_fields)
            super(XOSBase, self).save(*args, **kwargs)

        self.push_messagebus_event()

        self._initial = self._dict

    def tologdict(self):
        try:
            d = {"model_name": self.__class__.__name__, "pk": self.pk}
        except BaseException:
            d = {}

        return d

    # for the old django admin UI
    def __unicode__(self):
        if hasattr(self, "name") and self.name:
            return u"%s" % self.name
        elif hasattr(self, "id") and self.id:
            if hasattr(self, "leaf_model_name") and self.leaf_model_name:
                return u"%s-%s" % (self.leaf_model_name, self.id)
            else:
                return u"%s-%s" % (self.__class__.__name__, self.id)
        else:
            return u"%s-unsaved" % self.__class__.__name__

    def get_content_type_key(self):
        ct = ContentType.objects.get_for_model(self.__class__)
        return "%s.%s" % (ct.app_label, ct.model)

    @staticmethod
    def get_content_type_from_key(key):
        (app_name, model_name) = key.split(".")
        return ContentType.objects.get_by_natural_key(app_name, model_name)

    @staticmethod
    def get_content_object(content_type, object_id):
        ct = XOSBase.get_content_type_from_key(content_type)
        cls = ct.model_class()
        return cls.objects.get(id=object_id)

# 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.

from xos.exceptions import *
from xosbase_decl import *


class XOSBase(XOSBase_decl):
    objects = XOSBaseManager()
    deleted_objects = XOSBaseDeletionManager()

    class Meta:
        # Changing abstract to False would require the managers of subclasses of
        # XOSBase to be customized individually.
        abstract = True
        app_label = "core"

    def __init__(self, *args, **kwargs):
        super(XOSBase, self).__init__(*args, **kwargs)
        self._initial = self._dict  # for PlModelMixIn
        self.silent = False

    def get_controller(self):
        return self.controller

    def delete(self, *args, **kwds):
        # so we have something to give the observer
        purge = kwds.get("purge", False)
        if purge:
            del kwds["purge"]
        silent = kwds.get("silent", False)
        if silent:
            del kwds["silent"]
        try:
            purge = purge or observer_disabled
        except NameError:
            pass

        if purge:
            pk = self.pk
            super(XOSBase, self).delete(*args, **kwds)
            self.push_messagebus_event(deleted=True, pk=pk)
        else:
            if not self.write_protect:
                self.deleted = True
                self.enacted = None
                self.policed = None
                self.save(
                    update_fields=["enacted", "deleted", "policed"], silent=silent
                )

                collector = XOSCollector(
                    using=router.db_for_write(self.__class__, instance=self)
                )
                collector.collect([self])
                with transaction.atomic():
                    for (k, models) in collector.data.items():
                        for model in models:
                            if not hasattr(model, "deleted"):
                                # Automatically generated through relations from ManyToMany fields do not have soft-delete
                                # capability.
                                continue
                            if model.deleted:
                                # in case it's already been deleted, don't delete again
                                continue
                            model.deleted = True
                            model.enacted = None
                            model.policed = None
                            model.save(
                                update_fields=["enacted", "deleted", "policed"],
                                silent=silent,
                            )

    def verify_live_keys(self, update_fields):
        """ Check the fields to be updated, if they contain foreign keys, that the foreign keys only point
            to live objects in the database.

            This is to catch races between model policies where an object is being deleted while a model policy is
            still operating on it.
        """

        if getattr(self, "deleted", False):
            # If this model is already deleted, then no need to check anything. We only need to check for live
            # models that point to dead models. If a dead model points to other dead models, then we could
            # be updating something else in the dead model (backend_status, etc)
            return

        for field in self._meta.fields:
            try:
                f = getattr(self, field.name)
            except Exception as e:
                # Exception django.db.models.fields.related.RelatedObjectDoesNotExist
                # is thrown by django when you're creating an object that has a base and the base doesn't exist yet
                continue

            if f is None:
                # If field hold a null value, we don't care
                continue

            ftype = field.get_internal_type()
            if ftype != "ForeignKey":
                # If field isn't a foreign key, we don't care
                continue

            if (update_fields) and (field.name not in update_fields):
                # If update_fields is nonempty, and field is not to be updated, we don't care.
                continue

            if getattr(f, "deleted", False):
                raise Exception(
                    "Attempt to save object with deleted foreign key reference"
                )

    def has_important_changes(self):
        """ Determine whether the model has changes that should be reflected in one of the changed_by_* timestamps.
            Ignores various feedback and bookkeeping state set by synchronizers.
        """
        for field_name in self.changed_fields:
            if field_name in [
                "policed",
                "updated",
                "enacted",
                "changed_by_step",
                "changed_by_policy",
            ]:
                continue
            if field_name.startswith("backend_"):
                continue
            if field_name.startswith("policy_"):
                continue
            return True
        return False

    def save(self, *args, **kwargs):

        # let the user specify silence as either a kwarg or an instance varible
        silent = self.silent
        if "silent" in kwargs:
            silent = silent or kwargs.pop("silent")

        caller_kind = "unknown"

        if "caller_kind" in kwargs:
            caller_kind = kwargs.pop("caller_kind")

        update_fields = None
        if "update_fields" in kwargs:
            # NOTE(smbaker): modifying update_fields will cause kwargs["update_fields"] to be modified. This is
            # intended, as kwargs will be passed to save() below.
            update_fields = kwargs["update_fields"]

        # NOTE(smbaker): always_update_timestamp still has some relevance for event_steps and pull_steps that
        # want to cause an update. For model_policies or sync_steps it should no longer be required.
        always_update_timestamp = False
        if "always_update_timestamp" in kwargs:
            always_update_timestamp = always_update_timestamp or kwargs.pop(
                "always_update_timestamp"
            )

        is_sync_save = False
        if "is_sync_save" in kwargs:
            is_sync_save = kwargs.pop("is_sync_save")

        is_policy_save = False
        if "is_policy_save" in kwargs:
            is_policy_save = kwargs.pop("is_policy_save")

        # validate that only synchronizers can write feedback state

        # this for operator support via xossh in case if you want to force changes in feedback state
        allow_modify_feedback = False
        if "allow_modify_feedback" in kwargs:
            allow_modify_feedback = kwargs.pop("allow_modify_feedback")

        if (
            hasattr(self, "feedback_state_fields")
            and not allow_modify_feedback
            and not self.is_new
        ):
            feedback_changed = [
                field
                for field in self.changed_fields
                if field in self.feedback_state_fields
            ]

            if len(feedback_changed) > 0 and caller_kind != "synchronizer":
                log.error(
                    "A non Synchronizer is trying to update fields marked as feedback_state",
                    model=self._dict,
                    feedback_state_fields=self.feedback_state_fields,
                    caller_kind=caller_kind,
                    feedback_changed=feedback_changed,
                )
                raise XOSPermissionDenied(
                    "A non Synchronizer is trying to update fields marked as feedback_state: %s"
                    % feedback_changed
                )

        if (caller_kind != "synchronizer") or always_update_timestamp:
            # Non-synchronizers update the `updated` timestamp
            self.updated = timezone.now()
        else:
            # We're not auto-setting timestamp, but let's check to make sure that the caller hasn't tried to set our
            # timestamp backward...
            if (self.updated != self._initial["updated"]) and (
                (not update_fields) or ("updated" in update_fields)
            ):
                log.info(
                    "Synchronizer tried to change `updated` timestamp on model %s from %s to %s. Ignored."
                    % (self, self._initial["updated"], self.updated)
                )
                self.updated = self._initial["updated"]

        if is_sync_save and self.has_important_changes():
            self.changed_by_step = timezone.now()
            if update_fields:
                update_fields.append("changed_by_step")

        if is_policy_save and self.has_important_changes():
            self.changed_by_policy = timezone.now()
            if update_fields:
                update_fields.append("changed_by_policy")

        with transaction.atomic():
            self.verify_live_keys(update_fields=update_fields)
            super(XOSBase, self).save(*args, **kwargs)

        self.push_messagebus_event()

        self._initial = self._dict

    def tologdict(self):
        try:
            d = {"model_name": self.__class__.__name__, "pk": self.pk}
        except BaseException:
            d = {}

        return d

    # for the old django admin UI
    def __unicode__(self):
        if hasattr(self, "name") and self.name:
            return u"%s" % self.name
        elif hasattr(self, "id") and self.id:
            if hasattr(self, "leaf_model_name") and self.leaf_model_name:
                return u"%s-%s" % (self.leaf_model_name, self.id)
            else:
                return u"%s-%s" % (self.__class__.__name__, self.id)
        else:
            return u"%s-unsaved" % self.__class__.__name__

    def get_content_type_key(self):
        ct = ContentType.objects.get_for_model(self.__class__)
        return "%s.%s" % (ct.app_label, ct.model)

    @staticmethod
    def get_content_type_from_key(key):
        (app_name, model_name) = key.split(".")
        return ContentType.objects.get_by_natural_key(app_name, model_name)

    @staticmethod
    def get_content_object(content_type, object_id):
        ct = XOSBase.get_content_type_from_key(content_type)
        cls = ct.model_class()
        return cls.objects.get(id=object_id)
# 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. # Changing abstract to False would require the managers of subclasses of # XOSBase to be customized individually. # for PlModelMixIn # so we have something to give the observer # Automatically generated through relations from ManyToMany fields do not have soft-delete # capability. # in case it's already been deleted, don't delete again Check the fields to be updated, if they contain foreign keys, that the foreign keys only point             to live objects in the database.              This is to catch races between model policies where an object is being deleted while a model policy is             still operating on it. # If this model is already deleted, then no need to check anything. We only need to check for live # models that point to dead models. If a dead model points to other dead models, then we could # be updating something else in the dead model (backend_status, etc) # Exception django.db.models.fields.related.RelatedObjectDoesNotExist # is thrown by django when you're creating an object that has a base and the base doesn't exist yet # If field hold a null value, we don't care # If field isn't a foreign key, we don't care # If update_fields is nonempty, and field is not to be updated, we don't care. Determine whether the model has changes that should be reflected in one of the changed_by_* timestamps.             Ignores various feedback and bookkeeping state set by synchronizers. # let the user specify silence as either a kwarg or an instance varible # NOTE(smbaker): modifying update_fields will cause kwargs["update_fields"] to be modified. This is # intended, as kwargs will be passed to save() below. # NOTE(smbaker): always_update_timestamp still has some relevance for event_steps and pull_steps that # want to cause an update. For model_policies or sync_steps it should no longer be required. # validate that only synchronizers can write feedback state # this for operator support via xossh in case if you want to force changes in feedback state # Non-synchronizers update the `updated` timestamp # We're not auto-setting timestamp, but let's check to make sure that the caller hasn't tried to set our # timestamp backward... # for the old django admin UI