Move software service logic to own module

The size of EngineService is starting to affect maintainability.
This change creates service_software_config.SoftwareConfigService
and moves all the software config service method implementations
into it.

Software config calls to EngineService are delegated to
SoftwareConfigService. Unit tests remain in test_engine_service to
include the EngineService entrypoint methods in the test coverage.

Change-Id: Icf51dfd35423ac33a9126731862a90525c17306c
Related-Blueprint: software-config-trigger
This commit is contained in:
Steve Baker 2015-03-02 09:31:53 +13:00
parent fee3937550
commit 29b69028e5
3 changed files with 332 additions and 218 deletions

View File

@ -21,13 +21,9 @@ import eventlet
from oslo_config import cfg
from oslo_log import log as logging
import oslo_messaging as messaging
from oslo_serialization import jsonutils
from oslo_utils import timeutils
from oslo_utils import uuidutils
from osprofiler import profiler
import requests
import six
from six.moves.urllib import parse as urlparse
import webob
from heat.common import context
@ -48,6 +44,7 @@ from heat.engine import event as evt
from heat.engine import parameter_groups
from heat.engine import properties
from heat.engine import resources
from heat.engine import service_software_config
from heat.engine import service_stack_watch
from heat.engine import stack as parser
from heat.engine import stack_lock
@ -282,6 +279,7 @@ class EngineService(service.Service):
self.service_id = None
self.manage_thread_grp = None
self._rpc_server = None
self.software_config = service_software_config.SoftwareConfigService()
if cfg.CONF.instance_user:
warnings.warn('The "instance_user" option in heat.conf is '
@ -1408,234 +1406,80 @@ class EngineService(service.Service):
@context.request_context
def show_software_config(self, cnxt, config_id):
sc = db_api.software_config_get(cnxt, config_id)
return api.format_software_config(sc)
return self.software_config.show_software_config(cnxt, config_id)
@context.request_context
def create_software_config(self, cnxt, group, name, config,
inputs, outputs, options):
sc = db_api.software_config_create(cnxt, {
'group': group,
'name': name,
'config': {
'inputs': inputs,
'outputs': outputs,
'options': options,
'config': config
},
'tenant': cnxt.tenant_id})
return api.format_software_config(sc)
return self.software_config.create_software_config(
cnxt,
group=group,
name=name,
config=config,
inputs=inputs,
outputs=outputs,
options=options)
@context.request_context
def delete_software_config(self, cnxt, config_id):
db_api.software_config_delete(cnxt, config_id)
return self.software_config.delete_software_config(cnxt, config_id)
@context.request_context
def list_software_deployments(self, cnxt, server_id):
all_sd = db_api.software_deployment_get_all(cnxt, server_id)
result = [api.format_software_deployment(sd) for sd in all_sd]
return result
return self.software_config.list_software_deployments(
cnxt, server_id)
@context.request_context
def metadata_software_deployments(self, cnxt, server_id):
if not server_id:
raise ValueError(_('server_id must be specified'))
all_sd = db_api.software_deployment_get_all(cnxt, server_id)
# sort the configs by config name, to give the list of metadata a
# deterministic and controllable order.
all_sd_s = sorted(all_sd, key=lambda sd: sd.config.name)
result = [api.format_software_config(sd.config) for sd in all_sd_s]
return result
def _push_metadata_software_deployments(self, cnxt, server_id):
rs = db_api.resource_get_by_physical_resource_id(cnxt, server_id)
if not rs:
return
deployments = self.metadata_software_deployments(cnxt, server_id)
md = rs.rsrc_metadata or {}
md['deployments'] = deployments
rs.update_and_save({'rsrc_metadata': md})
metadata_put_url = None
for rd in rs.data:
if rd.key == 'metadata_put_url':
metadata_put_url = rd.value
break
if metadata_put_url:
json_md = jsonutils.dumps(md)
requests.put(metadata_put_url, json_md)
def _refresh_software_deployment(self, cnxt, sd, deploy_signal_id):
container, object_name = urlparse.urlparse(
deploy_signal_id).path.split('/')[-2:]
swift_plugin = cnxt.clients.client_plugin('swift')
swift = swift_plugin.client()
try:
headers = swift.head_object(container, object_name)
except Exception as ex:
# ignore not-found, in case swift is not consistent yet
if swift_plugin.is_not_found(ex):
LOG.info(_LI('Signal object not found: %(c)s %(o)s') % {
'c': container, 'o': object_name})
return sd
raise ex
lm = headers.get('last-modified')
last_modified = swift_plugin.parse_last_modified(lm)
prev_last_modified = sd.updated_at
if prev_last_modified:
# assume stored as utc, convert to offset-naive datetime
prev_last_modified = prev_last_modified.replace(tzinfo=None)
if prev_last_modified and (last_modified <= prev_last_modified):
return sd
try:
(headers, obj) = swift.get_object(container, object_name)
except Exception as ex:
# ignore not-found, in case swift is not consistent yet
if swift_plugin.is_not_found(ex):
LOG.info(_LI(
'Signal object not found: %(c)s %(o)s') % {
'c': container, 'o': object_name})
return sd
raise ex
if obj:
self.signal_software_deployment(
cnxt, sd.id, json.loads(obj),
timeutils.strtime(last_modified))
return db_api.software_deployment_get(cnxt, sd.id)
return self.software_config.metadata_software_deployments(
cnxt, server_id)
@context.request_context
def show_software_deployment(self, cnxt, deployment_id):
sd = db_api.software_deployment_get(cnxt, deployment_id)
if sd.status == rpc_api.SOFTWARE_DEPLOYMENT_IN_PROGRESS:
c = sd.config.config
input_values = dict((i['name'], i['value']) for i in c['inputs'])
transport = input_values.get('deploy_signal_transport')
if transport == 'TEMP_URL_SIGNAL':
sd = self._refresh_software_deployment(
cnxt, sd, input_values.get('deploy_signal_id'))
return api.format_software_deployment(sd)
return self.software_config.show_software_deployment(
cnxt, deployment_id)
@context.request_context
def create_software_deployment(self, cnxt, server_id, config_id,
input_values, action, status,
status_reason, stack_user_project_id):
sd = db_api.software_deployment_create(cnxt, {
'config_id': config_id,
'server_id': server_id,
'input_values': input_values,
'tenant': cnxt.tenant_id,
'stack_user_project_id': stack_user_project_id,
'action': action,
'status': status,
'status_reason': status_reason})
self._push_metadata_software_deployments(cnxt, server_id)
return api.format_software_deployment(sd)
return self.software_config.create_software_deployment(
cnxt, server_id=server_id,
config_id=config_id,
input_values=input_values,
action=action,
status=status,
status_reason=status_reason,
stack_user_project_id=stack_user_project_id)
@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,
return self.software_config.signal_software_deployment(
cnxt,
deployment_id=deployment_id,
details=details,
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,
status, status_reason, updated_at):
update_data = {}
if config_id:
update_data['config_id'] = config_id
if input_values:
update_data['input_values'] = input_values
if output_values:
update_data['output_values'] = output_values
if action:
update_data['action'] = action
if status:
update_data['status'] = status
if status_reason:
update_data['status_reason'] = status_reason
if updated_at:
update_data['updated_at'] = timeutils.normalize_time(
timeutils.parse_isotime(updated_at))
else:
update_data['updated_at'] = timeutils.utcnow()
sd = db_api.software_deployment_update(cnxt,
deployment_id, update_data)
# only push metadata if this update resulted in the config_id
# changing, since metadata is just a list of configs
if config_id:
self._push_metadata_software_deployments(cnxt, sd.server_id)
return api.format_software_deployment(sd)
return self.software_config.update_software_deployment(
cnxt,
deployment_id=deployment_id,
config_id=config_id,
input_values=input_values,
output_values=output_values,
action=action,
status=status,
status_reason=status_reason,
updated_at=updated_at)
@context.request_context
def delete_software_deployment(self, cnxt, deployment_id):
db_api.software_deployment_delete(cnxt, deployment_id)
return self.software_config.delete_software_deployment(
cnxt, deployment_id)
@context.request_context
def list_services(self, cnxt):

View File

@ -0,0 +1,254 @@
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import json
from oslo_log import log as logging
from oslo_serialization import jsonutils
from oslo_utils import timeutils
import requests
import six
from six.moves.urllib import parse as urlparse
from heat.common.i18n import _
from heat.common.i18n import _LI
from heat.db import api as db_api
from heat.engine import api
from heat.openstack.common import service
from heat.rpc import api as rpc_api
LOG = logging.getLogger(__name__)
class SoftwareConfigService(service.Service):
def show_software_config(self, cnxt, config_id):
sc = db_api.software_config_get(cnxt, config_id)
return api.format_software_config(sc)
def create_software_config(self, cnxt, group, name, config,
inputs, outputs, options):
sc = db_api.software_config_create(cnxt, {
'group': group,
'name': name,
'config': {
'inputs': inputs,
'outputs': outputs,
'options': options,
'config': config
},
'tenant': cnxt.tenant_id})
return api.format_software_config(sc)
def delete_software_config(self, cnxt, config_id):
db_api.software_config_delete(cnxt, config_id)
def list_software_deployments(self, cnxt, server_id):
all_sd = db_api.software_deployment_get_all(cnxt, server_id)
result = [api.format_software_deployment(sd) for sd in all_sd]
return result
def metadata_software_deployments(self, cnxt, server_id):
if not server_id:
raise ValueError(_('server_id must be specified'))
all_sd = db_api.software_deployment_get_all(cnxt, server_id)
# sort the configs by config name, to give the list of metadata a
# deterministic and controllable order.
all_sd_s = sorted(all_sd, key=lambda sd: sd.config.name)
result = [api.format_software_config(sd.config) for sd in all_sd_s]
return result
def _push_metadata_software_deployments(self, cnxt, server_id):
rs = db_api.resource_get_by_physical_resource_id(cnxt, server_id)
if not rs:
return
deployments = self.metadata_software_deployments(cnxt, server_id)
md = rs.rsrc_metadata or {}
md['deployments'] = deployments
rs.update_and_save({'rsrc_metadata': md})
metadata_put_url = None
for rd in rs.data:
if rd.key == 'metadata_put_url':
metadata_put_url = rd.value
break
if metadata_put_url:
json_md = jsonutils.dumps(md)
requests.put(metadata_put_url, json_md)
def _refresh_software_deployment(self, cnxt, sd, deploy_signal_id):
container, object_name = urlparse.urlparse(
deploy_signal_id).path.split('/')[-2:]
swift_plugin = cnxt.clients.client_plugin('swift')
swift = swift_plugin.client()
try:
headers = swift.head_object(container, object_name)
except Exception as ex:
# ignore not-found, in case swift is not consistent yet
if swift_plugin.is_not_found(ex):
LOG.info(_LI('Signal object not found: %(c)s %(o)s') % {
'c': container, 'o': object_name})
return sd
raise ex
lm = headers.get('last-modified')
last_modified = swift_plugin.parse_last_modified(lm)
prev_last_modified = sd.updated_at
if prev_last_modified:
# assume stored as utc, convert to offset-naive datetime
prev_last_modified = prev_last_modified.replace(tzinfo=None)
if prev_last_modified and (last_modified <= prev_last_modified):
return sd
try:
(headers, obj) = swift.get_object(container, object_name)
except Exception as ex:
# ignore not-found, in case swift is not consistent yet
if swift_plugin.is_not_found(ex):
LOG.info(_LI(
'Signal object not found: %(c)s %(o)s') % {
'c': container, 'o': object_name})
return sd
raise ex
if obj:
self.signal_software_deployment(
cnxt, sd.id, json.loads(obj),
timeutils.strtime(last_modified))
return db_api.software_deployment_get(cnxt, sd.id)
def show_software_deployment(self, cnxt, deployment_id):
sd = db_api.software_deployment_get(cnxt, deployment_id)
if sd.status == rpc_api.SOFTWARE_DEPLOYMENT_IN_PROGRESS:
c = sd.config.config
input_values = dict((i['name'], i['value']) for i in c['inputs'])
transport = input_values.get('deploy_signal_transport')
if transport == 'TEMP_URL_SIGNAL':
sd = self._refresh_software_deployment(
cnxt, sd, input_values.get('deploy_signal_id'))
return api.format_software_deployment(sd)
def create_software_deployment(self, cnxt, server_id, config_id,
input_values, action, status,
status_reason, stack_user_project_id):
sd = db_api.software_deployment_create(cnxt, {
'config_id': config_id,
'server_id': server_id,
'input_values': input_values,
'tenant': cnxt.tenant_id,
'stack_user_project_id': stack_user_project_id,
'action': action,
'status': status,
'status_reason': status_reason})
self._push_metadata_software_deployments(cnxt, server_id)
return api.format_software_deployment(sd)
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
def update_software_deployment(self, cnxt, deployment_id, config_id,
input_values, output_values, action,
status, status_reason, updated_at):
update_data = {}
if config_id:
update_data['config_id'] = config_id
if input_values:
update_data['input_values'] = input_values
if output_values:
update_data['output_values'] = output_values
if action:
update_data['action'] = action
if status:
update_data['status'] = status
if status_reason:
update_data['status_reason'] = status_reason
if updated_at:
update_data['updated_at'] = timeutils.normalize_time(
timeutils.parse_isotime(updated_at))
else:
update_data['updated_at'] = timeutils.utcnow()
sd = db_api.software_deployment_update(cnxt,
deployment_id, update_data)
# only push metadata if this update resulted in the config_id
# changing, since metadata is just a list of configs
if config_id:
self._push_metadata_software_deployments(cnxt, sd.server_id)
return api.format_software_deployment(sd)
def delete_software_deployment(self, cnxt, deployment_id):
db_api.software_deployment_delete(cnxt, deployment_id)

View File

@ -42,6 +42,7 @@ from heat.engine import properties
from heat.engine import resource as res
from heat.engine.resources.aws import instance as instances
from heat.engine import service
from heat.engine import service_software_config
from heat.engine import service_stack_watch
from heat.engine import stack as parser
from heat.engine import stack_lock
@ -3636,7 +3637,7 @@ class SoftwareConfigServiceTest(common.HeatTestCase):
deployment,
self.engine.show_software_deployment(self.ctx, deployment_id))
@mock.patch.object(service.EngineService,
@mock.patch.object(service_software_config.SoftwareConfigService,
'_push_metadata_software_deployments')
def test_signal_software_deployment(self, pmsd):
self.assertRaises(ValueError,
@ -3780,7 +3781,8 @@ class SoftwareConfigServiceTest(common.HeatTestCase):
self.assertEqual(deployment_id, deployment['id'])
self.assertEqual(kwargs['input_values'], deployment['input_values'])
@mock.patch.object(service.EngineService, '_refresh_software_deployment')
@mock.patch.object(service_software_config.SoftwareConfigService,
'_refresh_software_deployment')
def test_show_software_deployment_refresh(
self, _refresh_software_deployment):
temp_url = ('http://192.0.2.1/v1/AUTH_a/b/c'
@ -3814,13 +3816,14 @@ class SoftwareConfigServiceTest(common.HeatTestCase):
server_id = str(uuid.uuid4())
self.m.StubOutWithMock(
self.engine, '_push_metadata_software_deployments')
self.engine.software_config,
'_push_metadata_software_deployments')
# push on create
self.engine._push_metadata_software_deployments(
self.engine.software_config._push_metadata_software_deployments(
self.ctx, server_id).AndReturn(None)
# push on update with new config_id
self.engine._push_metadata_software_deployments(
self.engine.software_config._push_metadata_software_deployments(
self.ctx, server_id).AndReturn(None)
self.m.ReplayAll()
@ -3846,9 +3849,10 @@ class SoftwareConfigServiceTest(common.HeatTestCase):
server_id = str(uuid.uuid4())
self.m.StubOutWithMock(
self.engine, '_push_metadata_software_deployments')
self.engine.software_config,
'_push_metadata_software_deployments')
# push on create
self.engine._push_metadata_software_deployments(
self.engine.software_config._push_metadata_software_deployments(
self.ctx, server_id).AndReturn(None)
# _push_metadata_software_deployments should not be called
# on update because config_id isn't being updated
@ -3916,7 +3920,8 @@ class SoftwareConfigServiceTest(common.HeatTestCase):
deployment_ids = [x['id'] for x in deployments]
self.assertNotIn(deployment_id, deployment_ids)
@mock.patch.object(service.EngineService, 'metadata_software_deployments')
@mock.patch.object(service_software_config.SoftwareConfigService,
'metadata_software_deployments')
@mock.patch.object(service.db_api, 'resource_get_by_physical_resource_id')
@mock.patch.object(service.requests, 'put')
def test_push_metadata_software_deployments(self, put, res_get, md_sd):
@ -3933,12 +3938,14 @@ class SoftwareConfigServiceTest(common.HeatTestCase):
'deployments': {'deploy': 'this'}
}
self.engine._push_metadata_software_deployments(self.ctx, '1234')
self.engine.software_config._push_metadata_software_deployments(
self.ctx, '1234')
rs.update_and_save.assert_called_once_with(
{'rsrc_metadata': result_metadata})
put.side_effect = Exception('Unexpected requests.put')
@mock.patch.object(service.EngineService, 'metadata_software_deployments')
@mock.patch.object(service_software_config.SoftwareConfigService,
'metadata_software_deployments')
@mock.patch.object(service.db_api, 'resource_get_by_physical_resource_id')
@mock.patch.object(service.requests, 'put')
def test_push_metadata_software_deployments_temp_url(
@ -3959,14 +3966,15 @@ class SoftwareConfigServiceTest(common.HeatTestCase):
'deployments': {'deploy': 'this'}
}
self.engine._push_metadata_software_deployments(self.ctx, '1234')
self.engine.software_config._push_metadata_software_deployments(
self.ctx, '1234')
rs.update_and_save.assert_called_once_with(
{'rsrc_metadata': result_metadata})
put.assert_called_once_with(
'http://192.168.2.2/foo/bar', jsonutils.dumps(result_metadata))
@mock.patch.object(service.EngineService,
@mock.patch.object(service_software_config.SoftwareConfigService,
'signal_software_deployment')
@mock.patch.object(swift.SwiftClientPlugin, '_create')
def test_refresh_software_deployment(self, scc, ssd):
@ -4017,7 +4025,8 @@ class SoftwareConfigServiceTest(common.HeatTestCase):
self.assertEqual(
sd,
self.engine._refresh_software_deployment(self.ctx, sd, temp_url))
self.engine.software_config._refresh_software_deployment(
self.ctx, sd, temp_url))
sc.head_object.assert_called_once_with(container, object_name)
# no call to get_object or signal_last_modified
self.assertEqual([], sc.get_object.mock_calls)
@ -4026,16 +4035,20 @@ class SoftwareConfigServiceTest(common.HeatTestCase):
# poll with other error
sc.head_object.side_effect = swift_exc.ClientException(
'Ouch', http_status=409)
self.assertRaises(swift_exc.ClientException,
self.engine._refresh_software_deployment,
self.ctx, sd, temp_url)
self.assertRaises(
swift_exc.ClientException,
self.engine.software_config._refresh_software_deployment,
self.ctx,
sd,
temp_url)
# no call to get_object or signal_last_modified
self.assertEqual([], sc.get_object.mock_calls)
self.assertEqual([], ssd.mock_calls)
sc.head_object.side_effect = None
# first poll populates data signal_last_modified
self.engine._refresh_software_deployment(self.ctx, sd, temp_url)
self.engine.software_config._refresh_software_deployment(
self.ctx, sd, temp_url)
sc.head_object.assert_called_with(container, object_name)
sc.get_object.assert_called_once_with(container, object_name)
# signal_software_deployment called with signal
@ -4047,7 +4060,8 @@ class SoftwareConfigServiceTest(common.HeatTestCase):
self.ctx, deployment_id, {'updated_at': then})
sd = db_api.software_deployment_get(self.ctx, deployment_id)
self.assertEqual(then, sd.updated_at)
self.engine._refresh_software_deployment(self.ctx, sd, temp_url)
self.engine.software_config._refresh_software_deployment(
self.ctx, sd, temp_url)
sc.get_object.assert_called_once_with(container, object_name)
# signal_software_deployment has not been called again
ssd.assert_called_once_with(self.ctx, deployment_id, {"foo": "bar"},
@ -4057,7 +4071,8 @@ class SoftwareConfigServiceTest(common.HeatTestCase):
headers['last-modified'] = last_modified_2
sc.head_object.return_value = headers
sc.get_object.return_value = (headers, '{"bar": "baz"}')
self.engine._refresh_software_deployment(self.ctx, sd, temp_url)
self.engine.software_config._refresh_software_deployment(
self.ctx, sd, temp_url)
# two calls to signal_software_deployment, for then and now
self.assertEqual(2, len(ssd.mock_calls))
@ -4068,7 +4083,8 @@ class SoftwareConfigServiceTest(common.HeatTestCase):
db_api.software_deployment_update(
self.ctx, deployment_id, {'updated_at': now})
sd = db_api.software_deployment_get(self.ctx, deployment_id)
self.engine._refresh_software_deployment(self.ctx, sd, temp_url)
self.engine.software_config._refresh_software_deployment(
self.ctx, sd, temp_url)
self.assertEqual(2, len(ssd.mock_calls))