As per the community goal of migrating the policy file the format from JSON to YAML[1], we need to do two things: 1. Change the default value of '[oslo_policy] policy_file'' config option from 'policy.json' to 'policy.yaml' with upgrade checks. 2. Deprecate the JSON formatted policy file on the project side via warning in doc and releasenotes. Also replace policy.json to policy.yaml ref from doc and code. [1]https://governance.openstack.org/tc/goals/selected/wallaby/migrate-policy-format-from-json-to-yaml.html Change-Id: I1aa12bcd2638390f25d57ce8abeeec248121dc02
673 lines
25 KiB
Python
673 lines
25 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 CloudFormation v1 API."""
|
|
|
|
import socket
|
|
|
|
from oslo_log import log as logging
|
|
from oslo_serialization import jsonutils
|
|
|
|
from heat.api.aws import exception
|
|
from heat.api.aws import utils as api_utils
|
|
from heat.common import exception as heat_exception
|
|
from heat.common.i18n import _
|
|
from heat.common import identifier
|
|
from heat.common import policy
|
|
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 StackController(object):
|
|
|
|
"""WSGI controller for stacks resource in Heat CloudFormation v1 API.
|
|
|
|
Implements the API actions.
|
|
"""
|
|
|
|
def __init__(self, options):
|
|
self.options = options
|
|
self.rpc_client = rpc_client.EngineClient()
|
|
self.policy = policy.Enforcer(scope='cloudformation')
|
|
|
|
def default(self, req, **args):
|
|
raise exception.HeatInvalidActionError()
|
|
|
|
def _enforce(self, req, action):
|
|
"""Authorize an action against the policy.yaml and policies in code."""
|
|
try:
|
|
self.policy.enforce(req.context, action, is_registered_policy=True)
|
|
except heat_exception.Forbidden:
|
|
msg = _('Action %s not allowed for user') % action
|
|
raise exception.HeatAccessDeniedError(msg)
|
|
except Exception:
|
|
# We expect policy.enforce to either pass or raise Forbidden
|
|
# however, if anything else happens, we want to raise
|
|
# HeatInternalFailureError, failure to do this results in
|
|
# the user getting a big stacktrace spew as an API response
|
|
msg = _('Error authorizing action %s') % action
|
|
raise exception.HeatInternalFailureError(msg)
|
|
|
|
@staticmethod
|
|
def _id_format(resp):
|
|
"""Format the StackId field in the response as an ARN.
|
|
|
|
Also, process other IDs into the correct format.
|
|
"""
|
|
if 'StackId' in resp:
|
|
identity = identifier.HeatIdentifier(**resp['StackId'])
|
|
resp['StackId'] = identity.arn()
|
|
if 'EventId' in resp:
|
|
identity = identifier.EventIdentifier(**resp['EventId'])
|
|
resp['EventId'] = identity.event_id
|
|
return resp
|
|
|
|
@staticmethod
|
|
def _extract_user_params(params):
|
|
"""Extract a dictionary of user input parameters for the stack.
|
|
|
|
In the AWS API parameters, each user parameter appears as two key-value
|
|
pairs with keys of the form below::
|
|
|
|
Parameters.member.1.ParameterKey
|
|
Parameters.member.1.ParameterValue
|
|
"""
|
|
return api_utils.extract_param_pairs(params,
|
|
prefix='Parameters',
|
|
keyname='ParameterKey',
|
|
valuename='ParameterValue')
|
|
|
|
def _get_identity(self, con, stack_name):
|
|
"""Generate a stack identifier from the given stack name or ARN.
|
|
|
|
In the case of a stack name, the identifier will be looked up in the
|
|
engine over RPC.
|
|
"""
|
|
try:
|
|
return dict(identifier.HeatIdentifier.from_arn(stack_name))
|
|
except ValueError:
|
|
return self.rpc_client.identify_stack(con, stack_name)
|
|
|
|
def list(self, req):
|
|
"""Implements ListStacks API action.
|
|
|
|
Lists summary information for all stacks.
|
|
"""
|
|
self._enforce(req, 'ListStacks')
|
|
|
|
def format_stack_summary(s):
|
|
"""Reformat engine output into the AWS "StackSummary" format."""
|
|
# Map the engine-api format to the AWS StackSummary datatype
|
|
keymap = {
|
|
rpc_api.STACK_CREATION_TIME: 'CreationTime',
|
|
rpc_api.STACK_UPDATED_TIME: 'LastUpdatedTime',
|
|
rpc_api.STACK_ID: 'StackId',
|
|
rpc_api.STACK_NAME: 'StackName',
|
|
rpc_api.STACK_STATUS_DATA: 'StackStatusReason',
|
|
rpc_api.STACK_TMPL_DESCRIPTION: 'TemplateDescription',
|
|
}
|
|
|
|
result = api_utils.reformat_dict_keys(keymap, s)
|
|
|
|
action = s[rpc_api.STACK_ACTION]
|
|
status = s[rpc_api.STACK_STATUS]
|
|
result['StackStatus'] = '_'.join((action, status))
|
|
|
|
# AWS docs indicate DeletionTime is omitted for current stacks
|
|
# This is still TODO(unknown) in the engine, we don't keep data for
|
|
# stacks after they are deleted
|
|
if rpc_api.STACK_DELETION_TIME in s:
|
|
result['DeletionTime'] = s[rpc_api.STACK_DELETION_TIME]
|
|
|
|
return self._id_format(result)
|
|
|
|
con = req.context
|
|
try:
|
|
stack_list = self.rpc_client.list_stacks(con)
|
|
except Exception as ex:
|
|
return exception.map_remote_error(ex)
|
|
|
|
res = {'StackSummaries': [format_stack_summary(s) for s in stack_list]}
|
|
|
|
return api_utils.format_response('ListStacks', res)
|
|
|
|
def describe(self, req):
|
|
"""Implements DescribeStacks API action.
|
|
|
|
Gets detailed information for a stack (or all stacks).
|
|
"""
|
|
self._enforce(req, 'DescribeStacks')
|
|
|
|
def format_stack_outputs(o):
|
|
keymap = {
|
|
rpc_api.OUTPUT_DESCRIPTION: 'Description',
|
|
rpc_api.OUTPUT_KEY: 'OutputKey',
|
|
rpc_api.OUTPUT_VALUE: 'OutputValue',
|
|
}
|
|
|
|
def replacecolon(d):
|
|
return dict(map(lambda k_v: (k_v[0].replace(':', '.'), k_v[1]),
|
|
d.items()))
|
|
|
|
def transform(attrs):
|
|
"""Recursively replace all `:` with `.` in dict keys.
|
|
|
|
After that they are not interpreted as xml namespaces.
|
|
"""
|
|
new = replacecolon(attrs)
|
|
for key, value in new.items():
|
|
if isinstance(value, dict):
|
|
new[key] = transform(value)
|
|
return new
|
|
|
|
return api_utils.reformat_dict_keys(keymap, transform(o))
|
|
|
|
def format_stack(s):
|
|
"""Reformat engine output into the AWS "StackSummary" format."""
|
|
keymap = {
|
|
rpc_api.STACK_CAPABILITIES: 'Capabilities',
|
|
rpc_api.STACK_CREATION_TIME: 'CreationTime',
|
|
rpc_api.STACK_DESCRIPTION: 'Description',
|
|
rpc_api.STACK_DISABLE_ROLLBACK: 'DisableRollback',
|
|
rpc_api.STACK_NOTIFICATION_TOPICS: 'NotificationARNs',
|
|
rpc_api.STACK_PARAMETERS: 'Parameters',
|
|
rpc_api.STACK_ID: 'StackId',
|
|
rpc_api.STACK_NAME: 'StackName',
|
|
rpc_api.STACK_STATUS_DATA: 'StackStatusReason',
|
|
rpc_api.STACK_TIMEOUT: 'TimeoutInMinutes',
|
|
}
|
|
|
|
if s[rpc_api.STACK_UPDATED_TIME] is not None:
|
|
keymap[rpc_api.STACK_UPDATED_TIME] = 'LastUpdatedTime'
|
|
|
|
result = api_utils.reformat_dict_keys(keymap, s)
|
|
|
|
action = s[rpc_api.STACK_ACTION]
|
|
status = s[rpc_api.STACK_STATUS]
|
|
result['StackStatus'] = '_'.join((action, status))
|
|
|
|
# Reformat outputs, these are handled separately as they are
|
|
# only present in the engine output for a completely created
|
|
# stack
|
|
result['Outputs'] = []
|
|
if rpc_api.STACK_OUTPUTS in s:
|
|
for o in s[rpc_api.STACK_OUTPUTS]:
|
|
result['Outputs'].append(format_stack_outputs(o))
|
|
|
|
# Reformat Parameters dict-of-dict into AWS API format
|
|
# This is a list-of-dict with nasty "ParameterKey" : key
|
|
# "ParameterValue" : value format.
|
|
result['Parameters'] = [{'ParameterKey': k,
|
|
'ParameterValue': v}
|
|
for (k, v) in result['Parameters'].items()]
|
|
|
|
return self._id_format(result)
|
|
|
|
con = req.context
|
|
# If no StackName parameter is passed, we pass None into the engine
|
|
# this returns results for all stacks (visible to this user), which
|
|
# is the behavior described in the AWS DescribeStacks API docs
|
|
try:
|
|
if 'StackName' in req.params:
|
|
identity = self._get_identity(con, req.params['StackName'])
|
|
else:
|
|
identity = None
|
|
|
|
stack_list = self.rpc_client.show_stack(con, identity)
|
|
|
|
except Exception as ex:
|
|
return exception.map_remote_error(ex)
|
|
|
|
res = {'Stacks': [format_stack(s) for s in stack_list]}
|
|
|
|
return api_utils.format_response('DescribeStacks', res)
|
|
|
|
def _get_template(self, req):
|
|
"""Get template file contents, either from local file or URL."""
|
|
if 'TemplateBody' in req.params:
|
|
LOG.debug('TemplateBody ...')
|
|
return req.params['TemplateBody']
|
|
elif 'TemplateUrl' in req.params:
|
|
url = req.params['TemplateUrl']
|
|
LOG.debug('TemplateUrl %s' % url)
|
|
try:
|
|
return urlfetch.get(url)
|
|
except IOError as exc:
|
|
msg = _('Failed to fetch template: %s') % exc
|
|
raise exception.HeatInvalidParameterValueError(detail=msg)
|
|
|
|
return None
|
|
|
|
CREATE_OR_UPDATE_ACTION = (
|
|
CREATE_STACK, UPDATE_STACK,
|
|
) = (
|
|
"CreateStack", "UpdateStack",
|
|
)
|
|
|
|
def create(self, req):
|
|
self._enforce(req, 'CreateStack')
|
|
return self.create_or_update(req, self.CREATE_STACK)
|
|
|
|
def update(self, req):
|
|
self._enforce(req, 'UpdateStack')
|
|
return self.create_or_update(req, self.UPDATE_STACK)
|
|
|
|
def create_or_update(self, req, action=None):
|
|
"""Implements CreateStack and UpdateStack API actions.
|
|
|
|
Create or update stack as defined in template file.
|
|
"""
|
|
def extract_args(params):
|
|
"""Extract request params and reformat them to match engine API.
|
|
|
|
FIXME: we currently only support a subset of
|
|
the AWS defined parameters (both here and in the engine)
|
|
"""
|
|
# TODO(shardy) : Capabilities, NotificationARNs
|
|
keymap = {'TimeoutInMinutes': rpc_api.PARAM_TIMEOUT,
|
|
'DisableRollback': rpc_api.PARAM_DISABLE_ROLLBACK}
|
|
|
|
if 'DisableRollback' in params and 'OnFailure' in params:
|
|
msg = _('DisableRollback and OnFailure '
|
|
'may not be used together')
|
|
raise exception.HeatInvalidParameterCombinationError(
|
|
detail=msg)
|
|
|
|
result = {}
|
|
for k in keymap:
|
|
if k in params:
|
|
result[keymap[k]] = params[k]
|
|
|
|
if 'OnFailure' in params:
|
|
value = params['OnFailure']
|
|
if value == 'DO_NOTHING':
|
|
result[rpc_api.PARAM_DISABLE_ROLLBACK] = 'true'
|
|
elif value in ('ROLLBACK', 'DELETE'):
|
|
result[rpc_api.PARAM_DISABLE_ROLLBACK] = 'false'
|
|
|
|
return result
|
|
|
|
if action not in self.CREATE_OR_UPDATE_ACTION:
|
|
msg = _("Unexpected action %(action)s") % ({'action': action})
|
|
# This should not happen, so return HeatInternalFailureError
|
|
return exception.HeatInternalFailureError(detail=msg)
|
|
|
|
engine_action = {self.CREATE_STACK: self.rpc_client.create_stack,
|
|
self.UPDATE_STACK: self.rpc_client.update_stack}
|
|
|
|
con = req.context
|
|
|
|
# Extract the stack input parameters
|
|
stack_parms = self._extract_user_params(req.params)
|
|
|
|
# Extract any additional arguments ("Request Parameters")
|
|
create_args = extract_args(req.params)
|
|
|
|
try:
|
|
templ = self._get_template(req)
|
|
except socket.gaierror:
|
|
msg = _('Invalid Template URL')
|
|
return exception.HeatInvalidParameterValueError(detail=msg)
|
|
|
|
if templ is None:
|
|
msg = _("TemplateBody or TemplateUrl were not given.")
|
|
return exception.HeatMissingParameterError(detail=msg)
|
|
|
|
try:
|
|
stack = template_format.parse(templ)
|
|
except ValueError:
|
|
msg = _("The Template must be a JSON or YAML document.")
|
|
return exception.HeatInvalidParameterValueError(detail=msg)
|
|
|
|
args = {'template': stack,
|
|
'params': stack_parms,
|
|
'files': {},
|
|
'args': create_args}
|
|
try:
|
|
stack_name = req.params['StackName']
|
|
if action == self.CREATE_STACK:
|
|
args['stack_name'] = stack_name
|
|
else:
|
|
args['stack_identity'] = self._get_identity(con, stack_name)
|
|
|
|
result = engine_action[action](con, **args)
|
|
except Exception as ex:
|
|
return exception.map_remote_error(ex)
|
|
|
|
try:
|
|
identity = identifier.HeatIdentifier(**result)
|
|
except (ValueError, TypeError):
|
|
response = result
|
|
else:
|
|
response = {'StackId': identity.arn()}
|
|
|
|
return api_utils.format_response(action, response)
|
|
|
|
def cancel_update(self, req):
|
|
action = 'CancelUpdateStack'
|
|
self._enforce(req, action)
|
|
con = req.context
|
|
stack_name = req.params['StackName']
|
|
stack_identity = self._get_identity(con, stack_name)
|
|
try:
|
|
self.rpc_client.stack_cancel_update(
|
|
con, stack_identity=stack_identity, cancel_with_rollback=True)
|
|
except Exception as ex:
|
|
return exception.map_remote_error(ex)
|
|
|
|
return api_utils.format_response(action, {})
|
|
|
|
def get_template(self, req):
|
|
"""Implements the GetTemplate API action.
|
|
|
|
Get the template body for an existing stack.
|
|
"""
|
|
self._enforce(req, 'GetTemplate')
|
|
|
|
con = req.context
|
|
try:
|
|
identity = self._get_identity(con, req.params['StackName'])
|
|
templ = self.rpc_client.get_template(con, identity)
|
|
except Exception as ex:
|
|
return exception.map_remote_error(ex)
|
|
|
|
return api_utils.format_response('GetTemplate',
|
|
{'TemplateBody': templ})
|
|
|
|
def estimate_template_cost(self, req):
|
|
"""Implements the EstimateTemplateCost API action.
|
|
|
|
Get the estimated monthly cost of a template.
|
|
"""
|
|
self._enforce(req, 'EstimateTemplateCost')
|
|
|
|
return api_utils.format_response('EstimateTemplateCost',
|
|
{'Url':
|
|
'http://en.wikipedia.org/wiki/Gratis'
|
|
}
|
|
)
|
|
|
|
def validate_template(self, req):
|
|
"""Implements the ValidateTemplate API action.
|
|
|
|
Validates the specified template.
|
|
"""
|
|
self._enforce(req, 'ValidateTemplate')
|
|
|
|
con = req.context
|
|
try:
|
|
templ = self._get_template(req)
|
|
except socket.gaierror:
|
|
msg = _('Invalid Template URL')
|
|
return exception.HeatInvalidParameterValueError(detail=msg)
|
|
if templ is None:
|
|
msg = _("TemplateBody or TemplateUrl were not given.")
|
|
return exception.HeatMissingParameterError(detail=msg)
|
|
|
|
try:
|
|
template = template_format.parse(templ)
|
|
except ValueError:
|
|
msg = _("The Template must be a JSON or YAML document.")
|
|
return exception.HeatInvalidParameterValueError(detail=msg)
|
|
|
|
LOG.info('validate_template')
|
|
|
|
def format_validate_parameter(key, value):
|
|
"""Reformat engine output into AWS "ValidateTemplate" format."""
|
|
|
|
return {
|
|
'ParameterKey': key,
|
|
'DefaultValue': value.get(rpc_api.PARAM_DEFAULT, ''),
|
|
'Description': value.get(rpc_api.PARAM_DESCRIPTION, ''),
|
|
'NoEcho': value.get(rpc_api.PARAM_NO_ECHO, 'false')
|
|
}
|
|
|
|
try:
|
|
res = self.rpc_client.validate_template(con, template)
|
|
if 'Error' in res:
|
|
return api_utils.format_response('ValidateTemplate',
|
|
res['Error'])
|
|
|
|
res['Parameters'] = [format_validate_parameter(k, v)
|
|
for k, v in res['Parameters'].items()]
|
|
return api_utils.format_response('ValidateTemplate', res)
|
|
except Exception as ex:
|
|
return exception.map_remote_error(ex)
|
|
|
|
def delete(self, req):
|
|
"""Implements the DeleteStack API action.
|
|
|
|
Deletes the specified stack.
|
|
"""
|
|
self._enforce(req, 'DeleteStack')
|
|
|
|
con = req.context
|
|
try:
|
|
identity = self._get_identity(con, req.params['StackName'])
|
|
res = self.rpc_client.delete_stack(con, identity, cast=False)
|
|
|
|
except Exception as ex:
|
|
return exception.map_remote_error(ex)
|
|
|
|
if res is None:
|
|
return api_utils.format_response('DeleteStack', '')
|
|
else:
|
|
return api_utils.format_response('DeleteStack', res['Error'])
|
|
|
|
def events_list(self, req):
|
|
"""Implements the DescribeStackEvents API action.
|
|
|
|
Returns events related to a specified stack (or all stacks).
|
|
"""
|
|
self._enforce(req, 'DescribeStackEvents')
|
|
|
|
def format_stack_event(e):
|
|
"""Reformat engine output into AWS "StackEvent" format."""
|
|
keymap = {
|
|
rpc_api.EVENT_ID: 'EventId',
|
|
rpc_api.EVENT_RES_NAME: 'LogicalResourceId',
|
|
rpc_api.EVENT_RES_PHYSICAL_ID: 'PhysicalResourceId',
|
|
rpc_api.EVENT_RES_PROPERTIES: 'ResourceProperties',
|
|
rpc_api.EVENT_RES_STATUS_DATA: 'ResourceStatusReason',
|
|
rpc_api.EVENT_RES_TYPE: 'ResourceType',
|
|
rpc_api.EVENT_STACK_ID: 'StackId',
|
|
rpc_api.EVENT_STACK_NAME: 'StackName',
|
|
rpc_api.EVENT_TIMESTAMP: 'Timestamp',
|
|
}
|
|
|
|
result = api_utils.reformat_dict_keys(keymap, e)
|
|
action = e[rpc_api.EVENT_RES_ACTION]
|
|
status = e[rpc_api.EVENT_RES_STATUS]
|
|
result['ResourceStatus'] = '_'.join((action, status))
|
|
result['ResourceProperties'] = jsonutils.dumps(result[
|
|
'ResourceProperties'])
|
|
|
|
return self._id_format(result)
|
|
|
|
con = req.context
|
|
stack_name = req.params.get('StackName')
|
|
try:
|
|
identity = stack_name and self._get_identity(con, stack_name)
|
|
events = self.rpc_client.list_events(con, identity)
|
|
except Exception as ex:
|
|
return exception.map_remote_error(ex)
|
|
|
|
result = [format_stack_event(e) for e in events]
|
|
|
|
return api_utils.format_response('DescribeStackEvents',
|
|
{'StackEvents': result})
|
|
|
|
@staticmethod
|
|
def _resource_status(res):
|
|
action = res[rpc_api.RES_ACTION]
|
|
status = res[rpc_api.RES_STATUS]
|
|
return '_'.join((action, status))
|
|
|
|
def describe_stack_resource(self, req):
|
|
"""Implements the DescribeStackResource API action.
|
|
|
|
Return the details of the given resource belonging to the given stack.
|
|
"""
|
|
self._enforce(req, 'DescribeStackResource')
|
|
|
|
def format_resource_detail(r):
|
|
# Reformat engine output into the AWS "StackResourceDetail" format
|
|
keymap = {
|
|
rpc_api.RES_DESCRIPTION: 'Description',
|
|
rpc_api.RES_UPDATED_TIME: 'LastUpdatedTimestamp',
|
|
rpc_api.RES_NAME: 'LogicalResourceId',
|
|
rpc_api.RES_METADATA: 'Metadata',
|
|
rpc_api.RES_PHYSICAL_ID: 'PhysicalResourceId',
|
|
rpc_api.RES_STATUS_DATA: 'ResourceStatusReason',
|
|
rpc_api.RES_TYPE: 'ResourceType',
|
|
rpc_api.RES_STACK_ID: 'StackId',
|
|
rpc_api.RES_STACK_NAME: 'StackName',
|
|
}
|
|
|
|
result = api_utils.reformat_dict_keys(keymap, r)
|
|
|
|
result['ResourceStatus'] = self._resource_status(r)
|
|
|
|
return self._id_format(result)
|
|
|
|
con = req.context
|
|
|
|
try:
|
|
identity = self._get_identity(con, req.params['StackName'])
|
|
resource_details = self.rpc_client.describe_stack_resource(
|
|
con,
|
|
stack_identity=identity,
|
|
resource_name=req.params.get('LogicalResourceId'))
|
|
|
|
except Exception as ex:
|
|
return exception.map_remote_error(ex)
|
|
|
|
result = format_resource_detail(resource_details)
|
|
|
|
return api_utils.format_response('DescribeStackResource',
|
|
{'StackResourceDetail': result})
|
|
|
|
def describe_stack_resources(self, req):
|
|
"""Implements the DescribeStackResources API action.
|
|
|
|
Return details of resources specified by the parameters.
|
|
|
|
`StackName`: returns all resources belonging to the stack.
|
|
|
|
`PhysicalResourceId`: returns all resources belonging to the stack this
|
|
resource is associated with.
|
|
|
|
Only one of the parameters may be specified.
|
|
|
|
Optional parameter:
|
|
|
|
`LogicalResourceId`: filter the resources list by the logical resource
|
|
id.
|
|
"""
|
|
self._enforce(req, 'DescribeStackResources')
|
|
|
|
def format_stack_resource(r):
|
|
"""Reformat engine output into AWS "StackResource" format."""
|
|
keymap = {
|
|
rpc_api.RES_DESCRIPTION: 'Description',
|
|
rpc_api.RES_NAME: 'LogicalResourceId',
|
|
rpc_api.RES_PHYSICAL_ID: 'PhysicalResourceId',
|
|
rpc_api.RES_STATUS_DATA: 'ResourceStatusReason',
|
|
rpc_api.RES_TYPE: 'ResourceType',
|
|
rpc_api.RES_STACK_ID: 'StackId',
|
|
rpc_api.RES_STACK_NAME: 'StackName',
|
|
rpc_api.RES_UPDATED_TIME: 'Timestamp',
|
|
}
|
|
|
|
result = api_utils.reformat_dict_keys(keymap, r)
|
|
|
|
result['ResourceStatus'] = self._resource_status(r)
|
|
|
|
return self._id_format(result)
|
|
|
|
con = req.context
|
|
stack_name = req.params.get('StackName')
|
|
physical_resource_id = req.params.get('PhysicalResourceId')
|
|
if stack_name and physical_resource_id:
|
|
msg = 'Use `StackName` or `PhysicalResourceId` but not both'
|
|
return exception.HeatInvalidParameterCombinationError(detail=msg)
|
|
|
|
try:
|
|
if stack_name is not None:
|
|
identity = self._get_identity(con, stack_name)
|
|
else:
|
|
identity = self.rpc_client.find_physical_resource(
|
|
con,
|
|
physical_resource_id=physical_resource_id)
|
|
resources = self.rpc_client.describe_stack_resources(
|
|
con,
|
|
stack_identity=identity,
|
|
resource_name=req.params.get('LogicalResourceId'))
|
|
|
|
except Exception as ex:
|
|
return exception.map_remote_error(ex)
|
|
|
|
result = [format_stack_resource(r) for r in resources]
|
|
|
|
return api_utils.format_response('DescribeStackResources',
|
|
{'StackResources': result})
|
|
|
|
def list_stack_resources(self, req):
|
|
"""Implements the ListStackResources API action.
|
|
|
|
Return summary of the resources belonging to the specified stack.
|
|
"""
|
|
self._enforce(req, 'ListStackResources')
|
|
|
|
def format_resource_summary(r):
|
|
"""Reformat engine output to AWS "StackResourceSummary" format."""
|
|
keymap = {
|
|
rpc_api.RES_UPDATED_TIME: 'LastUpdatedTimestamp',
|
|
rpc_api.RES_NAME: 'LogicalResourceId',
|
|
rpc_api.RES_PHYSICAL_ID: 'PhysicalResourceId',
|
|
rpc_api.RES_STATUS_DATA: 'ResourceStatusReason',
|
|
rpc_api.RES_TYPE: 'ResourceType',
|
|
}
|
|
|
|
result = api_utils.reformat_dict_keys(keymap, r)
|
|
|
|
result['ResourceStatus'] = self._resource_status(r)
|
|
|
|
return result
|
|
|
|
con = req.context
|
|
|
|
try:
|
|
identity = self._get_identity(con, req.params['StackName'])
|
|
resources = self.rpc_client.list_stack_resources(
|
|
con,
|
|
stack_identity=identity)
|
|
except Exception as ex:
|
|
return exception.map_remote_error(ex)
|
|
|
|
summaries = [format_resource_summary(r) for r in resources]
|
|
|
|
return api_utils.format_response('ListStackResources',
|
|
{'StackResourceSummaries': summaries})
|
|
|
|
|
|
def create_resource(options):
|
|
"""Stacks resource factory method."""
|
|
deserializer = wsgi.JSONRequestDeserializer()
|
|
return wsgi.Resource(StackController(options), deserializer)
|