heat cli : Rework to separate cli tool from client-API wrappers

Rework to remove duplication between heat and heat-boto, and to
provide better separation between the CLI tool logic and the
underlying client API (should allow easier porting to new ReST API)

Ref #175 (partially fixes)
Fixes #192

Change-Id: Ib1f821667c40c78770a345204af923163daeffae
Signed-off-by: Steven Hardy <shardy@redhat.com>
This commit is contained in:
Steven Hardy 2012-08-15 14:09:54 +01:00
parent 619239527f
commit 5aa80047b6
4 changed files with 349 additions and 800 deletions

View File

@ -42,23 +42,16 @@ scriptname = os.path.basename(sys.argv[0])
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):
'''
@ -81,11 +74,10 @@ 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')
@ -98,8 +90,6 @@ def estimate_template_cost(options, arguments):
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:
@ -109,6 +99,7 @@ def estimate_template_cost(options, arguments):
return utils.FAILURE
c = get_client(options)
parameters.update(c.format_parameters(options))
result = c.estimate_template_cost(**parameters)
print result
@ -156,8 +147,6 @@ def stack_create(options, arguments):
logging.error("as the first argument")
return utils.FAILURE
parameters.update(format_parameters(options))
parameters['TimeoutInMinutes'] = options.timeout
if options.template_file:
@ -169,6 +158,7 @@ def stack_create(options, arguments):
return utils.FAILURE
c = get_client(options)
parameters.update(c.format_parameters(options))
result = c.create_stack(**parameters)
print result
@ -206,9 +196,8 @@ def stack_update(options, arguments):
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
@ -250,7 +239,7 @@ def stack_describe(options, arguments):
c = get_client(options)
result = c.describe_stacks(**parameters)
print result
print c.format_stack(result)
@utils.catch_error('event-list')
@ -268,7 +257,7 @@ def stack_events_list(options, arguments):
c = get_client(options)
result = c.list_stack_events(**parameters)
print result
print c.format_stack_event(result)
@utils.catch_error('resource')
@ -288,7 +277,7 @@ def stack_resource_show(options, arguments):
'LogicalResourceId': resource_name,
}
result = c.describe_stack_resource(**parameters)
print result
print c.format_stack_resource(result)
@utils.catch_error('resource-list')
@ -307,35 +296,7 @@ def stack_resources_list(options, arguments):
'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')
@ -357,26 +318,23 @@ def stack_resources_list_details(options, arguments):
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
@ -389,7 +347,7 @@ def stack_list(options, arguments):
'''
c = get_client(options)
result = c.list_stacks()
print result
print c.format_stack_summary(result)
def get_client(options):
@ -640,7 +598,7 @@ Commands:
NotImplementedError,
exception.ClientConfigurationError), ex:
oparser.print_usage()
logging.error("ERROR: " % ex)
logging.error("ERROR: %s" % ex)
sys.exit(1)

View File

@ -1,731 +0,0 @@
#!/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()

1
bin/heat-boto Symbolic link
View File

@ -0,0 +1 @@
heat

273
heat/boto_client.py Normal file
View File

@ -0,0 +1,273 @@
# 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

View File

@ -72,7 +72,21 @@ class V1Client(base_client.BaseClient):
return self.stack_request("DescribeStackResource", "GET", **kwargs)
def describe_stack_resources(self, **kwargs):
return self.stack_request("DescribeStackResources", "GET", **kwargs)
for lookup_key in ['StackName', 'PhysicalResourceId']:
lookup_value = kwargs['NameOrPid']
parameters = {
lookup_key: lookup_value,
'LogicalResourceId': kwargs['LogicalResourceId']}
try:
result = self.stack_request("DescribeStackResources", "GET",
**parameters)
except:
logger.debug("Failed to lookup resource details with key %s:%s"
% (lookup_key, lookup_value))
else:
logger.debug("Got lookup resource details with key %s:%s" %
(lookup_key, lookup_value))
return result
def list_stack_resources(self, **kwargs):
return self.stack_request("ListStackResources", "GET", **kwargs)
@ -86,6 +100,40 @@ class V1Client(base_client.BaseClient):
def estimate_template_cost(self, **kwargs):
return self.stack_request("EstimateTemplateCost", "GET", **kwargs)
# Dummy print functions for alignment with the boto-based client
# which has to extract class fields for printing, we could also
# align output format here by decoding the XML/JSON
def format_stack_event(self, event):
return str(event)
def format_stack(self, stack):
return str(stack)
def format_stack_resource(self, res):
return str(res)
def format_stack_resource_summary(self, res):
return str(res)
def format_stack_summary(self, summary):
return str(summary)
def format_template(self, template):
return str(template)
def format_parameters(self, options):
'''
Reformat parameters into dict of format expected by the API
'''
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
HeatClient = V1Client