def validate_template(self, req): """ Implements the ValidateTemplate API action Validates the specified template """ 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 = json.loads(templ) except ValueError: msg = _("The Template must be a JSON document.") return exception.HeatInvalidParameterValueError(detail=msg) logger.info('validate_template') try: res = self.engine_rpcapi.validate_template(con, template) return api_utils.format_response('ValidateTemplate', res) except rpc_common.RemoteError as ex: return exception.map_remote_error(ex)
def describe_alarms(self, req): """Implements DescribeAlarms API action.""" self._enforce(req, 'DescribeAlarms') def format_metric_alarm(a): """Reformat engine output into the AWS "MetricAlarm" format.""" keymap = { rpc_api.WATCH_ACTIONS_ENABLED: 'ActionsEnabled', rpc_api.WATCH_ALARM_ACTIONS: 'AlarmActions', rpc_api.WATCH_TOPIC: 'AlarmArn', rpc_api.WATCH_UPDATED_TIME: 'AlarmConfigurationUpdatedTimestamp', rpc_api.WATCH_DESCRIPTION: 'AlarmDescription', rpc_api.WATCH_NAME: 'AlarmName', rpc_api.WATCH_COMPARISON: 'ComparisonOperator', rpc_api.WATCH_DIMENSIONS: 'Dimensions', rpc_api.WATCH_PERIODS: 'EvaluationPeriods', rpc_api.WATCH_INSUFFICIENT_ACTIONS: 'InsufficientDataActions', rpc_api.WATCH_METRIC_NAME: 'MetricName', rpc_api.WATCH_NAMESPACE: 'Namespace', rpc_api.WATCH_OK_ACTIONS: 'OKActions', rpc_api.WATCH_PERIOD: 'Period', rpc_api.WATCH_STATE_REASON: 'StateReason', rpc_api.WATCH_STATE_REASON_DATA: 'StateReasonData', rpc_api.WATCH_STATE_UPDATED_TIME: 'StateUpdatedTimestamp', rpc_api.WATCH_STATE_VALUE: 'StateValue', rpc_api.WATCH_STATISTIC: 'Statistic', rpc_api.WATCH_THRESHOLD: 'Threshold', rpc_api.WATCH_UNIT: 'Unit', } # AWS doesn't return StackId in the main MetricAlarm # structure, so we add StackId as a dimension to all responses a[rpc_api.WATCH_DIMENSIONS].append({'StackId': a[rpc_api.WATCH_STACK_ID]}) # Reformat dimensions list into AWS API format a[rpc_api.WATCH_DIMENSIONS] = self._reformat_dimensions( a[rpc_api.WATCH_DIMENSIONS]) return api_utils.reformat_dict_keys(keymap, a) con = req.context parms = dict(req.params) try: name = parms['AlarmName'] except KeyError: name = None try: watch_list = self.rpc_client.show_watch(con, watch_name=name) except messaging.RemoteError as ex: return exception.map_remote_error(ex) res = {'MetricAlarms': [format_metric_alarm(a) for a in watch_list]} result = api_utils.format_response("DescribeAlarms", res) return result
def set_alarm_state(self, req): """ Implements SetAlarmState API action """ self._enforce(req, "SetAlarmState") # Map from AWS state names to those used in the engine state_map = { "OK": engine_api.WATCH_STATE_OK, "ALARM": engine_api.WATCH_STATE_ALARM, "INSUFFICIENT_DATA": engine_api.WATCH_STATE_NODATA, } con = req.context parms = dict(req.params) # Get mandatory parameters name = api_utils.get_param_value(parms, "AlarmName") state = api_utils.get_param_value(parms, "StateValue") if state not in state_map: msg = _("Invalid state %(state)s, " "expecting one of %(expect)s") % { "state": state, "expect": state_map.keys(), } logger.error(msg) return exception.HeatInvalidParameterValueError(msg) logger.debug(_("setting %(name)s to %(state)s") % {"name": name, "state": state_map[state]}) try: self.rpc_client.set_watch_state(con, watch_name=name, state=state_map[state]) except rpc_common.RemoteError as ex: return exception.map_remote_error(ex) return api_utils.format_response("SetAlarmState", "")
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 set_alarm_state(self, req): """ Implements SetAlarmState API action """ self._enforce(req, 'SetAlarmState') # Map from AWS state names to those used in the engine state_map = {'OK': engine_api.WATCH_STATE_OK, 'ALARM': engine_api.WATCH_STATE_ALARM, 'INSUFFICIENT_DATA': engine_api.WATCH_STATE_NODATA} con = req.context parms = dict(req.params) # Get mandatory parameters name = api_utils.get_param_value(parms, 'AlarmName') state = api_utils.get_param_value(parms, 'StateValue') if state not in state_map: msg = _('Invalid state %(state)s, ' 'expecting one of %(expect)s') % { 'state': state, 'expect': state_map.keys()} logger.error(msg) return exception.HeatInvalidParameterValueError(msg) logger.debug("setting %(name)s to %(state)s" % { 'name': name, 'state': state_map[state]}) try: self.rpc_client.set_watch_state(con, watch_name=name, state=state_map[state]) except rpc_common.RemoteError as ex: return exception.map_remote_error(ex) return api_utils.format_response("SetAlarmState", "")
def list_stack_resources(self, req): """ Implements the ListStackResources API action Return summary of the resources belonging to the specified stack. """ def format_resource_summary(r): """ Reformat engine output into the AWS "StackResourceSummary" format """ keymap = { engine_api.RES_UPDATED_TIME: 'LastUpdatedTimestamp', engine_api.RES_NAME: 'LogicalResourceId', engine_api.RES_PHYSICAL_ID: 'PhysicalResourceId', engine_api.RES_STATUS: 'ResourceStatus', engine_api.RES_STATUS_DATA: 'ResourceStatusReason', engine_api.RES_TYPE: 'ResourceType', } return api_utils.reformat_dict_keys(keymap, r) con = req.context try: identity = self._get_identity(con, req.params['StackName']) resources = self.engine_rpcapi.list_stack_resources(con, stack_identity=identity) except rpc_common.RemoteError 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 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 set_alarm_state(self, req): """ Implements SetAlarmState API action """ self._enforce(req, 'SetAlarmState') # Map from AWS state names to those used in the engine state_map = {'OK': engine_api.WATCH_STATE_OK, 'ALARM': engine_api.WATCH_STATE_ALARM, 'INSUFFICIENT_DATA': engine_api.WATCH_STATE_NODATA} con = req.context parms = dict(req.params) # Get mandatory parameters name = api_utils.get_param_value(parms, 'AlarmName') state = api_utils.get_param_value(parms, 'StateValue') if state not in state_map: msg = _('Invalid state %(state)s, ' 'expecting one of %(expect)s') % { 'state': state, 'expect': state_map.keys()} LOG.error(msg) return exception.HeatInvalidParameterValueError(msg) LOG.debug("setting %(name)s to %(state)s" % { 'name': name, 'state': state_map[state]}) try: self.rpc_client.set_watch_state(con, watch_name=name, state=state_map[state]) except messaging.RemoteError as ex: return exception.map_remote_error(ex) return api_utils.format_response("SetAlarmState", "")
def list_metrics(self, req): """ Implements ListMetrics API action Lists metric datapoints associated with a particular alarm, or all alarms if none specified """ self._enforce(req, "ListMetrics") def format_metric_data(d, fil={}): """ Reformat engine output into the AWS "Metric" format Takes an optional filter dict, which is traversed so a metric dict is only returned if all keys match the filter dict """ dimensions = [{"AlarmName": d[engine_api.WATCH_DATA_ALARM]}, {"Timestamp": d[engine_api.WATCH_DATA_TIME]}] for key in d[engine_api.WATCH_DATA]: dimensions.append({key: d[engine_api.WATCH_DATA][key]}) newdims = self._reformat_dimensions(dimensions) result = { "MetricName": d[engine_api.WATCH_DATA_METRIC], "Dimensions": newdims, "Namespace": d[engine_api.WATCH_DATA_NAMESPACE], } for f in fil: try: value = result[f] if value != fil[f]: # Filter criteria not met, return None return except KeyError: logger.warning(_("Invalid filter key %s, ignoring") % f) return result con = req.context parms = dict(req.params) # FIXME : Don't yet handle filtering by Dimensions filter_result = dict((k, v) for (k, v) in parms.iteritems() if k in ("MetricName", "Namespace")) logger.debug(_("filter parameters : %s") % filter_result) try: # Engine does not currently support query by namespace/metric # so we pass None/None and do any filtering locally null_kwargs = {"metric_namespace": None, "metric_name": None} watch_data = self.rpc_client.show_watch_metric(con, **null_kwargs) except rpc_common.RemoteError as ex: return exception.map_remote_error(ex) res = {"Metrics": []} for d in watch_data: metric = format_metric_data(d, filter_result) if metric: res["Metrics"].append(metric) result = api_utils.format_response("ListMetrics", res) return result
def list_stack_resources(self, req): """ Implements the ListStackResources API action Return summary of the resources belonging to the specified stack. """ def format_resource_summary(r): """ Reformat engine output into the AWS "StackResourceSummary" format """ keymap = { engine_api.RES_UPDATED_TIME: 'LastUpdatedTimestamp', engine_api.RES_NAME: 'LogicalResourceId', engine_api.RES_PHYSICAL_ID: 'PhysicalResourceId', engine_api.RES_STATUS: 'ResourceStatus', engine_api.RES_STATUS_DATA: 'ResourceStatusReason', engine_api.RES_TYPE: 'ResourceType', } return api_utils.reformat_dict_keys(keymap, r) con = req.context try: identity = self._get_identity(con, req.params['StackName']) resources = self.engine_rpcapi.list_stack_resources( con, stack_identity=identity) except rpc_common.RemoteError 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 validate_template(self, req): """ Implements the ValidateTemplate API action Validates the specified template """ con = req.context parms = dict(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: template = json.loads(templ) except ValueError: msg = _("The Template must be a JSON document.") return exception.HeatInvalidParameterValueError(detail=msg) logger.info('validate_template') try: return self.engine_rpcapi.validate_template(con, template) except rpc_common.RemoteError as ex: return exception.map_remote_error(ex)
def put_metric_data(self, req): """ Implements PutMetricData API action """ con = req.context parms = dict(req.params) namespace = api_utils.get_param_value(parms, 'Namespace') # Extract data from the request so we can pass it to the engine # We have to do this in two passes, because the AWS # query format nests the dimensions within the MetricData # query-parameter-list (see AWS PutMetricData docs) # extract_param_list gives a list-of-dict, which we then # need to process (each dict) for dimensions metric_data = api_utils.extract_param_list(parms, prefix='MetricData') if not len(metric_data): logger.error("Request does not contain required MetricData") return exception.HeatMissingParameterError("MetricData list") watch_name = None dimensions = [] for p in metric_data: dimension = api_utils.extract_param_pairs(p, prefix='Dimensions', keyname='Name', valuename='Value') if 'AlarmName' in dimension: watch_name = dimension['AlarmName'] else: dimensions.append(dimension) # We expect an AlarmName dimension as currently the engine # implementation requires metric data to be associated # with an alarm. When this is fixed, we can simply # parse the user-defined dimensions and add the list to # the metric data if not watch_name: logger.error("Request does not contain AlarmName dimension!") return exception.HeatMissingParameterError("AlarmName dimension") # Extract the required data from the metric_data # and format dict to pass to engine data = { 'Namespace': namespace, api_utils.get_param_value(metric_data[0], 'MetricName'): { 'Unit': api_utils.get_param_value(metric_data[0], 'Unit'), 'Value': api_utils.get_param_value(metric_data[0], 'Value'), 'Dimensions': dimensions } } try: self.engine_rpcapi.create_watch_data(con, watch_name, data) except rpc_common.RemoteError as ex: return exception.map_remote_error(ex) result = {'ResponseMetadata': None} return api_utils.format_response("PutMetricData", result)
def describe_alarms(self, req): """ Implements DescribeAlarms API action """ self._enforce(req, "DescribeAlarms") def format_metric_alarm(a): """ Reformat engine output into the AWS "MetricAlarm" format """ keymap = { engine_api.WATCH_ACTIONS_ENABLED: "ActionsEnabled", engine_api.WATCH_ALARM_ACTIONS: "AlarmActions", engine_api.WATCH_TOPIC: "AlarmArn", engine_api.WATCH_UPDATED_TIME: "AlarmConfigurationUpdatedTimestamp", engine_api.WATCH_DESCRIPTION: "AlarmDescription", engine_api.WATCH_NAME: "AlarmName", engine_api.WATCH_COMPARISON: "ComparisonOperator", engine_api.WATCH_DIMENSIONS: "Dimensions", engine_api.WATCH_PERIODS: "EvaluationPeriods", engine_api.WATCH_INSUFFICIENT_ACTIONS: "InsufficientDataActions", engine_api.WATCH_METRIC_NAME: "MetricName", engine_api.WATCH_NAMESPACE: "Namespace", engine_api.WATCH_OK_ACTIONS: "OKActions", engine_api.WATCH_PERIOD: "Period", engine_api.WATCH_STATE_REASON: "StateReason", engine_api.WATCH_STATE_REASON_DATA: "StateReasonData", engine_api.WATCH_STATE_UPDATED_TIME: "StateUpdatedTimestamp", engine_api.WATCH_STATE_VALUE: "StateValue", engine_api.WATCH_STATISTIC: "Statistic", engine_api.WATCH_THRESHOLD: "Threshold", engine_api.WATCH_UNIT: "Unit", } # AWS doesn't return StackId in the main MetricAlarm # structure, so we add StackId as a dimension to all responses a[engine_api.WATCH_DIMENSIONS].append({"StackId": a[engine_api.WATCH_STACK_ID]}) # Reformat dimensions list into AWS API format a[engine_api.WATCH_DIMENSIONS] = self._reformat_dimensions(a[engine_api.WATCH_DIMENSIONS]) return api_utils.reformat_dict_keys(keymap, a) con = req.context parms = dict(req.params) try: name = parms["AlarmName"] except KeyError: name = None try: watch_list = self.rpc_client.show_watch(con, watch_name=name) except rpc_common.RemoteError as ex: return exception.map_remote_error(ex) res = {"MetricAlarms": [format_metric_alarm(a) for a in watch_list]} result = api_utils.format_response("DescribeAlarms", res) return 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. """ def format_stack_resource(r): """ Reformat engine output into the AWS "StackResource" format """ keymap = { engine_api.RES_DESCRIPTION: 'Description', engine_api.RES_NAME: 'LogicalResourceId', engine_api.RES_PHYSICAL_ID: 'PhysicalResourceId', engine_api.RES_STATUS: 'ResourceStatus', engine_api.RES_STATUS_DATA: 'ResourceStatusReason', engine_api.RES_TYPE: 'ResourceType', engine_api.RES_STACK_ID: 'StackId', engine_api.RES_STACK_NAME: 'StackName', engine_api.RES_UPDATED_TIME: 'Timestamp', } result = api_utils.reformat_dict_keys(keymap, r) return self._stackid_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: identity = self._get_identity(con, stack_name) resources = self.engine_rpcapi.describe_stack_resources(con, stack_identity=identity, physical_resource_id=physical_resource_id, logical_resource_id=req.params.get('LogicalResourceId')) except rpc_common.RemoteError 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 describe_alarms(self, req): """Implements DescribeAlarms API action.""" self._enforce(req, 'DescribeAlarms') def format_metric_alarm(a): """Reformat engine output into the AWS "MetricAlarm" format.""" keymap = { rpc_api.WATCH_ACTIONS_ENABLED: 'ActionsEnabled', rpc_api.WATCH_ALARM_ACTIONS: 'AlarmActions', rpc_api.WATCH_TOPIC: 'AlarmArn', rpc_api.WATCH_UPDATED_TIME: 'AlarmConfigurationUpdatedTimestamp', rpc_api.WATCH_DESCRIPTION: 'AlarmDescription', rpc_api.WATCH_NAME: 'AlarmName', rpc_api.WATCH_COMPARISON: 'ComparisonOperator', rpc_api.WATCH_DIMENSIONS: 'Dimensions', rpc_api.WATCH_PERIODS: 'EvaluationPeriods', rpc_api.WATCH_INSUFFICIENT_ACTIONS: 'InsufficientDataActions', rpc_api.WATCH_METRIC_NAME: 'MetricName', rpc_api.WATCH_NAMESPACE: 'Namespace', rpc_api.WATCH_OK_ACTIONS: 'OKActions', rpc_api.WATCH_PERIOD: 'Period', rpc_api.WATCH_STATE_REASON: 'StateReason', rpc_api.WATCH_STATE_REASON_DATA: 'StateReasonData', rpc_api.WATCH_STATE_UPDATED_TIME: 'StateUpdatedTimestamp', rpc_api.WATCH_STATE_VALUE: 'StateValue', rpc_api.WATCH_STATISTIC: 'Statistic', rpc_api.WATCH_THRESHOLD: 'Threshold', rpc_api.WATCH_UNIT: 'Unit', } # AWS doesn't return StackId in the main MetricAlarm # structure, so we add StackId as a dimension to all responses a[rpc_api.WATCH_DIMENSIONS].append( {'StackId': a[rpc_api.WATCH_STACK_ID]}) # Reformat dimensions list into AWS API format a[rpc_api.WATCH_DIMENSIONS] = self._reformat_dimensions( a[rpc_api.WATCH_DIMENSIONS]) return api_utils.reformat_dict_keys(keymap, a) con = req.context parms = dict(req.params) try: name = parms['AlarmName'] except KeyError: name = None try: watch_list = self.rpc_client.show_watch(con, watch_name=name) except messaging.RemoteError as ex: return exception.map_remote_error(ex) res = {'MetricAlarms': [format_metric_alarm(a) for a in watch_list]} result = api_utils.format_response("DescribeAlarms", res) return result
def put_metric_data(self, req): """ Implements PutMetricData API action """ self._enforce(req, 'PutMetricData') con = req.context parms = dict(req.params) namespace = api_utils.get_param_value(parms, 'Namespace') # Extract data from the request so we can pass it to the engine # We have to do this in two passes, because the AWS # query format nests the dimensions within the MetricData # query-parameter-list (see AWS PutMetricData docs) # extract_param_list gives a list-of-dict, which we then # need to process (each dict) for dimensions metric_data = api_utils.extract_param_list(parms, prefix='MetricData') if not len(metric_data): logger.error("Request does not contain required MetricData") return exception.HeatMissingParameterError("MetricData list") watch_name = None dimensions = [] for p in metric_data: dimension = api_utils.extract_param_pairs(p, prefix='Dimensions', keyname='Name', valuename='Value') if 'AlarmName' in dimension: watch_name = dimension['AlarmName'] else: dimensions.append(dimension) # We expect an AlarmName dimension as currently the engine # implementation requires metric data to be associated # with an alarm. When this is fixed, we can simply # parse the user-defined dimensions and add the list to # the metric data if not watch_name: logger.error("Request does not contain AlarmName dimension!") return exception.HeatMissingParameterError("AlarmName dimension") # Extract the required data from the metric_data # and format dict to pass to engine data = {'Namespace': namespace, api_utils.get_param_value(metric_data[0], 'MetricName'): { 'Unit': api_utils.get_param_value(metric_data[0], 'Unit'), 'Value': api_utils.get_param_value(metric_data[0], 'Value'), 'Dimensions': dimensions}} try: self.engine_rpcapi.create_watch_data(con, watch_name, data) except rpc_common.RemoteError as ex: return exception.map_remote_error(ex) result = {'ResponseMetadata': None} return api_utils.format_response("PutMetricData", result)
def signal(self, req, arn, body=None): con = req.context identity = identifier.ResourceIdentifier.from_arn(arn) try: self.rpc_client.resource_signal( con, stack_identity=dict(identity.stack()), resource_name=identity.resource_name, details=body ) except Exception as ex: return exception.map_remote_error(ex)
def signal(self, req, arn, body=None): con = req.context identity = identifier.ResourceIdentifier.from_arn(arn) try: self.engine.resource_signal(con, stack_identity=dict(identity.stack()), resource_name=identity.resource_name, details=body) except Exception as ex: return exception.map_remote_error(ex)
def update_waitcondition(self, req, body, arn): con = req.context identity = identifier.ResourceIdentifier.from_arn(arn) try: md = self.engine.metadata_update( con, stack_identity=dict(identity.stack()), resource_name=identity.resource_name, metadata=body ) except Exception as ex: return exception.map_remote_error(ex) return {"resource": identity.resource_name, "metadata": md}
def signal(self, req, body, arn): con = req.context identity = identifier.ResourceIdentifier.from_arn(arn) try: md = self.engine.resource_signal( con, stack_identity=dict(identity.stack()), resource_name=identity.resource_name, details=body) except rpc_common.RemoteError as ex: return exception.map_remote_error(ex)
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 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) except Exception as ex: return exception.map_remote_error(ex) return api_utils.format_response(action, {})
def update_waitcondition(self, req, body, arn): con = req.context identity = identifier.ResourceIdentifier.from_arn(arn) try: md = self.rpc_client.metadata_update( con, stack_identity=dict(identity.stack()), resource_name=identity.resource_name, metadata=body) except Exception as ex: return exception.map_remote_error(ex) return {'resource': identity.resource_name, 'metadata': md}
def update_waitcondition(self, req, body, arn): con = req.context identity = identifier.ResourceIdentifier.from_arn(arn) try: md = self.rpc_client.resource_signal( con, stack_identity=dict(identity.stack()), resource_name=identity.resource_name, details=body) except Exception as ex: return exception.map_remote_error(ex) return {'resource': identity.resource_name, 'metadata': md}
def put_metric_data(self, req): """ Implements PutMetricData API action """ self._enforce(req, 'PutMetricData') con = req.context parms = dict(req.params) namespace = api_utils.get_param_value(parms, 'Namespace') # Extract data from the request so we can pass it to the engine # We have to do this in two passes, because the AWS # query format nests the dimensions within the MetricData # query-parameter-list (see AWS PutMetricData docs) # extract_param_list gives a list-of-dict, which we then # need to process (each dict) for dimensions metric_data = api_utils.extract_param_list(parms, prefix='MetricData') if not len(metric_data): LOG.error(_LE("Request does not contain required MetricData")) return exception.HeatMissingParameterError("MetricData list") watch_name = None dimensions = [] for p in metric_data: dimension = api_utils.extract_param_pairs(p, prefix='Dimensions', keyname='Name', valuename='Value') if 'AlarmName' in dimension: watch_name = dimension['AlarmName'] else: dimensions.append(dimension) # Extract the required data from the metric_data # and format dict to pass to engine data = { 'Namespace': namespace, api_utils.get_param_value(metric_data[0], 'MetricName'): { 'Unit': api_utils.get_param_value(metric_data[0], 'Unit'), 'Value': api_utils.get_param_value(metric_data[0], 'Value'), 'Dimensions': dimensions } } try: self.rpc_client.create_watch_data(con, watch_name, data) except messaging.RemoteError as ex: return exception.map_remote_error(ex) result = {'ResponseMetadata': None} return api_utils.format_response("PutMetricData", result)
def set_alarm_state(self, req): """ Implements SetAlarmState API action """ self._enforce(req, 'SetAlarmState') # Map from AWS state names to those used in the engine state_map = { 'OK': engine_api.WATCH_STATE_OK, 'ALARM': engine_api.WATCH_STATE_ALARM, 'INSUFFICIENT_DATA': engine_api.WATCH_STATE_NODATA } con = req.context parms = dict(req.params) # Get mandatory parameters name = api_utils.get_param_value(parms, 'AlarmName') state = api_utils.get_param_value(parms, 'StateValue') if state not in state_map: msg = _('Invalid state %(state)s, ' 'expecting one of %(expect)s') % { 'state': state, 'expect': state_map.keys() } logger.error(msg) return exception.HeatInvalidParameterValueError(msg) # Check for optional parameters # FIXME : We don't actually do anything with these in the engine yet.. state_reason = None state_reason_data = None if 'StateReason' in parms: state_reason = parms['StateReason'] if 'StateReasonData' in parms: state_reason_data = parms['StateReasonData'] logger.debug( _("setting %(name)s to %(state)s") % { 'name': name, 'state': state_map[state] }) try: self.engine_rpcapi.set_watch_state(con, watch_name=name, state=state_map[state]) except rpc_common.RemoteError as ex: return exception.map_remote_error(ex) return api_utils.format_response("SetAlarmState", "")
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) logger.info('validate_template') def format_validate_parameter(key, value): """ Reformat engine output into the AWS "ValidateTemplate" format """ return { 'ParameterKey': key, 'DefaultValue': value.get(engine_api.PARAM_DEFAULT, ''), 'Description': value.get(engine_api.PARAM_DESCRIPTION, ''), 'NoEcho': value.get(engine_api.PARAM_NO_ECHO, 'false') } try: res = self.engine_rpcapi.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 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 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) logger.info('validate_template') def format_validate_parameter(key, value): """ Reformat engine output into the AWS "ValidateTemplate" format """ return { 'ParameterKey': key, 'DefaultValue': value.get(engine_api.PARAM_DEFAULT, ''), 'Description': value.get(engine_api.PARAM_DESCRIPTION, ''), 'NoEcho': value.get(engine_api.PARAM_NO_ECHO, 'false') } try: res = self.engine_rpcapi.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 rpc_common.RemoteError as ex: return exception.map_remote_error(ex)
def put_metric_data(self, req): """ Implements PutMetricData API action """ self._enforce(req, "PutMetricData") con = req.context parms = dict(req.params) namespace = api_utils.get_param_value(parms, "Namespace") # Extract data from the request so we can pass it to the engine # We have to do this in two passes, because the AWS # query format nests the dimensions within the MetricData # query-parameter-list (see AWS PutMetricData docs) # extract_param_list gives a list-of-dict, which we then # need to process (each dict) for dimensions metric_data = api_utils.extract_param_list(parms, prefix="MetricData") if not len(metric_data): logger.error(_("Request does not contain required MetricData")) return exception.HeatMissingParameterError("MetricData list") watch_name = None dimensions = [] for p in metric_data: dimension = api_utils.extract_param_pairs(p, prefix="Dimensions", keyname="Name", valuename="Value") if "AlarmName" in dimension: watch_name = dimension["AlarmName"] else: dimensions.append(dimension) # Extract the required data from the metric_data # and format dict to pass to engine data = { "Namespace": namespace, api_utils.get_param_value(metric_data[0], "MetricName"): { "Unit": api_utils.get_param_value(metric_data[0], "Unit"), "Value": api_utils.get_param_value(metric_data[0], "Value"), "Dimensions": dimensions, }, } try: self.rpc_client.create_watch_data(con, watch_name, data) except rpc_common.RemoteError as ex: return exception.map_remote_error(ex) result = {"ResponseMetadata": None} return api_utils.format_response("PutMetricData", result)
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 the AWS "StackEvent" format """ keymap = { engine_api.EVENT_ID: 'EventId', engine_api.EVENT_RES_NAME: 'LogicalResourceId', engine_api.EVENT_RES_PHYSICAL_ID: 'PhysicalResourceId', engine_api.EVENT_RES_PROPERTIES: 'ResourceProperties', engine_api.EVENT_RES_STATUS: 'ResourceStatus', engine_api.EVENT_RES_STATUS_DATA: 'ResourceStatusReason', engine_api.EVENT_RES_TYPE: 'ResourceType', engine_api.EVENT_STACK_ID: 'StackId', engine_api.EVENT_STACK_NAME: 'StackName', engine_api.EVENT_TIMESTAMP: 'Timestamp', } result = api_utils.reformat_dict_keys(keymap, e) action = e[engine_api.EVENT_RES_ACTION] status = e[engine_api.EVENT_RES_STATUS] if action and status: result['ResourceStatus'] = '_'.join((action, status)) else: result['ResourceStatus'] = status result['ResourceProperties'] = json.dumps(result[ 'ResourceProperties']) return self._id_format(result) con = req.context stack_name = req.params.get('StackName', None) try: identity = stack_name and self._get_identity(con, stack_name) events = self.engine_rpcapi.list_events(con, identity) except rpc_common.RemoteError 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})
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 list(self, req): """ Implements ListStacks API action Lists summary information for all stacks """ def format_stack_summary(s): """ Reformat engine output into the AWS "StackSummary" format """ # Map the engine-api format to the AWS StackSummary datatype keymap = { engine_api.STACK_CREATION_TIME: 'CreationTime', engine_api.STACK_UPDATED_TIME: 'LastUpdatedTime', engine_api.STACK_ID: 'StackId', engine_api.STACK_NAME: 'StackName', engine_api.STACK_STATUS: 'StackStatus', engine_api.STACK_STATUS_DATA: 'StackStatusReason', engine_api.STACK_TMPL_DESCRIPTION: 'TemplateDescription', } result = api_utils.reformat_dict_keys(keymap, s) # AWS docs indicate DeletionTime is ommitted for current stacks # This is still TODO in the engine, we don't keep data for # stacks after they are deleted if engine_api.STACK_DELETION_TIME in s: result['DeletionTime'] = s[engine_api.STACK_DELETION_TIME] return self._stackid_addprefix(result) con = req.context parms = dict(req.params) try: # Note show_stack returns details for all stacks when called with # no stack_name, we only use a subset of the result here though stack_list = self.engine_rpcapi.show_stack(con, stack_name=None, params=parms) except rpc_common.RemoteError as ex: return exception.map_remote_error(ex) res = {'StackSummaries': [format_stack_summary(s) for s in stack_list['stacks']]} return api_utils.format_response('ListStacks', res)
def list(self, req): """ Implements ListStacks API action Lists summary information for all stacks """ def format_stack_summary(s): """ Reformat engine output into the AWS "StackSummary" format """ # Map the engine-api format to the AWS StackSummary datatype keymap = { engine_api.STACK_CREATION_TIME: 'CreationTime', engine_api.STACK_UPDATED_TIME: 'LastUpdatedTime', engine_api.STACK_ID: 'StackId', engine_api.STACK_NAME: 'StackName', engine_api.STACK_STATUS: 'StackStatus', engine_api.STACK_STATUS_DATA: 'StackStatusReason', engine_api.STACK_TMPL_DESCRIPTION: 'TemplateDescription', } result = api_utils.reformat_dict_keys(keymap, s) # AWS docs indicate DeletionTime is ommitted for current stacks # This is still TODO in the engine, we don't keep data for # stacks after they are deleted if engine_api.STACK_DELETION_TIME in s: result['DeletionTime'] = s[engine_api.STACK_DELETION_TIME] return self._stackid_format(result) con = req.context parms = dict(req.params) try: # Note show_stack returns details for all stacks when called with # no stack_name, we only use a subset of the result here though stack_list = self.engine_rpcapi.show_stack(con, None) except rpc_common.RemoteError as ex: return exception.map_remote_error(ex) res = { 'StackSummaries': [format_stack_summary(s) for s in stack_list['stacks']] } return api_utils.format_response('ListStacks', res)
def delete(self, req): """ Implements the DeleteStack API action Deletes the specified stack """ con = req.context try: identity = self._get_identity(con, req.params['StackName']) res = self.engine_rpcapi.delete_stack(con, identity, cast=False) except rpc_common.RemoteError 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 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) logger.info("validate_template") def format_validate_parameter(key, value): """ Reformat engine output into the AWS "ValidateTemplate" format """ return { "ParameterKey": key, "DefaultValue": value.get(engine_api.PARAM_DEFAULT, ""), "Description": value.get(engine_api.PARAM_DESCRIPTION, ""), "NoEcho": value.get(engine_api.PARAM_NO_ECHO, "false"), } try: res = self.engine_rpcapi.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 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 = { engine_api.RES_DESCRIPTION: 'Description', engine_api.RES_UPDATED_TIME: 'LastUpdatedTimestamp', engine_api.RES_NAME: 'LogicalResourceId', engine_api.RES_METADATA: 'Metadata', engine_api.RES_PHYSICAL_ID: 'PhysicalResourceId', engine_api.RES_STATUS_DATA: 'ResourceStatusReason', engine_api.RES_TYPE: 'ResourceType', engine_api.RES_STACK_ID: 'StackId', engine_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.engine_rpcapi.describe_stack_resource( con, stack_identity=identity, resource_name=req.params.get('LogicalResourceId')) except rpc_common.RemoteError 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_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 = { engine_api.RES_DESCRIPTION: 'Description', engine_api.RES_UPDATED_TIME: 'LastUpdatedTimestamp', engine_api.RES_NAME: 'LogicalResourceId', engine_api.RES_METADATA: 'Metadata', engine_api.RES_PHYSICAL_ID: 'PhysicalResourceId', engine_api.RES_STATUS_DATA: 'ResourceStatusReason', engine_api.RES_TYPE: 'ResourceType', engine_api.RES_STACK_ID: 'StackId', engine_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.engine_rpcapi.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 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 = { engine_api.STACK_CREATION_TIME: 'CreationTime', engine_api.STACK_UPDATED_TIME: 'LastUpdatedTime', engine_api.STACK_ID: 'StackId', engine_api.STACK_NAME: 'StackName', engine_api.STACK_STATUS_DATA: 'StackStatusReason', engine_api.STACK_TMPL_DESCRIPTION: 'TemplateDescription', } result = api_utils.reformat_dict_keys(keymap, s) action = s[engine_api.STACK_ACTION] status = s[engine_api.STACK_STATUS] result['StackStatus'] = '_'.join((action, status)) # AWS docs indicate DeletionTime is ommitted for current stacks # This is still TODO(unknown) in the engine, we don't keep data for # stacks after they are deleted if engine_api.STACK_DELETION_TIME in s: result['DeletionTime'] = s[engine_api.STACK_DELETION_TIME] return self._id_format(result) con = req.context try: stack_list = self.engine_rpcapi.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 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 = { engine_api.STACK_CREATION_TIME: 'CreationTime', engine_api.STACK_UPDATED_TIME: 'LastUpdatedTime', engine_api.STACK_ID: 'StackId', engine_api.STACK_NAME: 'StackName', engine_api.STACK_STATUS_DATA: 'StackStatusReason', engine_api.STACK_TMPL_DESCRIPTION: 'TemplateDescription', } result = api_utils.reformat_dict_keys(keymap, s) action = s[engine_api.STACK_ACTION] status = s[engine_api.STACK_STATUS] result['StackStatus'] = '_'.join((action, status)) # AWS docs indicate DeletionTime is ommitted for current stacks # This is still TODO(unknown) in the engine, we don't keep data for # stacks after they are deleted if engine_api.STACK_DELETION_TIME in s: result['DeletionTime'] = s[engine_api.STACK_DELETION_TIME] return self._id_format(result) con = req.context try: stack_list = self.engine_rpcapi.list_stacks(con) except rpc_common.RemoteError 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 set_alarm_state(self, req): """ Implements SetAlarmState API action """ self._enforce(req, 'SetAlarmState') # Map from AWS state names to those used in the engine state_map = {'OK': engine_api.WATCH_STATE_OK, 'ALARM': engine_api.WATCH_STATE_ALARM, 'INSUFFICIENT_DATA': engine_api.WATCH_STATE_NODATA} con = req.context parms = dict(req.params) # Get mandatory parameters name = api_utils.get_param_value(parms, 'AlarmName') state = api_utils.get_param_value(parms, 'StateValue') if state not in state_map: msg = _('Invalid state %(state)s, ' 'expecting one of %(expect)s') % { 'state': state, 'expect': state_map.keys()} logger.error(msg) return exception.HeatInvalidParameterValueError(msg) # Check for optional parameters # FIXME : We don't actually do anything with these in the engine yet.. state_reason = None state_reason_data = None if 'StateReason' in parms: state_reason = parms['StateReason'] if 'StateReasonData' in parms: state_reason_data = parms['StateReasonData'] logger.debug(_("setting %(name)s to %(state)s") % { 'name': name, 'state': state_map[state]}) try: self.engine_rpcapi.set_watch_state(con, watch_name=name, state=state_map[state]) except rpc_common.RemoteError as ex: return exception.map_remote_error(ex) return api_utils.format_response("SetAlarmState", "")
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 the AWS "StackEvent" format """ keymap = { engine_api.EVENT_ID: 'EventId', engine_api.EVENT_RES_NAME: 'LogicalResourceId', engine_api.EVENT_RES_PHYSICAL_ID: 'PhysicalResourceId', engine_api.EVENT_RES_PROPERTIES: 'ResourceProperties', engine_api.EVENT_RES_STATUS_DATA: 'ResourceStatusReason', engine_api.EVENT_RES_TYPE: 'ResourceType', engine_api.EVENT_STACK_ID: 'StackId', engine_api.EVENT_STACK_NAME: 'StackName', engine_api.EVENT_TIMESTAMP: 'Timestamp', } result = api_utils.reformat_dict_keys(keymap, e) action = e[engine_api.EVENT_RES_ACTION] status = e[engine_api.EVENT_RES_STATUS] result['ResourceStatus'] = '_'.join((action, status)) result['ResourceProperties'] = json.dumps(result[ 'ResourceProperties']) return self._id_format(result) con = req.context stack_name = req.params.get('StackName', None) try: identity = stack_name and self._get_identity(con, stack_name) events = self.engine_rpcapi.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})
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.engine_rpcapi.get_template(con, identity) except Exception as ex: return exception.map_remote_error(ex) if templ is None: msg = _("stack not not found") return exception.HeatInvalidParameterValueError(detail=msg) return api_utils.format_response("GetTemplate", {"TemplateBody": templ})
def get_template(self, req): """ Implements the GetTemplate API action Get the template body for an existing stack """ con = req.context try: identity = self._get_identity(con, req.params['StackName']) templ = self.engine_rpcapi.get_template(con, identity) except rpc_common.RemoteError as ex: return exception.map_remote_error(ex) if templ is None: msg = _('stack not not found') return exception.HeatInvalidParameterValueError(detail=msg) return api_utils.format_response('GetTemplate', {'TemplateBody': templ})
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 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) """ def format_stack_event(e): """ Reformat engine output into the AWS "StackEvent" format """ keymap = { engine_api.EVENT_ID: 'EventId', engine_api.EVENT_RES_NAME: 'LogicalResourceId', engine_api.EVENT_RES_PHYSICAL_ID: 'PhysicalResourceId', engine_api.EVENT_RES_PROPERTIES: 'ResourceProperties', engine_api.EVENT_RES_STATUS: 'ResourceStatus', engine_api.EVENT_RES_STATUS_DATA: 'ResourceStatusData', engine_api.EVENT_RES_TYPE: 'ResourceType', engine_api.EVENT_STACK_ID: 'StackId', engine_api.EVENT_STACK_NAME: 'StackName', engine_api.EVENT_TIMESTAMP: 'Timestamp', } result = api_utils.reformat_dict_keys(keymap, e) return self._stackid_addprefix(result) con = req.context parms = dict(req.params) stack_name = req.params.get('StackName', None) try: event_res = self.engine_rpcapi.list_events(con, stack_name=stack_name, params=parms) except rpc_common.RemoteError as ex: return exception.map_remote_error(ex) events = 'Error' not in event_res and event_res['events'] or [] result = [format_stack_event(e) for e in events] return api_utils.format_response('DescribeStackEvents', {'StackEvents': result})
def test_map_remote_error_request_limit_exceeded(self): ex = common_exception.RequestLimitExceeded(message="testing") expected = aws_exception.HeatRequestLimitExceeded self.assertIsInstance(aws_exception.map_remote_error(ex), expected)
def test_map_remote_error_inval_param_error(self): ex = AttributeError() expected = aws_exception.HeatInvalidParameterValueError self.assertIsInstance(aws_exception.map_remote_error(ex), expected)
def test_map_remote_error_denied_error(self): ex = common_exception.Forbidden() expected = aws_exception.HeatAccessDeniedError self.assertIsInstance(aws_exception.map_remote_error(ex), expected)
def test_map_remote_error_already_exists_error(self): ex = common_exception.StackExists(stack_name="teststack") expected = aws_exception.AlreadyExistsError self.assertIsInstance(aws_exception.map_remote_error(ex), expected)
def test_map_remote_error_invalid_action_error(self): ex = common_exception.ActionInProgress(stack_name="teststack", action="testing") expected = aws_exception.HeatActionInProgressError self.assertIsInstance(aws_exception.map_remote_error(ex), expected)
def describe(self, req): """ Implements DescribeStacks API action Gets detailed information for a stack (or all stacks) """ def format_stack_outputs(o): keymap = { engine_api.OUTPUT_DESCRIPTION: 'Description', engine_api.OUTPUT_KEY: 'OutputKey', engine_api.OUTPUT_VALUE: 'OutputValue', } return api_utils.reformat_dict_keys(keymap, o) def format_stack(s): """ Reformat engine output into the AWS "StackSummary" format """ keymap = { engine_api.STACK_CAPABILITIES: 'Capabilities', engine_api.STACK_CREATION_TIME: 'CreationTime', engine_api.STACK_DESCRIPTION: 'Description', engine_api.STACK_DISABLE_ROLLBACK: 'DisableRollback', engine_api.STACK_UPDATED_TIME: 'LastUpdatedTime', engine_api.STACK_NOTIFICATION_TOPICS: 'NotificationARNs', engine_api.STACK_PARAMETERS: 'Parameters', engine_api.STACK_ID: 'StackId', engine_api.STACK_NAME: 'StackName', engine_api.STACK_STATUS: 'StackStatus', engine_api.STACK_STATUS_DATA: 'StackStatusReason', engine_api.STACK_TIMEOUT: 'TimeoutInMinutes', } result = api_utils.reformat_dict_keys(keymap, s) # Reformat outputs, these are handled separately as they are # only present in the engine output for a completely created # stack result['Outputs'] = [] if engine_api.STACK_OUTPUTS in s: for o in s[engine_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._stackid_format(result) con = req.context parms = dict(req.params) # 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.engine_rpcapi.show_stack(con, identity) except rpc_common.RemoteError as ex: return exception.map_remote_error(ex) res = {'Stacks': [format_stack(s) for s in stack_list['stacks']]} return api_utils.format_response('DescribeStacks', res)
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 parameters/arguments and reformat them to match the engine API. FIXME: we currently only support a subset of the AWS defined parameters (both here and in the engine) """ # TODO : Capabilities, DisableRollback, NotificationARNs keymap = {'TimeoutInMinutes': engine_api.PARAM_TIMEOUT, } result = {} for k in keymap: if k in req.params: result[keymap[k]] = params[k] return result if action not in self.CREATE_OR_UPDATE_ACTION: msg = _("Unexpected action %s" % action) # This should not happen, so return HeatInternalFailureError return exception.HeatInternalFailureError(detail=msg) engine_action = {self.CREATE_STACK: self.engine_rpcapi.create_stack, self.UPDATE_STACK: self.engine_rpcapi.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 = json.loads(templ) except ValueError: msg = _("The Template must be a JSON document.") return exception.HeatInvalidParameterValueError(detail=msg) args = {'template': stack, 'params': stack_parms, '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 rpc_common.RemoteError 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 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 = { engine_api.OUTPUT_DESCRIPTION: 'Description', engine_api.OUTPUT_KEY: 'OutputKey', engine_api.OUTPUT_VALUE: 'OutputValue', } def replacecolon(d): return dict(map(lambda (k, v): (k.replace(':', '.'), v), d.items())) def transform(attrs): """ Recursively replace all : with . in dict keys so 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 = { engine_api.STACK_CAPABILITIES: 'Capabilities', engine_api.STACK_CREATION_TIME: 'CreationTime', engine_api.STACK_DESCRIPTION: 'Description', engine_api.STACK_DISABLE_ROLLBACK: 'DisableRollback', engine_api.STACK_UPDATED_TIME: 'LastUpdatedTime', engine_api.STACK_NOTIFICATION_TOPICS: 'NotificationARNs', engine_api.STACK_PARAMETERS: 'Parameters', engine_api.STACK_ID: 'StackId', engine_api.STACK_NAME: 'StackName', engine_api.STACK_STATUS_DATA: 'StackStatusReason', engine_api.STACK_TIMEOUT: 'TimeoutInMinutes', } result = api_utils.reformat_dict_keys(keymap, s) action = s[engine_api.STACK_ACTION] status = s[engine_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 engine_api.STACK_OUTPUTS in s: for o in s[engine_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.engine_rpcapi.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 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 parameters/arguments and reformat them to match the 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': engine_api.PARAM_TIMEOUT, 'DisableRollback': engine_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[engine_api.PARAM_DISABLE_ROLLBACK] = 'true' elif value in ('ROLLBACK', 'DELETE'): result[engine_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.engine_rpcapi.create_stack, self.UPDATE_STACK: self.engine_rpcapi.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 list_metrics(self, req): """ Implements ListMetrics API action Lists metric datapoints associated with a particular alarm, or all alarms if none specified """ self._enforce(req, 'ListMetrics') def format_metric_data(d, fil={}): """ Reformat engine output into the AWS "Metric" format Takes an optional filter dict, which is traversed so a metric dict is only returned if all keys match the filter dict """ dimensions = [{ 'AlarmName': d[engine_api.WATCH_DATA_ALARM] }, { 'Timestamp': d[engine_api.WATCH_DATA_TIME] }] for key in d[engine_api.WATCH_DATA]: dimensions.append({key: d[engine_api.WATCH_DATA][key]}) newdims = self._reformat_dimensions(dimensions) result = { 'MetricName': d[engine_api.WATCH_DATA_METRIC], 'Dimensions': newdims, 'Namespace': d[engine_api.WATCH_DATA_NAMESPACE], } for f in fil: try: value = result[f] if value != fil[f]: # Filter criteria not met, return None return except KeyError: logger.warning(_("Invalid filter key %s, ignoring") % f) return result con = req.context parms = dict(req.params) # FIXME : Don't yet handle filtering by Dimensions filter_result = dict((k, v) for (k, v) in parms.iteritems() if k in ("MetricName", "Namespace")) logger.debug(_("filter parameters : %s") % filter_result) try: # Engine does not currently support query by namespace/metric # so we pass None/None and do any filtering locally null_kwargs = {'metric_namespace': None, 'metric_name': None} watch_data = self.rpc_client.show_watch_metric(con, **null_kwargs) except rpc_common.RemoteError as ex: return exception.map_remote_error(ex) res = {'Metrics': []} for d in watch_data: metric = format_metric_data(d, filter_result) if metric: res['Metrics'].append(metric) result = api_utils.format_response("ListMetrics", res) return result