heat/heat/engine/api.py
Richard Lee 25788a13c2 Display stack owner when formatting stacks
Stacks are launched by users but scoped to tenants, so users in the same
tenant currently have no way to know owns each stack.  The same is
especially true to unscoped stack lists.  Since humans are much better
with names than they are with numbers, this adds the stack owner
username to the data returned with each stack.

Co-Authored-By: Anderson Mesquita <andersonvom@gmail.com>
Implements: blueprint stack-display-fields
Change-Id: Ia1386531025096e0eeaf9e98b478c4453fc8cb01
2014-08-12 11:06:44 -04:00

382 lines
13 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.
from heat.common import param_utils
from heat.common import template_format
from heat.engine import constraints as constr
from heat.openstack.common.gettextutils import _
from heat.openstack.common import log as logging
from heat.openstack.common import timeutils
from heat.rpc import api
LOG = logging.getLogger(__name__)
def extract_args(params):
'''
Extract any arguments passed as parameters through the API and return them
as a dictionary. This allows us to filter the passed args and do type
conversion where appropriate
'''
kwargs = {}
timeout_mins = params.get(api.PARAM_TIMEOUT)
if timeout_mins not in ('0', 0, None):
try:
timeout = int(timeout_mins)
except (ValueError, TypeError):
LOG.exception(_('Timeout conversion failed'))
else:
if timeout > 0:
kwargs[api.PARAM_TIMEOUT] = timeout
else:
raise ValueError(_('Invalid timeout value %s') % timeout)
if api.PARAM_DISABLE_ROLLBACK in params:
disable_rollback = param_utils.extract_bool(
params[api.PARAM_DISABLE_ROLLBACK])
kwargs[api.PARAM_DISABLE_ROLLBACK] = disable_rollback
if api.PARAM_SHOW_DELETED in params:
params[api.PARAM_SHOW_DELETED] = param_utils.extract_bool(
params[api.PARAM_SHOW_DELETED])
adopt_data = params.get(api.PARAM_ADOPT_STACK_DATA)
if adopt_data:
adopt_data = template_format.simple_parse(adopt_data)
if not isinstance(adopt_data, dict):
raise ValueError(
_('Unexpected adopt data "%s". Adopt data must be a dict.')
% adopt_data)
kwargs[api.PARAM_ADOPT_STACK_DATA] = adopt_data
return kwargs
def format_stack_outputs(stack, outputs):
'''
Return a representation of the given output template for the given stack
that matches the API output expectations.
'''
def format_stack_output(k):
output = {
api.OUTPUT_DESCRIPTION: outputs[k].get('Description',
'No description given'),
api.OUTPUT_KEY: k,
api.OUTPUT_VALUE: stack.output(k)
}
if outputs[k].get('error_msg'):
output.update({api.OUTPUT_ERROR: outputs[k].get('error_msg')})
return output
return [format_stack_output(key) for key in outputs]
def format_stack(stack):
'''
Return a representation of the given stack that matches the API output
expectations.
'''
updated_time = stack.updated_time and timeutils.isotime(stack.updated_time)
info = {
api.STACK_NAME: stack.name,
api.STACK_ID: dict(stack.identifier()),
api.STACK_CREATION_TIME: timeutils.isotime(stack.created_time),
api.STACK_UPDATED_TIME: updated_time,
api.STACK_NOTIFICATION_TOPICS: [], # TODO Not implemented yet
api.STACK_PARAMETERS: stack.parameters.map(str),
api.STACK_DESCRIPTION: stack.t[stack.t.DESCRIPTION],
api.STACK_TMPL_DESCRIPTION: stack.t[stack.t.DESCRIPTION],
api.STACK_ACTION: stack.action or '',
api.STACK_STATUS: stack.status or '',
api.STACK_STATUS_DATA: stack.status_reason,
api.STACK_CAPABILITIES: [], # TODO Not implemented yet
api.STACK_DISABLE_ROLLBACK: stack.disable_rollback,
api.STACK_TIMEOUT: stack.timeout_mins,
api.STACK_OWNER: stack.username,
}
# allow users to view the outputs of stacks
if (stack.action != stack.DELETE and stack.status != stack.IN_PROGRESS):
info[api.STACK_OUTPUTS] = format_stack_outputs(stack, stack.outputs)
return info
def format_resource_properties(resource):
def get_property(prop):
try:
return resource.properties[prop]
except (KeyError, ValueError):
return None
return dict((prop, get_property(prop))
for prop in resource.properties_schema.keys())
def format_stack_resource(resource, detail=True, with_props=False):
'''
Return a representation of the given resource that matches the API output
expectations.
'''
last_updated_time = resource.updated_time or resource.created_time
res = {
api.RES_UPDATED_TIME: timeutils.isotime(last_updated_time),
api.RES_NAME: resource.name,
api.RES_PHYSICAL_ID: resource.resource_id or '',
api.RES_ACTION: resource.action,
api.RES_STATUS: resource.status,
api.RES_STATUS_DATA: resource.status_reason,
api.RES_TYPE: resource.type(),
api.RES_ID: dict(resource.identifier()),
api.RES_STACK_ID: dict(resource.stack.identifier()),
api.RES_STACK_NAME: resource.stack.name,
api.RES_REQUIRED_BY: resource.required_by(),
}
if (hasattr(resource, 'nested') and callable(resource.nested) and
resource.nested()):
res[api.RES_NESTED_STACK_ID] = dict(resource.nested().identifier())
if resource.stack.parent_resource:
res[api.RES_PARENT_RESOURCE] = resource.stack.parent_resource.name
if detail:
res[api.RES_DESCRIPTION] = resource.t.description
res[api.RES_METADATA] = resource.metadata_get()
if with_props:
res[api.RES_SCHEMA_PROPERTIES] = format_resource_properties(resource)
return res
def format_stack_preview(stack):
def format_resource(res):
if isinstance(res, list):
return map(format_resource, res)
return format_stack_resource(res, with_props=True)
fmt_stack = format_stack(stack)
fmt_resources = map(format_resource, stack.preview_resources())
fmt_stack['resources'] = fmt_resources
return fmt_stack
def format_event(event):
stack_identifier = event.stack.identifier()
result = {
api.EVENT_ID: dict(event.identifier()),
api.EVENT_STACK_ID: dict(stack_identifier),
api.EVENT_STACK_NAME: stack_identifier.stack_name,
api.EVENT_TIMESTAMP: timeutils.isotime(event.timestamp),
api.EVENT_RES_NAME: event.resource_name,
api.EVENT_RES_PHYSICAL_ID: event.physical_resource_id,
api.EVENT_RES_ACTION: event.action,
api.EVENT_RES_STATUS: event.status,
api.EVENT_RES_STATUS_DATA: event.reason,
api.EVENT_RES_TYPE: event.resource_type,
api.EVENT_RES_PROPERTIES: event.resource_properties,
}
return result
def format_notification_body(stack):
# some other possibilities here are:
# - template name
# - template size
# - resource count
if stack.status is not None and stack.action is not None:
state = '_'.join(stack.state)
else:
state = 'Unknown'
result = {
api.NOTIFY_TENANT_ID: stack.context.tenant_id,
api.NOTIFY_USER_ID: stack.context.user,
api.NOTIFY_STACK_ID: stack.identifier().arn(),
api.NOTIFY_STACK_NAME: stack.name,
api.NOTIFY_STATE: state,
api.NOTIFY_STATE_REASON: stack.status_reason,
api.NOTIFY_CREATE_AT: timeutils.isotime(stack.created_time),
}
return result
def format_watch(watch):
result = {
api.WATCH_ACTIONS_ENABLED: watch.rule.get(api.RULE_ACTIONS_ENABLED),
api.WATCH_ALARM_ACTIONS: watch.rule.get(api.RULE_ALARM_ACTIONS),
api.WATCH_TOPIC: watch.rule.get(api.RULE_TOPIC),
api.WATCH_UPDATED_TIME: timeutils.isotime(watch.updated_at),
api.WATCH_DESCRIPTION: watch.rule.get(api.RULE_DESCRIPTION),
api.WATCH_NAME: watch.name,
api.WATCH_COMPARISON: watch.rule.get(api.RULE_COMPARISON),
api.WATCH_DIMENSIONS: watch.rule.get(api.RULE_DIMENSIONS) or [],
api.WATCH_PERIODS: watch.rule.get(api.RULE_PERIODS),
api.WATCH_INSUFFICIENT_ACTIONS:
watch.rule.get(api.RULE_INSUFFICIENT_ACTIONS),
api.WATCH_METRIC_NAME: watch.rule.get(api.RULE_METRIC_NAME),
api.WATCH_NAMESPACE: watch.rule.get(api.RULE_NAMESPACE),
api.WATCH_OK_ACTIONS: watch.rule.get(api.RULE_OK_ACTIONS),
api.WATCH_PERIOD: watch.rule.get(api.RULE_PERIOD),
api.WATCH_STATE_REASON: watch.rule.get(api.RULE_STATE_REASON),
api.WATCH_STATE_REASON_DATA:
watch.rule.get(api.RULE_STATE_REASON_DATA),
api.WATCH_STATE_UPDATED_TIME: timeutils.isotime(
watch.rule.get(api.RULE_STATE_UPDATED_TIME)),
api.WATCH_STATE_VALUE: watch.state,
api.WATCH_STATISTIC: watch.rule.get(api.RULE_STATISTIC),
api.WATCH_THRESHOLD: watch.rule.get(api.RULE_THRESHOLD),
api.WATCH_UNIT: watch.rule.get(api.RULE_UNIT),
api.WATCH_STACK_ID: watch.stack_id
}
return result
def format_watch_data(wd):
# Demangle DB format data into something more easily used in the API
# We are expecting a dict with exactly two items, Namespace and
# a metric key
namespace = wd.data['Namespace']
metric = [(k, v) for k, v in wd.data.items() if k != 'Namespace']
if len(metric) == 1:
metric_name, metric_data = metric[0]
else:
LOG.error(_("Unexpected number of keys in watch_data.data!"))
return
result = {
api.WATCH_DATA_ALARM: wd.watch_rule.name,
api.WATCH_DATA_METRIC: metric_name,
api.WATCH_DATA_TIME: timeutils.isotime(wd.created_at),
api.WATCH_DATA_NAMESPACE: namespace,
api.WATCH_DATA: metric_data
}
return result
def format_validate_parameter(param):
"""
Format a template parameter for validate template API call
Formats a template parameter and its schema information from the engine's
internal representation (i.e. a Parameter object and its associated
Schema object) to a representation expected by the current API (for example
to be compatible to CFN syntax).
"""
# map of Schema object types to API expected types
schema_to_api_types = {
param.schema.STRING: api.PARAM_TYPE_STRING,
param.schema.NUMBER: api.PARAM_TYPE_NUMBER,
param.schema.LIST: api.PARAM_TYPE_COMMA_DELIMITED_LIST,
param.schema.MAP: api.PARAM_TYPE_JSON
}
res = {
api.PARAM_TYPE: schema_to_api_types.get(param.schema.type,
param.schema.type),
api.PARAM_DESCRIPTION: param.description(),
api.PARAM_NO_ECHO: 'true' if param.hidden() else 'false',
api.PARAM_LABEL: param.label()
}
if param.has_default():
res[api.PARAM_DEFAULT] = param.default()
constraint_description = []
# build constraints
for c in param.schema.constraints:
if isinstance(c, constr.Length):
if c.min is not None:
res[api.PARAM_MIN_LENGTH] = c.min
if c.max is not None:
res[api.PARAM_MAX_LENGTH] = c.max
elif isinstance(c, constr.Range):
if c.min is not None:
res[api.PARAM_MIN_VALUE] = c.min
if c.max is not None:
res[api.PARAM_MAX_VALUE] = c.max
elif isinstance(c, constr.AllowedValues):
res[api.PARAM_ALLOWED_VALUES] = list(c.allowed)
elif isinstance(c, constr.AllowedPattern):
res[api.PARAM_ALLOWED_PATTERN] = c.pattern
elif isinstance(c, constr.CustomConstraint):
res[api.PARAM_CUSTOM_CONSTRAINT] = c.name
if c.description:
constraint_description.append(c.description)
if constraint_description:
res[api.PARAM_CONSTRAINT_DESCRIPTION] = " ".join(
constraint_description)
return res
def format_software_config(sc):
if sc is None:
return
result = {
api.SOFTWARE_CONFIG_ID: sc.id,
api.SOFTWARE_CONFIG_NAME: sc.name,
api.SOFTWARE_CONFIG_GROUP: sc.group,
api.SOFTWARE_CONFIG_CONFIG: sc.config['config'],
api.SOFTWARE_CONFIG_INPUTS: sc.config['inputs'],
api.SOFTWARE_CONFIG_OUTPUTS: sc.config['outputs'],
api.SOFTWARE_CONFIG_OPTIONS: sc.config['options']
}
return result
def format_software_deployment(sd):
if sd is None:
return
result = {
api.SOFTWARE_DEPLOYMENT_ID: sd.id,
api.SOFTWARE_DEPLOYMENT_SERVER_ID: sd.server_id,
api.SOFTWARE_DEPLOYMENT_INPUT_VALUES: sd.input_values,
api.SOFTWARE_DEPLOYMENT_OUTPUT_VALUES: sd.output_values,
api.SOFTWARE_DEPLOYMENT_ACTION: sd.action,
api.SOFTWARE_DEPLOYMENT_STATUS: sd.status,
api.SOFTWARE_DEPLOYMENT_STATUS_REASON: sd.status_reason,
api.SOFTWARE_DEPLOYMENT_CONFIG_ID: sd.config.id,
}
return result
def format_snapshot(snapshot):
if snapshot is None:
return
result = {
api.SNAPSHOT_ID: snapshot.id,
api.SNAPSHOT_NAME: snapshot.name,
api.SNAPSHOT_STATUS: snapshot.status,
api.SNAPSHOT_STATUS_REASON: snapshot.status_reason,
api.SNAPSHOT_DATA: snapshot.data,
}
return result