diff --git a/heat/engine/service.py b/heat/engine/service.py index be43d73ed8..4f9c529434 100644 --- a/heat/engine/service.py +++ b/heat/engine/service.py @@ -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): diff --git a/heat/engine/service_software_config.py b/heat/engine/service_software_config.py new file mode 100644 index 0000000000..b95f7e3aaa --- /dev/null +++ b/heat/engine/service_software_config.py @@ -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) diff --git a/heat/tests/test_engine_service.py b/heat/tests/test_engine_service.py index 6430b8ed54..7df115a944 100644 --- a/heat/tests/test_engine_service.py +++ b/heat/tests/test_engine_service.py @@ -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))