heat/heat/api/openstack/v1/stacks.py

619 lines
21 KiB
Python

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