From 35748e7a9b3445a60522e088e38bf25e6bfef25e Mon Sep 17 00:00:00 2001 From: Mark Doffman Date: Tue, 1 Mar 2016 13:38:19 -0600 Subject: [PATCH] Add aggregates tables to the API db. CellsV2 requires that aggregates be available in the API db. Create the 'aggregates', 'aggregate_metadata' and 'aggregate_hosts' tables in the API db. blueprint cells-aggregate-api-db Change-Id: I19fa3a28181831c5cd7b19cd2a5a2ea0d40e45f8 --- .../migrate_repo/versions/017_aggregates.py | 73 +++++++++++++++++++ nova/db/sqlalchemy/api_models.py | 62 ++++++++++++++++ .../functional/db/api/test_migrations.py | 37 ++++++++++ .../functional/db/test_aggregate_model.py | 57 +++++++++++++++ 4 files changed, 229 insertions(+) create mode 100644 nova/db/sqlalchemy/api_migrations/migrate_repo/versions/017_aggregates.py create mode 100644 nova/tests/functional/db/test_aggregate_model.py diff --git a/nova/db/sqlalchemy/api_migrations/migrate_repo/versions/017_aggregates.py b/nova/db/sqlalchemy/api_migrations/migrate_repo/versions/017_aggregates.py new file mode 100644 index 00000000000..482a6aa9d01 --- /dev/null +++ b/nova/db/sqlalchemy/api_migrations/migrate_repo/versions/017_aggregates.py @@ -0,0 +1,73 @@ +# 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. +"""API Database migrations for aggregates""" + +from migrate import UniqueConstraint +from sqlalchemy import Column +from sqlalchemy import DateTime +from sqlalchemy import ForeignKey +from sqlalchemy import Index +from sqlalchemy import Integer +from sqlalchemy import MetaData +from sqlalchemy import String +from sqlalchemy import Table + + +def upgrade(migrate_engine): + meta = MetaData() + meta.bind = migrate_engine + + aggregates = Table('aggregates', meta, + Column('created_at', DateTime), + Column('updated_at', DateTime), + Column('id', Integer, primary_key=True, nullable=False), + Column('uuid', String(length=36)), + Column('name', String(length=255)), + Index('aggregate_uuid_idx', 'uuid'), + UniqueConstraint('name', name='uniq_aggregate0name'), + mysql_engine='InnoDB', + mysql_charset='utf8' + ) + + aggregates.create(checkfirst=True) + + aggregate_hosts = Table('aggregate_hosts', meta, + Column('created_at', DateTime), + Column('updated_at', DateTime), + Column('id', Integer, primary_key=True, nullable=False), + Column('host', String(length=255)), + Column('aggregate_id', Integer, ForeignKey('aggregates.id'), + nullable=False), + UniqueConstraint('host', 'aggregate_id', + name='uniq_aggregate_hosts0host0aggregate_id'), + mysql_engine='InnoDB', + mysql_charset='utf8' + ) + + aggregate_hosts.create(checkfirst=True) + + aggregate_metadata = Table('aggregate_metadata', meta, + Column('created_at', DateTime), + Column('updated_at', DateTime), + Column('id', Integer, primary_key=True, nullable=False), + Column('aggregate_id', Integer, ForeignKey('aggregates.id'), + nullable=False), + Column('key', String(length=255), nullable=False), + Column('value', String(length=255), nullable=False), + UniqueConstraint('aggregate_id', 'key', + name='uniq_aggregate_metadata0aggregate_id0key'), + Index('aggregate_metadata_key_idx', 'key'), + mysql_engine='InnoDB', + mysql_charset='utf8' + ) + + aggregate_metadata.create(checkfirst=True) diff --git a/nova/db/sqlalchemy/api_models.py b/nova/db/sqlalchemy/api_models.py index ca568066b00..e3dc3796219 100644 --- a/nova/db/sqlalchemy/api_models.py +++ b/nova/db/sqlalchemy/api_models.py @@ -37,6 +37,68 @@ class _NovaAPIBase(models.ModelBase, models.TimestampMixin): API_BASE = declarative_base(cls=_NovaAPIBase) +class AggregateHost(API_BASE): + """Represents a host that is member of an aggregate.""" + __tablename__ = 'aggregate_hosts' + __table_args__ = (schema.UniqueConstraint( + "host", "aggregate_id", + name="uniq_aggregate_hosts0host0aggregate_id" + ), + ) + id = Column(Integer, primary_key=True, autoincrement=True) + host = Column(String(255)) + aggregate_id = Column(Integer, ForeignKey('aggregates.id'), nullable=False) + + +class AggregateMetadata(API_BASE): + """Represents a metadata key/value pair for an aggregate.""" + __tablename__ = 'aggregate_metadata' + __table_args__ = ( + schema.UniqueConstraint("aggregate_id", "key", + name="uniq_aggregate_metadata0aggregate_id0key" + ), + Index('aggregate_metadata_key_idx', 'key'), + ) + id = Column(Integer, primary_key=True) + key = Column(String(255), nullable=False) + value = Column(String(255), nullable=False) + aggregate_id = Column(Integer, ForeignKey('aggregates.id'), nullable=False) + + +class Aggregate(API_BASE): + """Represents a cluster of hosts that exists in this zone.""" + __tablename__ = 'aggregates' + __table_args__ = (Index('aggregate_uuid_idx', 'uuid'), + schema.UniqueConstraint( + "name", name="uniq_aggregate0name") + ) + id = Column(Integer, primary_key=True, autoincrement=True) + uuid = Column(String(36)) + name = Column(String(255)) + _hosts = orm.relationship(AggregateHost, + primaryjoin='Aggregate.id == AggregateHost.aggregate_id') + _metadata = orm.relationship(AggregateMetadata, + primaryjoin='Aggregate.id == AggregateMetadata.aggregate_id') + + @property + def _extra_keys(self): + return ['hosts', 'metadetails', 'availability_zone'] + + @property + def hosts(self): + return [h.host for h in self._hosts] + + @property + def metadetails(self): + return {m.key: m.value for m in self._metadata} + + @property + def availability_zone(self): + if 'availability_zone' not in self.metadetails: + return None + return self.metadetails['availability_zone'] + + class CellMapping(API_BASE): """Contains information on communicating with a cell""" __tablename__ = 'cell_mappings' diff --git a/nova/tests/functional/db/api/test_migrations.py b/nova/tests/functional/db/api/test_migrations.py index eeb254f7e60..d464b5206a2 100644 --- a/nova/tests/functional/db/api/test_migrations.py +++ b/nova/tests/functional/db/api/test_migrations.py @@ -348,6 +348,43 @@ def _check_016(self, engine, data): self.assertColumnExists(engine, 'resource_provider_aggregates', 'aggregate_id') + def _check_017(self, engine, data): + # aggregate_metadata + for column in ['created_at', + 'updated_at', + 'id', + 'aggregate_id', + 'key', + 'value']: + self.assertColumnExists(engine, 'aggregate_metadata', column) + + self.assertUniqueConstraintExists(engine, 'aggregate_metadata', + ['aggregate_id', 'key']) + self.assertIndexExists(engine, 'aggregate_metadata', + 'aggregate_metadata_key_idx') + + # aggregate_hosts + for column in ['created_at', + 'updated_at', + 'id', + 'host', + 'aggregate_id']: + self.assertColumnExists(engine, 'aggregate_hosts', column) + + self.assertUniqueConstraintExists(engine, 'aggregate_hosts', + ['host', 'aggregate_id']) + + # aggregates + for column in ['created_at', + 'updated_at', + 'id', + 'name']: + self.assertColumnExists(engine, 'aggregates', column) + + self.assertIndexExists(engine, 'aggregates', + 'aggregate_uuid_idx') + self.assertUniqueConstraintExists(engine, 'aggregates', ['name']) + class TestNovaAPIMigrationsWalkSQLite(NovaAPIMigrationsWalk, test_base.DbTestCase, diff --git a/nova/tests/functional/db/test_aggregate_model.py b/nova/tests/functional/db/test_aggregate_model.py new file mode 100644 index 00000000000..774436bc555 --- /dev/null +++ b/nova/tests/functional/db/test_aggregate_model.py @@ -0,0 +1,57 @@ +# 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 nova.db.sqlalchemy import api_models +from nova.db.sqlalchemy import models +from nova import test + + +class AggregateTablesCompareTestCase(test.NoDBTestCase): + def _get_column_list(self, model): + column_list = [m.key for m in model.__table__.columns] + return column_list + + def _check_column_list(self, + columns_new, + columns_old, + added=None, + removed=None): + for c in added or []: + columns_new.remove(c) + for c in removed or []: + columns_old.remove(c) + intersect = set(columns_new).intersection(set(columns_old)) + if intersect != set(columns_new) or intersect != set(columns_old): + return False + return True + + def _compare_models(self, m_a, m_b, + added=None, removed=None): + added = added or [] + removed = removed or ['deleted_at', 'deleted'] + c_a = self._get_column_list(m_a) + c_b = self._get_column_list(m_b) + self.assertTrue(self._check_column_list(c_a, c_b, + added=added, + removed=removed)) + + def test_tables_aggregate_hosts(self): + self._compare_models(api_models.AggregateHost(), + models.AggregateHost()) + + def test_tables_aggregate_metadata(self): + self._compare_models(api_models.AggregateMetadata(), + models.AggregateMetadata()) + + def test_tables_aggregates(self): + self._compare_models(api_models.Aggregate(), + models.Aggregate())