From 604595a39c9345d214f9f02c2ab8d7d1d768fcbe Mon Sep 17 00:00:00 2001 From: Steven Hardy Date: Thu, 1 Oct 2015 17:16:01 +0100 Subject: [PATCH] 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 --- etc/heat/policy.json | 3 +- heat/api/openstack/v1/__init__.py | 6 + heat/api/openstack/v1/stacks.py | 16 +++ heat/engine/service.py | 131 ++++++++++----------- heat/tests/api/openstack_v1/test_stacks.py | 40 +++++++ 5 files changed, 125 insertions(+), 71 deletions(-) diff --git a/etc/heat/policy.json b/etc/heat/policy.json index 8043d8b031..1c8cd02719 100644 --- a/etc/heat/policy.json +++ b/etc/heat/policy.json @@ -53,8 +53,9 @@ "stacks:show": "rule:deny_stack_user", "stacks:template": "rule:deny_stack_user", "stacks:update": "rule:deny_stack_user", - "stacks:preview_update": "rule:deny_stack_user", "stacks:update_patch": "rule:deny_stack_user", + "stacks:preview_update": "rule:deny_stack_user", + "stacks:preview_update_patch": "rule:deny_stack_user", "stacks:validate_template": "rule:deny_stack_user", "stacks:snapshot": "rule:deny_stack_user", "stacks:show_snapshot": "rule:deny_stack_user", diff --git a/heat/api/openstack/v1/__init__.py b/heat/api/openstack/v1/__init__.py index 7ae216a1d7..2ae7014c4c 100644 --- a/heat/api/openstack/v1/__init__.py +++ b/heat/api/openstack/v1/__init__.py @@ -213,6 +213,12 @@ class API(wsgi.Router): 'action': 'preview_update', '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}', diff --git a/heat/api/openstack/v1/stacks.py b/heat/api/openstack/v1/stacks.py index db5fcdf4bd..280cf8eca2 100644 --- a/heat/api/openstack/v1/stacks.py +++ b/heat/api/openstack/v1/stacks.py @@ -481,6 +481,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 d6ac7d0441..b1b6bfa7dd 100644 --- a/heat/engine/service.py +++ b/heat/engine/service.py @@ -720,7 +720,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. @@ -729,66 +729,11 @@ 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 """ - max_resources = cfg.CONF.max_resources_per_stack - if max_resources != -1 and len(tmpl[tmpl.RESOURCES]) > max_resources: - raise exception.RequestLimitExceeded( - message=exception.StackResourceLimitExceeded.msg_fmt) - - stack_name = current_stack.name - current_kwargs = current_stack.get_kwargs_for_cloning() - - common_params = api.extract_args(args) - common_params.setdefault(rpc_api.PARAM_TIMEOUT, - current_stack.timeout_mins) - common_params.setdefault(rpc_api.PARAM_DISABLE_ROLLBACK, - current_stack.disable_rollback) - - current_kwargs.update(common_params) - updated_stack = parser.Stack(cnxt, stack_name, tmpl, - **current_kwargs) - self.resource_enforcer.enforce_stack(updated_stack) - updated_stack.parameters.set_stack_id(current_stack.identifier()) - - self._validate_deferred_auth_context(cnxt, updated_stack) - updated_stack.validate() - - return current_stack, updated_stack - - @context.request_context - def update_stack(self, cnxt, stack_identity, template, params, - files, args): - """Updates an existing stack based on the provided template and params. - - Note that at this stage the template has already been fetched from the - heat-api process if using a template-url. - - :param cnxt: RPC context. - :param stack_identity: Name of the stack you want to create. - :param template: Template of stack you want to create. - :param params: Stack Input Params - :param files: Files referenced from the template - :param args: Request parameters/args passed from API - """ - # Get the database representation of the existing stack - db_stack = self._get_stack(cnxt, stack_identity) - LOG.info(_LI('Updating stack %s'), db_stack.name) - - current_stack = parser.Stack.load(cnxt, stack=db_stack) - self.resource_enforcer.enforce_stack(current_stack) - - if current_stack.action == current_stack.SUSPEND: - msg = _('Updating a stack when it is suspended') - raise exception.NotSupported(feature=msg) - - if current_stack.action == current_stack.DELETE: - 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 @@ -837,8 +782,63 @@ class EngineService(service.Service): 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) + max_resources = cfg.CONF.max_resources_per_stack + if max_resources != -1 and len(tmpl[tmpl.RESOURCES]) > max_resources: + raise exception.RequestLimitExceeded( + message=exception.StackResourceLimitExceeded.msg_fmt) + + stack_name = current_stack.name + current_kwargs = current_stack.get_kwargs_for_cloning() + + common_params = api.extract_args(args) + common_params.setdefault(rpc_api.PARAM_TIMEOUT, + current_stack.timeout_mins) + common_params.setdefault(rpc_api.PARAM_DISABLE_ROLLBACK, + current_stack.disable_rollback) + + current_kwargs.update(common_params) + updated_stack = parser.Stack(cnxt, stack_name, tmpl, + **current_kwargs) + self.resource_enforcer.enforce_stack(updated_stack) + updated_stack.parameters.set_stack_id(current_stack.identifier()) + + self._validate_deferred_auth_context(cnxt, updated_stack) + updated_stack.validate() + + return tmpl, current_stack, updated_stack + + @context.request_context + def update_stack(self, cnxt, stack_identity, template, params, + files, args): + """Updates an existing stack based on the provided template and params. + + Note that at this stage the template has already been fetched from the + heat-api process if using a template-url. + + :param cnxt: RPC context. + :param stack_identity: Name of the stack you want to create. + :param template: Template of stack you want to create. + :param params: Stack Input Params + :param files: Files referenced from the template + :param args: Request parameters/args passed from API + """ + # Get the database representation of the existing stack + db_stack = self._get_stack(cnxt, stack_identity) + LOG.info(_LI('Updating stack %s'), db_stack.name) + + current_stack = parser.Stack.load(cnxt, stack=db_stack) + self.resource_enforcer.enforce_stack(current_stack) + + if current_stack.action == current_stack.SUSPEND: + msg = _('Updating a stack when it is suspended') + raise exception.NotSupported(feature=msg) + + if current_stack.action == current_stack.DELETE: + msg = _('Updating a stack when it is deleting') + raise exception.NotSupported(feature=msg) + + 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, @@ -878,17 +878,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 6a6bee484d..0c1e51320f 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')