# # 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. """ from oslo_log import log as logging import six from six.moves.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 environment_format from heat.common.i18n import _ from heat.common.i18n import _LW 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__) class InstantiationData(object): """ The data accompanying a PUT or POST request to create or update a stack. """ PARAMS = ( PARAM_STACK_NAME, PARAM_TEMPLATE, PARAM_TEMPLATE_URL, PARAM_USER_PARAMS, PARAM_ENVIRONMENT, PARAM_FILES, ) = ( 'stack_name', 'template', 'template_url', 'parameters', 'environment', 'files', ) 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 if patch: self.data[rpc_api.PARAM_EXISTING] = True @staticmethod def format_parse(data, data_type): """ Parse the supplied data as JSON or YAML, raising the appropriate exception if it is in the wrong format. """ try: if data_type == 'Environment': return environment_format.parse(data) else: return template_format.parse(data) except ValueError as parse_ex: mdict = {'type': data_type, 'error': six.text_type(parse_ex)} msg = _("%(type)s not in valid format: %(error)s") % mdict raise exc.HTTPBadRequest(msg) 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] def template(self): """ Get template file contents, either inline, from stack adopt data or from a URL, in JSON or YAML format. """ 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) 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): 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) else: raise exc.HTTPBadRequest(_("No template specified")) return self.format_parse(template_data, 'Template') 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 = {} if self.PARAM_ENVIRONMENT in self.data: env_data = self.data[self.PARAM_ENVIRONMENT] if isinstance(env_data, dict): env = env_data else: env = self.format_parse(env_data, 'Environment') environment_format.default_for_missing(env) parameters = self.data.get(self.PARAM_USER_PARAMS, {}) env[self.PARAM_USER_PARAMS].update(parameters) return env def files(self): return self.data.get(self.PARAM_FILES, {}) 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) 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.json) REQUEST_SCOPE = 'stacks' def __init__(self, options): self.options = options self.rpc_client = rpc_client.EngineClient() 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(six.text_type(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(six.text_type(e)) def _extract_tags_param(self, tags): try: return param_utils.extract_tags(tags) except ValueError as e: raise exc.HTTPBadRequest(six.text_type(e)) def _index(self, req, tenant_safe=True): filter_whitelist = { 'id': 'mixed', 'status': 'mixed', 'name': 'mixed', 'action': 'mixed', 'tenant': 'mixed', 'username': 'mixed', 'owner_id': 'mixed', } whitelist = { 'limit': 'single', 'marker': 'single', 'sort_dir': 'single', 'sort_keys': 'multi', 'show_deleted': 'single', 'show_nested': 'single', 'show_hidden': 'single', 'tags': 'single', 'tags_any': 'single', 'not_tags': 'single', 'not_tags_any': 'single', } params = util.get_allowed_params(req.params, whitelist) filter_params = util.get_allowed_params(req.params, filter_whitelist) 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 stacks = self.rpc_client.list_stacks(req.context, filters=filter_params, tenant_safe=tenant_safe, **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(req.context, filters=filter_params, tenant_safe=tenant_safe, 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.warn(_LW("Old Engine Version: %s"), ex) return stacks_view.collection(req, stacks=stacks, count=count, tenant_safe=tenant_safe) @util.policy_enforce def global_index(self, req): return self._index(req, tenant_safe=False) @util.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) @util.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]} @util.policy_enforce def preview(self, req, body): """ Preview the outcome of a template and its params """ data = InstantiationData(body) result = self.rpc_client.preview_stack(req.context, data.stack_name(), data.template(), data.environment(), data.files(), data.args()) formatted_stack = stacks_view.format_stack(req, result) return {'stack': formatted_stack} @util.policy_enforce def create(self, req, body): """ Create a new stack """ data = InstantiationData(body) args = data.args() key = rpc_api.PARAM_TIMEOUT if key in args: args[key] = self._extract_int_param(key, args[key]) result = self.rpc_client.create_stack(req.context, data.stack_name(), data.template(), data.environment(), data.files(), args) formatted_stack = stacks_view.format_stack( req, {rpc_api.STACK_ID: result} ) return {'stack': formatted_stack} @util.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) @util.identified_stack def show(self, req, identity): """ Gets detailed information for a stack """ stack_list = self.rpc_client.show_stack(req.context, identity) if not stack_list: raise exc.HTTPInternalServerError() stack = stack_list[0] return {'stack': stacks_view.format_stack(req, stack)} @util.identified_stack def template(self, req, identity): """ Get the template body for an existing stack """ templ = self.rpc_client.get_template(req.context, identity) if templ is None: raise exc.HTTPNotFound() # TODO(zaneb): always set Content-type to application/json return templ @util.identified_stack def update(self, req, identity, body): """ Update an existing stack with a new template and/or parameters """ data = InstantiationData(body) args = data.args() key = rpc_api.PARAM_TIMEOUT if key in args: args[key] = self._extract_int_param(key, args[key]) self.rpc_client.update_stack(req.context, identity, data.template(), data.environment(), data.files(), args) raise exc.HTTPAccepted() @util.identified_stack def update_patch(self, req, identity, body): """ 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) args = data.args() key = rpc_api.PARAM_TIMEOUT if key in args: args[key] = self._extract_int_param(key, args[key]) self.rpc_client.update_stack(req.context, identity, data.template(), data.environment(), data.files(), args) raise exc.HTTPAccepted() @util.identified_stack def delete(self, req, identity): """ Delete the specified stack """ res = self.rpc_client.delete_stack(req.context, identity, cast=False) if res is not None: raise exc.HTTPBadRequest(res['Error']) raise exc.HTTPNoContent() @util.identified_stack def abandon(self, req, identity): """ 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) @util.policy_enforce def validate_template(self, req, body): """ Implements the ValidateTemplate API action Validates the specified template """ data = InstantiationData(body) result = self.rpc_client.validate_template(req.context, data.template(), data.environment()) if 'Error' in result: raise exc.HTTPBadRequest(result['Error']) return result @util.policy_enforce def list_resource_types(self, req): """ Returns a list of valid resource types that may be used in a template. """ support_status = req.params.get('support_status') return { 'resource_types': self.rpc_client.list_resource_types(req.context, support_status)} @util.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) } @util.policy_enforce def list_template_functions(self, req, template_version): """ Returns a list of available functions in a given template """ return { 'template_functions': self.rpc_client.list_template_functions(req.context, template_version) } @util.policy_enforce def resource_schema(self, req, type_name): """ Returns the schema of the given resource type. """ return self.rpc_client.resource_schema(req.context, type_name) @util.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(six.text_type(msg)) return self.rpc_client.generate_template(req.context, type_name, template_type) @util.identified_stack def snapshot(self, req, identity, body): name = body.get('name') return self.rpc_client.stack_snapshot(req.context, identity, name) @util.identified_stack def show_snapshot(self, req, identity, snapshot_id): snapshot = self.rpc_client.show_snapshot( req.context, identity, snapshot_id) return {'snapshot': snapshot} @util.identified_stack def delete_snapshot(self, req, identity, snapshot_id): self.rpc_client.delete_snapshot(req.context, identity, snapshot_id) raise exc.HTTPNoContent() @util.identified_stack def list_snapshots(self, req, identity): return { 'snapshots': self.rpc_client.stack_list_snapshots( req.context, identity) } @util.identified_stack def restore_snapshot(self, req, identity, snapshot_id): self.rpc_client.stack_restore(req.context, identity, snapshot_id) raise exc.HTTPAccepted() 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.encode('utf-8') response.headers['Content-Type'] = 'application/json' return response def create(self, response, result): self._populate_response_header(response, result['stack']['links'][0]['href'], 201) response.body = self.to_json(result) return response def create_resource(options): """ Stacks resource factory method. """ deserializer = wsgi.JSONRequestDeserializer() serializer = StackSerializer() return wsgi.Resource(StackController(options), deserializer, serializer)