Add extra columns for resource table
This patch adds following columns for resource table: - `needed_by` (a list of Resource keys) - `requires` (a list of Resource keys) - `replaces` (a single Resource key, Null by default) - `replaced_by` (a single Resource key, Null by default) - `current_template_id` (a single RawTemplate key) Co-Authored-by: Angus Salkeld <asalkeld@mirantis.com> Co-Authored-By: Qiming Teng <tengqim@cn.ibm.com> Change-Id: I65e1032e84b40cb7ae3126fa6b63c914988cc970 Implements: blueprint convergence-resource-table
This commit is contained in:
parent
d98e868980
commit
2c21d660b3
heat
db/sqlalchemy
engine
objects
tests/db
@ -0,0 +1,89 @@
|
|||||||
|
#
|
||||||
|
# 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 migrate.changeset import constraint
|
||||||
|
import sqlalchemy
|
||||||
|
|
||||||
|
from heat.db.sqlalchemy import types
|
||||||
|
from heat.db.sqlalchemy import utils as migrate_utils
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade(migrate_engine):
|
||||||
|
meta = sqlalchemy.MetaData(bind=migrate_engine)
|
||||||
|
|
||||||
|
resource = sqlalchemy.Table('resource', meta, autoload=True)
|
||||||
|
raw_template = sqlalchemy.Table('raw_template', meta, autoload=True)
|
||||||
|
|
||||||
|
needed_by = sqlalchemy.Column('needed_by', types.List)
|
||||||
|
requires = sqlalchemy.Column('requires', types.List)
|
||||||
|
replaces = sqlalchemy.Column('replaces', sqlalchemy.Integer)
|
||||||
|
replaced_by = sqlalchemy.Column('replaced_by', sqlalchemy.Integer)
|
||||||
|
current_template_id = sqlalchemy.Column('current_template_id',
|
||||||
|
sqlalchemy.Integer)
|
||||||
|
needed_by.create(resource)
|
||||||
|
requires.create(resource)
|
||||||
|
replaces.create(resource)
|
||||||
|
replaced_by.create(resource)
|
||||||
|
current_template_id.create(resource)
|
||||||
|
|
||||||
|
fkey = constraint.ForeignKeyConstraint(
|
||||||
|
columns=[resource.c.current_template_id],
|
||||||
|
refcolumns=[raw_template.c.id],
|
||||||
|
name='current_template_fkey_ref')
|
||||||
|
fkey.create()
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade(migrate_engine):
|
||||||
|
if migrate_engine.name == 'sqlite':
|
||||||
|
_downgrade_sqlite(migrate_engine)
|
||||||
|
return
|
||||||
|
|
||||||
|
meta = sqlalchemy.MetaData(bind=migrate_engine)
|
||||||
|
|
||||||
|
resource = sqlalchemy.Table('resource', meta, autoload=True)
|
||||||
|
raw_template = sqlalchemy.Table('raw_template', meta, autoload=True)
|
||||||
|
|
||||||
|
fkey = constraint.ForeignKeyConstraint(
|
||||||
|
columns=[resource.c.current_template_id],
|
||||||
|
refcolumns=[raw_template.c.id],
|
||||||
|
name='current_template_fkey_ref')
|
||||||
|
fkey.drop()
|
||||||
|
resource.c.current_template_id.drop()
|
||||||
|
resource.c.needed_by.drop()
|
||||||
|
resource.c.requires.drop()
|
||||||
|
resource.c.replaces.drop()
|
||||||
|
resource.c.replaced_by.drop()
|
||||||
|
|
||||||
|
|
||||||
|
def _downgrade_sqlite(migrate_engine):
|
||||||
|
meta = sqlalchemy.MetaData()
|
||||||
|
meta.bind = migrate_engine
|
||||||
|
|
||||||
|
resource_table = sqlalchemy.Table('resource', meta, autoload=True)
|
||||||
|
|
||||||
|
ignorecons = ['current_template_fkey_ref']
|
||||||
|
ignorecols = [resource_table.c.current_template_id.name,
|
||||||
|
resource_table.c.needed_by.name,
|
||||||
|
resource_table.c.requires.name,
|
||||||
|
resource_table.c.replaces.name,
|
||||||
|
resource_table.c.replaced_by.name]
|
||||||
|
new_resource = migrate_utils.clone_table('new_resource',
|
||||||
|
resource_table,
|
||||||
|
meta, ignorecols=ignorecols,
|
||||||
|
ignorecons=ignorecons)
|
||||||
|
|
||||||
|
# migrate resources to new table
|
||||||
|
migrate_utils.migrate_data(migrate_engine,
|
||||||
|
resource_table,
|
||||||
|
new_resource,
|
||||||
|
skip_columns=ignorecols)
|
@ -310,6 +310,17 @@ class Resource(BASE, HeatBase, StateAware):
|
|||||||
engine_id = sqlalchemy.Column(sqlalchemy.String(36))
|
engine_id = sqlalchemy.Column(sqlalchemy.String(36))
|
||||||
atomic_key = sqlalchemy.Column(sqlalchemy.Integer)
|
atomic_key = sqlalchemy.Column(sqlalchemy.Integer)
|
||||||
|
|
||||||
|
needed_by = sqlalchemy.Column('needed_by', types.List)
|
||||||
|
requires = sqlalchemy.Column('requires', types.List)
|
||||||
|
replaces = sqlalchemy.Column('replaces', sqlalchemy.Integer,
|
||||||
|
default=None)
|
||||||
|
replaced_by = sqlalchemy.Column('replaced_by', sqlalchemy.Integer,
|
||||||
|
default=None)
|
||||||
|
current_template_id = sqlalchemy.Column(
|
||||||
|
'current_template_id',
|
||||||
|
sqlalchemy.Integer,
|
||||||
|
sqlalchemy.ForeignKey('raw_template.id'))
|
||||||
|
|
||||||
|
|
||||||
class WatchRule(BASE, HeatBase):
|
class WatchRule(BASE, HeatBase):
|
||||||
"""Represents a watch_rule created by the heat engine."""
|
"""Represents a watch_rule created by the heat engine."""
|
||||||
|
@ -42,5 +42,60 @@ class Json(LongText):
|
|||||||
return loads(value)
|
return loads(value)
|
||||||
|
|
||||||
|
|
||||||
|
class MutableList(mutable.Mutable, list):
|
||||||
|
@classmethod
|
||||||
|
def coerce(cls, key, value):
|
||||||
|
if not isinstance(value, cls):
|
||||||
|
if isinstance(value, list):
|
||||||
|
return cls(value)
|
||||||
|
return mutable.Mutable.coerce(key, value)
|
||||||
|
else:
|
||||||
|
return value
|
||||||
|
|
||||||
|
def __delitem__(self, key):
|
||||||
|
list.__delitem__(self, key)
|
||||||
|
self.changed()
|
||||||
|
|
||||||
|
def __setitem__(self, key, value):
|
||||||
|
list.__setitem__(self, key, value)
|
||||||
|
self.changed()
|
||||||
|
|
||||||
|
def __getstate__(self):
|
||||||
|
return list(self)
|
||||||
|
|
||||||
|
def __setstate__(self, state):
|
||||||
|
len = list.__len__(self)
|
||||||
|
list.__delslice__(self, 0, len)
|
||||||
|
list.__add__(self, state)
|
||||||
|
self.changed()
|
||||||
|
|
||||||
|
def append(self, value):
|
||||||
|
list.append(self, value)
|
||||||
|
self.changed()
|
||||||
|
|
||||||
|
def remove(self, value):
|
||||||
|
list.remove(self, value)
|
||||||
|
self.changed()
|
||||||
|
|
||||||
|
|
||||||
|
class List(types.TypeDecorator):
|
||||||
|
impl = types.Text
|
||||||
|
|
||||||
|
def load_dialect_impl(self, dialect):
|
||||||
|
if dialect.name == 'mysql':
|
||||||
|
return dialect.type_descriptor(mysql.LONGTEXT())
|
||||||
|
else:
|
||||||
|
return self.impl
|
||||||
|
|
||||||
|
def process_bind_param(self, value, dialect):
|
||||||
|
return dumps(value)
|
||||||
|
|
||||||
|
def process_result_value(self, value, dialect):
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
return loads(value)
|
||||||
|
|
||||||
|
|
||||||
|
MutableList.associate_with(List)
|
||||||
mutable.MutableDict.associate_with(LongText)
|
mutable.MutableDict.associate_with(LongText)
|
||||||
mutable.MutableDict.associate_with(Json)
|
mutable.MutableDict.associate_with(Json)
|
||||||
|
@ -171,6 +171,11 @@ class Resource(object):
|
|||||||
self.created_time = None
|
self.created_time = None
|
||||||
self.updated_time = None
|
self.updated_time = None
|
||||||
self._rpc_client = None
|
self._rpc_client = None
|
||||||
|
self.needed_by = None
|
||||||
|
self.requires = None
|
||||||
|
self.replaces = None
|
||||||
|
self.replaced_by = None
|
||||||
|
self.current_template_id = stack.t.id
|
||||||
|
|
||||||
resource = stack.db_resource_get(name)
|
resource = stack.db_resource_get(name)
|
||||||
if resource:
|
if resource:
|
||||||
@ -199,6 +204,11 @@ class Resource(object):
|
|||||||
self._stored_properties_data = resource.properties_data
|
self._stored_properties_data = resource.properties_data
|
||||||
self.created_time = resource.created_at
|
self.created_time = resource.created_at
|
||||||
self.updated_time = resource.updated_at
|
self.updated_time = resource.updated_at
|
||||||
|
self.needed_by = resource.needed_by
|
||||||
|
self.requires = resource.requires
|
||||||
|
self.replaces = resource.replaces
|
||||||
|
self.replaced_by = resource.replaced_by
|
||||||
|
self.current_template_id = resource.current_template_id
|
||||||
|
|
||||||
def reparse(self):
|
def reparse(self):
|
||||||
self.properties = self.t.properties(self.properties_schema,
|
self.properties = self.t.properties(self.properties_schema,
|
||||||
@ -906,6 +916,11 @@ class Resource(object):
|
|||||||
'name': self.name,
|
'name': self.name,
|
||||||
'rsrc_metadata': metadata,
|
'rsrc_metadata': metadata,
|
||||||
'properties_data': self._stored_properties_data,
|
'properties_data': self._stored_properties_data,
|
||||||
|
'needed_by': self.needed_by,
|
||||||
|
'requires': self.requires,
|
||||||
|
'replaces': self.replaces,
|
||||||
|
'replaced_by': self.replaced_by,
|
||||||
|
'current_template_id': self.current_template_id,
|
||||||
'stack_name': self.stack.name}
|
'stack_name': self.stack.name}
|
||||||
|
|
||||||
new_rs = resource_objects.Resource.create(self.context, rs)
|
new_rs = resource_objects.Resource.create(self.context, rs)
|
||||||
@ -939,6 +954,11 @@ class Resource(object):
|
|||||||
'stack_id': self.stack.id,
|
'stack_id': self.stack.id,
|
||||||
'updated_at': self.updated_time,
|
'updated_at': self.updated_time,
|
||||||
'properties_data': self._stored_properties_data,
|
'properties_data': self._stored_properties_data,
|
||||||
|
'needed_by': self.needed_by,
|
||||||
|
'requires': self.requires,
|
||||||
|
'replaces': self.replaces,
|
||||||
|
'replaced_by': self.replaced_by,
|
||||||
|
'current_template_id': self.current_template_id,
|
||||||
'nova_instance': self.resource_id})
|
'nova_instance': self.resource_id})
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
LOG.error(_LE('DB error %s'), ex)
|
LOG.error(_LE('DB error %s'), ex)
|
||||||
|
@ -34,3 +34,7 @@ class Json(fields.FieldType):
|
|||||||
|
|
||||||
class JsonField(fields.AutoTypedField):
|
class JsonField(fields.AutoTypedField):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ListField(fields.AutoTypedField):
|
||||||
|
pass
|
||||||
|
@ -52,6 +52,11 @@ class Resource(
|
|||||||
'stack': fields.ObjectField(stack.Stack, nullable=False),
|
'stack': fields.ObjectField(stack.Stack, nullable=False),
|
||||||
'engine_id': fields.StringField(nullable=True),
|
'engine_id': fields.StringField(nullable=True),
|
||||||
'atomic_key': fields.IntegerField(nullable=True),
|
'atomic_key': fields.IntegerField(nullable=True),
|
||||||
|
'current_template_id': fields.IntegerField(),
|
||||||
|
'needed_by': heat_fields.ListField(nullable=True, default=None),
|
||||||
|
'requires': heat_fields.ListField(nullable=True, default=None),
|
||||||
|
'replaces': fields.IntegerField(nullable=True),
|
||||||
|
'replaced_by': fields.IntegerField(nullable=True),
|
||||||
}
|
}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@ -584,6 +584,12 @@ class HeatMigrationsCheckers(test_migrations.WalkVersionsMixin,
|
|||||||
else:
|
else:
|
||||||
self.assertColumnIsNullable(engine, 'sync_point', column[0])
|
self.assertColumnIsNullable(engine, 'sync_point', column[0])
|
||||||
|
|
||||||
|
def _check_060(self, engine, data):
|
||||||
|
column_list = ['needed_by', 'requires', 'replaces', 'replaced_by',
|
||||||
|
'current_template_id']
|
||||||
|
for column in column_list:
|
||||||
|
self.assertColumnExists(engine, 'resource', column)
|
||||||
|
|
||||||
|
|
||||||
class TestHeatMigrationsMySQL(HeatMigrationsCheckers,
|
class TestHeatMigrationsMySQL(HeatMigrationsCheckers,
|
||||||
test_base.MySQLOpportunisticTestCase):
|
test_base.MySQLOpportunisticTestCase):
|
||||||
|
58
heat/tests/db/test_sqlalchemy_types.py
Normal file
58
heat/tests/db/test_sqlalchemy_types.py
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
#
|
||||||
|
# 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 sqlalchemy.dialects.mysql import base as mysql_base
|
||||||
|
from sqlalchemy.dialects.sqlite import base as sqlite_base
|
||||||
|
from sqlalchemy import types
|
||||||
|
import testtools
|
||||||
|
|
||||||
|
from heat.db.sqlalchemy import types as db_types
|
||||||
|
|
||||||
|
|
||||||
|
class ListTest(testtools.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(ListTest, self).setUp()
|
||||||
|
self.sqltype = db_types.List()
|
||||||
|
|
||||||
|
def test_load_dialect_impl(self):
|
||||||
|
dialect = mysql_base.MySQLDialect()
|
||||||
|
impl = self.sqltype.load_dialect_impl(dialect)
|
||||||
|
self.assertNotEqual(types.Text, type(impl))
|
||||||
|
dialect = sqlite_base.SQLiteDialect()
|
||||||
|
impl = self.sqltype.load_dialect_impl(dialect)
|
||||||
|
self.assertEqual(types.Text, type(impl))
|
||||||
|
|
||||||
|
def test_process_bind_param(self):
|
||||||
|
dialect = None
|
||||||
|
value = ['foo', 'bar']
|
||||||
|
result = self.sqltype.process_bind_param(value, dialect)
|
||||||
|
self.assertEqual('["foo", "bar"]', result)
|
||||||
|
|
||||||
|
def test_process_bind_param_null(self):
|
||||||
|
dialect = None
|
||||||
|
value = None
|
||||||
|
result = self.sqltype.process_bind_param(value, dialect)
|
||||||
|
self.assertEqual('null', result)
|
||||||
|
|
||||||
|
def test_process_result_value(self):
|
||||||
|
dialect = None
|
||||||
|
value = '["foo", "bar"]'
|
||||||
|
result = self.sqltype.process_result_value(value, dialect)
|
||||||
|
self.assertEqual(['foo', 'bar'], result)
|
||||||
|
|
||||||
|
def test_process_result_value_null(self):
|
||||||
|
dialect = None
|
||||||
|
value = None
|
||||||
|
result = self.sqltype.process_result_value(value, dialect)
|
||||||
|
self.assertIsNone(result)
|
Loading…
x
Reference in New Issue
Block a user