gettext.install('heat', unicode=1)
-from heat import client as heat_client
+if scriptname == 'heat-boto':
+ from heat import boto_client as heat_client
+else:
+ from heat import client as heat_client
from heat import version
from heat.common import config
from heat.common import exception
from heat import utils
-def format_parameters(options):
- parameters = {}
- if options.parameters:
- for count, p in enumerate(options.parameters.split(';'), 1):
- (n, v) = p.split('=')
- parameters['Parameters.member.%d.ParameterKey' % count] = n
- parameters['Parameters.member.%d.ParameterValue' % count] = v
- return parameters
-
-
@utils.catch_error('validate')
def template_validate(options, arguments):
'''
logging.error('Please specify a template file or url')
return utils.FAILURE
- parameters.update(format_parameters(options))
-
- client = get_client(options)
- result = client.validate_template(**parameters)
- print result
+ c = get_client(options)
+ parameters.update(c.format_parameters(options))
+ result = c.validate_template(**parameters)
+ print c.format_template(result)
@utils.catch_error('estimatetemplatecost')
logging.error("as the first argument")
return utils.FAILURE
- parameters.update(format_parameters(options))
-
if options.template_file:
parameters['TemplateBody'] = open(options.template_file).read()
elif options.template_url:
return utils.FAILURE
c = get_client(options)
+ parameters.update(c.format_parameters(options))
result = c.estimate_template_cost(**parameters)
print result
logging.error("as the first argument")
return utils.FAILURE
- parameters.update(format_parameters(options))
-
parameters['TimeoutInMinutes'] = options.timeout
if options.template_file:
return utils.FAILURE
c = get_client(options)
+ parameters.update(c.format_parameters(options))
result = c.create_stack(**parameters)
print result
elif options.template_url:
parameters['TemplateUrl'] = options.template_url
- parameters.update(format_parameters(options))
-
c = get_client(options)
+ parameters.update(c.format_parameters(options))
result = c.update_stack(**parameters)
print result
c = get_client(options)
result = c.describe_stacks(**parameters)
- print result
+ print c.format_stack(result)
@utils.catch_error('event-list')
c = get_client(options)
result = c.list_stack_events(**parameters)
- print result
+ print c.format_stack_event(result)
@utils.catch_error('resource')
'LogicalResourceId': resource_name,
}
result = c.describe_stack_resource(**parameters)
- print result
+ print c.format_stack_resource(result)
@utils.catch_error('resource-list')
'StackName': stack_name,
}
result = c.list_stack_resources(**parameters)
- print result
-
-
-def _resources_list_details(options, lookup_key='StackName',
- lookup_value=None, log_resid=None):
- '''
- Helper function to reduce duplication in stack_resources_list_details
- Looks up resource details based on StackName or PhysicalResourceId
- '''
- c = get_client(options)
- parameters = {}
-
- if lookup_key in ['StackName', 'PhysicalResourceId']:
- parameters[lookup_key] = lookup_value
- else:
- logging.error("Unexpected key %s" % lookup_key)
- return
-
- if log_resid:
- parameters['LogicalResourceId'] = log_resid
-
- try:
- result = c.describe_stack_resources(**parameters)
- except:
- logging.debug("Failed to lookup resource details with key %s:%s" %
- (lookup_key, lookup_value))
- return
-
- return result
+ print c.format_stack_resource_summary(result)
@utils.catch_error('resource-list-details')
try:
name_or_pid = arguments.pop(0)
except IndexError:
- logging.error("No valid stack_name or physical_resource_id")
+ logging.error("Must pass a stack_name or physical_resource_id")
print usage
return
logical_resource_id = arguments.pop(0) if arguments else None
+ parameters = {
+ 'NameOrPid': name_or_pid,
+ 'LogicalResourceId': logical_resource_id, }
- # Try StackName first as it seems the most likely..
- lookup_keys = ['StackName', 'PhysicalResourceId']
- for key in lookup_keys:
- logging.debug("Looking up resources for %s:%s" % (key, name_or_pid))
- result = _resources_list_details(options, lookup_key=key,
- lookup_value=name_or_pid,
- log_resid=logical_resource_id)
- if result:
- break
+ c = get_client(options)
+ result = c.describe_stack_resources(**parameters)
if result:
- print result
+ print c.format_stack_resource(result)
else:
- logging.error("No valid stack_name or physical_resource_id")
+ logging.error("Invalid stack_name, physical_resource_id " +
+ "or logical_resource_id")
print usage
'''
c = get_client(options)
result = c.list_stacks()
- print result
+ print c.format_stack_summary(result)
def get_client(options):
NotImplementedError,
exception.ClientConfigurationError), ex:
oparser.print_usage()
- logging.error("ERROR: " % ex)
+ logging.error("ERROR: %s" % ex)
sys.exit(1)
+++ /dev/null
-#!/usr/bin/env python
-# vim: tabstop=4 shiftwidth=4 softtabstop=4
-#
-# 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.
-
-"""
-This is the administration program for heat. It is simply a command-line
-interface for adding, modifying, and retrieving information about the stacks
-belonging to a user. It is a convenience application that talks to the heat
-API server.
-"""
-
-import gettext
-import optparse
-import os
-import os.path
-import sys
-import time
-import json
-import logging
-
-from urlparse import urlparse
-
-# If ../heat/__init__.py exists, add ../ to Python search path, so that
-# it will override what happens to be installed in /usr/(local/)lib/python...
-possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]),
- os.pardir,
- os.pardir))
-if os.path.exists(os.path.join(possible_topdir, 'heat', '__init__.py')):
- sys.path.insert(0, possible_topdir)
-
-scriptname = os.path.basename(sys.argv[0])
-
-gettext.install('heat', unicode=1)
-
-import boto
-from heat import version
-from heat.common import config
-from heat.common import exception
-from heat import utils
-
-
-# FIXME : would be better if each of the boto response classes
-# implemented __str__ or a print method, so we could just print
-# them, avoiding all these print functions, boto patch TODO
-
-
-def print_stack_event(event):
- '''
- Print contents of a boto.cloudformation.stack.StackEvent object
- '''
- print "EventId : %s" % event.event_id
- print "LogicalResourceId : %s" % event.logical_resource_id
- print "PhysicalResourceId : %s" % event.physical_resource_id
- print "ResourceProperties : %s" % event.resource_properties
- print "ResourceStatus : %s" % event.resource_status
- print "ResourceStatusReason : %s" % event.resource_status_reason
- print "ResourceType : %s" % event.resource_type
- print "StackId : %s" % event.stack_id
- print "StackName : %s" % event.stack_name
- print "Timestamp : %s" % event.timestamp
- print "--"
-
-
-def print_stack(s):
- '''
- Print contents of a boto.cloudformation.stack.Stack object
- '''
- print "Capabilities : %s" % s.capabilities
- print "CreationTime : %s" % s.creation_time
- print "Description : %s" % s.description
- print "DisableRollback : %s" % s.disable_rollback
- # FIXME : boto doesn't populate this field, but AWS defines it.
- # bit unclear because the docs say LastUpdatedTime, where all
- # other response structures define LastUpdatedTimestamp
- # need confirmation of real AWS format, probably a documentation bug..
-# print "LastUpdatedTime : %s" % s.last_updated_time
- print "NotificationARNs : %s" % s.notification_arns
- print "Outputs : %s" % s.outputs
- print "Parameters : %s" % s.parameters
- print "StackId : %s" % s.stack_id
- print "StackName : %s" % s.stack_name
- print "StackStatus : %s" % s.stack_status
- print "StackStatusReason : %s" % s.stack_status_reason
- print "TimeoutInMinutes : %s" % s.timeout_in_minutes
- print "--"
-
-
-def print_stack_resource(res):
- '''
- Print contents of a boto.cloudformation.stack.StackResource object
- '''
- print "LogicalResourceId : %s" % res.logical_resource_id
- print "PhysicalResourceId : %s" % res.physical_resource_id
- print "ResourceStatus : %s" % res.resource_status
- print "ResourceStatusReason : %s" % res.resource_status_reason
- print "ResourceType : %s" % res.resource_type
- print "StackId : %s" % res.stack_id
- print "StackName : %s" % res.stack_name
- print "Timestamp : %s" % res.timestamp
- print "--"
-
-
-def print_stack_resource_summary(res):
- '''
- Print contents of a boto.cloudformation.stack.StackResourceSummary object
- '''
- print "LastUpdatedTimestamp : %s" % res.last_updated_timestamp
- print "LogicalResourceId : %s" % res.logical_resource_id
- print "PhysicalResourceId : %s" % res.physical_resource_id
- print "ResourceStatus : %s" % res.resource_status
- print "ResourceStatusReason : %s" % res.resource_status_reason
- print "ResourceType : %s" % res.resource_type
- print "--"
-
-
-def print_stack_summary(s):
- '''
- Print contents of a boto.cloudformation.stack.StackSummary object
- '''
- print "StackId : %s" % s.stack_id
- print "StackName : %s" % s.stack_name
- print "CreationTime : %s" % s.creation_time
- print "StackStatus : %s" % s.stack_status
- print "TemplateDescription : %s" % s.template_description
- print "--"
-
-
-@utils.catch_error('validate')
-def template_validate(options, arguments):
- '''
- Validate a template. This command parses a template and verifies that
- it is in the correct format.
-
- Usage: heat validate \\
- [--template-file=<template file>|--template-url=<template URL>] \\
- [options]
-
- --template-file: Specify a local template file.
- --template-url: Specify a URL pointing to a stack description template.
- '''
- parameters = {}
- if options.template_file:
- parameters['TemplateBody'] = open(options.template_file).read()
- elif options.template_url:
- parameters['TemplateUrl'] = options.template_url
- else:
- logging.error('Please specify a template file or url')
- return utils.FAILURE
-
- if options.parameters:
- count = 1
- for p in options.parameters.split(';'):
- (n, v) = p.split('=')
- parameters['Parameters.member.%d.ParameterKey' % count] = n
- parameters['Parameters.member.%d.ParameterValue' % count] = v
- count = count + 1
-
- client = get_client(options)
- result = client.validate_template(**parameters)
- print result
-
-
-@utils.catch_error('estimatetemplatecost')
-def estimate_template_cost(options, arguments):
- parameters = {}
- try:
- parameters['StackName'] = arguments.pop(0)
- except IndexError:
- logging.error("Please specify the stack name you wish to estimate")
- logging.error("as the first argument")
- return utils.FAILURE
-
- if options.parameters:
- count = 1
- for p in options.parameters.split(';'):
- (n, v) = p.split('=')
- parameters['Parameters.member.%d.ParameterKey' % count] = n
- parameters['Parameters.member.%d.ParameterValue' % count] = v
- count = count + 1
-
- if options.template_file:
- parameters['TemplateBody'] = open(options.template_file).read()
- elif options.template_url:
- parameters['TemplateUrl'] = options.template_url
- else:
- logging.error('Please specify a template file or url')
- return utils.FAILURE
-
- c = get_client(options)
- result = c.estimate_template_cost(**parameters)
- print result
-
-
-@utils.catch_error('gettemplate')
-def get_template(options, arguments):
- '''
- Gets an existing stack template.
- '''
- if len(arguments):
- stack_name = arguments.pop(0)
- else:
- logging.error("Please specify the stack you wish to get")
- logging.error("as the first argument")
- return utils.FAILURE
-
- c = get_client(options)
- result = c.get_template(stack_name)
- print json.dumps(result)
-
-
-@utils.catch_error('create')
-def stack_create(options, arguments):
- '''
- Create a new stack from a template.
-
- Usage: heat create <stack name> \\
- [--template-file=<template file>|--template-url=<template URL>] \\
- [options]
-
- Stack Name: The user specified name of the stack you wish to create.
-
- --template-file: Specify a local template file containing a valid
- stack description template.
- --template-url: Specify a URL pointing to a valid stack description
- template.
- '''
-
- if len(arguments):
- stack_name = arguments.pop(0)
- else:
- logging.error("Please specify the stack name you wish to create")
- logging.error("as the first argument")
- return utils.FAILURE
-
- params = []
- if options.parameters:
- for p in options.parameters.split(';'):
- (n, v) = p.split('=')
- # Boto expects parameters as a list-of-tuple
- params.append((n, v))
-
- result = None
- c = get_client(options)
- if options.template_file:
- t = open(options.template_file).read()
- result = c.create_stack(stack_name, template_body=t, parameters=params)
- elif options.template_url:
- result = c.create_stack(stack_name, template_url=options.template_url)
- else:
- logging.error('Please specify a template file or url')
- return utils.FAILURE
-
- print result
-
-
-@utils.catch_error('update')
-def stack_update(options, arguments):
- '''
- Update an existing stack.
-
- Usage: heat update <stack name> \\
- [--template-file=<template file>|--template-url=<template URL>] \\
- [options]
-
- Stack Name: The name of the stack you wish to modify.
-
- --template-file: Specify a local template file containing a valid
- stack description template.
- --template-url: Specify a URL pointing to a valid stack description
- template.
-
- Options:
- --parameters: A list of key/value pairs separated by ';'s used
- to specify allowed values in the template file.
- '''
- parameters = {}
- if len(arguments):
- stack_name = arguments.pop(0)
- else:
- logging.error("Please specify the stack name you wish to update")
- logging.error("as the first argument")
- return utils.FAILURE
-
- result = None
- c = get_client(options)
- if options.template_file:
- t = open(options.template_file).read()
- result = c.update_stack(stack_name, template_body=t)
- elif options.template_url:
- t = options.template_url
- result = c.update_stack(stack_name, template_url=t)
-
- if options.parameters:
- count = 1
- for p in options.parameters.split(';'):
- (n, v) = p.split('=')
- parameters['Parameters.member.%d.ParameterKey' % count] = n
- parameters['Parameters.member.%d.ParameterValue' % count] = v
- count = count + 1
-
- print result
-
-
-@utils.catch_error('delete')
-def stack_delete(options, arguments):
- '''
- Delete an existing stack. This shuts down all VMs associated with
- the stack and (perhaps wrongly) also removes all events associated
- with the given stack.
-
- Usage: heat delete <stack name>
- '''
- if len(arguments):
- stack_name = arguments.pop(0)
- else:
- logging.error("Please specify the stack name you wish to delete")
- logging.error("as the first argument")
- return utils.FAILURE
-
- c = get_client(options)
- result = c.delete_stack(stack_name)
- print result
-
-
-@utils.catch_error('describe')
-def stack_describe(options, arguments):
- '''
- Describes an existing stack.
-
- Usage: heat describe <stack name>
- '''
- if len(arguments):
- stack_name = arguments.pop(0)
- else:
- logging.info("No stack name passed, getting results for ALL stacks")
- stack_name = None
-
- c = get_client(options)
- result = c.describe_stacks(stack_name)
- for s in result:
- print_stack(s)
-
-
-@utils.catch_error('event-list')
-def stack_events_list(options, arguments):
- '''
- List events associated with the given stack.
-
- Usage: heat event-list <stack name>
- '''
- if len(arguments):
- stack_name = arguments.pop(0)
- else:
- logging.info("No stack name passed, getting events for ALL stacks")
- stack_name = None
-
- c = get_client(options)
- result = c.describe_stack_events(stack_name)
- for e in result:
- print_stack_event(e)
-
-
-@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
-
- result = c.describe_stack_resources(stack_name, resource_name)
- for r in result:
- print_stack_resource(r)
-
-
-@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
-
- result = c.list_stack_resources(stack_name)
- for r in result:
- print_stack_resource_summary(r)
-
-
-@utils.catch_error('resource-list-details')
-def stack_resources_list_details(options, arguments):
- '''
- Display details of resources in the specified stack.
-
- - If stack name is specified, all associated resources are returned
- - If physical resource ID is specified, all associated resources of the
- stack the resource belongs to are returned
- - You must specify stack name *or* physical resource ID
- - You may optionally specify a Logical resource ID to filter the result
- '''
- usage = ('''Usage:
-%s resource-list-details stack_name [logical_resource_id]
-%s resource-list-details physical_resource_id [logical_resource_id]''' %
- (scriptname, scriptname))
-
- try:
- name_or_pid = arguments.pop(0)
- except IndexError:
- logging.error("Must pass a stack_name or physical_resource_id")
- print usage
- return
-
- logical_resource_id = arguments.pop(0) if arguments else None
-
- c = get_client(options)
-
- # Check if this is a StackName, if not assume it's a physical res ID
- # Note this is slower (for the common case, which is probably StackName)
- # than just doing a try/catch over the StackName case then retrying
- # on failure with name_or_pid as the physical resource ID, however
- # boto spews errors when raising an exception so we can't do that
- list_stacks = c.list_stacks()
- stack_names = [s.stack_name for s in list_stacks]
- if name_or_pid in stack_names:
- logging.debug("Looking up resources for StackName:%s" % name_or_pid)
- result = c.describe_stack_resources(stack_name_or_id=name_or_pid,
- logical_resource_id=logical_resource_id)
- else:
- logging.debug("Looking up resources for PhysicalResourceId:%s" %
- name_or_pid)
- result = c.describe_stack_resources(stack_name_or_id=None,
- logical_resource_id=logical_resource_id,
- physical_resource_id=name_or_pid)
-
- if result:
- for r in result:
- print_stack_resource(r)
- else:
- logging.error("Invalid stack_name, physical_resource_id " +
- "or logical_resource_id")
- print usage
-
-
-@utils.catch_error('list')
-def stack_list(options, arguments):
- '''
- List all running stacks.
-
- Usage: heat list
- '''
- c = get_client(options)
- result = c.list_stacks()
- for s in result:
- print_stack_summary(s)
-
-
-def get_client(options):
- """
- Returns a new boto client object to a heat server
- """
-
- # FIXME : reopen boto pull-request so that --host can override the
- # host specified in the hostname by passing it via the constructor
- # I implemented this previously, but they preferred the config-file
- # solution..
- # Note we pass None/None for the keys so boto reads /etc/boto.cfg
- cloudformation = boto.connect_cloudformation(aws_access_key_id=None,
- aws_secret_access_key=None, debug=options.debug, is_secure=False,
- port=options.port, path="/v1")
- if cloudformation:
- logging.debug("Got CF connection object OK")
- else:
- logging.error("Error establishing connection!")
- sys.exit(1)
-
- return cloudformation
-
-
-def create_options(parser):
- """
- Sets up the CLI and config-file options that may be
- parsed and program commands.
-
- :param parser: The option parser
- """
- parser.add_option('-v', '--verbose', default=False, action="store_true",
- help="Print more verbose output")
- parser.add_option('-d', '--debug', default=False, action="store_true",
- help="Print more verbose output")
- parser.add_option('-y', '--yes', default=False, action="store_true",
- help="Don't prompt for user input; assume the answer to "
- "every question is 'yes'.")
- parser.add_option('-H', '--host', metavar="ADDRESS", default="0.0.0.0",
- help="Address of heat API host. "
- "Default: %default")
- parser.add_option('-p', '--port', dest="port", metavar="PORT",
- type=int, default=config.DEFAULT_PORT,
- help="Port the heat API host listens on. "
- "Default: %default")
- parser.add_option('-U', '--url', metavar="URL", default=None,
- help="URL of heat service. This option can be used "
- "to specify the hostname, port and protocol "
- "(http/https) of the heat server, for example "
- "-U https://localhost:" + str(config.DEFAULT_PORT) +
- "/v1 Default: No<F3>ne")
- parser.add_option('-k', '--insecure', dest="insecure",
- default=False, action="store_true",
- help="Explicitly allow heat to perform \"insecure\" "
- "SSL (https) requests. The server's certificate will "
- "not be verified against any certificate authorities. "
- "This option should be used with caution.")
- parser.add_option('-A', '--auth_token', dest="auth_token",
- metavar="TOKEN", default=None,
- help="Authentication token to use to identify the "
- "client to the heat server")
- parser.add_option('-I', '--username', dest="username",
- metavar="USER", default=None,
- help="User name used to acquire an authentication token")
- parser.add_option('-K', '--password', dest="password",
- metavar="PASSWORD", default=None,
- help="Password used to acquire an authentication token")
- parser.add_option('-T', '--tenant', dest="tenant",
- metavar="TENANT", default=None,
- help="Tenant name used for Keystone authentication")
- parser.add_option('-R', '--region', dest="region",
- metavar="REGION", default=None,
- help="Region name. When using keystone authentication "
- "version 2.0 or later this identifies the region "
- "name to use when selecting the service endpoint. A "
- "region name must be provided if more than one "
- "region endpoint is available")
- parser.add_option('-N', '--auth_url', dest="auth_url",
- metavar="AUTH_URL", default=None,
- help="Authentication URL")
- parser.add_option('-S', '--auth_strategy', dest="auth_strategy",
- metavar="STRATEGY", default=None,
- help="Authentication strategy (keystone or noauth)")
-
- parser.add_option('-u', '--template-url', metavar="template_url",
- default=None, help="URL of template. Default: None")
- parser.add_option('-f', '--template-file', metavar="template_file",
- default=None, help="Path to the template. Default: None")
- parser.add_option('-t', '--timeout', metavar="timeout",
- default='60',
- help='Stack creation timeout in minutes. Default: 60')
-
- parser.add_option('-P', '--parameters', metavar="parameters", default=None,
- help="Parameter values used to create the stack.")
-
-
-def credentials_from_env():
- return dict(username=os.getenv('OS_USERNAME'),
- password=os.getenv('OS_PASSWORD'),
- tenant=os.getenv('OS_TENANT_NAME'),
- auth_url=os.getenv('OS_AUTH_URL'),
- auth_strategy=os.getenv('OS_AUTH_STRATEGY'))
-
-
-def parse_options(parser, cli_args):
- """
- Returns the parsed CLI options, command to run and its arguments, merged
- with any same-named options found in a configuration file
-
- :param parser: The option parser
- """
- if not cli_args:
- cli_args.append('-h') # Show options in usage output...
-
- (options, args) = parser.parse_args(cli_args)
- env_opts = credentials_from_env()
- for option, env_val in env_opts.items():
- if not getattr(options, option):
- setattr(options, option, env_val)
-
- if options.url is not None:
- u = urlparse(options.url)
- options.port = u.port
- options.host = u.hostname
-
- if not options.auth_strategy:
- options.auth_strategy = 'noauth'
-
- options.use_ssl = (options.url is not None and u.scheme == 'https')
-
- # HACK(sirp): Make the parser available to the print_help method
- # print_help is a command, so it only accepts (options, args); we could
- # one-off have it take (parser, options, args), however, for now, I think
- # this little hack will suffice
- options.__parser = parser
-
- if not args:
- parser.print_usage()
- sys.exit(0)
-
- command_name = args.pop(0)
- command = lookup_command(parser, command_name)
-
- if options.debug:
- logging.basicConfig(format='%(levelname)s:%(message)s',
- level=logging.DEBUG)
- logging.debug("Debug level logging enabled")
- elif options.verbose:
- logging.basicConfig(format='%(levelname)s:%(message)s',
- level=logging.INFO)
- else:
- logging.basicConfig(format='%(levelname)s:%(message)s',
- level=logging.WARNING)
-
- return (options, command, args)
-
-
-def print_help(options, args):
- """
- Print help specific to a command
- """
- parser = options.__parser
-
- if not args:
- parser.print_usage()
-
- subst = {'prog': os.path.basename(sys.argv[0])}
- docs = [lookup_command(parser, cmd).__doc__ % subst for cmd in args]
- print '\n\n'.join(docs)
-
-
-def lookup_command(parser, command_name):
- base_commands = {'help': print_help}
-
- stack_commands = {
- 'create': stack_create,
- 'update': stack_update,
- 'delete': stack_delete,
- 'list': stack_list,
- '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,
- 'describe': stack_describe}
-
- commands = {}
- for command_set in (base_commands, stack_commands):
- commands.update(command_set)
-
- try:
- command = commands[command_name]
- except KeyError:
- parser.print_usage()
- sys.exit("Unknown command: %s" % command_name)
-
- return command
-
-
-def main():
- '''
- '''
- usage = """
-%prog <command> [options] [args]
-
-Commands:
-
- help <command> Output help for one of the commands below
-
- create Create the stack
-
- delete Delete the stack
-
- describe Describe the stack
-
- update Update the stack
-
- list List the user's stacks
-
- gettemplate Get the template
-
- estimate-template-cost Returns the estimated monthly cost of a template
-
- validate Validate a template
-
- 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'
- % version.version_string(),
- usage=usage.strip())
- create_options(oparser)
- (opts, cmd, args) = parse_options(oparser, sys.argv[1:])
-
- try:
- start_time = time.time()
- result = cmd(opts, args)
- end_time = time.time()
- logging.debug("Completed in %-0.4f sec." % (end_time - start_time))
- sys.exit(result)
- except (RuntimeError,
- NotImplementedError,
- exception.ClientConfigurationError), ex:
- oparser.print_usage()
- logging.error("ERROR: " % ex)
- sys.exit(1)
-
-
-if __name__ == '__main__':
- main()
--- /dev/null
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+#
+# 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.
+
+"""
+Client implementation based on the boto AWS client library
+"""
+
+from heat.openstack.common import log as logging
+logger = logging.getLogger(__name__)
+
+from boto.cloudformation import CloudFormationConnection
+
+
+class BotoClient(CloudFormationConnection):
+
+ def list_stacks(self, **kwargs):
+ return super(BotoClient, self).list_stacks()
+
+ def describe_stacks(self, **kwargs):
+ try:
+ stack_name = kwargs['StackName']
+ except KeyError:
+ stack_name = None
+ return super(BotoClient, self).describe_stacks(stack_name)
+
+ def create_stack(self, **kwargs):
+ if 'TemplateUrl' in kwargs:
+ return super(BotoClient, self).create_stack(kwargs['StackName'],
+ template_url=kwargs['TemplateUrl'],
+ parameters=kwargs['Parameters'])
+ elif 'TemplateBody' in kwargs:
+ return super(BotoClient, self).create_stack(kwargs['StackName'],
+ template_body=kwargs['TemplateBody'],
+ parameters=kwargs['Parameters'])
+ else:
+ logger.error("Must specify TemplateUrl or TemplateBody!")
+
+ def update_stack(self, **kwargs):
+ if 'TemplateUrl' in kwargs:
+ return super(BotoClient, self).update_stack(kwargs['StackName'],
+ template_url=kwargs['TemplateUrl'],
+ parameters=kwargs['Parameters'])
+ elif 'TemplateBody' in kwargs:
+ return super(BotoClient, self).update_stack(kwargs['StackName'],
+ template_body=kwargs['TemplateBody'],
+ parameters=kwargs['Parameters'])
+ else:
+ logger.error("Must specify TemplateUrl or TemplateBody!")
+
+ def delete_stack(self, **kwargs):
+ return super(BotoClient, self).delete_stack(kwargs['StackName'])
+
+ def list_stack_events(self, **kwargs):
+ return super(BotoClient, self).describe_stack_events(
+ kwargs['StackName'])
+
+ def describe_stack_resource(self, **kwargs):
+ return super(BotoClient, self).describe_stack_resource(
+ kwargs['StackName'], kwargs['LogicalResourceId'])
+
+ def describe_stack_resources(self, **kwargs):
+ # Check if this is a StackName, if not assume it's a physical res ID
+ # Note this is slower for the common case, which is probably StackName
+ # than just doing a try/catch over the StackName case then retrying
+ # on failure with kwargs['NameOrPid'] as the physical resource ID,
+ # however boto spews errors when raising an exception so we can't
+ list_stacks = self.list_stacks()
+ stack_names = [s.stack_name for s in list_stacks]
+ if kwargs['NameOrPid'] in stack_names:
+ logger.debug("Looking up resources for StackName:%s" %
+ kwargs['NameOrPid'])
+ return super(BotoClient, self).describe_stack_resources(
+ stack_name_or_id=kwargs['NameOrPid'],
+ logical_resource_id=kwargs['LogicalResourceId'])
+ else:
+ logger.debug("Looking up resources for PhysicalResourceId:%s" %
+ kwargs['NameOrPid'])
+ return super(BotoClient, self).describe_stack_resources(
+ stack_name_or_id=None,
+ logical_resource_id=kwargs['LogicalResourceId'],
+ physical_resource_id=kwargs['NameOrPid'])
+
+ def list_stack_resources(self, **kwargs):
+ return super(BotoClient, self).list_stack_resources(
+ kwargs['StackName'])
+
+ def validate_template(self, **kwargs):
+ if 'TemplateUrl' in kwargs:
+ return super(BotoClient, self).validate_template(
+ template_url=kwargs['TemplateUrl'])
+ elif 'TemplateBody' in kwargs:
+ return super(BotoClient, self).validate_template(
+ template_body=kwargs['TemplateBody'])
+ else:
+ logger.error("Must specify TemplateUrl or TemplateBody!")
+
+ def get_template(self, **kwargs):
+ return super(BotoClient, self).get_template(kwargs['StackName'])
+
+ def estimate_template_cost(self, **kwargs):
+ if 'TemplateUrl' in kwargs:
+ return super(BotoClient, self).estimate_template_cost(
+ kwargs['StackName'],
+ template_url=kwargs['TemplateUrl'],
+ parameters=kwargs['Parameters'])
+ elif 'TemplateBody' in kwargs:
+ return super(BotoClient, self).estimate_template_cost(
+ kwargs['StackName'],
+ template_body=kwargs['TemplateBody'],
+ parameters=kwargs['Parameters'])
+ else:
+ logger.error("Must specify TemplateUrl or TemplateBody!")
+
+ def format_stack_event(self, events):
+ '''
+ Return string formatted representation of
+ boto.cloudformation.stack.StackEvent objects
+ '''
+ ret = []
+ for event in events:
+ ret.append("EventId : %s" % event.event_id)
+ ret.append("LogicalResourceId : %s" % event.logical_resource_id)
+ ret.append("PhysicalResourceId : %s" % event.physical_resource_id)
+ ret.append("ResourceProperties : %s" % event.resource_properties)
+ ret.append("ResourceStatus : %s" % event.resource_status)
+ ret.append("ResourceStatusReason : %s" %
+ event.resource_status_reason)
+ ret.append("ResourceType : %s" % event.resource_type)
+ ret.append("StackId : %s" % event.stack_id)
+ ret.append("StackName : %s" % event.stack_name)
+ ret.append("Timestamp : %s" % event.timestamp)
+ ret.append("--")
+ return '\n'.join(ret)
+
+ def format_stack(self, stacks):
+ '''
+ Return string formatted representation of
+ boto.cloudformation.stack.Stack objects
+ '''
+ ret = []
+ for s in stacks:
+ ret.append("Capabilities : %s" % s.capabilities)
+ ret.append("CreationTime : %s" % s.creation_time)
+ ret.append("Description : %s" % s.description)
+ ret.append("DisableRollback : %s" % s.disable_rollback)
+ ret.append("NotificationARNs : %s" % s.notification_arns)
+ ret.append("Outputs : %s" % s.outputs)
+ ret.append("Parameters : %s" % s.parameters)
+ ret.append("StackId : %s" % s.stack_id)
+ ret.append("StackName : %s" % s.stack_name)
+ ret.append("StackStatus : %s" % s.stack_status)
+ ret.append("StackStatusReason : %s" % s.stack_status_reason)
+ ret.append("TimeoutInMinutes : %s" % s.timeout_in_minutes)
+ ret.append("--")
+ return '\n'.join(ret)
+
+ def format_stack_resource(self, resources):
+ '''
+ Return string formatted representation of
+ boto.cloudformation.stack.StackResource objects
+ '''
+ ret = []
+ for res in resources:
+ ret.append("LogicalResourceId : %s" % res.logical_resource_id)
+ ret.append("PhysicalResourceId : %s" % res.physical_resource_id)
+ ret.append("ResourceStatus : %s" % res.resource_status)
+ ret.append("ResourceStatusReason : %s" %
+ res.resource_status_reason)
+ ret.append("ResourceType : %s" % res.resource_type)
+ ret.append("StackId : %s" % res.stack_id)
+ ret.append("StackName : %s" % res.stack_name)
+ ret.append("Timestamp : %s" % res.timestamp)
+ ret.append("--")
+ return '\n'.join(ret)
+
+ def format_stack_resource_summary(self, resources):
+ '''
+ Return string formatted representation of
+ boto.cloudformation.stack.StackResourceSummary objects
+ '''
+ ret = []
+ for res in resources:
+ ret.append("LastUpdatedTimestamp : %s" %
+ res.last_updated_timestamp)
+ ret.append("LogicalResourceId : %s" % res.logical_resource_id)
+ ret.append("PhysicalResourceId : %s" % res.physical_resource_id)
+ ret.append("ResourceStatus : %s" % res.resource_status)
+ ret.append("ResourceStatusReason : %s" %
+ res.resource_status_reason)
+ ret.append("ResourceType : %s" % res.resource_type)
+ ret.append("--")
+ return '\n'.join(ret)
+
+ def format_stack_summary(self, summaries):
+ '''
+ Return string formatted representation of
+ boto.cloudformation.stack.StackSummary objects
+ '''
+ ret = []
+ for s in summaries:
+ ret.append("StackId : %s" % s.stack_id)
+ ret.append("StackName : %s" % s.stack_name)
+ ret.append("CreationTime : %s" % s.creation_time)
+ ret.append("StackStatus : %s" % s.stack_status)
+ ret.append("TemplateDescription : %s" % s.template_description)
+ ret.append("--")
+ return '\n'.join(ret)
+
+ def format_template(self, template):
+ '''
+ String formatted representation of
+ boto.cloudformation.template.Template object
+ '''
+ ret = []
+ ret.append("Description : %s" % template.description)
+ for p in template.template_parameters:
+ ret.append("Parameter : ")
+ ret.append(" NoEcho : %s" % p.no_echo)
+ ret.append(" Description : %s" % p.description)
+ ret.append(" ParameterKey : %s" % p.parameter_key)
+ ret.append("--")
+ return '\n'.join(ret)
+
+ def format_parameters(self, options):
+ '''
+ Returns a dict containing list-of-tuple format
+ as expected by boto for request parameters
+ '''
+ parameters = {}
+ params = []
+ if options.parameters:
+ for p in options.parameters.split(';'):
+ (n, v) = p.split('=')
+ params.append((n, v))
+ parameters['Parameters'] = params
+ return parameters
+
+
+def get_client(host, port=None, username=None,
+ password=None, tenant=None,
+ auth_url=None, auth_strategy=None,
+ auth_token=None, region=None,
+ is_silent_upload=False, insecure=True):
+
+ """
+ Returns a new boto client object to a heat server
+ """
+
+ # Note we pass None/None for the keys so boto reads /etc/boto.cfg
+ # Also note is_secure is defaulted to False as HTTPS connections
+ # don't seem to work atm, FIXME
+ cloudformation = BotoClient(aws_access_key_id=None,
+ aws_secret_access_key=None, is_secure=False,
+ port=port, path="/v1")
+ if cloudformation:
+ logger.debug("Got CF connection object OK")
+ else:
+ logger.error("Error establishing connection!")
+ sys.exit(1)
+
+ return cloudformation