Move standard attr out of model_base

The model_base file is going to move to Neutron lib in
I2087c6f5f66154cdaa4d8fa3d86f5e493f1d24d9. This will mainly leave
behind only the standard attributes related stuff so the name
'model_base' won't make much sense. This moves the standard attribute
related things into its own module so model_base can conceivably be
eliminated entirely.

Change-Id: Icaf3220fbc5723f2b5421a494371ef274d7073c7
This commit is contained in:
Kevin Benton 2016-08-19 04:12:18 -07:00 committed by Henry Gessau
parent 89cf28e3dc
commit bdd7298284
15 changed files with 167 additions and 137 deletions

View File

@ -76,7 +76,7 @@ many-to-one because each object then needs its own table for the attributes
To address this issue, the 'standardattribute' table is available. Any model
can add support for this table by inheriting the 'HasStandardAttributes' mixin
in neutron.db.model_base. This mixin will add a standard_attr_id BigInteger
in neutron.db.standard_attr. This mixin will add a standard_attr_id BigInteger
column to the model with a foreign key relationship to the 'standardattribute'
table. The model will then be able to access any columns of the
'standardattribute' table and any tables related to it.

View File

@ -44,6 +44,7 @@ from neutron.db import common_db_mixin
from neutron.db import l3_agentschedulers_db as l3_agt
from neutron.db import model_base
from neutron.db import models_v2
from neutron.db import standard_attr
from neutron.db import standardattrdescription_db as st_attr
from neutron.extensions import external_net
from neutron.extensions import l3
@ -90,7 +91,7 @@ class RouterPort(model_base.BASEV2):
lazy='joined')
class Router(model_base.HasStandardAttributes, model_base.BASEV2,
class Router(standard_attr.HasStandardAttributes, model_base.BASEV2,
model_base.HasId, model_base.HasProject):
"""Represents a v2 neutron router."""
@ -110,7 +111,7 @@ class Router(model_base.HasStandardAttributes, model_base.BASEV2,
secondary=l3_agt.RouterL3AgentBinding.__table__)
class FloatingIP(model_base.HasStandardAttributes, model_base.BASEV2,
class FloatingIP(standard_attr.HasStandardAttributes, model_base.BASEV2,
model_base.HasId, model_base.HasProject):
"""Represents a floating IP address.

View File

@ -17,7 +17,6 @@ import debtcollector
from oslo_db.sqlalchemy import models
from oslo_utils import uuidutils
import sqlalchemy as sa
from sqlalchemy.ext.associationproxy import association_proxy
from sqlalchemy.ext import declarative
from sqlalchemy import orm
@ -127,114 +126,6 @@ class NeutronBaseV2(NeutronBase):
BASEV2 = declarative.declarative_base(cls=NeutronBaseV2)
class StandardAttribute(BASEV2, models.TimestampMixin):
"""Common table to associate all Neutron API resources.
By having Neutron objects related to this table, we can associate new
tables that apply to many Neutron objects (e.g. timestamps, rbac entries)
to this table to avoid schema duplication while maintaining referential
integrity.
NOTE(kevinbenton): This table should not have more columns added to it
unless we are absolutely certain the new column will have a value for
every single type of Neutron resource. Otherwise this table will be filled
with NULL entries for combinations that don't make sense. Additionally,
by keeping this table small we can ensure that performance isn't adversely
impacted for queries on objects.
"""
# sqlite doesn't support auto increment on big integers so we use big int
# for everything but sqlite
id = sa.Column(sa.BigInteger().with_variant(sa.Integer(), 'sqlite'),
primary_key=True, autoincrement=True)
# NOTE(kevinbenton): this column is redundant information, but it allows
# operators/devs to look at the contents of this table and know which table
# the corresponding object is in.
# 255 was selected as a max just because it's the varchar ceiling in mysql
# before a 2-byte prefix is required. We shouldn't get anywhere near this
# limit with our table names...
resource_type = sa.Column(sa.String(255), nullable=False)
description = sa.Column(sa.String(attr.DESCRIPTION_MAX_LEN))
revision_number = sa.Column(
sa.BigInteger().with_variant(sa.Integer(), 'sqlite'),
server_default='0', nullable=False)
__mapper_args__ = {
# see http://docs.sqlalchemy.org/en/latest/orm/versioning.html for
# details about how this works
"version_id_col": revision_number
}
class HasStandardAttributes(object):
@declarative.declared_attr
def standard_attr_id(cls):
return sa.Column(
sa.BigInteger().with_variant(sa.Integer(), 'sqlite'),
sa.ForeignKey(StandardAttribute.id, ondelete="CASCADE"),
unique=True,
nullable=False
)
# NOTE(kevinbenton): we have to disable the following pylint check because
# it thinks we are overriding this method in the __init__ method.
#pylint: disable=method-hidden
@declarative.declared_attr
def standard_attr(cls):
return orm.relationship(StandardAttribute,
lazy='joined',
cascade='all, delete-orphan',
single_parent=True,
uselist=False)
def __init__(self, *args, **kwargs):
standard_attr_keys = ['description', 'created_at',
'updated_at', 'revision_number']
standard_attr_kwargs = {}
for key in standard_attr_keys:
if key in kwargs:
standard_attr_kwargs[key] = kwargs.pop(key)
super(HasStandardAttributes, self).__init__(*args, **kwargs)
# here we automatically create the related standard attribute object
self.standard_attr = StandardAttribute(
resource_type=self.__tablename__, **standard_attr_kwargs)
@declarative.declared_attr
def description(cls):
return association_proxy('standard_attr', 'description')
@declarative.declared_attr
def created_at(cls):
return association_proxy('standard_attr', 'created_at')
@declarative.declared_attr
def updated_at(cls):
return association_proxy('standard_attr', 'updated_at')
def update(self, new_dict):
# ignore the timestamps if they were passed in. For example, this
# happens if code calls update_port with modified results of get_port
new_dict.pop('created_at', None)
new_dict.pop('updated_at', None)
super(HasStandardAttributes, self).update(new_dict)
@declarative.declared_attr
def revision_number(cls):
return association_proxy('standard_attr', 'revision_number')
def bump_revision(self):
# SQLAlchemy will bump the version for us automatically if the
# standard attr record is being modified, but we must call this
# for all other modifications or when relevant children are being
# modified (e.g. fixed_ips change should bump port revision)
if self.standard_attr.revision_number is None:
# this is a brand new object uncommited so we don't bump now
return
self.standard_attr.revision_number += 1
def get_unique_keys(model):
try:
constraints = model.__table__.constraints

View File

@ -18,9 +18,10 @@ from sqlalchemy import orm
from neutron.api.v2 import attributes
from neutron.db import model_base
from neutron.db import models_v2
from neutron.db import standard_attr
class SecurityGroup(model_base.HasStandardAttributes, model_base.BASEV2,
class SecurityGroup(standard_attr.HasStandardAttributes, model_base.BASEV2,
model_base.HasId, model_base.HasProject):
"""Represents a v2 neutron security group."""
@ -60,7 +61,7 @@ class SecurityGroupPortBinding(model_base.BASEV2):
lazy='joined', cascade='delete'))
class SecurityGroupRule(model_base.HasStandardAttributes, model_base.BASEV2,
class SecurityGroupRule(standard_attr.HasStandardAttributes, model_base.BASEV2,
model_base.HasId, model_base.HasProject):
"""Represents a v2 neutron security group rule."""

View File

@ -22,6 +22,7 @@ from neutron.api.v2 import attributes as attr
from neutron.db import model_base
from neutron.db.network_dhcp_agent_binding import models as ndab_model
from neutron.db import rbac_db_models
from neutron.db import standard_attr
# NOTE(kevinbenton): these are here for external projects that expect them
@ -114,7 +115,7 @@ class SubnetRoute(model_base.BASEV2, Route):
primary_key=True)
class Port(model_base.HasStandardAttributes, model_base.BASEV2,
class Port(standard_attr.HasStandardAttributes, model_base.BASEV2,
HasId, HasTenant):
"""Represents a port on a Neutron v2 network."""
@ -173,7 +174,7 @@ class DNSNameServer(model_base.BASEV2):
order = sa.Column(sa.Integer, nullable=False, server_default='0')
class Subnet(model_base.HasStandardAttributes, model_base.BASEV2,
class Subnet(standard_attr.HasStandardAttributes, model_base.BASEV2,
HasId, HasTenant):
"""Represents a neutron subnet.
@ -242,7 +243,7 @@ class SubnetPoolPrefix(model_base.BASEV2):
primary_key=True)
class SubnetPool(model_base.HasStandardAttributes, model_base.BASEV2,
class SubnetPool(standard_attr.HasStandardAttributes, model_base.BASEV2,
HasId, HasTenant):
"""Represents a neutron subnet pool.
"""
@ -264,7 +265,7 @@ class SubnetPool(model_base.HasStandardAttributes, model_base.BASEV2,
lazy='joined')
class Network(model_base.HasStandardAttributes, model_base.BASEV2,
class Network(standard_attr.HasStandardAttributes, model_base.BASEV2,
HasId, HasTenant):
"""Represents a v2 neutron network."""

View File

@ -23,6 +23,7 @@ from neutron.callbacks import resources
from neutron.db import api as db_api
from neutron.db import model_base
from neutron.db import models_v2
from neutron.db import standard_attr
LOG = logging.getLogger(__name__)
PROVISIONING_COMPLETE = 'provisioning_complete'
@ -36,7 +37,7 @@ class ProvisioningBlock(model_base.BASEV2):
# the standard attr id of the thing we want to block
standard_attr_id = (
sa.Column(sa.BigInteger().with_variant(sa.Integer(), 'sqlite'),
sa.ForeignKey(model_base.StandardAttribute.id,
sa.ForeignKey(standard_attr.StandardAttribute.id,
ondelete="CASCADE"),
primary_key=True))
# the entity that wants to block the status change (e.g. L2 Agent)

129
neutron/db/standard_attr.py Normal file
View File

@ -0,0 +1,129 @@
#
# 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 oslo_db.sqlalchemy import models
import sqlalchemy as sa
from sqlalchemy.ext.associationproxy import association_proxy
from sqlalchemy.ext import declarative
from neutron.api.v2 import attributes as attr
from neutron.db import model_base
class StandardAttribute(model_base.BASEV2, models.TimestampMixin):
"""Common table to associate all Neutron API resources.
By having Neutron objects related to this table, we can associate new
tables that apply to many Neutron objects (e.g. timestamps, rbac entries)
to this table to avoid schema duplication while maintaining referential
integrity.
NOTE(kevinbenton): This table should not have more columns added to it
unless we are absolutely certain the new column will have a value for
every single type of Neutron resource. Otherwise this table will be filled
with NULL entries for combinations that don't make sense. Additionally,
by keeping this table small we can ensure that performance isn't adversely
impacted for queries on objects.
"""
# sqlite doesn't support auto increment on big integers so we use big int
# for everything but sqlite
id = sa.Column(sa.BigInteger().with_variant(sa.Integer(), 'sqlite'),
primary_key=True, autoincrement=True)
# NOTE(kevinbenton): this column is redundant information, but it allows
# operators/devs to look at the contents of this table and know which table
# the corresponding object is in.
# 255 was selected as a max just because it's the varchar ceiling in mysql
# before a 2-byte prefix is required. We shouldn't get anywhere near this
# limit with our table names...
resource_type = sa.Column(sa.String(255), nullable=False)
description = sa.Column(sa.String(attr.DESCRIPTION_MAX_LEN))
revision_number = sa.Column(
sa.BigInteger().with_variant(sa.Integer(), 'sqlite'),
server_default='0', nullable=False)
__mapper_args__ = {
# see http://docs.sqlalchemy.org/en/latest/orm/versioning.html for
# details about how this works
"version_id_col": revision_number
}
class HasStandardAttributes(object):
@declarative.declared_attr
def standard_attr_id(cls):
return sa.Column(
sa.BigInteger().with_variant(sa.Integer(), 'sqlite'),
sa.ForeignKey(StandardAttribute.id, ondelete="CASCADE"),
unique=True,
nullable=False
)
# NOTE(kevinbenton): we have to disable the following pylint check because
# it thinks we are overriding this method in the __init__ method.
#pylint: disable=method-hidden
@declarative.declared_attr
def standard_attr(cls):
return sa.orm.relationship(StandardAttribute,
lazy='joined',
cascade='all, delete-orphan',
single_parent=True,
uselist=False)
def __init__(self, *args, **kwargs):
standard_attr_keys = ['description', 'created_at',
'updated_at', 'revision_number']
standard_attr_kwargs = {}
for key in standard_attr_keys:
if key in kwargs:
standard_attr_kwargs[key] = kwargs.pop(key)
super(HasStandardAttributes, self).__init__(*args, **kwargs)
# here we automatically create the related standard attribute object
self.standard_attr = StandardAttribute(
resource_type=self.__tablename__, **standard_attr_kwargs)
@declarative.declared_attr
def description(cls):
return association_proxy('standard_attr', 'description')
@declarative.declared_attr
def created_at(cls):
return association_proxy('standard_attr', 'created_at')
@declarative.declared_attr
def updated_at(cls):
return association_proxy('standard_attr', 'updated_at')
def update(self, new_dict):
# ignore the timestamps if they were passed in. For example, this
# happens if code calls update_port with modified results of get_port
new_dict.pop('created_at', None)
new_dict.pop('updated_at', None)
super(HasStandardAttributes, self).update(new_dict)
@declarative.declared_attr
def revision_number(cls):
return association_proxy('standard_attr', 'revision_number')
def bump_revision(self):
# SQLAlchemy will bump the version for us automatically if the
# standard attr record is being modified, but we must call this
# for all other modifications or when relevant children are being
# modified (e.g. fixed_ips change should bump port revision)
if self.standard_attr.revision_number is None:
# this is a brand new object uncommited so we don't bump now
return
self.standard_attr.revision_number += 1

View File

@ -17,12 +17,13 @@ from sqlalchemy import orm
from sqlalchemy.orm import aliased
from neutron.db import model_base
from neutron.db import standard_attr
class Tag(model_base.BASEV2):
standard_attr_id = sa.Column(
sa.BigInteger().with_variant(sa.Integer(), 'sqlite'),
sa.ForeignKey(model_base.StandardAttribute.id, ondelete="CASCADE"),
sa.ForeignKey(standard_attr.StandardAttribute.id, ondelete="CASCADE"),
nullable=False, primary_key=True)
tag = sa.Column(sa.String(60), nullable=False, primary_key=True)
standard_attr = orm.relationship(

View File

@ -24,6 +24,7 @@ import six
from neutron._i18n import _
from neutron.db import api as db_api
from neutron.db import model_base
from neutron.db import standard_attr
from neutron.objects.db import api as obj_db_api
from neutron.objects.extensions import standardattributes
@ -307,7 +308,8 @@ class NeutronDbObject(NeutronObject):
@classmethod
def has_standard_attributes(cls):
return bool(cls.db_model and
issubclass(cls.db_model, model_base.HasStandardAttributes))
issubclass(cls.db_model,
standard_attr.HasStandardAttributes))
@classmethod
def modify_fields_to_db(cls, fields):

View File

@ -30,8 +30,8 @@ from neutron.db import api as db_api
from neutron.db import common_db_mixin
from neutron.db import db_base_plugin_v2
from neutron.db import external_net_db
from neutron.db import model_base
from neutron.db import models_v2
from neutron.db import standard_attr
from neutron.extensions import l3
from neutron import manager
from neutron.plugins.common import constants
@ -227,8 +227,8 @@ class AutoAllocatedTopologyMixin(common_db_mixin.CommonDbMixin):
external_net_db.ExternalNetwork).
filter_by(is_default=sql.true()).
join(models_v2.Network).
join(model_base.StandardAttribute).
order_by(model_base.StandardAttribute.id).all())
join(standard_attr.StandardAttribute).
order_by(standard_attr.StandardAttribute.id).all())
if not default_external_networks:
LOG.error(_LE("Unable to find default external network "

View File

@ -18,7 +18,7 @@ from sqlalchemy.orm import session as se
from neutron._i18n import _, _LW
from neutron.db import db_base_plugin_v2
from neutron.db import model_base
from neutron.db import standard_attr
from neutron.extensions import revisions
from neutron.services import service_base
@ -40,7 +40,7 @@ class RevisionPlugin(service_base.ServicePluginBase):
def bump_revisions(self, session, context, instances):
# bump revision number for any updated objects in the session
for obj in session.dirty:
if isinstance(obj, model_base.HasStandardAttributes):
if isinstance(obj, standard_attr.HasStandardAttributes):
obj.bump_revision()
# see if any created/updated/deleted objects bump the revision

View File

@ -23,7 +23,7 @@ from sqlalchemy import exc as sql_exc
from sqlalchemy.orm import session as se
from neutron._i18n import _LW
from neutron.db import model_base
from neutron.db import standard_attr
LOG = log.getLogger(__name__)
@ -58,10 +58,10 @@ class TimeStamp_db_mixin(object):
changed_since = (timeutils.
normalize_time(changed_since_string))
target_model_class = list(query._mapper_adapter_map.keys())[0]
query = query.join(model_base.StandardAttribute,
query = query.join(standard_attr.StandardAttribute,
target_model_class.standard_attr_id ==
model_base.StandardAttribute.id).filter(
model_base.StandardAttribute.updated_at
standard_attr.StandardAttribute.id).filter(
standard_attr.StandardAttribute.updated_at
>= changed_since)
return query
@ -70,17 +70,17 @@ class TimeStamp_db_mixin(object):
while objs_list:
obj = objs_list.pop()
if (isinstance(obj, model_base.HasStandardAttributes)
if (isinstance(obj, standard_attr.HasStandardAttributes)
and obj.standard_attr_id):
obj.updated_at = timeutils.utcnow()
def register_db_events(self):
event.listen(model_base.StandardAttribute, 'before_insert',
event.listen(standard_attr.StandardAttribute, 'before_insert',
self._add_timestamp)
event.listen(se.Session, 'before_flush', self.update_timestamp)
def unregister_db_events(self):
self._unregister_db_event(model_base.StandardAttribute,
self._unregister_db_event(standard_attr.StandardAttribute,
'before_insert', self._add_timestamp)
self._unregister_db_event(se.Session, 'before_flush',
self.update_timestamp)

View File

@ -19,10 +19,11 @@ from sqlalchemy import sql
from neutron.api.v2 import attributes
from neutron.db import model_base
from neutron.db import models_v2
from neutron.db import standard_attr
from neutron.services.trunk import constants
class Trunk(model_base.HasStandardAttributes, model_base.BASEV2,
class Trunk(standard_attr.HasStandardAttributes, model_base.BASEV2,
model_base.HasId, model_base.HasProject):
admin_state_up = sa.Column(

View File

@ -52,6 +52,7 @@ from neutron.db import ipam_backend_mixin
from neutron.db import l3_db
from neutron.db.models import securitygroup as sg_models
from neutron.db import models_v2
from neutron.db import standard_attr
from neutron import manager
from neutron.tests import base
from neutron.tests import tools
@ -6055,8 +6056,8 @@ class DbModelMixin(object):
def _get_neutron_attr(self, ctx, attr_id):
return ctx.session.query(
models_v2.model_base.StandardAttribute).filter(
models_v2.model_base.StandardAttribute.id == attr_id).one()
standard_attr.StandardAttribute).filter(
standard_attr.StandardAttribute.id == attr_id).one()
def _test_standardattr_removed_on_obj_delete(self, ctx, obj):
attr_id = obj.standard_attr_id

View File

@ -17,13 +17,14 @@ from oslo_versionedobjects import fields as obj_fields
import sqlalchemy as sa
from neutron.db import model_base
from neutron.db import standard_attr
from neutron.objects import base as objects_base
from neutron.tests.unit.objects import test_base
from neutron.tests.unit import testlib_api
class FakeDbModelWithStandardAttributes(
model_base.HasStandardAttributes, model_base.BASEV2):
standard_attr.HasStandardAttributes, model_base.BASEV2):
id = sa.Column(sa.String(36), primary_key=True, nullable=False)
item = sa.Column(sa.String(64))