Rename DB columns: tenant -> project

All occurences of ``tenant_id`` across the database are renamed
to ``project_id``. Both options are equally valid, but ``project_id``
is preferred.
To inform external users about the change, HasTenant class was
deprecated.

UpgradeImpact
Partially-Implements: blueprint keystone-v3

Change-Id: I87a8ef342ccea004731ba0192b23a8e79bc382dc
This commit is contained in:
Dariusz Smigiel 2016-06-30 02:04:57 +00:00 committed by Kevin Benton
parent 8ff526df04
commit df9411dc11
16 changed files with 324 additions and 71 deletions

View File

@ -25,7 +25,7 @@ from neutron.extensions import address_scope as ext_address_scope
from neutron.objects import subnetpool as subnetpool_obj
class AddressScope(model_base.BASEV2, model_base.HasId, model_base.HasTenant):
class AddressScope(model_base.BASEV2, model_base.HasId, model_base.HasProject):
"""Represents a neutron address scope."""
__tablename__ = "address_scopes"

View File

@ -88,7 +88,7 @@ class RouterPort(model_base.BASEV2):
class Router(model_base.HasStandardAttributes, model_base.BASEV2,
model_base.HasId, model_base.HasTenant):
model_base.HasId, model_base.HasProject):
"""Represents a v2 neutron router."""
name = sa.Column(sa.String(attributes.NAME_MAX_LEN))
@ -108,7 +108,7 @@ class Router(model_base.HasStandardAttributes, model_base.BASEV2,
class FloatingIP(model_base.HasStandardAttributes, model_base.BASEV2,
model_base.HasId, model_base.HasTenant):
model_base.HasId, model_base.HasProject):
"""Represents a floating IP address.
This IP address may or may not be allocated to a tenant, and may or

View File

@ -124,7 +124,7 @@ class L3HARouterAgentPortBinding(model_base.BASEV2):
server_default=n_const.HA_ROUTER_STATE_STANDBY)
class L3HARouterNetwork(model_base.BASEV2):
class L3HARouterNetwork(model_base.BASEV2, model_base.HasProjectPrimaryKey):
"""Host HA network for a tenant.
One HA Network is used per tenant, all HA router ports are created
@ -133,8 +133,6 @@ class L3HARouterNetwork(model_base.BASEV2):
__tablename__ = 'ha_router_networks'
tenant_id = sa.Column(sa.String(attributes.TENANT_ID_MAX_LEN),
primary_key=True, nullable=False)
network_id = sa.Column(sa.String(36),
sa.ForeignKey('networks.id', ondelete="CASCADE"),
nullable=False, primary_key=True)

View File

