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
This commit is contained in:
Steve Baker
2015-02-02 14:30:13 +13:00
parent 556f933f42
commit ea349ea48b
6 changed files with 260 additions and 164 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -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',

View File

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