You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
618 lines
21 KiB
618 lines
21 KiB
# |
|
# 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)
|
|
|