@ -38,7 +38,9 @@ class MeteringLabelRule(model_base.BASEV2, model_base.HasId):
excluded = sa.Column(sa.Boolean, default=False, server_default=sql.false())
class MeteringLabel(model_base.BASEV2, model_base.HasId, model_base.HasTenant):
class MeteringLabel(model_base.BASEV2,
model_base.HasId,
model_base.HasProject):
name = sa.Column(sa.String(attr.NAME_MAX_LEN))
description = sa.Column(sa.String(attr.LONG_DESCRIPTION_MAX_LEN))
rules = orm.relationship(MeteringLabelRule, backref="label",

View File

@ -1 +1 @@
a84ccf28f06a
7d9d8eeec6ad

View File

@ -0,0 +1,161 @@
# Copyright 2016 Intel Corporation
#
# 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.
#
"""rename tenant to project
Revision ID: 7d9d8eeec6ad
Create Date: 2016-06-29 19:42:17.862721
"""
# revision identifiers, used by Alembic.
revision = '7d9d8eeec6ad'
down_revision = 'a84ccf28f06a'
depends_on = ('5abc0278ca73',)
from alembic import op
import sqlalchemy as sa
_INSPECTOR = None
def get_inspector():
"""Reuse inspector"""
global _INSPECTOR
if _INSPECTOR:
return _INSPECTOR
else:
bind = op.get_bind()
_INSPECTOR = sa.engine.reflection.Inspector.from_engine(bind)
return _INSPECTOR
def get_tables():
"""
Returns hardcoded list of tables which have ``tenant_id`` column.
DB head can be changed. To prevent possible problems, when models will be
updated, return hardcoded list of tables, up-to-date for this day.
Output retrieved by using:
>>> metadata = head.get_metadata()
>>> all_tables = metadata.sorted_tables
>>> tenant_tables = []
>>> for table in all_tables:
... for column in table.columns:
... if column.name == 'tenant_id':
... tenant_tables.append((table, column))
"""
tables = [
'address_scopes',
'floatingips',
'meteringlabels',
'networkrbacs',
'networks',
'ports',
'qos_policies',
'qospolicyrbacs',
'quotas',
'reservations',
'routers',
'securitygrouprules',
'securitygroups',
'subnetpools',
'subnets',
'trunks',
'auto_allocated_topologies',
'default_security_group',
'ha_router_networks',
'quotausages',
]
return tables
def get_columns(table):
"""Returns list of columns for given table."""
inspector = get_inspector()
return inspector.get_columns(table)
def get_data():
"""Returns combined list of tuples: [(table, column)].
List is built, based on retrieved tables, where column with name
``tenant_id`` exists.
"""
output = []
tables = get_tables()
for table in tables:
columns = get_columns(table)
for column in columns:
if column['name'] == 'tenant_id':
output.append((table, column))
return output
def alter_column(table, column):
old_name = 'tenant_id'
new_name = 'project_id'
op.alter_column(
table_name=table,
column_name=old_name,
new_column_name=new_name,
existing_type=column['type'],
existing_nullable=column['nullable']
)
def recreate_index(index, table_name):
old_name = index['name']
new_name = old_name.replace('tenant', 'project')
op.drop_index(op.f(old_name), table_name)
op.create_index(new_name, table_name, ['project_id'])
def upgrade():
inspector = get_inspector()
data = get_data()
for table, column in data:
alter_column(table, column)
indexes = inspector.get_indexes(table)
for index in indexes:
if 'tenant_id' in index['name']:
recreate_index(index, table)
def contract_creation_exceptions():
"""Special migration for the blueprint to support Keystone V3.
We drop all tenant_id columns and create project_id columns instead.
"""
return {
sa.Column: ['.'.join([table, 'project_id']) for table in get_tables()],
sa.Index: get_tables()
}

View File

@ -13,6 +13,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import debtcollector
from oslo_db.sqlalchemy import models
from oslo_utils import uuidutils
import sqlalchemy as sa
@ -23,16 +24,63 @@ from sqlalchemy import orm
from neutron.api.v2 import attributes as attr
class HasTenant(object):
"""Tenant mixin, add to subclasses that have a tenant."""
class HasProject(object):
"""Project mixin, add to subclasses that have a user."""
# NOTE(jkoelker) tenant_id is just a free form string ;(
tenant_id = sa.Column(sa.String(attr.TENANT_ID_MAX_LEN), index=True)
# NOTE(jkoelker) project_id is just a free form string ;(
project_id = sa.Column(sa.String(attr.TENANT_ID_MAX_LEN), index=True)
def __init__(self, *args, **kwargs):
# NOTE(dasm): debtcollector requires init in class
super(HasProject, self).__init__(*args, **kwargs)
def get_tenant_id(self):
return self.project_id
def set_tenant_id(self, value):
self.project_id = value
@declarative.declared_attr
@debtcollector.moves.moved_property('project_id')
def tenant_id(cls):
return orm.synonym(
'project_id',
descriptor=property(cls.get_tenant_id, cls.set_tenant_id))
HasTenant = debtcollector.moves.moved_class(HasProject, "HasTenant", __name__)
class HasProjectNoIndex(HasProject):
"""Project mixin, add to subclasses that have a user."""
# NOTE(jkoelker) project_id is just a free form string ;(
project_id = sa.Column(sa.String(attr.TENANT_ID_MAX_LEN))
class HasProjectPrimaryKeyIndex(HasProject):
"""Project mixin, add to subclasses that have a user."""
# NOTE(jkoelker) project_id is just a free form string ;(
project_id = sa.Column(sa.String(attr.TENANT_ID_MAX_LEN), nullable=False,
primary_key=True, index=True)
class HasProjectPrimaryKey(HasProject):
"""Project mixin, add to subclasses that have a user."""
# NOTE(jkoelker) project_id is just a free form string ;(
project_id = sa.Column(sa.String(attr.TENANT_ID_MAX_LEN), nullable=False,
primary_key=True)
class HasId(object):
"""id mixin, add to subclasses that have an id."""
def __init__(self, *args, **kwargs):
# NOTE(dasm): debtcollector requires init in class
super(HasId, self).__init__(*args, **kwargs)
id = sa.Column(sa.String(36),
primary_key=True,
default=uuidutils.generate_uuid)
@ -41,6 +89,10 @@ class HasId(object):
class HasStatusDescription(object):
"""Status with description mixin."""
def __init__(self, *args, **kwargs):
# NOTE(dasm): debtcollector requires init in class
super(HasStatusDescription, self).__init__(*args, **kwargs)
status = sa.Column(sa.String(16), nullable=False)
status_description = sa.Column(sa.String(attr.DESCRIPTION_MAX_LEN))

View File

@ -21,7 +21,7 @@ from neutron.db import models_v2
from neutron.db import rbac_db_models
class QosPolicy(model_base.BASEV2, model_base.HasId, model_base.HasTenant):
class QosPolicy(model_base.BASEV2, model_base.HasId, model_base.HasProject):
__tablename__ = 'qos_policies'
name = sa.Column(sa.String(attrs.NAME_MAX_LEN))
description = sa.Column(sa.String(attrs.DESCRIPTION_MAX_LEN))

View File

@ -16,7 +16,6 @@ import sqlalchemy as sa
from sqlalchemy import orm
from sqlalchemy import sql
from neutron.api.v2 import attributes as attr
from neutron.db import model_base
@ -31,8 +30,8 @@ class ResourceDelta(model_base.BASEV2):
amount = sa.Column(sa.Integer)
class Reservation(model_base.BASEV2, model_base.HasId):
tenant_id = sa.Column(sa.String(attr.TENANT_ID_MAX_LEN))
class Reservation(model_base.BASEV2, model_base.HasId,
model_base.HasProjectNoIndex):
expiration = sa.Column(sa.DateTime())
resource_deltas = orm.relationship(ResourceDelta,
backref='reservation',
@ -40,7 +39,7 @@ class Reservation(model_base.BASEV2, model_base.HasId):
cascade='all, delete-orphan')
class Quota(model_base.BASEV2, model_base.HasId, model_base.HasTenant):
class Quota(model_base.BASEV2, model_base.HasId, model_base.HasProject):
"""Represent a single quota override for a tenant.
If there is no row for a given tenant id and resource, then the
@ -50,13 +49,11 @@ class Quota(model_base.BASEV2, model_base.HasId, model_base.HasTenant):
limit = sa.Column(sa.Integer)
class QuotaUsage(model_base.BASEV2):
class QuotaUsage(model_base.BASEV2, model_base.HasProjectPrimaryKeyIndex):
"""Represents the current usage for a given resource."""
resource = sa.Column(sa.String(255), nullable=False,
primary_key=True, index=True)
tenant_id = sa.Column(sa.String(attr.TENANT_ID_MAX_LEN), nullable=False,
primary_key=True, index=True)
dirty = sa.Column(sa.Boolean, nullable=False, server_default=sql.false())
in_use = sa.Column(sa.Integer, nullable=False,

View File

@ -35,7 +35,7 @@ class InvalidActionForType(n_exc.InvalidInput):
"'%(object_type)s'. Valid actions: %(valid_actions)s")
class RBACColumns(model_base.HasId, model_base.HasTenant):
class RBACColumns(model_base.HasId, model_base.HasProject):
"""Mixin that object-specific RBAC tables should inherit.
All RBAC tables should inherit directly from this one because

View File

@ -41,17 +41,15 @@ LOG = logging.getLogger(__name__)
class SecurityGroup(model_base.HasStandardAttributes, model_base.BASEV2,
model_base.HasId, model_base.HasTenant):
model_base.HasId, model_base.HasProject):
"""Represents a v2 neutron security group."""
name = sa.Column(sa.String(attributes.NAME_MAX_LEN))
class DefaultSecurityGroup(model_base.BASEV2):
class DefaultSecurityGroup(model_base.BASEV2, model_base.HasProjectPrimaryKey):
__tablename__ = 'default_security_group'
tenant_id = sa.Column(sa.String(attributes.TENANT_ID_MAX_LEN),
primary_key=True, nullable=False)
security_group_id = sa.Column(sa.String(36),
sa.ForeignKey("securitygroups.id",
ondelete="CASCADE"),
@ -83,7 +81,7 @@ class SecurityGroupPortBinding(model_base.BASEV2):
class SecurityGroupRule(model_base.HasStandardAttributes, model_base.BASEV2,
model_base.HasId, model_base.HasTenant):
model_base.HasId, model_base.HasProject):
"""Represents a v2 neutron security group rule."""
security_group_id = sa.Column(sa.String(36),

View File

@ -18,12 +18,11 @@ import sqlalchemy as sa
from neutron.db import model_base
class AutoAllocatedTopology(model_base.BASEV2):
class AutoAllocatedTopology(model_base.BASEV2,
model_base.HasProjectPrimaryKey):
__tablename__ = 'auto_allocated_topologies'
tenant_id = sa.Column(sa.String(255), primary_key=True)
network_id = sa.Column(sa.String(36),
sa.ForeignKey('networks.id',
ondelete='CASCADE'),

View File

@ -23,7 +23,7 @@ from neutron.services.trunk import constants
class Trunk(model_base.HasStandardAttributes, model_base.BASEV2,
model_base.HasId, model_base.HasTenant):
model_base.HasId, model_base.HasProject):
admin_state_up = sa.Column(
sa.Boolean(), nullable=False, server_default=sql.true())

View File

@ -6007,7 +6007,7 @@ class TestSubnetPoolsV2(NeutronDbPluginV2TestCase):
self.assertEqual(400, res.status_int)
class DbModelTestCase(testlib_api.SqlTestCase):
class DbModelMixin(object):
"""DB model tests."""
def test_repr(self):
"""testing the string representation of 'model' classes."""
@ -6016,7 +6016,7 @@ class DbModelTestCase(testlib_api.SqlTestCase):
actual_repr_output = repr(network)
exp_start_with = "<neutron.db.models_v2.Network"
exp_middle = "[object at %x]" % id(network)
exp_end_with = (" {tenant_id=None, id=None, "
exp_end_with = (" {project_id=None, id=None, "
"name='net_net', status='OK', "
"admin_state_up=True, "
"vlan_transparent=None, "
@ -6025,43 +6025,6 @@ class DbModelTestCase(testlib_api.SqlTestCase):
final_exp = exp_start_with + exp_middle + exp_end_with
self.assertEqual(final_exp, actual_repr_output)
def _make_network(self, ctx):
with ctx.session.begin():
network = models_v2.Network(name="net_net", status="OK",
tenant_id='dbcheck',
admin_state_up=True)
ctx.session.add(network)
return network
def _make_subnet(self, ctx, network_id):
with ctx.session.begin():
subnet = models_v2.Subnet(name="subsub", ip_version=4,
tenant_id='dbcheck',
cidr='turn_down_for_what',
network_id=network_id)
ctx.session.add(subnet)
return subnet
def _make_port(self, ctx, network_id):
with ctx.session.begin():
port = models_v2.Port(network_id=network_id, mac_address='1',
tenant_id='dbcheck',
admin_state_up=True, status="COOL",
device_id="devid", device_owner="me")
ctx.session.add(port)
return port
def _make_subnetpool(self, ctx):
with ctx.session.begin():
subnetpool = models_v2.SubnetPool(
ip_version=4, default_prefixlen=4, min_prefixlen=4,
max_prefixlen=4, shared=False, default_quota=4,
address_scope_id='f', tenant_id='dbcheck',
is_default=False
)
ctx.session.add(subnetpool)
return subnetpool
def _make_security_group_and_rule(self, ctx):
with ctx.session.begin():
sg = sgdb.SecurityGroup(name='sg', description='sg')
@ -6251,6 +6214,84 @@ class DbModelTestCase(testlib_api.SqlTestCase):
disc, obj.standard_attr.resource_type)
class DbModelTenantTestCase(DbModelMixin, testlib_api.SqlTestCase):
def _make_network(self, ctx):
with ctx.session.begin():
network = models_v2.Network(name="net_net", status="OK",
tenant_id='dbcheck',
admin_state_up=True)
ctx.session.add(network)
return network
def _make_subnet(self, ctx, network_id):
with ctx.session.begin():
subnet = models_v2.Subnet(name="subsub", ip_version=4,
tenant_id='dbcheck',
cidr='turn_down_for_what',
network_id=network_id)
ctx.session.add(subnet)
return subnet
def _make_port(self, ctx, network_id):
with ctx.session.begin():
port = models_v2.Port(network_id=network_id, mac_address='1',
tenant_id='dbcheck',
admin_state_up=True, status="COOL",
device_id="devid", device_owner="me")
ctx.session.add(port)
return port
def _make_subnetpool(self, ctx):
with ctx.session.begin():
subnetpool = models_v2.SubnetPool(
ip_version=4, default_prefixlen=4, min_prefixlen=4,
max_prefixlen=4, shared=False, default_quota=4,
address_scope_id='f', tenant_id='dbcheck',
is_default=False
)
ctx.session.add(subnetpool)
return subnetpool
class DbModelProjectTestCase(DbModelMixin, testlib_api.SqlTestCase):
def _make_network(self, ctx):
with ctx.session.begin():
network = models_v2.Network(name="net_net", status="OK",
project_id='dbcheck',
admin_state_up=True)
ctx.session.add(network)
return network
def _make_subnet(self, ctx, network_id):
with ctx.session.begin():
subnet = models_v2.Subnet(name="subsub", ip_version=4,
project_id='dbcheck',
cidr='turn_down_for_what',
network_id=network_id)
ctx.session.add(subnet)
return subnet
def _make_port(self, ctx, network_id):
with ctx.session.begin():
port = models_v2.Port(network_id=network_id, mac_address='1',
project_id='dbcheck',
admin_state_up=True, status="COOL",
device_id="devid", device_owner="me")
ctx.session.add(port)
return port
def _make_subnetpool(self, ctx):
with ctx.session.begin():
subnetpool = models_v2.SubnetPool(
ip_version=4, default_prefixlen=4, min_prefixlen=4,
max_prefixlen=4, shared=False, default_quota=4,
address_scope_id='f', project_id='dbcheck',
is_default=False
)
ctx.session.add(subnetpool)
return subnetpool
class NeutronDbPluginV2AsMixinTestCase(NeutronDbPluginV2TestCase,
testlib_api.SqlTestCase):
"""Tests for NeutronDbPluginV2 as Mixin.

View File

@ -19,9 +19,9 @@ from neutron.db import model_base
# Model classes for test resources
class MehModel(model_base.BASEV2, model_base.HasTenant):
class MehModel(model_base.BASEV2, model_base.HasProject):
meh = sa.Column(sa.String(8), primary_key=True)
class OtherMehModel(model_base.BASEV2, model_base.HasTenant):
class OtherMehModel(model_base.BASEV2, model_base.HasProject):
othermeh = sa.Column(sa.String(8), primary_key=True)

View File

@ -0,0 +1,5 @@
---
upgrade:
- |
tenant_id column has been renamed to project_id.
This database migration is required to be applied as offline migration.