diff --git a/heat/engine/parser.py b/heat/engine/parser.py index fb46a24214..c91792773e 100644 --- a/heat/engine/parser.py +++ b/heat/engine/parser.py @@ -22,7 +22,7 @@ import logging from heat.common import exception from heat.engine import checkeddict from heat.engine import dependencies -from heat.engine.resources import Resource +from heat.engine import resources from heat.db import api as db_api @@ -234,6 +234,9 @@ class Stack(object): DELETE_FAILED = 'DELETE_FAILED' DELETE_COMPLETE = 'DELETE_COMPLETE' + created_time = resources.Timestamp(db_api.stack_get, 'created_at') + updated_time = resources.Timestamp(db_api.stack_get, 'updated_at') + def __init__(self, context, stack_name, template, parameters=None, stack_id=None, state=None, state_description='', timeout_mins=60): @@ -257,7 +260,7 @@ class Stack(object): self.outputs = self.resolve_static_data(self.t[OUTPUTS]) self.resources = dict((name, - Resource(name, data, self)) + resources.Resource(name, data, self)) for (name, data) in self.t[RESOURCES].items()) self.dependencies = self._get_dependencies(self.resources.itervalues()) diff --git a/heat/engine/resources.py b/heat/engine/resources.py index 40b8822291..2d24693914 100644 --- a/heat/engine/resources.py +++ b/heat/engine/resources.py @@ -64,6 +64,32 @@ class Metadata(object): return None +class Timestamp(object): + ''' + A descriptor for fetching an up-to-date timestamp from the database. + ''' + + def __init__(self, db_fetch, attribute): + ''' + Initialise with a function to fetch the database representation of an + object (given a context and ID) and the name of the attribute to + retrieve. + ''' + self.db_fetch = db_fetch + self.attribute = attribute + + def __get__(self, obj, obj_class): + ''' + Get the latest data from the database for the given object and class. + ''' + if obj is None or obj.id is None: + return None + + o = self.db_fetch(obj.context, obj.id) + o.refresh(attrs=[self.attribute]) + return getattr(o, self.attribute) + + class Resource(object): CREATE_IN_PROGRESS = 'IN_PROGRESS' CREATE_FAILED = 'CREATE_FAILED' @@ -78,6 +104,9 @@ class Resource(object): # If True, this resource must be created before it can be referenced. strict_dependency = True + created_time = Timestamp(db_api.resource_get, 'created_at') + updated_time = Timestamp(db_api.resource_get, 'updated_at') + metadata = Metadata() def __new__(cls, name, json, stack): diff --git a/heat/tests/test_parser.py b/heat/tests/test_parser.py index 8ad8ac3fe7..d58ed45591 100644 --- a/heat/tests/test_parser.py +++ b/heat/tests/test_parser.py @@ -17,8 +17,9 @@ import nose import unittest from nose.plugins.attrib import attr import mox - import json + +from heat.common import context from heat.common import exception from heat.engine import parser from heat.engine import checkeddict @@ -318,6 +319,20 @@ class ParametersTest(unittest.TestCase): @attr(tag=['unit', 'parser', 'stack']) @attr(speed='fast') class StackTest(unittest.TestCase): + def setUp(self): + self.username = 'parser_stack_test_user' + + self.m = mox.Mox() + + self.ctx = context.get_admin_context() + self.m.StubOutWithMock(self.ctx, 'username') + self.ctx.username = self.username + + self.m.ReplayAll() + + def tearDown(self): + self.m.UnsetStubs() + def test_state_defaults(self): stack = parser.Stack(None, 'test_stack', parser.Template({})) self.assertEqual(stack.state, None) @@ -341,6 +356,23 @@ class StackTest(unittest.TestCase): self.assertRaises(exception.NotFound, parser.Stack.load, None, -1) + def test_created_time(self): + stack = parser.Stack(self.ctx, 'creation_time_test', + parser.Template({})) + self.assertEqual(stack.created_time, None) + stack.store() + self.assertNotEqual(stack.created_time, None) + + def test_updated_time(self): + stack = parser.Stack(self.ctx, 'update_time_test', + parser.Template({})) + self.assertEqual(stack.updated_time, None) + stack.store() + stored_time = stack.updated_time + stack.state_set(stack.IN_PROGRESS, 'testing') + self.assertNotEqual(stack.updated_time, None) + self.assertNotEqual(stack.updated_time, stored_time) + # allows testing of the test directly, shown below if __name__ == '__main__': sys.argv.append(__file__) diff --git a/heat/tests/test_resource.py b/heat/tests/test_resource.py index 915a2e1499..875e975de4 100644 --- a/heat/tests/test_resource.py +++ b/heat/tests/test_resource.py @@ -28,11 +28,12 @@ from heat.engine import resources @attr(speed='fast') class ResourceTest(unittest.TestCase): def setUp(self): - self.stack = parser.Stack(None, 'test_stack', parser.Template({})) + self.stack = parser.Stack(None, 'test_stack', parser.Template({}), + stack_id=-1) def test_state_defaults(self): tmpl = {'Type': 'Foo'} - res = resources.GenericResource('test_resource', tmpl, self.stack) + res = resources.GenericResource('test_res_def', tmpl, self.stack) self.assertEqual(res.state, None) self.assertEqual(res.state_description, '') @@ -48,6 +49,22 @@ class ResourceTest(unittest.TestCase): res.state_set('blarg', 'wibble') self.assertEqual(res.state_description, 'wibble') + def test_created_time(self): + tmpl = {'Type': 'Foo'} + res = resources.GenericResource('test_res_new', tmpl, self.stack) + self.assertEqual(res.created_time, None) + res._store() + self.assertNotEqual(res.created_time, None) + + def test_updated_time(self): + tmpl = {'Type': 'Foo'} + res = resources.GenericResource('test_res_upd', tmpl, self.stack) + res._store() + stored_time = res.updated_time + res.state_set(res.CREATE_IN_PROGRESS, 'testing') + self.assertNotEqual(res.updated_time, None) + self.assertNotEqual(res.updated_time, stored_time) + def test_parsed_template(self): tmpl = { 'Type': 'Foo',