From: Steven Hardy Date: Mon, 14 Jan 2013 16:59:03 +0000 (+0000) Subject: heat engine : Implement Count property for WaitCondition X-Git-Tag: 2014.1~1012 X-Git-Url: https://review.fuel-infra.org/gitweb?a=commitdiff_plain;h=21c960aca3d44ba598500aaa7b338c746adff5a9;p=openstack-build%2Fheat-build.git heat engine : Implement Count property for WaitCondition The Count property of WaitCondition is currently ignored, so regardless of the number specified the WaitCondition resource will always go to CREATE_COMPLETE on the first WaitConditionHandle signal. This patch fixes this so the Count property is handled correctly. fixes bug 1097813 Change-Id: I71348c553702c0282a8826488c953ac3c2c76e38 Signed-off-by: Steven Hardy --- diff --git a/heat/engine/resources/wait_condition.py b/heat/engine/resources/wait_condition.py index 49557b78..fb18e371 100644 --- a/heat/engine/resources/wait_condition.py +++ b/heat/engine/resources/wait_condition.py @@ -118,32 +118,60 @@ class WaitConditionHandle(resource.Resource): Check the format of the provided metadata is as expected. metadata must use the following format: { - "Status" : "Status (should be SUCCESS or FAILURE)" - "UniqueId" : "Some ID, must be unique for Count>1", + "Status" : "Status (must be SUCCESS or FAILURE)" + "UniqueId" : "Some ID, should be unique for Count>1", "Data" : "Arbitrary Data", "Reason" : "Reason String" } """ expected_keys = ['Data', 'Reason', 'Status', 'UniqueId'] - return sorted(metadata.keys()) == expected_keys + if sorted(metadata.keys()) == expected_keys: + return metadata['Status'] in (SUCCESS, FAILURE) def metadata_update(self, metadata): ''' Validate and update the resource metadata ''' if self._metadata_format_ok(metadata): - self.metadata = metadata + rsrc_metadata = self.metadata + if metadata['UniqueId'] in rsrc_metadata: + logger.warning("Overwriting Metadata item for UniqueId %s!" % + metadata['UniqueId']) + new_metadata = {} + for k in ('Data', 'Reason', 'Status'): + new_metadata[k] = metadata[k] + # Note we can't update self.metadata directly, as it + # is a Metadata descriptor object which only supports get/set + rsrc_metadata.update({metadata['UniqueId']: new_metadata}) + self.metadata = rsrc_metadata else: logger.error("Metadata failed validation for %s" % self.name) raise ValueError("Metadata format invalid") + def get_status(self): + ''' + Return a list of the Status values for the handle signals + ''' + return [self.metadata[s]['Status'] + for s in self.metadata] + + def get_status_reason(self, status): + ''' + Return the reason associated with a particular status + If there is more than one handle signal matching the specified status + then return a semicolon delimited string containing all reasons + ''' + return ';'.join([self.metadata[s]['Reason'] + for s in self.metadata + if self.metadata[s]['Status'] == status]) + WAIT_STATUSES = ( - WAITING, + FAILURE, TIMEDOUT, SUCCESS, ) = ( - 'WAITING', + 'FAILURE', 'TIMEDOUT', 'SUCCESS', ) @@ -179,15 +207,13 @@ class WaitCondition(resource.Resource): handle_id = identifier.ResourceIdentifier.from_arn_url(handle_url) return handle_id.resource_name - def _get_status_reason(self, handle): - return (handle.metadata.get('Status', WAITING), - handle.metadata.get('Reason', 'Reason not provided')) - def _create_timeout(self): return eventlet.Timeout(self.timeout) def handle_create(self): tmo = None + status = FAILURE + reason = "Unknown reason" try: # keep polling our Metadata to see if the cfn-signal has written # it yet. The execution here is limited by timeout. @@ -196,15 +222,24 @@ class WaitCondition(resource.Resource): handle = self.stack[handle_res_name] self.resource_id_set(handle_res_name) - (status, reason) = (WAITING, '') - - while status == WAITING: - (status, reason) = self._get_status_reason(handle) - if status == WAITING: - logger.debug('Polling for WaitCondition completion,' + - ' sleeping for %s seconds, timeout %s' % - (self.sleep_time, self.timeout)) - eventlet.sleep(self.sleep_time) + # Poll for WaitConditionHandle signals indicating + # SUCCESS/FAILURE. We need self.count SUCCESS signals + # before we can declare the WaitCondition CREATE_COMPLETE + handle_status = handle.get_status() + while (FAILURE not in handle_status + and len(handle_status) < self.count): + logger.debug('Polling for WaitCondition completion,' + + ' sleeping for %s seconds, timeout %s' % + (self.sleep_time, self.timeout)) + eventlet.sleep(self.sleep_time) + handle_status = handle.get_status() + + if FAILURE in handle_status: + reason = handle.get_status_reason(FAILURE) + elif (len(handle_status) == self.count and + handle_status == [SUCCESS] * self.count): + logger.debug("WaitCondition %s SUCCESS" % self.name) + status = SUCCESS except eventlet.Timeout as t: if t is not tmo: diff --git a/heat/tests/test_waitcondition.py b/heat/tests/test_waitcondition.py index c11e1864..d134d177 100644 --- a/heat/tests/test_waitcondition.py +++ b/heat/tests/test_waitcondition.py @@ -50,14 +50,35 @@ test_template_waitcondition = ''' } ''' +test_template_wc_count = ''' +{ + "AWSTemplateFormatVersion" : "2010-09-09", + "Description" : "Just a WaitCondition.", + "Parameters" : {}, + "Resources" : { + "WaitHandle" : { + "Type" : "AWS::CloudFormation::WaitConditionHandle" + }, + "WaitForTheHandle" : { + "Type" : "AWS::CloudFormation::WaitCondition", + "Properties" : { + "Handle" : {"Ref" : "WaitHandle"}, + "Timeout" : "5", + "Count" : "3" + } + } + } +} +''' + @attr(tag=['unit', 'resource', 'WaitCondition']) @attr(speed='slow') class WaitConditionTest(unittest.TestCase): def setUp(self): self.m = mox.Mox() - self.m.StubOutWithMock(wc.WaitCondition, - '_get_status_reason') + self.m.StubOutWithMock(wc.WaitConditionHandle, + 'get_status') self.m.StubOutWithMock(wc.WaitCondition, '_create_timeout') self.m.StubOutWithMock(eventlet, 'sleep') @@ -68,91 +89,137 @@ class WaitConditionTest(unittest.TestCase): self.fc = fakes.FakeKeystoneClient() def tearDown(self): + self.stack.delete() + self.m.VerifyAll() self.m.UnsetStubs() - def create_stack(self, stack_name, temp, params): + def create_stack(self, stack_name='test_stack', + template=test_template_waitcondition, params={}): + temp = template_format.parse(template) template = parser.Template(temp) parameters = parser.Parameters(stack_name, template, params) stack = parser.Stack(context.get_admin_context(), stack_name, template, parameters) stack.store() + + self.m.StubOutWithMock(wc.WaitConditionHandle, 'keystone') + wc.WaitConditionHandle.keystone().MultipleTimes().AndReturn(self.fc) + + id = identifier.ResourceIdentifier('test_tenant', stack.name, + stack.id, '', 'WaitHandle') + self.m.StubOutWithMock(wc.WaitConditionHandle, 'identifier') + wc.WaitConditionHandle.identifier().MultipleTimes().AndReturn(id) + return stack def test_post_success_to_handle(self): + self.stack = self.create_stack() + wc.WaitCondition._create_timeout().AndReturn(eventlet.Timeout(5)) + wc.WaitConditionHandle.get_status().AndReturn([]) + eventlet.sleep(1).AndReturn(None) + wc.WaitConditionHandle.get_status().AndReturn([]) + eventlet.sleep(1).AndReturn(None) + wc.WaitConditionHandle.get_status().AndReturn(['SUCCESS']) - t = template_format.parse(test_template_waitcondition) - stack = self.create_stack('test_stack', t, {}) + self.m.ReplayAll() + + self.stack.create() + + resource = self.stack.resources['WaitForTheHandle'] + self.assertEqual(resource.state, + 'CREATE_COMPLETE') + + r = db_api.resource_get_by_name_and_stack(None, 'WaitHandle', + self.stack.id) + self.assertEqual(r.name, 'WaitHandle') + def test_post_failure_to_handle(self): + self.stack = self.create_stack() wc.WaitCondition._create_timeout().AndReturn(eventlet.Timeout(5)) - wc.WaitCondition._get_status_reason( - mox.IgnoreArg()).AndReturn(('WAITING', '')) + wc.WaitConditionHandle.get_status().AndReturn([]) eventlet.sleep(1).AndReturn(None) - wc.WaitCondition._get_status_reason( - mox.IgnoreArg()).AndReturn(('WAITING', '')) + wc.WaitConditionHandle.get_status().AndReturn([]) eventlet.sleep(1).AndReturn(None) - wc.WaitCondition._get_status_reason( - mox.IgnoreArg()).AndReturn(('SUCCESS', 'woot toot')) + wc.WaitConditionHandle.get_status().AndReturn(['FAILURE']) - self.m.StubOutWithMock(wc.WaitConditionHandle, 'keystone') - wc.WaitConditionHandle.keystone().MultipleTimes().AndReturn(self.fc) + self.m.ReplayAll() - id = identifier.ResourceIdentifier('test_tenant', stack.name, - stack.id, '', 'WaitHandle') - self.m.StubOutWithMock(wc.WaitConditionHandle, 'identifier') - wc.WaitConditionHandle.identifier().MultipleTimes().AndReturn(id) + self.stack.create() + + resource = self.stack.resources['WaitForTheHandle'] + self.assertEqual(resource.state, + 'CREATE_FAILED') + + r = db_api.resource_get_by_name_and_stack(None, 'WaitHandle', + self.stack.id) + self.assertEqual(r.name, 'WaitHandle') + + def test_post_success_to_handle_count(self): + self.stack = self.create_stack(template=test_template_wc_count) + wc.WaitCondition._create_timeout().AndReturn(eventlet.Timeout(5)) + wc.WaitConditionHandle.get_status().AndReturn([]) + eventlet.sleep(1).AndReturn(None) + wc.WaitConditionHandle.get_status().AndReturn(['SUCCESS']) + eventlet.sleep(1).AndReturn(None) + wc.WaitConditionHandle.get_status().AndReturn(['SUCCESS', 'SUCCESS']) + eventlet.sleep(1).AndReturn(None) + wc.WaitConditionHandle.get_status().AndReturn(['SUCCESS', 'SUCCESS', + 'SUCCESS']) self.m.ReplayAll() - stack.create() + self.stack.create() - resource = stack.resources['WaitForTheHandle'] + resource = self.stack.resources['WaitForTheHandle'] self.assertEqual(resource.state, 'CREATE_COMPLETE') r = db_api.resource_get_by_name_and_stack(None, 'WaitHandle', - stack.id) + self.stack.id) self.assertEqual(r.name, 'WaitHandle') - self.m.VerifyAll() + def test_post_failure_to_handle_count(self): + self.stack = self.create_stack(template=test_template_wc_count) + wc.WaitCondition._create_timeout().AndReturn(eventlet.Timeout(5)) + wc.WaitConditionHandle.get_status().AndReturn([]) + eventlet.sleep(1).AndReturn(None) + wc.WaitConditionHandle.get_status().AndReturn(['SUCCESS']) + eventlet.sleep(1).AndReturn(None) + wc.WaitConditionHandle.get_status().AndReturn(['SUCCESS', 'FAILURE']) - def test_timeout(self): + self.m.ReplayAll() + + self.stack.create() + + resource = self.stack.resources['WaitForTheHandle'] + self.assertEqual(resource.state, + 'CREATE_FAILED') - t = template_format.parse(test_template_waitcondition) - stack = self.create_stack('test_stack', t, {}) + r = db_api.resource_get_by_name_and_stack(None, 'WaitHandle', + self.stack.id) + self.assertEqual(r.name, 'WaitHandle') + def test_timeout(self): + self.stack = self.create_stack() tmo = eventlet.Timeout(6) wc.WaitCondition._create_timeout().AndReturn(tmo) - wc.WaitCondition._get_status_reason( - mox.IgnoreArg()).AndReturn(('WAITING', '')) + wc.WaitConditionHandle.get_status().AndReturn([]) eventlet.sleep(1).AndReturn(None) - wc.WaitCondition._get_status_reason( - mox.IgnoreArg()).AndReturn(('WAITING', '')) + wc.WaitConditionHandle.get_status().AndReturn([]) eventlet.sleep(1).AndRaise(tmo) - self.m.StubOutWithMock(wc.WaitConditionHandle, 'keystone') - wc.WaitConditionHandle.keystone().MultipleTimes().AndReturn(self.fc) - - id = identifier.ResourceIdentifier('test_tenant', stack.name, - stack.id, '', 'WaitHandle') - self.m.StubOutWithMock(wc.WaitConditionHandle, 'identifier') - wc.WaitConditionHandle.identifier().MultipleTimes().AndReturn(id) - self.m.ReplayAll() - stack.create() + self.stack.create() - resource = stack.resources['WaitForTheHandle'] + resource = self.stack.resources['WaitForTheHandle'] self.assertEqual(resource.state, 'CREATE_FAILED') self.assertEqual(wc.WaitCondition.UPDATE_REPLACE, resource.handle_update()) - stack.delete() - - self.m.VerifyAll() - @attr(tag=['unit', 'resource', 'WaitConditionHandle']) @attr(speed='fast') @@ -163,8 +230,11 @@ class WaitConditionHandleTest(unittest.TestCase): 'http://127.0.0.1:8000/v1/waitcondition') self.fc = fakes.FakeKeystoneClient() + self.stack = self.create_stack() def tearDown(self): + self.stack.delete() + self.m.VerifyAll() self.m.UnsetStubs() def create_stack(self, stack_name='test_stack2', params={}): @@ -180,9 +250,8 @@ class WaitConditionHandleTest(unittest.TestCase): stack.store() # Stub waitcondition status so all goes CREATE_COMPLETE - self.m.StubOutWithMock(wc.WaitCondition, '_get_status_reason') - wc.WaitCondition._get_status_reason( - mox.IgnoreArg()).AndReturn(('SUCCESS', 'woot toot')) + self.m.StubOutWithMock(wc.WaitConditionHandle, 'get_status') + wc.WaitConditionHandle.get_status().AndReturn(['SUCCESS']) self.m.StubOutWithMock(wc.WaitCondition, '_create_timeout') wc.WaitCondition._create_timeout().AndReturn(eventlet.Timeout(5)) @@ -195,20 +264,19 @@ class WaitConditionHandleTest(unittest.TestCase): self.m.StubOutWithMock(wc.WaitConditionHandle, 'identifier') wc.WaitConditionHandle.identifier().MultipleTimes().AndReturn(id) + self.m.ReplayAll() + stack.create() + return stack def test_handle(self): - stack = self.create_stack() - # Stub time to a fixed value so we can get an expected signature t = time.gmtime(1354196977) self.m.StubOutWithMock(time, 'gmtime') time.gmtime().MultipleTimes().AndReturn(t) - self.m.ReplayAll() - stack.create() - resource = stack.resources['WaitHandle'] + resource = self.stack.resources['WaitHandle'] self.assertEqual(resource.state, 'CREATE_COMPLETE') expected_url = "".join([ @@ -227,50 +295,96 @@ class WaitConditionHandleTest(unittest.TestCase): self.assertEqual(resource.UPDATE_REPLACE, resource.handle_update()) - stack.delete() - - self.m.VerifyAll() - def test_metadata_update(self): - stack = self.create_stack() - self.m.ReplayAll() - stack.create() - - resource = stack.resources['WaitHandle'] + resource = self.stack.resources['WaitHandle'] self.assertEqual(resource.state, 'CREATE_COMPLETE') test_metadata = {'Data': 'foo', 'Reason': 'bar', 'Status': 'SUCCESS', 'UniqueId': '123'} resource.metadata_update(test_metadata) - self.assertEqual(resource.metadata, test_metadata) - - stack.delete() - - self.m.VerifyAll() + handle_metadata = {u'123': {u'Data': u'foo', + u'Reason': u'bar', + u'Status': u'SUCCESS'}} + self.assertEqual(resource.metadata, handle_metadata) def test_metadata_update_invalid(self): - stack = self.create_stack() - self.m.ReplayAll() - stack.create() - - resource = stack.resources['WaitHandle'] + resource = self.stack.resources['WaitHandle'] self.assertEqual(resource.state, 'CREATE_COMPLETE') # metadata_update should raise a ValueError if the metadata # is missing any of the expected keys - err1_metadata = {'Data': 'foo', 'Status': 'SUCCESS', 'UniqueId': '123'} - self.assertRaises(ValueError, resource.metadata_update, err1_metadata) + err_metadata = {'Data': 'foo', 'Status': 'SUCCESS', 'UniqueId': '123'} + self.assertRaises(ValueError, resource.metadata_update, err_metadata) + + err_metadata = {'Data': 'foo', 'Reason': 'bar', 'UniqueId': '1234'} + self.assertRaises(ValueError, resource.metadata_update, err_metadata) + + err_metadata = {'Data': 'foo', 'Reason': 'bar', 'UniqueId': '1234'} + self.assertRaises(ValueError, resource.metadata_update, err_metadata) + + err_metadata = {'data': 'foo', 'reason': 'bar', + 'status': 'SUCCESS', 'uniqueid': '1234'} + self.assertRaises(ValueError, resource.metadata_update, err_metadata) + + # Also any Status other than SUCCESS or FAILURE should be rejected + err_metadata = {'Data': 'foo', 'Reason': 'bar', + 'Status': 'UCCESS', 'UniqueId': '123'} + self.assertRaises(ValueError, resource.metadata_update, err_metadata) + err_metadata = {'Data': 'foo', 'Reason': 'bar', + 'Status': 'wibble', 'UniqueId': '123'} + self.assertRaises(ValueError, resource.metadata_update, err_metadata) + err_metadata = {'Data': 'foo', 'Reason': 'bar', + 'Status': 'success', 'UniqueId': '123'} + self.assertRaises(ValueError, resource.metadata_update, err_metadata) + err_metadata = {'Data': 'foo', 'Reason': 'bar', + 'Status': 'FAIL', 'UniqueId': '123'} + self.assertRaises(ValueError, resource.metadata_update, err_metadata) + + def test_get_status(self): + resource = self.stack.resources['WaitHandle'] + self.assertEqual(resource.state, 'CREATE_COMPLETE') - err1_metadata = {'Data': 'foo', 'Reason': 'bar', 'UniqueId': '1234'} - self.assertRaises(ValueError, resource.metadata_update, err1_metadata) + # UnsetStubs before teardown, don't want get_status stubbed anymore.. + self.m.UnsetStubs() + + # ..and re-Stub the stuff we still need stubbed + self.m.StubOutWithMock(wc.WaitConditionHandle, 'keystone') + wc.WaitConditionHandle.keystone().MultipleTimes().AndReturn(self.fc) - err1_metadata = {'Data': 'foo', 'Reason': 'bar', 'UniqueId': '1234'} - self.assertRaises(ValueError, resource.metadata_update, err1_metadata) + id = identifier.ResourceIdentifier('test_tenant', self.stack.name, + self.stack.id, '', 'WaitHandle') + self.m.StubOutWithMock(wc.WaitConditionHandle, 'identifier') + wc.WaitConditionHandle.identifier().MultipleTimes().AndReturn(id) - err1_metadata = {'data': 'foo', 'reason': 'bar', - 'status': 'SUCCESS', 'uniqueid': '1234'} - self.assertRaises(ValueError, resource.metadata_update, err1_metadata) + self.m.ReplayAll() - stack.delete() + self.assertEqual(resource.get_status(), []) - self.m.VerifyAll() + test_metadata = {'Data': 'foo', 'Reason': 'bar', + 'Status': 'SUCCESS', 'UniqueId': '123'} + resource.metadata_update(test_metadata) + self.assertEqual(resource.get_status(), ['SUCCESS']) + + test_metadata = {'Data': 'foo', 'Reason': 'bar', + 'Status': 'SUCCESS', 'UniqueId': '456'} + resource.metadata_update(test_metadata) + self.assertEqual(resource.get_status(), ['SUCCESS', 'SUCCESS']) + + def test_get_status_reason(self): + resource = self.stack.resources['WaitHandle'] + self.assertEqual(resource.state, 'CREATE_COMPLETE') + + test_metadata = {'Data': 'foo', 'Reason': 'bar', + 'Status': 'SUCCESS', 'UniqueId': '123'} + resource.metadata_update(test_metadata) + self.assertEqual(resource.get_status_reason('SUCCESS'), 'bar') + + test_metadata = {'Data': 'dog', 'Reason': 'cat', + 'Status': 'SUCCESS', 'UniqueId': '456'} + resource.metadata_update(test_metadata) + self.assertEqual(resource.get_status_reason('SUCCESS'), 'bar;cat') + + test_metadata = {'Data': 'boo', 'Reason': 'hoo', + 'Status': 'FAILURE', 'UniqueId': '789'} + resource.metadata_update(test_metadata) + self.assertEqual(resource.get_status_reason('FAILURE'), 'hoo') diff --git a/templates/WordPress_Single_Instance_With_HA.template b/templates/WordPress_Single_Instance_With_HA.template index a3c864ce..bb048fad 100644 --- a/templates/WordPress_Single_Instance_With_HA.template +++ b/templates/WordPress_Single_Instance_With_HA.template @@ -277,6 +277,7 @@ "DependsOn" : "WikiDatabase", "Properties" : { "Handle" : {"Ref" : "WaitHandle"}, + "Count" : "1", "Timeout" : "600" } }