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:
Sergey Kraynev 2015-03-11 13:28:23 +10:00
parent d98e868980
commit 2c21d660b3
8 changed files with 248 additions and 0 deletions

View File

@ -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)

View File

@ -310,6 +310,17 @@ class Resource(BASE, HeatBase, StateAware):
engine_id = sqlalchemy.Column(sqlalchemy.String(36))
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):
"""Represents a watch_rule created by the heat engine."""

View File

@ -42,5 +42,60 @@ class Json(LongText):
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(Json)

View File

@ -171,6 +171,11 @@ class Resource(object):
self.created_time = None
self.updated_time = 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)
if resource:
@ -199,6 +204,11 @@ class Resource(object):
self._stored_properties_data = resource.properties_data
self.created_time = resource.created_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):
self.properties = self.t.properties(self.properties_schema,
@ -906,6 +916,11 @@ class Resource(object):
'name': self.name,
'rsrc_metadata': metadata,
'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}
new_rs = resource_objects.Resource.create(self.context, rs)
@ -939,6 +954,11 @@ class Resource(object):
'stack_id': self.stack.id,
'updated_at': self.updated_time,
'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})
except Exception as ex:
LOG.error(_LE('DB error %s'), ex)

View File

@ -34,3 +34,7 @@ class Json(fields.FieldType):
class JsonField(fields.AutoTypedField):
pass
class ListField(fields.AutoTypedField):
pass

View File

@ -52,6 +52,11 @@ class Resource(
'stack': fields.ObjectField(stack.Stack, nullable=False),
'engine_id': fields.StringField(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

View File

@ -584,6 +584,12 @@ class HeatMigrationsCheckers(test_migrations.WalkVersionsMixin,
else:
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,
test_base.MySQLOpportunisticTestCase):

View 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)