Example #1
0
def stdattrs_extended_resources(attributes):
    r_map = standard_attr.get_standard_attr_resource_model_map(
        include_resources=True,
        include_sub_resources=False)
    sr_map = standard_attr.get_standard_attr_resource_model_map(
        include_resources=False,
        include_sub_resources=True)
    return dict(itertools.chain(
        {r: attributes for r in r_map}.items(),
        {sr: {'parameters': attributes} for sr in sr_map}.items()
    ))
Example #2
0
    def _get_constrained_instance_match(self, session):
        """Returns instance and constraint of if-match criterion if present.

        Checks the context associated with the session for compare-and-swap
        update revision number constraints. If one is found, this returns the
        instance that is constrained as well as the requested revision number
        to match.
        """
        context = session.info.get('using_context')
        if context:
            # NOTE(ralonsoh): use "pop_transaction_constraint" once implemented
            criteria = context.get_transaction_constraint()
            context.clear_transaction_constraint()
        else:
            criteria = None
        if not criteria:
            return None, None
        match = criteria.if_revision_match
        mmap = standard_attr.get_standard_attr_resource_model_map()
        model = mmap.get(criteria.resource)
        if not model:
            msg = _("Revision matching not supported for this resource")
            raise exc.BadRequest(resource=criteria.resource, msg=msg)
        instance = self._find_instance_by_column_value(
            session, model, 'id', criteria.resource_id)
        return instance, match
Example #3
0
class TimeStamp_db_mixin(object):
    """Mixin class to add Time Stamp methods."""
    def __new__(cls, *args, **kwargs):
        rs_model_maps = standard_attr.get_standard_attr_resource_model_map()
        for model in rs_model_maps.values():
            model_query.register_hook(
                model,
                "change_since_query",
                query_hook=None,
                filter_hook=None,
                result_filters=_change_since_result_filter_hook)
        return super(TimeStamp_db_mixin, cls).__new__(cls, *args, **kwargs)

    def register_db_events(self):
        listen = db_api.sqla_listen
        listen(standard_attr.StandardAttribute, 'before_insert',
               _add_timestamp)
        listen(se.Session, 'before_flush', _update_timestamp)

    @staticmethod
    @resource_extend.extends(
        list(standard_attr.get_standard_attr_resource_model_map()))
    def _extend_resource_dict_timestamp(resource_res, resource_db):
        if (resource_db and resource_db.created_at and resource_db.updated_at):
            _format_timestamp(resource_db, resource_res)
Example #4
0
class StandardAttrDescriptionMixin(object):
    supported_extension_aliases = ['standard-attr-description']

    @staticmethod
    @resource_extend.extends(
        list(standard_attr.get_standard_attr_resource_model_map()))
    def _extend_standard_attr_description(res, db_object):
        if not hasattr(db_object, 'description'):
            return
        res['description'] = db_object.description
Example #5
0
 def __new__(cls, *args, **kwargs):
     rs_model_maps = standard_attr.get_standard_attr_resource_model_map()
     for model in rs_model_maps.values():
         model_query.register_hook(
             model,
             "change_since_query",
             query_hook=None,
             filter_hook=None,
             result_filters=_change_since_result_filter_hook)
     return super(TimeStamp_db_mixin, cls).__new__(cls, *args, **kwargs)
Example #6
0
    def test_standard_attr_resource_model_map(self):
        rs_map = standard_attr.get_standard_attr_resource_model_map()
        base = self._make_decl_base()

        class MyModel(standard_attr.HasStandardAttributes,
                      standard_attr.model_base.HasId,
                      base):
            api_collections = ['my_resource', 'my_resource2']
            api_sub_resources = ['my_subresource']

        rs_map = standard_attr.get_standard_attr_resource_model_map()
        self.assertEqual(MyModel, rs_map['my_resource'])
        self.assertEqual(MyModel, rs_map['my_resource2'])
        self.assertEqual(MyModel, rs_map['my_subresource'])

        sub_rs_map = standard_attr.get_standard_attr_resource_model_map(
            include_resources=False,
            include_sub_resources=True)
        self.assertNotIn('my_resource', sub_rs_map)
        self.assertNotIn('my_resource2', sub_rs_map)
        self.assertEqual(MyModel, sub_rs_map['my_subresource'])

        nosub_rs_map = standard_attr.get_standard_attr_resource_model_map(
            include_resources=True,
            include_sub_resources=False)
        self.assertEqual(MyModel, nosub_rs_map['my_resource'])
        self.assertEqual(MyModel, nosub_rs_map['my_resource2'])
        self.assertNotIn('my_subresource', nosub_rs_map)

        class Dup(standard_attr.HasStandardAttributes,
                  standard_attr.model_base.HasId,
                  base):
            api_collections = ['my_resource']

        with testtools.ExpectedException(RuntimeError):
            standard_attr.get_standard_attr_resource_model_map()
