Source code for heat.api.openstack.v1.stacks

#
#    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.

"""Stack endpoint for Heat v1 REST API."""

import contextlib
from oslo_log import log as logging
from urllib import parse
from webob import exc

from heat.api.openstack.v1 import util
from heat.api.openstack.v1.views import stacks_view
from heat.common import context
from heat.common import environment_format
from heat.common.i18n import _
from heat.common import identifier
from heat.common import param_utils
from heat.common import serializers
from heat.common import template_format
from heat.common import urlfetch
from heat.common import wsgi
from heat.rpc import api as rpc_api
from heat.rpc import client as rpc_client

LOG = logging.getLogger(__name__)


[docs]class InstantiationData(object): """The data to create or update a stack. The data accompanying a PUT or POST request. """ PARAMS = ( PARAM_STACK_NAME, PARAM_TEMPLATE, PARAM_TEMPLATE_URL, PARAM_USER_PARAMS, PARAM_ENVIRONMENT, PARAM_FILES, PARAM_ENVIRONMENT_FILES, PARAM_FILES_CONTAINER ) = ( 'stack_name', 'template', 'template_url', 'parameters', 'environment', 'files', 'environment_files', 'files_container' ) def __init__(self, data, patch=False): """Initialise from the request object. If called from the PATCH api, insert a flag for the engine code to distinguish. """ self.data = data self.patch = patch if patch: self.data[rpc_api.PARAM_EXISTING] = True
[docs] @staticmethod @contextlib.contextmanager def parse_error_check(data_type): try: yield except ValueError as parse_ex: mdict = {'type': data_type, 'error': str(parse_ex)} msg = _("%(type)s not in valid format: %(error)s") % mdict raise exc.HTTPBadRequest(msg)
[docs] def stack_name(self): """Return the stack name.""" if self.PARAM_STACK_NAME not in self.data: raise exc.HTTPBadRequest(_("No stack name specified")) return self.data[self.PARAM_STACK_NAME]
[docs] def template(self): """Get template file contents. Get template file contents, either inline, from stack adopt data or from a URL, in JSON or YAML format. """ template_data = None if rpc_api.PARAM_ADOPT_STACK_DATA in self.data: adopt_data = self.data[rpc_api.PARAM_ADOPT_STACK_DATA] try: adopt_data = template_format.simple_parse(adopt_data) template_format.validate_template_limit( str(adopt_data['template'])) return adopt_data['template'] except (ValueError, KeyError) as ex: err_reason = _('Invalid adopt data: %s') % ex raise exc.HTTPBadRequest(err_reason) elif self.PARAM_TEMPLATE in self.data: template_data = self.data[self.PARAM_TEMPLATE] if isinstance(template_data, dict): template_format.validate_template_limit(str( template_data)) return template_data elif self.PARAM_TEMPLATE_URL in self.data: url = self.data[self.PARAM_TEMPLATE_URL] LOG.debug('TemplateUrl %s' % url) try: template_data = urlfetch.get(url) except IOError as ex: err_reason = _('Could not retrieve template: %s') % ex raise exc.HTTPBadRequest(err_reason) if template_data is None: if self.patch: return None else: raise exc.HTTPBadRequest(_("No template specified")) with self.parse_error_check('Template'): return template_format.parse(template_data)
[docs] def environment(self): """Get the user-supplied environment for the stack in YAML format. If the user supplied Parameters then merge these into the environment global options. """ env = {} # Don't use merged environment, if environment_files are supplied. if (self.PARAM_ENVIRONMENT in self.data and not self.data.get(self.PARAM_ENVIRONMENT_FILES)): env_data = self.data[self.PARAM_ENVIRONMENT] with self.parse_error_check('Environment'): if isinstance(env_data, dict): env = environment_format.validate(env_data) else: env = environment_format.parse(env_data) environment_format.default_for_missing(env) parameters = self.data.get(self.PARAM_USER_PARAMS, {}) env[self.PARAM_USER_PARAMS].update(parameters) return env
[docs] def files(self): return self.data.get(self.PARAM_FILES, {})
[docs] def environment_files(self): return self.data.get(self.PARAM_ENVIRONMENT_FILES, None)
[docs] def files_container(self): return self.data.get(self.PARAM_FILES_CONTAINER, None)
[docs] def args(self): """Get any additional arguments supplied by the user.""" params = self.data.items() return dict((k, v) for k, v in params if k not in self.PARAMS)
[docs] def no_change(self): assert self.patch return ((self.template() is None) and (self.environment() == environment_format.default_for_missing({})) and (not self.files()) and (not self.environment_files()) and (self.files_container() is None) and (not any(k != rpc_api.PARAM_EXISTING for k in self.args().keys())))
[docs]class StackController(object): """WSGI controller for stacks resource in Heat v1 API. Implements the API actions. """ # Define request scope (must match what is in policy.yaml or policies in # code) REQUEST_SCOPE = 'stacks' def __init__(self, options): self.options = options self.rpc_client = rpc_client.EngineClient()
[docs] def default(self, req, **args): raise exc.HTTPNotFound()
def _extract_bool_param(self, name, value): try: return param_utils.extract_bool(name, value) except ValueError as e: raise exc.HTTPBadRequest(str(e)) def _extract_int_param(self, name, value, allow_zero=True, allow_negative=False): try: return param_utils.extract_int(name, value, allow_zero, allow_negative) except ValueError as e: raise exc.HTTPBadRequest(str(e)) def _extract_tags_param(self, tags): try: return param_utils.extract_tags(tags) except ValueError as e: raise exc.HTTPBadRequest(str(e)) def _index(self, req, use_admin_cnxt=False): filter_param_types = { # usage of keys in this list are not encouraged, please use # rpc_api.STACK_KEYS instead 'id': util.PARAM_TYPE_MIXED, 'status': util.PARAM_TYPE_MIXED, 'name': util.PARAM_TYPE_MIXED, 'action': util.PARAM_TYPE_MIXED, 'tenant': util.PARAM_TYPE_MIXED, 'username': util.PARAM_TYPE_MIXED, 'owner_id': util.PARAM_TYPE_MIXED, } param_types = { 'limit': util.PARAM_TYPE_SINGLE, 'marker': util.PARAM_TYPE_SINGLE, 'sort_dir': util.PARAM_TYPE_SINGLE, 'sort_keys': util.PARAM_TYPE_MULTI, 'show_deleted': util.PARAM_TYPE_SINGLE, 'show_nested': util.PARAM_TYPE_SINGLE, 'show_hidden': util.PARAM_TYPE_SINGLE, 'tags': util.PARAM_TYPE_SINGLE, 'tags_any': util.PARAM_TYPE_SINGLE, 'not_tags': util.PARAM_TYPE_SINGLE, 'not_tags_any': util.PARAM_TYPE_SINGLE, } params = util.get_allowed_params(req.params, param_types) stack_keys = dict.fromkeys(rpc_api.STACK_KEYS, util.PARAM_TYPE_MIXED) unsupported = ( rpc_api.STACK_ID, # not user visible rpc_api.STACK_CAPABILITIES, # not supported rpc_api.STACK_CREATION_TIME, # don't support timestamp rpc_api.STACK_DELETION_TIME, # don't support timestamp rpc_api.STACK_DESCRIPTION, # not supported rpc_api.STACK_NOTIFICATION_TOPICS, # not supported rpc_api.STACK_OUTPUTS, # not in database rpc_api.STACK_PARAMETERS, # not in this table rpc_api.STACK_TAGS, # tags query following a specific guideline rpc_api.STACK_TMPL_DESCRIPTION, # not supported rpc_api.STACK_UPDATED_TIME, # don't support timestamp ) for key in unsupported: stack_keys.pop(key) # downward compatibility stack_keys.update(filter_param_types) filter_params = util.get_allowed_params(req.params, stack_keys) show_deleted = False p_name = rpc_api.PARAM_SHOW_DELETED if p_name in params: params[p_name] = self._extract_bool_param(p_name, params[p_name]) show_deleted = params[p_name] show_nested = False p_name = rpc_api.PARAM_SHOW_NESTED if p_name in params: params[p_name] = self._extract_bool_param(p_name, params[p_name]) show_nested = params[p_name] key = rpc_api.PARAM_LIMIT if key in params: params[key] = self._extract_int_param(key, params[key]) show_hidden = False p_name = rpc_api.PARAM_SHOW_HIDDEN if p_name in params: params[p_name] = self._extract_bool_param(p_name, params[p_name]) show_hidden = params[p_name] tags = None if rpc_api.PARAM_TAGS in params: params[rpc_api.PARAM_TAGS] = self._extract_tags_param( params[rpc_api.PARAM_TAGS]) tags = params[rpc_api.PARAM_TAGS] tags_any = None if rpc_api.PARAM_TAGS_ANY in params: params[rpc_api.PARAM_TAGS_ANY] = self._extract_tags_param( params[rpc_api.PARAM_TAGS_ANY]) tags_any = params[rpc_api.PARAM_TAGS_ANY] not_tags = None if rpc_api.PARAM_NOT_TAGS in params: params[rpc_api.PARAM_NOT_TAGS] = self._extract_tags_param( params[rpc_api.PARAM_NOT_TAGS]) not_tags = params[rpc_api.PARAM_NOT_TAGS] not_tags_any = None if rpc_api.PARAM_NOT_TAGS_ANY in params: params[rpc_api.PARAM_NOT_TAGS_ANY] = self._extract_tags_param( params[rpc_api.PARAM_NOT_TAGS_ANY]) not_tags_any = params[rpc_api.PARAM_NOT_TAGS_ANY] # get the with_count value, if invalid, raise ValueError with_count = False if req.params.get('with_count'): with_count = self._extract_bool_param( 'with_count', req.params.get('with_count')) if not filter_params: filter_params = None if use_admin_cnxt: cnxt = context.get_admin_context() else: cnxt = req.context stacks = self.rpc_client.list_stacks(cnxt, filters=filter_params, **params) count = None if with_count: try: # Check if engine has been updated to a version with # support to count_stacks before trying to use it. count = self.rpc_client.count_stacks(cnxt, filters=filter_params, show_deleted=show_deleted, show_nested=show_nested, show_hidden=show_hidden, tags=tags, tags_any=tags_any, not_tags=not_tags, not_tags_any=not_tags_any) except AttributeError as ex: LOG.warning("Old Engine Version: %s", ex) return stacks_view.collection(req, stacks=stacks, count=count, include_project=cnxt.is_admin)
[docs] @util.registered_policy_enforce def global_index(self, req): return self._index(req, use_admin_cnxt=True)
[docs] @util.registered_policy_enforce def index(self, req): """Lists summary information for all stacks.""" global_tenant = False name = rpc_api.PARAM_GLOBAL_TENANT if name in req.params: global_tenant = self._extract_bool_param( name, req.params.get(name)) if global_tenant: return self.global_index(req, req.context.tenant_id) return self._index(req)
[docs] @util.registered_policy_enforce def detail(self, req): """Lists detailed information for all stacks.""" stacks = self.rpc_client.list_stacks(req.context) return {'stacks': [stacks_view.format_stack(req, s) for s in stacks]}
[docs] @util.registered_policy_enforce def preview(self, req, body): """Preview the outcome of a template and its params.""" data = InstantiationData(body) args = self.prepare_args(data) result = self.rpc_client.preview_stack( req.context, data.stack_name(), data.template(), data.environment(), data.files(), args, environment_files=data.environment_files(), files_container=data.files_container()) formatted_stack = stacks_view.format_stack(req, result) return {'stack': formatted_stack}
[docs] def prepare_args(self, data, is_update=False): args = data.args() key = rpc_api.PARAM_TIMEOUT if key in args: args[key] = self._extract_int_param(key, args[key]) key = rpc_api.PARAM_TAGS if args.get(key) is not None: args[key] = self._extract_tags_param(args[key]) key = rpc_api.PARAM_CONVERGE if not is_update and key in args: msg = _("%s flag only supported in stack update (or update " "preview) request.") % key raise exc.HTTPBadRequest(str(msg)) return args
[docs] @util.registered_policy_enforce def create(self, req, body): """Create a new stack.""" data = InstantiationData(body) args = self.prepare_args(data) result = self.rpc_client.create_stack( req.context, data.stack_name(), data.template(), data.environment(), data.files(), args, environment_files=data.environment_files(), files_container=data.files_container()) formatted_stack = stacks_view.format_stack( req, {rpc_api.STACK_ID: result} ) return {'stack': formatted_stack}
[docs] @util.registered_policy_enforce def lookup(self, req, stack_name, path='', body=None): """Redirect to the canonical URL for a stack.""" try: identity = dict(identifier.HeatIdentifier.from_arn(stack_name)) except ValueError: identity = self.rpc_client.identify_stack(req.context, stack_name) location = util.make_url(req, identity) if path: location = '/'.join([location, path]) params = req.params if params: location += '?%s' % parse.urlencode(params, True) raise exc.HTTPFound(location=location)
[docs] @util.registered_identified_stack def show(self, req, identity): """Gets detailed information for a stack.""" params = req.params p_name = rpc_api.RESOLVE_OUTPUTS if rpc_api.RESOLVE_OUTPUTS in params: resolve_outputs = self._extract_bool_param( p_name, params[p_name]) else: resolve_outputs = True stack_list = self.rpc_client.show_stack(req.context, identity, resolve_outputs) if not stack_list: raise exc.HTTPInternalServerError() stack = stack_list[0] return {'stack': stacks_view.format_stack(req, stack)}
[docs] @util.registered_identified_stack def template(self, req, identity): """Get the template body for an existing stack.""" templ = self.rpc_client.get_template(req.context, identity) # TODO(zaneb): always set Content-type to application/json return templ
[docs] @util.registered_identified_stack def environment(self, req, identity): """Get the environment for an existing stack.""" env = self.rpc_client.get_environment(req.context, identity) return env
[docs] @util.registered_identified_stack def files(self, req, identity): """Get the files for an existing stack.""" return self.rpc_client.get_files(req.context, identity)
[docs] @util.registered_identified_stack def update(self, req, identity, body): """Update an existing stack with a new template and/or parameters.""" data = InstantiationData(body) args = self.prepare_args(data, is_update=True) self.rpc_client.update_stack( req.context, identity, data.template(), data.environment(), data.files(), args, environment_files=data.environment_files(), files_container=data.files_container()) raise exc.HTTPAccepted()
[docs] @util.no_policy_enforce @util._identified_stack def update_patch(self, req, identity, body): """Update an existing stack with a new template. Update an existing stack with a new template by patching the parameters Add the flag patch to the args so the engine code can distinguish """ data = InstantiationData(body, patch=True) _target = {"project_id": req.context.tenant_id} policy_act = 'update_no_change' if data.no_change() else 'update_patch' allowed = req.context.policy.enforce( context=req.context, action=policy_act, scope=self.REQUEST_SCOPE, target=_target, is_registered_policy=True) if not allowed: raise exc.HTTPForbidden() args = self.prepare_args(data, is_update=True) self.rpc_client.update_stack( req.context, identity, data.template(), data.environment(), data.files(), args, environment_files=data.environment_files(), files_container=data.files_container()) raise exc.HTTPAccepted()
def _param_show_nested(self, req): param_types = {'show_nested': util.PARAM_TYPE_SINGLE} params = util.get_allowed_params(req.params, param_types) p_name = 'show_nested' if p_name in params: return self._extract_bool_param(p_name, params[p_name])
[docs] @util.registered_identified_stack def preview_update(self, req, identity, body): """Preview update for existing stack with a new template/parameters.""" data = InstantiationData(body) args = self.prepare_args(data, is_update=True) show_nested = self._param_show_nested(req) if show_nested is not None: args[rpc_api.PARAM_SHOW_NESTED] = show_nested changes = self.rpc_client.preview_update_stack( req.context, identity, data.template(), data.environment(), data.files(), args, environment_files=data.environment_files(), files_container=data.files_container()) return {'resource_changes': changes}
[docs] @util.registered_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, is_update=True) show_nested = self._param_show_nested(req) if show_nested is not None: args['show_nested'] = show_nested changes = self.rpc_client.preview_update_stack( req.context, identity, data.template(), data.environment(), data.files(), args, environment_files=data.environment_files(), files_container=data.files_container()) return {'resource_changes': changes}
[docs] @util.registered_identified_stack def delete(self, req, identity): """Delete the specified stack.""" self.rpc_client.delete_stack(req.context, identity, cast=False) raise exc.HTTPNoContent()
[docs] @util.registered_identified_stack def abandon(self, req, identity): """Abandons specified stack. Abandons specified stack by deleting the stack and it's resources from the database, but underlying resources will not be deleted. """ return self.rpc_client.abandon_stack(req.context, identity)
[docs] @util.registered_identified_stack def export(self, req, identity): """Export specified stack. Return stack data in JSON format. """ return self.rpc_client.export_stack(req.context, identity)
[docs] @util.registered_policy_enforce def validate_template(self, req, body): """Implements the ValidateTemplate API action. Validates the specified template. """ data = InstantiationData(body) param_types = {'show_nested': util.PARAM_TYPE_SINGLE, 'ignore_errors': util.PARAM_TYPE_SINGLE} params = util.get_allowed_params(req.params, param_types) show_nested = False p_name = rpc_api.PARAM_SHOW_NESTED if p_name in params: params[p_name] = self._extract_bool_param(p_name, params[p_name]) show_nested = params[p_name] if rpc_api.PARAM_IGNORE_ERRORS in params: ignorable_errors = params[rpc_api.PARAM_IGNORE_ERRORS].split(',') else: ignorable_errors = None result = self.rpc_client.validate_template( req.context, data.template(), data.environment(), files=data.files(), environment_files=data.environment_files(), files_container=data.files_container(), show_nested=show_nested, ignorable_errors=ignorable_errors) if 'Error' in result: raise exc.HTTPBadRequest(result['Error']) return result
[docs] @util.registered_policy_enforce def list_resource_types(self, req): """Returns a resource types list which may be used in template.""" support_status = req.params.get('support_status') type_name = req.params.get('name') version = req.params.get('version') if req.params.get('with_description') is not None: with_description = self._extract_bool_param( 'with_description', req.params.get('with_description')) else: # Add backward compatibility support for case when heatclient # version is lower than version with this parameter. with_description = False return { 'resource_types': self.rpc_client.list_resource_types( req.context, support_status=support_status, type_name=type_name, heat_version=version, with_description=with_description)}
[docs] @util.registered_policy_enforce def list_template_versions(self, req): """Returns a list of available template versions.""" return { 'template_versions': self.rpc_client.list_template_versions(req.context) }
[docs] @util.registered_policy_enforce def list_template_functions(self, req, template_version): """Returns a list of available functions in a given template.""" if req.params.get('with_condition_func') is not None: with_condition = self._extract_bool_param( 'with_condition_func', req.params.get('with_condition_func')) else: with_condition = False return { 'template_functions': self.rpc_client.list_template_functions(req.context, template_version, with_condition) }
[docs] @util.registered_policy_enforce def resource_schema(self, req, type_name, with_description=False): """Returns the schema of the given resource type.""" return self.rpc_client.resource_schema( req.context, type_name, self._extract_bool_param('with_description', with_description))
[docs] @util.registered_policy_enforce def generate_template(self, req, type_name): """Generates a template based on the specified type.""" template_type = 'cfn' if rpc_api.TEMPLATE_TYPE in req.params: try: template_type = param_utils.extract_template_type( req.params.get(rpc_api.TEMPLATE_TYPE)) except ValueError as ex: msg = _("Template type is not supported: %s") % ex raise exc.HTTPBadRequest(str(msg)) return self.rpc_client.generate_template(req.context, type_name, template_type)
[docs] @util.registered_identified_stack def snapshot(self, req, identity, body): name = body.get('name') return self.rpc_client.stack_snapshot(req.context, identity, name)
[docs] @util.registered_identified_stack def show_snapshot(self, req, identity, snapshot_id): snapshot = self.rpc_client.show_snapshot( req.context, identity, snapshot_id) return {'snapshot': snapshot}
[docs] @util.registered_identified_stack def delete_snapshot(self, req, identity, snapshot_id): self.rpc_client.delete_snapshot(req.context, identity, snapshot_id) raise exc.HTTPNoContent()
[docs] @util.registered_identified_stack def list_snapshots(self, req, identity): return { 'snapshots': self.rpc_client.stack_list_snapshots( req.context, identity) }
[docs] @util.registered_identified_stack def restore_snapshot(self, req, identity, snapshot_id): self.rpc_client.stack_restore(req.context, identity, snapshot_id) raise exc.HTTPAccepted()
[docs] @util.registered_identified_stack def list_outputs(self, req, identity): return { 'outputs': self.rpc_client.list_outputs( req.context, identity) }
[docs] @util.registered_identified_stack def show_output(self, req, identity, output_key): return {'output': self.rpc_client.show_output(req.context, identity, output_key)}
[docs]class StackSerializer(serializers.JSONResponseSerializer): """Handles serialization of specific controller method responses.""" def _populate_response_header(self, response, location, status): response.status = status response.headers['Location'] = location response.headers['Content-Type'] = 'application/json' return response
[docs] def create(self, response, result): self._populate_response_header(response, result['stack']['links'][0]['href'], 201) response.body = self.to_json(result).encode('latin-1') return response
[docs]def create_resource(options): """Stacks resource factory method.""" deserializer = wsgi.JSONRequestDeserializer() serializer = StackSerializer() return wsgi.Resource(StackController(options), deserializer, serializer)