diff options
author | Steven Hardy <shardy@redhat.com> | 2015-10-01 17:16:01 +0100 |
---|---|---|
committer | Steve Baker <sbaker@redhat.com> | 2015-10-06 13:52:50 +1300 |
commit | 4b30adeae16a908e2415d379fbd262e7913d88da (patch) | |
tree | dbaddb1b27a93f07c7afc05393962bad249c8c4b /heat | |
parent | 182034904f7da306c677e77c880a6266b1b5cd50 (diff) | |
download | heat-4b30adeae16a908e2415d379fbd262e7913d88da.tar.gz |
Update preview_update_stack to align with PATCH updates
Currently attempting to do a preview update call with PATCH fails,
because we didn't align the behavior of preview update with the
actual update in the recent additions to fix bug #1224828
So, refactor to ensure both preview_update & update use the same
code, and add a PATCH path to the update API.
Change-Id: I8ce5c0ea4035a7b9563db10ea10433e7f5f99a4f
Closes-Bug: #1501207
(cherry picked from commit 604595a39c9345d214f9f02c2ab8d7d1d768fcbe)
Diffstat (limited to 'heat')
-rw-r--r-- | heat/api/openstack/v1/__init__.py | 6 | ||||
-rw-r--r-- | heat/api/openstack/v1/stacks.py | 16 | ||||
-rw-r--r-- | heat/engine/service.py | 119 | ||||
-rw-r--r-- | heat/tests/api/openstack_v1/test_stacks.py | 40 |
4 files changed, 117 insertions, 64 deletions
diff --git a/heat/api/openstack/v1/__init__.py b/heat/api/openstack/v1/__init__.py index 6dc6829d4..b4cd061ba 100644 --- a/heat/api/openstack/v1/__init__.py +++ b/heat/api/openstack/v1/__init__.py @@ -215,6 +215,12 @@ class API(wsgi.Router): 'method': 'PUT' }, { + 'name': 'preview_stack_update_patch', + 'url': '/stacks/{stack_name}/{stack_id}/preview', + 'action': 'preview_update_patch', + 'method': 'PATCH' + }, + { 'name': 'stack_delete', 'url': '/stacks/{stack_name}/{stack_id}', 'action': 'delete', diff --git a/heat/api/openstack/v1/stacks.py b/heat/api/openstack/v1/stacks.py index 60d056b29..a41f8ac5f 100644 --- a/heat/api/openstack/v1/stacks.py +++ b/heat/api/openstack/v1/stacks.py @@ -503,6 +503,22 @@ class StackController(object): return {'resource_changes': changes} @util.identified_stack + def preview_update_patch(self, req, identity, body): + """Preview PATCH update for existing stack.""" + data = InstantiationData(body, patch=True) + + args = self.prepare_args(data) + changes = self.rpc_client.preview_update_stack( + req.context, + identity, + data.template(), + data.environment(), + data.files(), + args) + + return {'resource_changes': changes} + + @util.identified_stack def delete(self, req, identity): """ Delete the specified stack diff --git a/heat/engine/service.py b/heat/engine/service.py index d91b5a7ae..ec4359e7a 100644 --- a/heat/engine/service.py +++ b/heat/engine/service.py @@ -728,7 +728,7 @@ class EngineService(service.Service): return dict(stack.identifier()) - def _prepare_stack_updates(self, cnxt, current_stack, tmpl, params, + def _prepare_stack_updates(self, cnxt, current_stack, template, params, files, args): """Return the current and updated stack. @@ -737,11 +737,59 @@ class EngineService(service.Service): :param cnxt: RPC context. :param stack: A stack to be updated. - :param tmpl: Template object of stack you want to update to. + :param template: Template of stack you want to update to. :param params: Stack Input Params :param files: Files referenced from the template :param args: Request parameters/args passed from API """ + # Now parse the template and any parameters for the updated + # stack definition. If PARAM_EXISTING is specified, we merge + # any environment provided into the existing one and attempt + # to use the existing stack template, if one is not provided. + if args.get(rpc_api.PARAM_EXISTING, None): + existing_env = current_stack.env.user_env_as_dict() + existing_params = existing_env[env_fmt.PARAMETERS] + clear_params = set(args.get(rpc_api.PARAM_CLEAR_PARAMETERS, [])) + retained = dict((k, v) for k, v in existing_params.items() + if k not in clear_params) + existing_env[env_fmt.PARAMETERS] = retained + new_env = environment.Environment(existing_env) + new_env.load(params) + + new_files = current_stack.t.files.copy() + new_files.update(files or {}) + + if template is not None: + new_template = template + elif (current_stack.convergence or + current_stack.status == current_stack.COMPLETE): + # If convergence is enabled, or the stack is complete, we can + # just use the current template... + new_template = current_stack.t.t + else: + # ..but if it's FAILED without convergence things may be in an + # inconsistent state, so we try to fall back on a stored copy + # of the previous template + if current_stack.prev_raw_template_id is not None: + # Use the stored previous template + prev_t = templatem.Template.load( + cnxt, current_stack.prev_raw_template_id) + new_template = prev_t.t + else: + # Nothing we can do, the failed update happened before + # we started storing prev_raw_template_id + LOG.error("PATCH update to FAILED stack only possible if " + "convergence enabled or previous template " + "stored") + msg = _('PATCH update to non-COMPLETE stack') + raise exception.NotSupported(feature=msg) + else: + new_env = environment.Environment(params) + new_files = files + new_template = template + + tmpl = templatem.Template(new_template, files=new_files, env=new_env) + max_resources = cfg.CONF.max_resources_per_stack if max_resources != -1 and len(tmpl[tmpl.RESOURCES]) > max_resources: raise exception.RequestLimitExceeded( @@ -765,7 +813,7 @@ class EngineService(service.Service): self._validate_deferred_auth_context(cnxt, updated_stack) updated_stack.validate() - return current_stack, updated_stack + return tmpl, current_stack, updated_stack @context.request_context def update_stack(self, cnxt, stack_identity, template, params, @@ -797,56 +845,8 @@ class EngineService(service.Service): msg = _('Updating a stack when it is deleting') raise exception.NotSupported(feature=msg) - # Now parse the template and any parameters for the updated - # stack definition. If PARAM_EXISTING is specified, we merge - # any environment provided into the existing one and attempt - # to use the existing stack template, if one is not provided. - if args.get(rpc_api.PARAM_EXISTING, None): - existing_env = current_stack.env.user_env_as_dict() - existing_params = existing_env[env_fmt.PARAMETERS] - clear_params = set(args.get(rpc_api.PARAM_CLEAR_PARAMETERS, [])) - retained = dict((k, v) for k, v in existing_params.items() - if k not in clear_params) - existing_env[env_fmt.PARAMETERS] = retained - new_env = environment.Environment(existing_env) - new_env.load(params) - - new_files = current_stack.t.files.copy() - new_files.update(files or {}) - - if template is not None: - new_template = template - elif (current_stack.convergence or - current_stack.status == current_stack.COMPLETE): - # If convergence is enabled, or the stack is complete, we can - # just use the current template... - new_template = current_stack.t.t - else: - # ..but if it's FAILED without convergence things may be in an - # inconsistent state, so we try to fall back on a stored copy - # of the previous template - if current_stack.prev_raw_template_id is not None: - # Use the stored previous template - prev_t = templatem.Template.load( - cnxt, current_stack.prev_raw_template_id) - new_template = prev_t.t - else: - # Nothing we can do, the failed update happened before - # we started storing prev_raw_template_id - LOG.error("PATCH update to FAILED stack only possible if " - "convergence enabled or previous template " - "stored") - msg = _('PATCH update to non-COMPLETE stack') - raise exception.NotSupported(feature=msg) - else: - new_env = environment.Environment(params) - new_files = files - new_template = template - - tmpl = templatem.Template(new_template, files=new_files, env=new_env) - - current_stack, updated_stack = self._prepare_stack_updates( - cnxt, current_stack, tmpl, params, files, args) + tmpl, current_stack, updated_stack = self._prepare_stack_updates( + cnxt, current_stack, template, params, files, args) if current_stack.convergence: current_stack.converge_stack(template=tmpl, @@ -886,17 +886,8 @@ class EngineService(service.Service): current_stack = parser.Stack.load(cnxt, stack=db_stack) - # Now parse the template and any parameters for the updated - # stack definition. - env = environment.Environment(params) - if args.get(rpc_api.PARAM_EXISTING, None): - env.patch_previous_parameters( - current_stack.env, - args.get(rpc_api.PARAM_CLEAR_PARAMETERS, [])) - tmpl = templatem.Template(template, files=files, env=env) - - current_stack, updated_stack = self._prepare_stack_updates( - cnxt, current_stack, tmpl, params, files, args) + tmpl, current_stack, updated_stack = self._prepare_stack_updates( + cnxt, current_stack, template, params, files, args) update_task = update.StackUpdate(current_stack, updated_stack, None) diff --git a/heat/tests/api/openstack_v1/test_stacks.py b/heat/tests/api/openstack_v1/test_stacks.py index a98c55ef3..b8b813cfe 100644 --- a/heat/tests/api/openstack_v1/test_stacks.py +++ b/heat/tests/api/openstack_v1/test_stacks.py @@ -1200,6 +1200,46 @@ class StackControllerTest(tools.ControllerTest, common.HeatTestCase): self.assertEqual({'resource_changes': resource_changes}, result) self.m.VerifyAll() + def test_preview_update_stack_patch(self, mock_enforce): + self._mock_enforce_setup(mock_enforce, 'preview_update_patch', True) + identity = identifier.HeatIdentifier(self.tenant, 'wordpress', '6') + parameters = {u'InstanceType': u'm1.xlarge'} + body = {'template': None, + 'parameters': parameters, + 'files': {}, + 'timeout_mins': 30} + + req = self._patch('/stacks/%(stack_name)s/%(stack_id)s/preview' % + identity, json.dumps(body)) + resource_changes = {'updated': [], + 'deleted': [], + 'unchanged': [], + 'added': [], + 'replaced': []} + + self.m.StubOutWithMock(rpc_client.EngineClient, 'call') + rpc_client.EngineClient.call( + req.context, + ('preview_update_stack', + {'stack_identity': dict(identity), + 'template': None, + 'params': {'parameters': parameters, + 'encrypted_param_names': [], + 'parameter_defaults': {}, + 'resource_registry': {}}, + 'files': {}, + 'args': {rpc_api.PARAM_EXISTING: True, + 'timeout_mins': 30}}), + version='1.15' + ).AndReturn(resource_changes) + self.m.ReplayAll() + + result = self.controller.preview_update_patch( + req, tenant_id=identity.tenant, stack_name=identity.stack_name, + stack_id=identity.stack_id, body=body) + self.assertEqual({'resource_changes': resource_changes}, result) + self.m.VerifyAll() + def test_lookup(self, mock_enforce): self._mock_enforce_setup(mock_enforce, 'lookup', True) identity = identifier.HeatIdentifier(self.tenant, 'wordpress', '1') |