From ea349ea48bbf6ce0bb1a0731d44d50ab4dcc9850 Mon Sep 17 00:00:00 2001 From: Steve Baker Date: Mon, 2 Feb 2015 14:30:13 +1300 Subject: [PATCH] Move deployment handle_signal to rpc call This allows deployments which haven't been created by a heat resource to be signalled, which is required for blueprint software-config-trigger. This was reverted due to tripleo regression bug #1423126. Calling timeutils.normalize_time before saving the date should prevent the database error which caused the regression. Partial-Blueprint: software-config-trigger Change-Id: I6038090ef1e9aff5908dd21e08ba403748f10424 --- .../software_config/software_deployment.py | 57 +------ heat/engine/service.py | 65 +++++++- heat/rpc/api.py | 20 +++ heat/rpc/client.py | 9 ++ heat/tests/test_engine_service.py | 121 +++++++++++++- heat/tests/test_software_deployment.py | 152 +++++------------- 6 files changed, 260 insertions(+), 164 deletions(-) diff --git a/heat/engine/resources/software_config/software_deployment.py b/heat/engine/resources/software_config/software_deployment.py index 89a11e93af..8b2c258ec8 100644 --- a/heat/engine/resources/software_config/software_deployment.py +++ b/heat/engine/resources/software_config/software_deployment.py @@ -15,7 +15,7 @@ import copy import uuid from oslo_log import log as logging -import six +from oslo_utils import timeutils from heat.common import exception from heat.common.i18n import _ @@ -448,58 +448,9 @@ class SoftwareDeployment(signal_responder.SignalResponder): return self._check_complete() def handle_signal(self, details): - sd = self.rpc_client().show_software_deployment( - self.context, self.resource_id) - sc = self.rpc_client().show_software_config( - self.context, self.properties[self.CONFIG]) - status = sd[rpc_api.SOFTWARE_DEPLOYMENT_STATUS] - - if not status == self.IN_PROGRESS: - # output values are only expected when in an IN_PROGRESS state - return - - details = details or {} - - ov = sd[rpc_api.SOFTWARE_DEPLOYMENT_OUTPUT_VALUES] or {} - status = None - status_reasons = {} - status_code = details.get(self.STATUS_CODE) - if status_code and str(status_code) != '0': - status = self.FAILED - status_reasons[self.STATUS_CODE] = _( - 'Deployment exited with non-zero status code: %s' - ) % details.get(self.STATUS_CODE) - event_reason = 'deployment failed (%s)' % status_code - else: - event_reason = 'deployment succeeded' - - for output in sc[rpc_api.SOFTWARE_CONFIG_OUTPUTS] or []: - out_key = output['name'] - if out_key in details: - ov[out_key] = details[out_key] - if output.get('error_output', False): - status = self.FAILED - status_reasons[out_key] = details[out_key] - event_reason = 'deployment failed' - - for out_key in self.ATTRIBUTES: - ov[out_key] = details.get(out_key) - - if status == self.FAILED: - # build a status reason out of all of the values of outputs - # flagged as error_output - status_reasons = [' : '.join((k, six.text_type(status_reasons[k]))) - for k in status_reasons] - status_reason = ', '.join(status_reasons) - else: - status = self.COMPLETE - status_reason = _('Outputs received') - - self.rpc_client().update_software_deployment( - self.context, deployment_id=self.resource_id, - output_values=ov, status=status, status_reason=status_reason) - # Return a string describing the outcome of handling the signal data - return event_reason + return self.rpc_client().signal_software_deployment( + self.context, self.resource_id, details, + timeutils.strtime()) def FnGetAtt(self, key, *path): ''' diff --git a/heat/engine/service.py b/heat/engine/service.py index f20bead6eb..4bd7bd2f92 100644 --- a/heat/engine/service.py +++ b/heat/engine/service.py @@ -260,7 +260,7 @@ class EngineService(service.Service): by the RPC caller. """ - RPC_API_VERSION = '1.5' + RPC_API_VERSION = '1.6' def __init__(self, host, topic, manager=None): super(EngineService, self).__init__() @@ -1487,6 +1487,66 @@ class EngineService(service.Service): self._push_metadata_software_deployments(cnxt, server_id) return api.format_software_deployment(sd) + @context.request_context + def signal_software_deployment(self, cnxt, deployment_id, details, + updated_at): + + if not deployment_id: + raise ValueError(_('deployment_id must be specified')) + + sd = db_api.software_deployment_get(cnxt, deployment_id) + status = sd.status + + if not status == rpc_api.SOFTWARE_DEPLOYMENT_IN_PROGRESS: + # output values are only expected when in an IN_PROGRESS state + return + + details = details or {} + + output_status_code = rpc_api.SOFTWARE_DEPLOYMENT_OUTPUT_STATUS_CODE + ov = sd.output_values or {} + status = None + status_reasons = {} + status_code = details.get(output_status_code) + if status_code and str(status_code) != '0': + status = rpc_api.SOFTWARE_DEPLOYMENT_FAILED + status_reasons[output_status_code] = _( + 'Deployment exited with non-zero status code: %s' + ) % details.get(output_status_code) + event_reason = 'deployment failed (%s)' % status_code + else: + event_reason = 'deployment succeeded' + + for output in sd.config.config['outputs'] or []: + out_key = output['name'] + if out_key in details: + ov[out_key] = details[out_key] + if output.get('error_output', False): + status = rpc_api.SOFTWARE_DEPLOYMENT_FAILED + status_reasons[out_key] = details[out_key] + event_reason = 'deployment failed' + + for out_key in rpc_api.SOFTWARE_DEPLOYMENT_OUTPUTS: + ov[out_key] = details.get(out_key) + + if status == rpc_api.SOFTWARE_DEPLOYMENT_FAILED: + # build a status reason out of all of the values of outputs + # flagged as error_output + status_reasons = [' : '.join((k, six.text_type(status_reasons[k]))) + for k in status_reasons] + status_reason = ', '.join(status_reasons) + else: + status = rpc_api.SOFTWARE_DEPLOYMENT_COMPLETE + status_reason = _('Outputs received') + + self.update_software_deployment( + cnxt, deployment_id=deployment_id, + output_values=ov, status=status, status_reason=status_reason, + config_id=None, input_values=None, action=None, + updated_at=updated_at) + # Return a string describing the outcome of handling the signal data + return event_reason + @context.request_context def update_software_deployment(self, cnxt, deployment_id, config_id, input_values, output_values, action, @@ -1505,7 +1565,8 @@ class EngineService(service.Service): if status_reason: update_data['status_reason'] = status_reason if updated_at: - update_data['updated_at'] = timeutils.parse_isotime(updated_at) + update_data['updated_at'] = timeutils.normalize_time( + timeutils.parse_isotime(updated_at)) else: update_data['updated_at'] = timeutils.utcnow() diff --git a/heat/rpc/api.py b/heat/rpc/api.py index 78c10e9ea1..4a619e44e4 100644 --- a/heat/rpc/api.py +++ b/heat/rpc/api.py @@ -235,6 +235,26 @@ SOFTWARE_DEPLOYMENT_KEYS = ( 'updated_time' ) +SOFTWARE_DEPLOYMENT_STATUSES = ( + SOFTWARE_DEPLOYMENT_IN_PROGRESS, + SOFTWARE_DEPLOYMENT_FAILED, + SOFTWARE_DEPLOYMENT_COMPLETE +) = ( + 'IN_PROGRESS', + 'FAILED', + 'COMPLETE' +) + +SOFTWARE_DEPLOYMENT_OUTPUTS = ( + SOFTWARE_DEPLOYMENT_OUTPUT_STDOUT, + SOFTWARE_DEPLOYMENT_OUTPUT_STDERR, + SOFTWARE_DEPLOYMENT_OUTPUT_STATUS_CODE +) = ( + 'deploy_stdout', + 'deploy_stderr', + 'deploy_status_code' +) + SNAPSHOT_KEYS = ( SNAPSHOT_ID, SNAPSHOT_NAME, diff --git a/heat/rpc/client.py b/heat/rpc/client.py index 1e1ecb5d9f..80a6e96fdd 100644 --- a/heat/rpc/client.py +++ b/heat/rpc/client.py @@ -546,6 +546,15 @@ class EngineClient(object): return self.call(cnxt, self.make_msg('delete_software_deployment', deployment_id=deployment_id)) + def signal_software_deployment(self, cnxt, deployment_id, details, + updated_at=None): + return self.call( + cnxt, self.make_msg('signal_software_deployment', + deployment_id=deployment_id, + details=details, + updated_at=updated_at), + version='1.6') + def stack_snapshot(self, ctxt, stack_identity, name): return self.call(ctxt, self.make_msg('stack_snapshot', stack_identity=stack_identity, diff --git a/heat/tests/test_engine_service.py b/heat/tests/test_engine_service.py index 017a124054..ee2aef4801 100644 --- a/heat/tests/test_engine_service.py +++ b/heat/tests/test_engine_service.py @@ -1657,7 +1657,7 @@ class StackServiceTest(common.HeatTestCase): def test_make_sure_rpc_version(self): self.assertEqual( - '1.5', + '1.6', service.EngineService.RPC_API_VERSION, ('RPC version is changed, please update this test to new version ' 'and make sure additional test cases are added for RPC APIs ' @@ -3634,6 +3634,125 @@ class SoftwareConfigServiceTest(common.HeatTestCase): deployment, self.engine.show_software_deployment(self.ctx, deployment_id)) + @mock.patch.object(service.EngineService, + '_push_metadata_software_deployments') + def test_signal_software_deployment(self, pmsd): + self.assertRaises(ValueError, + self.engine.signal_software_deployment, + self.ctx, None, {}, None) + deployment_id = str(uuid.uuid4()) + ex = self.assertRaises(dispatcher.ExpectedException, + self.engine.signal_software_deployment, + self.ctx, deployment_id, {}, None) + self.assertEqual(exception.NotFound, ex.exc_info[0]) + + deployment = self._create_software_deployment() + deployment_id = deployment['id'] + + # signal is ignore unless deployment is IN_PROGRESS + self.assertIsNone(self.engine.signal_software_deployment( + self.ctx, deployment_id, {}, None)) + + # simple signal, no data + deployment = self._create_software_deployment( + action='INIT', status='IN_PROGRESS') + deployment_id = deployment['id'] + self.assertEqual( + 'deployment succeeded', + self.engine.signal_software_deployment( + self.ctx, deployment_id, {}, None)) + sd = db_api.software_deployment_get(self.ctx, deployment_id) + self.assertEqual('COMPLETE', sd.status) + self.assertEqual('Outputs received', sd.status_reason) + self.assertEqual({ + 'deploy_status_code': None, + 'deploy_stderr': None, + 'deploy_stdout': None + }, sd.output_values) + self.assertIsNotNone(sd.updated_at) + + # simple signal, some data + config = self._create_software_config(outputs=[{'name': 'foo'}]) + deployment = self._create_software_deployment( + config_id=config['id'], action='INIT', status='IN_PROGRESS') + deployment_id = deployment['id'] + result = self.engine.signal_software_deployment( + self.ctx, + deployment_id, + {'foo': 'bar', 'deploy_status_code': 0}, + None) + self.assertEqual('deployment succeeded', result) + sd = db_api.software_deployment_get(self.ctx, deployment_id) + self.assertEqual('COMPLETE', sd.status) + self.assertEqual('Outputs received', sd.status_reason) + self.assertEqual({ + 'deploy_status_code': 0, + 'foo': 'bar', + 'deploy_stderr': None, + 'deploy_stdout': None + }, sd.output_values) + self.assertIsNotNone(sd.updated_at) + + # failed signal on deploy_status_code + config = self._create_software_config(outputs=[ + {'name': 'foo'}]) + deployment = self._create_software_deployment( + config_id=config['id'], action='INIT', status='IN_PROGRESS') + deployment_id = deployment['id'] + result = self.engine.signal_software_deployment( + self.ctx, + deployment_id, + { + 'foo': 'bar', + 'deploy_status_code': -1, + 'deploy_stderr': 'Its gone Pete Tong' + }, + None) + self.assertEqual('deployment failed (-1)', result) + sd = db_api.software_deployment_get(self.ctx, deployment_id) + self.assertEqual('FAILED', sd.status) + self.assertEqual( + ('deploy_status_code : Deployment exited with non-zero ' + 'status code: -1'), + sd.status_reason) + self.assertEqual({ + 'deploy_status_code': -1, + 'foo': 'bar', + 'deploy_stderr': 'Its gone Pete Tong', + 'deploy_stdout': None + }, sd.output_values) + self.assertIsNotNone(sd.updated_at) + + # failed signal on error_output foo + config = self._create_software_config(outputs=[ + {'name': 'foo', 'error_output': True}]) + deployment = self._create_software_deployment( + config_id=config['id'], action='INIT', status='IN_PROGRESS') + deployment_id = deployment['id'] + result = self.engine.signal_software_deployment( + self.ctx, + deployment_id, + { + 'foo': 'bar', + 'deploy_status_code': -1, + 'deploy_stderr': 'Its gone Pete Tong' + }, + None) + self.assertEqual('deployment failed', result) + sd = db_api.software_deployment_get(self.ctx, deployment_id) + self.assertEqual('FAILED', sd.status) + self.assertEqual( + ('foo : bar, deploy_status_code : Deployment exited with ' + 'non-zero status code: -1'), + sd.status_reason) + self.assertEqual({ + 'deploy_status_code': -1, + 'foo': 'bar', + 'deploy_stderr': 'Its gone Pete Tong', + 'deploy_stdout': None + }, sd.output_values) + self.assertIsNotNone(sd.updated_at) + def test_create_software_deployment(self): kwargs = { 'group': 'Heat::Chef', diff --git a/heat/tests/test_software_deployment.py b/heat/tests/test_software_deployment.py index a4163e737b..c137261142 100644 --- a/heat/tests/test_software_deployment.py +++ b/heat/tests/test_software_deployment.py @@ -641,155 +641,91 @@ class SoftwareDeploymentTest(common.HeatTestCase): def test_handle_signal_ok_zero(self): self._create_stack(self.template) self.deployment.resource_id = 'c8a19429-7fde-47ea-a42f-40045488226c' - sc = { - 'outputs': [ - {'name': 'foo'}, - {'name': 'foo2'}, - {'name': 'failed', 'error_output': True} - ] - } - sd = { - 'output_values': {}, - 'status': self.deployment.IN_PROGRESS - } - self.rpc_client.show_software_deployment.return_value = sd - self.rpc_client.show_software_config.return_value = sc + rpcc = self.rpc_client + rpcc.signal_software_deployment.return_value = 'deployment succeeded' details = { 'foo': 'bar', 'deploy_status_code': 0 } ret = self.deployment.handle_signal(details) self.assertEqual('deployment succeeded', ret) - self.assertEqual({ - 'deployment_id': 'c8a19429-7fde-47ea-a42f-40045488226c', - 'output_values': { - 'foo': 'bar', - 'deploy_status_code': 0, - 'deploy_stderr': None, - 'deploy_stdout': None - }, - 'status': 'COMPLETE', - 'status_reason': 'Outputs received'}, - self.rpc_client.update_software_deployment.call_args[1]) + ca = rpcc.signal_software_deployment.call_args[0] + self.assertEqual(self.ctx, ca[0]) + self.assertEqual('c8a19429-7fde-47ea-a42f-40045488226c', ca[1]) + self.assertEqual({'foo': 'bar', 'deploy_status_code': 0}, ca[2]) + self.assertIsNotNone(ca[3]) def test_handle_signal_ok_str_zero(self): self._create_stack(self.template) self.deployment.resource_id = 'c8a19429-7fde-47ea-a42f-40045488226c' - sc = { - 'outputs': [ - {'name': 'foo'}, - {'name': 'foo2'}, - {'name': 'failed', 'error_output': True} - ] - } - sd = { - 'output_values': {}, - 'status': self.deployment.IN_PROGRESS - } - self.rpc_client.show_software_deployment.return_value = sd - self.rpc_client.show_software_config.return_value = sc + rpcc = self.rpc_client + rpcc.signal_software_deployment.return_value = 'deployment succeeded' details = { 'foo': 'bar', 'deploy_status_code': '0' } ret = self.deployment.handle_signal(details) self.assertEqual('deployment succeeded', ret) - self.assertEqual({ - 'deployment_id': 'c8a19429-7fde-47ea-a42f-40045488226c', - 'output_values': { - 'foo': 'bar', - 'deploy_status_code': '0', - 'deploy_stderr': None, - 'deploy_stdout': None - }, - 'status': 'COMPLETE', - 'status_reason': 'Outputs received'}, - self.rpc_client.update_software_deployment.call_args[1]) + ca = rpcc.signal_software_deployment.call_args[0] + self.assertEqual(self.ctx, ca[0]) + self.assertEqual('c8a19429-7fde-47ea-a42f-40045488226c', ca[1]) + self.assertEqual({'foo': 'bar', 'deploy_status_code': '0'}, ca[2]) + self.assertIsNotNone(ca[3]) def test_handle_signal_failed(self): self._create_stack(self.template) self.deployment.resource_id = 'c8a19429-7fde-47ea-a42f-40045488226c' - sc = { - 'outputs': [ - {'name': 'foo'}, - {'name': 'foo2'}, - {'name': 'failed', 'error_output': True} - ] - } - sd = { - 'output_values': {}, - 'status': self.deployment.IN_PROGRESS - } - self.rpc_client.show_software_deployment.return_value = sd - self.rpc_client.show_software_config.return_value = sc + rpcc = self.rpc_client + rpcc.signal_software_deployment.return_value = 'deployment failed' + details = {'failed': 'no enough memory found.'} ret = self.deployment.handle_signal(details) self.assertEqual('deployment failed', ret) - self.assertEqual({ - 'deployment_id': 'c8a19429-7fde-47ea-a42f-40045488226c', - 'output_values': { - 'deploy_status_code': None, - 'deploy_stderr': None, - 'deploy_stdout': None, - 'failed': 'no enough memory found.' - }, - 'status': 'FAILED', - 'status_reason': 'failed : no enough memory found.'}, - self.rpc_client.update_software_deployment.call_args[1]) + ca = rpcc.signal_software_deployment.call_args[0] + self.assertEqual(self.ctx, ca[0]) + self.assertEqual('c8a19429-7fde-47ea-a42f-40045488226c', ca[1]) + self.assertEqual(details, ca[2]) + self.assertIsNotNone(ca[3]) # Test bug 1332355, where details contains a translateable message details = {'failed': _('need more memory.')} - self.deployment.handle_signal(details) - self.assertEqual({ - 'deployment_id': 'c8a19429-7fde-47ea-a42f-40045488226c', - 'output_values': { - 'deploy_status_code': None, - 'deploy_stderr': None, - 'deploy_stdout': None, - 'failed': 'need more memory.' - }, - 'status': 'FAILED', - 'status_reason': 'failed : need more memory.'}, - self.rpc_client.update_software_deployment.call_args[1]) + ret = self.deployment.handle_signal(details) + self.assertEqual('deployment failed', ret) + ca = rpcc.signal_software_deployment.call_args[0] + self.assertEqual(self.ctx, ca[0]) + self.assertEqual('c8a19429-7fde-47ea-a42f-40045488226c', ca[1]) + self.assertEqual(details, ca[2]) + self.assertIsNotNone(ca[3]) def test_handle_status_code_failed(self): self._create_stack(self.template) self.deployment.resource_id = 'c8a19429-7fde-47ea-a42f-40045488226c' - sd = { - 'outputs': [], - 'output_values': {}, - 'status': self.deployment.IN_PROGRESS - } - self.rpc_client.show_software_deployment.return_value = sd + rpcc = self.rpc_client + rpcc.signal_software_deployment.return_value = 'deployment failed' + details = { 'deploy_stdout': 'A thing happened', 'deploy_stderr': 'Then it broke', 'deploy_status_code': -1 } self.deployment.handle_signal(details) - self.assertEqual( - 'c8a19429-7fde-47ea-a42f-40045488226c', - self.rpc_client.show_software_deployment.call_args[0][1]) - self.assertEqual({ - 'deployment_id': 'c8a19429-7fde-47ea-a42f-40045488226c', - 'output_values': { - 'deploy_stdout': 'A thing happened', - 'deploy_stderr': 'Then it broke', - 'deploy_status_code': -1 - }, - 'status': 'FAILED', - 'status_reason': ('deploy_status_code : Deployment exited ' - 'with non-zero status code: -1')}, - self.rpc_client.update_software_deployment.call_args[1]) + ca = rpcc.signal_software_deployment.call_args[0] + self.assertEqual(self.ctx, ca[0]) + self.assertEqual('c8a19429-7fde-47ea-a42f-40045488226c', ca[1]) + self.assertEqual(details, ca[2]) + self.assertIsNotNone(ca[3]) def test_handle_signal_not_waiting(self): self._create_stack(self.template) - sd = { - 'status': self.deployment.COMPLETE - } - self.rpc_client.show_software_deployment.return_value = sd + rpcc = self.rpc_client + rpcc.signal_software_deployment.return_value = None details = None self.assertIsNone(self.deployment.handle_signal(details)) + ca = rpcc.signal_software_deployment.call_args[0] + self.assertEqual(self.ctx, ca[0]) + self.assertIsNone(ca[1]) + self.assertIsNone(ca[2]) + self.assertIsNotNone(ca[3]) def test_fn_get_att(self): self._create_stack(self.template)