Example #7
0
from neutron_lib.db import api as db_api
from neutron_lib.db import model_query
from neutron_lib.db import resource_extend
from neutron_lib.db import standard_attr
from neutron_lib.objects import exceptions as obj_exc
from neutron_lib.plugins import directory
from oslo_log import helpers as log_helpers
from sqlalchemy.orm import exc

from neutron.extensions import tagging
from neutron.objects import tag as tag_obj


# Taggable resources
resource_model_map = standard_attr.get_standard_attr_resource_model_map()


@resource_extend.has_resource_extenders
class TagPlugin(tagging.TagPluginBase):
    """Implementation of the Neutron Tag Service Plugin."""

    supported_extension_aliases = ['standard-attr-tag']

    __filter_validation_support = True

    def __new__(cls, *args, **kwargs):
        inst = super(TagPlugin, cls).__new__(cls, *args, **kwargs)
        tag_obj.register_tag_hooks()
        return inst
Example #8
0
from oslo_config import cfg
from oslo_log import log
from oslo_utils import timeutils
import sqlalchemy as sa
from sqlalchemy.orm import exc

from neutron.common.ovn import utils as ovn_utils
from neutron.db.models import l3  # noqa
from neutron.db.models import ovn as ovn_models
from neutron.db.models import securitygroup  # noqa
from neutron.db import models_v2  # noqa

LOG = log.getLogger(__name__)
CONF = cfg.CONF

STD_ATTR_MAP = standard_attr.get_standard_attr_resource_model_map()

# NOTE(ralonsoh): to be moved to neutron-lib
TYPE_NETWORKS = 'networks'
TYPE_PORTS = 'ports'
TYPE_SECURITY_GROUP_RULES = 'security_group_rules'
TYPE_ROUTERS = 'routers'
TYPE_ROUTER_PORTS = 'router_ports'
TYPE_SECURITY_GROUPS = 'security_groups'
TYPE_FLOATINGIPS = 'floatingips'
TYPE_SUBNETS = 'subnets'

_TYPES_PRIORITY_ORDER = (TYPE_NETWORKS, TYPE_SECURITY_GROUPS, TYPE_SUBNETS,
                         TYPE_ROUTERS, TYPE_PORTS, TYPE_ROUTER_PORTS,
                         TYPE_FLOATINGIPS, TYPE_SECURITY_GROUP_RULES)
