From 139fe4009e8776a3e56ed5023e9745577caacc4e Mon Sep 17 00:00:00 2001 From: Zane Bitter Date: Tue, 3 Jul 2012 18:17:05 +0200 Subject: [PATCH] Add a helper class for metadata With this patch we can now access resource metadata through the 'metadata' attribute and have it always fetch the latest data from the database. This reduces the chance of accidentally accessing stale data (due to the metadata being updated in a different database session via the metadata server) by providing a single implementation to access it through. Change-Id: Id411ae891d3eace746d16008a7d58bb19b4f652d Signed-off-by: Zane Bitter --- heat/engine/instance.py | 4 ++-- heat/engine/resources.py | 40 ++++++++++++++++++++++++++++++----- heat/engine/wait_condition.py | 2 +- heat/tests/test_resource.py | 38 ++++++++++++++++++++++++++++++++- 4 files changed, 75 insertions(+), 9 deletions(-) diff --git a/heat/engine/instance.py b/heat/engine/instance.py index 0bc12238..a8f147dc 100644 --- a/heat/engine/instance.py +++ b/heat/engine/instance.py @@ -187,8 +187,8 @@ class Instance(resources.Resource): attachments.append((json.dumps(metadata), 'cfn-init-data', 'x-cfninitdata')) - metadata_server = resources.metadata_server() - if metadata_server: + metadata_server = resources.Metadata.server() + if metadata_server is not None: attachments.append((metadata_server, 'cfn-metadata-server', 'x-cfninitdata')) diff --git a/heat/engine/resources.py b/heat/engine/resources.py index 5fa519ae..99c7e85c 100644 --- a/heat/engine/resources.py +++ b/heat/engine/resources.py @@ -29,11 +29,39 @@ from heat.engine import auth logger = logging.getLogger('heat.engine.resources') -def metadata_server(): - try: - return config.FLAGS.heat_metadata_server_url - except AttributeError: - return None +class Metadata(object): + ''' + A descriptor for accessing the metadata of a resource while ensuring the + most up-to-date data is always obtained from the database. + ''' + + def __get__(self, resource, resource_class): + '''Return the metadata for the owning resource.''' + if resource is None: + return None + if resource.id is None: + return resource.parsed_template('Metadata') + rs = db_api.resource_get(resource.stack.context, resource.id) + rs.refresh(attrs=['rsrc_metadata']) + return rs.rsrc_metadata + + def __set__(self, resource, metadata): + '''Update the metadata for the owning resource.''' + if resource.id is None: + raise AttributeError("Resource has not yet been created") + rs = db_api.resource_get(resource.stack.context, resource.id) + rs.update_and_save({'rsrc_metadata': metadata}) + + @staticmethod + def server(): + ''' + Get the address of the currently registered metadata server. Return + None if no server is registered. + ''' + try: + return config.FLAGS.heat_metadata_server_url + except AttributeError: + return None class Resource(object): @@ -50,6 +78,8 @@ class Resource(object): # If True, this resource must be created before it can be referenced. strict_dependency = True + metadata = Metadata() + def __new__(cls, name, json, stack): '''Create a new Resource of the appropriate class for its type.''' diff --git a/heat/engine/wait_condition.py b/heat/engine/wait_condition.py index 22d50806..46c20499 100644 --- a/heat/engine/wait_condition.py +++ b/heat/engine/wait_condition.py @@ -39,7 +39,7 @@ class WaitConditionHandle(resources.Resource): def handle_create(self): self.instance_id = '%s/stacks/%s/resources/%s' % \ - (resources.metadata_server(), + (resources.Metadata.server(), self.stack.id, self.name) diff --git a/heat/tests/test_resource.py b/heat/tests/test_resource.py index a5bf9db4..915a2e14 100644 --- a/heat/tests/test_resource.py +++ b/heat/tests/test_resource.py @@ -19,11 +19,12 @@ from nose.plugins.attrib import attr import mox import json +from heat.common import context from heat.engine import parser from heat.engine import resources -@attr(tag=['unit', 'parser', 'stack']) +@attr(tag=['unit', 'resource']) @attr(speed='fast') class ResourceTest(unittest.TestCase): def setUp(self): @@ -67,6 +68,41 @@ class ResourceTest(unittest.TestCase): self.assertEqual(res.parsed_template('foo'), {}) self.assertEqual(res.parsed_template('foo', 'bar'), 'bar') + def test_metadata_default(self): + tmpl = {'Type': 'Foo'} + res = resources.GenericResource('test_resource', tmpl, self.stack) + self.assertEqual(res.metadata, {}) + + +@attr(tag=['unit', 'resource']) +@attr(speed='fast') +class MetadataTest(unittest.TestCase): + def setUp(self): + self.m = mox.Mox() + tmpl = { + 'Type': 'Foo', + 'Metadata': {'Test': 'Initial metadata'} + } + ctx = context.get_admin_context() + self.m.StubOutWithMock(ctx, 'username') + ctx.username = 'metadata_test_user' + self.stack = parser.Stack(ctx, 'test_stack', parser.Template({})) + self.stack.store() + self.res = resources.GenericResource('metadata_resource', + tmpl, self.stack) + self.res.create() + + def tearDown(self): + self.stack.delete() + self.m.UnsetStubs() + + def test_read_initial(self): + self.assertEqual(self.res.metadata, {'Test': 'Initial metadata'}) + + def test_write(self): + test_data = {'Test': 'Newly-written data'} + self.res.metadata = test_data + self.assertEqual(self.res.metadata, test_data) # allows testing of the test directly, shown below if __name__ == '__main__': -- 2.45.2