diff --git a/bin/heat b/bin/heat index 6d71b69ca7..8ee6b92a6a 100755 --- a/bin/heat +++ b/bin/heat @@ -297,6 +297,61 @@ def stack_events_list(options, arguments): print result +@utils.catch_error('resource') +def stack_resource_show(options, arguments): + ''' + Display details of the specified resource. + ''' + c = get_client(options) + try: + stack_name, resource_name = arguments + except ValueError: + print 'Enter stack name and logical resource id' + return + + parameters = { + 'StackName': stack_name, + 'LogicalResourceId': resource_name, + } + result = c.describe_stack_resource(**parameters) + print result + + +@utils.catch_error('resource-list') +def stack_resources_list(options, arguments): + ''' + Display summary of all resources in the specified stack. + ''' + c = get_client(options) + try: + stack_name = arguments.pop(0) + except IndexError: + print 'Enter stack name' + return + + parameters = { + 'StackName': stack_name, + } + result = c.list_stack_resources(**parameters) + print result + + +@utils.catch_error('resource-list-details') +def stack_resources_list_details(options, arguments): + ''' + Display details of all resources in the specified stack. + ''' + c = get_client(options) + logical_resource_id = arguments.pop(0) if arguments else None + parameters = { + 'StackName': options.stack_name, + 'PhysicalResourceId': options.physical_resource_id, + 'LogicalResourceId': logical_resource_id, + } + result = c.describe_stack_resources(**parameters) + print result + + @utils.catch_error('list') def stack_list(options, arguments): ''' @@ -397,6 +452,10 @@ def create_options(parser): parser.add_option('-P', '--parameters', metavar="parameters", default=None, help="Parameter values used to create the stack.") + parser.add_option('-n', '--stack-name', default=None, + help="Name of the queried stack") + parser.add_option('-c', '--physical-resource-id', default=None, + help="Physical ID of the queried resource") def credentials_from_env(): @@ -484,6 +543,9 @@ def lookup_command(parser, command_name): 'list': stack_list, 'events_list': stack_events_list, # DEPRECATED 'event-list': stack_events_list, + 'resource': stack_resource_show, + 'resource-list': stack_resources_list, + 'resource-list-details': stack_resources_list_details, 'validate': template_validate, 'gettemplate': get_template, 'estimate-template-cost': estimate_template_cost, @@ -530,6 +592,12 @@ Commands: event-list List events for a stack + resource Describe the resource + + resource-list Show list of resources belonging to a stack + + resource-list-details Detailed view of resources belonging to a stack + """ oparser = optparse.OptionParser(version='%%prog %s' diff --git a/heat/api/v1/__init__.py b/heat/api/v1/__init__.py index f54a184331..6091665710 100644 --- a/heat/api/v1/__init__.py +++ b/heat/api/v1/__init__.py @@ -127,6 +127,9 @@ class API(wsgi.Router): 'validate_template': 'ValidateTemplate', 'get_template': 'GetTemplate', 'estimate_template_cost': 'EstimateTemplateCost', + 'describe_stack_resource': 'DescribeStackResource', + 'describe_stack_resources': 'DescribeStackResources', + 'list_stack_resources': 'ListStackResources', } def __init__(self, conf, **local_conf): diff --git a/heat/api/v1/stacks.py b/heat/api/v1/stacks.py index 5831163615..53c4b7bb00 100644 --- a/heat/api/v1/stacks.py +++ b/heat/api/v1/stacks.py @@ -243,6 +243,98 @@ class StackController(object): return {'DescribeStackEventsResult': {'StackEvents': events}} + def describe_stack_resource(self, req): + """ + Return the details of the given resource belonging to the given stack. + """ + con = req.context + args = { + 'stack_name': req.params.get('StackName'), + 'resource_name': req.params.get('LogicalResourceId'), + } + + try: + resource_details = rpc.call(con, 'engine', + {'method': 'describe_stack_resource', + 'args': args}) + + except rpc_common.RemoteError as ex: + return webob.exc.HTTPBadRequest(str(ex)) + + return { + 'DescribeStackResourceResponse': { + 'DescribeStackResourceResult': { + 'StackResourceDetail': resource_details, + }, + }, + } + + def describe_stack_resources(self, req): + """ + 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. + """ + 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 webob.exc.HTTPBadRequest(msg) + + args = { + 'stack_name': stack_name, + 'physical_resource_id': physical_resource_id, + 'logical_resource_id': req.params.get('LogicalResourceId'), + } + + try: + resources = rpc.call(con, 'engine', + {'method': 'describe_stack_resources', + 'args': args}) + + except rpc_common.RemoteError as ex: + return webob.exc.HTTPBadRequest(str(ex)) + + response = { + 'DescribeStackResourcesResult': { + 'StackResources': resources, + } + } + return response + + def list_stack_resources(self, req): + """ + Return summary of the resources belonging to the specified stack. + + """ + con = req.context + + try: + resources = rpc.call(con, 'engine', { + 'method': 'list_stack_resources', + 'args': {'stack_name': req.params.get('StackName')} + }) + except rpc_common.RemoteError as ex: + return webob.exc.HTTPBadRequest(str(ex)) + + return { + 'ListStackResourcesResponse': { + 'ListStackResourcesResult': { + 'StackResourceSummaries': resources, + }, + }, + } + def create_resource(options): """ diff --git a/heat/client.py b/heat/client.py index d370a461cf..4a5dc1f03c 100644 --- a/heat/client.py +++ b/heat/client.py @@ -65,6 +65,15 @@ class V1Client(base_client.BaseClient): def list_stack_events(self, **kwargs): return self.stack_request("DescribeStackEvents", "GET", **kwargs) + def describe_stack_resource(self, **kwargs): + return self.stack_request("DescribeStackResource", "GET", **kwargs) + + def describe_stack_resources(self, **kwargs): + return self.stack_request("DescribeStackResources", "GET", **kwargs) + + def list_stack_resources(self, **kwargs): + return self.stack_request("ListStackResources", "GET", **kwargs) + def validate_template(self, **kwargs): return self.stack_request("ValidateTemplate", "GET", **kwargs) diff --git a/heat/cloudformations.py b/heat/cloudformations.py index c6ef264455..5ad1fb3a99 100644 --- a/heat/cloudformations.py +++ b/heat/cloudformations.py @@ -16,4 +16,6 @@ SUPPORTED_PARAMS = ('StackName', 'TemplateBody', 'TemplateUrl', 'NotificationARNs', 'Parameters', 'Version', 'SignatureVersion', 'Timestamp', 'AWSAccessKeyId', - 'Signature', 'KeyStoneCreds', 'Timeout') + 'Signature', 'KeyStoneCreds', 'Timeout', + 'LogicalResourceId', 'PhysicalResourceId', 'NextToken', +) diff --git a/heat/db/api.py b/heat/db/api.py index 8dc2aa55e2..d784e8c8a9 100644 --- a/heat/db/api.py +++ b/heat/db/api.py @@ -95,6 +95,11 @@ def resource_get_by_name_and_stack(context, resource_name, stack_id): resource_name, stack_id) +def resource_get_by_physical_resource_id(context, physical_resource_id): + return IMPL.resource_get_by_physical_resource_id(context, + physical_resource_id) + + def stack_get(context, stack_id): return IMPL.stack_get(context, stack_id) diff --git a/heat/db/sqlalchemy/api.py b/heat/db/sqlalchemy/api.py index 61366bbfc9..f31b08d481 100644 --- a/heat/db/sqlalchemy/api.py +++ b/heat/db/sqlalchemy/api.py @@ -98,6 +98,13 @@ def resource_get_by_name_and_stack(context, resource_name, stack_id): return result +def resource_get_by_physical_resource_id(context, physical_resource_id): + result = (model_query(context, models.Resource) + .filter_by(nova_instance=physical_resource_id) + .first()) + return result + + def resource_get_all(context): results = model_query(context, models.Resource).all() diff --git a/heat/engine/manager.py b/heat/engine/manager.py index 5de0ffe78d..4dea7c572a 100644 --- a/heat/engine/manager.py +++ b/heat/engine/manager.py @@ -391,6 +391,67 @@ class EngineManager(manager.Manager): msg = 'Error creating event' return [msg, None] + def describe_stack_resource(self, context, stack_name, resource_name): + self._authenticate(context) + + stack = db_api.stack_get(context, stack_name) + if not stack: + raise AttributeError('Unknown stack name') + resource = db_api.resource_get_by_name_and_stack(context, + resource_name, + stack.id) + if not resource: + raise AttributeError('Unknown resource name') + return format_resource_attributes(stack, resource) + + def describe_stack_resources(self, context, stack_name, + physical_resource_id, logical_resource_id): + self._authenticate(context) + + if stack_name: + stack = db_api.stack_get(context, stack_name) + else: + resource = db_api.resource_get_by_physical_resource_id(context, + physical_resource_id) + if not resource: + msg = "The specified PhysicalResourceId doesn't exist" + raise AttributeError(msg) + stack = resource.stack + + if not stack: + raise AttributeError("The specified stack doesn't exist") + + resources = [] + for r in stack.resources: + if logical_resource_id and r.name != logical_resource_id: + continue + formatted = format_resource_attributes(stack, r) + # this API call uses Timestamp instead of LastUpdatedTimestamp + formatted['Timestamp'] = formatted['LastUpdatedTimestamp'] + del formatted['LastUpdatedTimestamp'] + resources.append(formatted) + + return resources + + def list_stack_resources(self, context, stack_name): + self._authenticate(context) + + stack = db_api.stack_get(context, stack_name) + if not stack: + raise AttributeError('Unknown stack name') + + resources = [] + response_keys = ('ResourceStatus', 'LogicalResourceId', + 'LastUpdatedTimestamp', 'PhysicalResourceId', + 'ResourceType') + for r in stack.resources: + formatted = format_resource_attributes(stack, r) + for key in formatted.keys(): + if not key in response_keys: + del formatted[key] + resources.append(formatted) + return resources + def metadata_register_address(self, context, url): config.FLAGS.heat_metadata_server_url = url @@ -520,3 +581,23 @@ class EngineManager(manager.Manager): self.run_rule(None, wr) return [None, wd.data] + + +def format_resource_attributes(stack, resource): + """ + Return a representation of the given resource that mathes the API output + expectations. + """ + template = resource.parsed_template.template + template_resources = template.get('Resources', {}) + resource_type = template_resources.get(resource.name, {}).get('Type', '') + last_updated_time = resource.updated_at or resource.created_at + return { + 'StackId': stack.id, + 'StackName': stack.name, + 'LogicalResourceId': resource.name, + 'PhysicalResourceId': resource.nova_instance or '', + 'ResourceType': resource_type, + 'LastUpdatedTimestamp': last_updated_time.isoformat(), + 'ResourceStatus': resource.state, + }