Example #9
0
class RevisionPlugin(service_base.ServicePluginBase):
    """Plugin to populate revision numbers into standard attr resources."""

    supported_extension_aliases = ['standard-attr-revisions',
                                   revisionifmatch.ALIAS]

    __filter_validation_support = True

    def __init__(self):
        super(RevisionPlugin, self).__init__()
        # background on these event hooks:
        # https://docs.sqlalchemy.org/en/latest/orm/session_events.html
        db_api.sqla_listen(se.Session, 'before_flush', self.bump_revisions)
        db_api.sqla_listen(
            se.Session, "after_flush_postexec",
            self._emit_related_revision_bumps)
        db_api.sqla_listen(se.Session, 'after_commit',
                           self._clear_rev_bumped_flags)
        db_api.sqla_listen(se.Session, 'after_rollback',
                           self._clear_rev_bumped_flags)

    def bump_revisions(self, session, context, instances):
        self._enforce_if_match_constraints(session)
        # bump revision number for any updated objects in the session
        self._bump_obj_revisions(
            session,
            [
                obj for obj in session.dirty
                if isinstance(obj, standard_attr.HasStandardAttributes)]
        )

        # see if any created/updated/deleted objects bump the revision
        # of another object
        objects_with_related_revisions = [
            o for o in session.deleted | session.dirty | session.new
            if getattr(o, 'revises_on_change', ())
        ]
        collected = session.info.setdefault('_related_bumped', set())
        self._collect_related_tobump(
            session, objects_with_related_revisions, collected)

    def _emit_related_revision_bumps(self, session, context):
        # within after_flush_postexec, emit an UPDATE statement to increment
        # revision flags for related objects that were located in the
        # before_flush phase.
        #
        # note that this event isn't called if the flush fails;
        # in that case, the transaction is rolled back and the
        # after_rollback event will invoke self._clear_rev_bumped_flags
        # to clean out state.
        collected = session.info.get('_related_bumped', None)
        if collected:
            try:
                self._bump_obj_revisions(
                    session, collected, version_check=False)
            finally:
                collected.clear()

    def _collect_related_tobump(self, session, objects, collected):
        for obj in objects:
            if obj in collected:
                continue
            for revises_col in getattr(obj, 'revises_on_change', ()):
                related_obj = self._find_related_obj(obj, revises_col)
                if not related_obj:
                    LOG.warning("Could not find related %(col)s for "
                                "resource %(obj)s to bump revision.",
                                {'obj': obj, 'col': revises_col})
                    continue
                # if related object revises others, bump those as well
                self._collect_related_tobump(session, [related_obj], collected)
                # no need to bump revisions on related objects being deleted
                if related_obj not in session.deleted:
                    collected.add(related_obj)
        return collected

    def get_plugin_type(self):
        return "revision_plugin"

    def get_plugin_description(self):
        return "Adds revision numbers to resources."

    @staticmethod
    @resource_extend.extends(
        list(standard_attr.get_standard_attr_resource_model_map()))
    def extend_resource_dict_revision(resource_res, resource_db):
        resource_res['revision_number'] = resource_db.revision_number

    def _find_related_obj(self, obj, relationship_col):
        """Gets a related object off of a relationship.

        Raises a runtime error if the relationship isn't configured correctly
        for revision bumping.
        """
        # first check to see if it's directly attached to the object already
        try:
            related_obj = getattr(obj, relationship_col)
        except exc.ObjectDeletedError:
            # object was in session but another writer deleted it
            return None

        if related_obj:
            return related_obj
        for rel in sqlalchemy.inspect(obj).mapper.relationships:
            if rel.key != relationship_col:
                continue
            if not rel.load_on_pending:
                raise RuntimeError(_("revises_on_change relationships must "
                                     "have load_on_pending set to True to "
                                     "bump parent revisions on create: %s")
                                   % relationship_col)

    def _clear_rev_bumped_flags(self, session):
        """This clears all flags on commit/rollback to enable rev bumps."""
        session.info.pop('_related_bumped', None)
        for inst in session:
            setattr(inst, '_rev_bumped', False)

    def _bump_obj_revisions(self, session, objects, version_check=True):
        """Increment object revisions.

        If version_check=True, uses SQLAlchemy ORM's compare-and-swap feature
        (known as "version_id_col" in the ORM mapping), which is part of the
        StandardAttribute class.

        If version_check=False, runs an UPDATE statement directly against
        the set of all StandardAttribute objects at once, without using
        any compare and swap logic.

        If a revision number constraint rule was associated with the Session,
        this is retrieved and each object is tested to see if it matches
        this condition; if so, the constraint is enforced.

        """

        # filter objects for which we've already bumped the revision
        to_bump = [
            obj for obj in objects if not getattr(obj, '_rev_bumped', False)]

        if not to_bump:
            return

        self._run_constrained_instance_match_check(session, to_bump)

        if not version_check:
            # this UPDATE statement could alternatively be written to run
            # as an UPDATE-per-object with Python-generated revision numbers
            # as parameters.
            session.query(standard_attr.StandardAttribute).filter(
                standard_attr.StandardAttribute.id.in_(
                    [obj._effective_standard_attribute_id for obj in to_bump]
                )
            ).update({
                # note that SQLAlchemy runs the onupdate function for
                # the updated_at column and applies it to the SET clause as
                # well.
                standard_attr.StandardAttribute.revision_number:
                standard_attr.StandardAttribute.revision_number + 1},
                synchronize_session=False)

            # run a SELECT to get back the new values we just generated.
            # if MySQL supported RETURNING, we could get these numbers
            # back from the UPDATE without running another SELECT.
            retrieve_revision_numbers = {
                row.id: (row.revision_number, row.updated_at)
                for row in
                session.query(
                    standard_attr.StandardAttribute.id,
                    standard_attr.StandardAttribute.revision_number,
                    standard_attr.StandardAttribute.updated_at,
                ).filter(
                    standard_attr.StandardAttribute.id.in_(
                        [
                            obj._effective_standard_attribute_id
                            for obj in to_bump
                        ]
                    )
                )
            }

        for obj in to_bump:
            if version_check:
                # full version check, run the ORM routine to UPDATE
                # the row with a WHERE clause
                obj.bump_revision()
            else:
                # no version check - get back what we did in our one-step
                # UPDATE statement and set it without causing change in
                # ORM flush state
                try:
                    new_version_id, new_updated_at = retrieve_revision_numbers[
                        obj._effective_standard_attribute_id
                    ]
                except KeyError:
                    # in case the object was deleted concurrently
                    LOG.warning(
                        "No standard attr row found for resource: %(obj)s",
                        {'obj': obj})
                else:
                    obj._set_updated_revision_number(
                        new_version_id, new_updated_at)
            setattr(obj, '_rev_bumped', True)

    def _run_constrained_instance_match_check(self, session, objects):
        instance, match = self._get_constrained_instance_match(session)
        for obj in objects:
            if instance and instance == obj:
                # one last check before bumping revision
                self._enforce_if_match_constraints(session)

    def _find_instance_by_column_value(self, session, model, column, value):
        """Lookup object in session or from DB based on a column's value."""
        for session_obj in session:
            if not isinstance(session_obj, model):
                continue
            if getattr(session_obj, column) == value:
                return session_obj
        # object isn't in session so we have to query for it
        related_obj = (session.query(model).filter_by(**{column: value}).
                       first())
        return related_obj

    def _get_constrained_instance_match(self, session):
        """Returns instance and constraint of if-match criterion if present.

        Checks the context associated with the session for compare-and-swap
        update revision number constraints. If one is found, this returns the
        instance that is constrained as well as the requested revision number
        to match.
        """
        context = session.info.get('using_context')
        if context:
            # NOTE(ralonsoh): use "pop_transaction_constraint" once implemented
            criteria = context.get_transaction_constraint()
            context.clear_transaction_constraint()
        else:
            criteria = None
        if not criteria:
            return None, None
        match = criteria.if_revision_match
        mmap = standard_attr.get_standard_attr_resource_model_map()
        model = mmap.get(criteria.resource)
        if not model:
            msg = _("Revision matching not supported for this resource")
            raise exc.BadRequest(resource=criteria.resource, msg=msg)
        instance = self._find_instance_by_column_value(
            session, model, 'id', criteria.resource_id)
        return instance, match

    def _enforce_if_match_constraints(self, session):
        """Check for if-match constraints and raise exception if violated.

        We determine the collection being modified and look for any
        objects of the collection type in the dirty/deleted items in
        the session. If they don't match the revision_number constraint
        supplied, we throw an exception.

        We are protected from a concurrent update because if we match
        revision number here and another update commits to the database
        first, the compare and swap of revision_number will fail and a
        StaleDataError (or deadlock in galera multi-writer) will be raised,
        at which point this will be retried and fail to match.
        """
        instance, match = self._get_constrained_instance_match(session)
        if not instance or getattr(instance, '_rev_bumped', False):
            # no constraints present or constrain satisfied in this transaction
            return
        if instance.revision_number != match:
            raise RevisionNumberConstraintFailed(match,
                                                 instance.revision_number)