#
#    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__)
[docs]
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')
[docs]
    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)
[docs]
    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) 
[docs]
    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",
    )
[docs]
    def create(self, req):
        self._enforce(req, 'CreateStack')
        return self.create_or_update(req, self.CREATE_STACK) 
[docs]
    def update(self, req):
        self._enforce(req, 'UpdateStack')
        return self.create_or_update(req, self.UPDATE_STACK) 
[docs]
    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) 
[docs]
    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, {}) 
[docs]
    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}) 
[docs]
    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'
                                          }
                                         ) 
[docs]
    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) 
[docs]
    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']) 
[docs]
    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))
[docs]
    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}) 
[docs]
    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}) 
[docs]
    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}) 
 
[docs]
def create_resource(options):
    """Stacks resource factory method."""
    deserializer = wsgi.JSONRequestDeserializer()
    return wsgi.Resource(StackController(options), deserializer)