2e000a6d28
The --host option cannot work for heat-boto, so print a helpful error if the user tries to use it. ref bug 1102101 Change-Id: I63129b314eabd7bc84647efb96e9f10b1963b8b2
683 lines
23 KiB
Python
Executable File
683 lines
23 KiB
Python
Executable File
#!/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 logging
|
|
|
|
import httplib
|
|
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)
|
|
|
|
if scriptname == 'heat-boto':
|
|
from heat.cfn_client import boto_client as heat_client
|
|
else:
|
|
from heat.cfn_client import client as heat_client
|
|
from heat.version import version_info as version
|
|
from heat.common import config
|
|
from heat.common import exception
|
|
from heat.cfn_client import utils
|
|
from keystoneclient.v2_0 import client
|
|
|
|
|
|
def get_swift_template(options):
|
|
'''
|
|
Retrieve a template from the swift object store, using
|
|
the provided URL. We request a keystone token to authenticate
|
|
'''
|
|
template_body = None
|
|
if options.auth_strategy == 'keystone':
|
|
# we use the keystone credentials to get a token
|
|
# to pass in the request header
|
|
keystone = client.Client(username=options.username,
|
|
password=options.password,
|
|
tenant_name=options.tenant,
|
|
auth_url=options.auth_url)
|
|
logging.info("Getting template from swift URL: %s" %
|
|
options.template_object)
|
|
url = urlparse(options.template_object)
|
|
if url.scheme == 'https':
|
|
conn = httplib.HTTPSConnection(url.netloc)
|
|
else:
|
|
conn = httplib.HTTPConnection(url.netloc)
|
|
headers = {'X-Auth-Token': keystone.auth_token}
|
|
conn.request("GET", url.path, headers=headers)
|
|
r1 = conn.getresponse()
|
|
logging.info('status %d' % r1.status)
|
|
if r1.status == 200:
|
|
template_body = r1.read()
|
|
conn.close()
|
|
else:
|
|
logging.error("template-object option requires keystone")
|
|
|
|
return template_body
|
|
|
|
|
|
def get_template_param(options):
|
|
'''
|
|
Helper function to extract the template in whatever
|
|
format has been specified by the cli options
|
|
'''
|
|
param = {}
|
|
if options.template_file:
|
|
param['TemplateBody'] = open(options.template_file).read()
|
|
elif options.template_url:
|
|
param['TemplateUrl'] = options.template_url
|
|
elif options.template_object:
|
|
template_body = get_swift_template(options)
|
|
if template_body:
|
|
param['TemplateBody'] = template_body
|
|
else:
|
|
logging.error("Error reading swift template")
|
|
|
|
return param
|
|
|
|
|
|
@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-cfn 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 = {}
|
|
templ_param = get_template_param(options)
|
|
if templ_param:
|
|
parameters.update(templ_param)
|
|
else:
|
|
logging.error('Please specify a template file or url')
|
|
return utils.FAILURE
|
|
|
|
c = get_client(options)
|
|
parameters.update(c.format_parameters(options))
|
|
result = c.validate_template(**parameters)
|
|
print c.format_template(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
|
|
|
|
templ_param = get_template_param(options)
|
|
if templ_param:
|
|
parameters.update(templ_param)
|
|
else:
|
|
logging.error('Please specify a template file or url')
|
|
return utils.FAILURE
|
|
|
|
c = get_client(options)
|
|
parameters.update(c.format_parameters(options))
|
|
result = c.estimate_template_cost(**parameters)
|
|
print result
|
|
|
|
|
|
@utils.catch_error('gettemplate')
|
|
def get_template(options, arguments):
|
|
'''
|
|
Gets an existing stack template.
|
|
'''
|
|
parameters = {}
|
|
try:
|
|
parameters['StackName'] = arguments.pop(0)
|
|
except IndexError:
|
|
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(**parameters)
|
|
print result
|
|
|
|
|
|
@utils.catch_error('create')
|
|
def stack_create(options, arguments):
|
|
'''
|
|
Create a new stack from a template.
|
|
|
|
Usage: heat-cfn 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.
|
|
'''
|
|
|
|
parameters = {}
|
|
try:
|
|
parameters['StackName'] = arguments.pop(0)
|
|
except IndexError:
|
|
logging.error("Please specify the stack name you wish to create")
|
|
logging.error("as the first argument")
|
|
return utils.FAILURE
|
|
|
|
parameters['TimeoutInMinutes'] = options.timeout
|
|
|
|
if options.enable_rollback:
|
|
parameters['DisableRollback'] = 'False'
|
|
|
|
templ_param = get_template_param(options)
|
|
if templ_param:
|
|
parameters.update(templ_param)
|
|
else:
|
|
logging.error('Please specify a template file or url')
|
|
return utils.FAILURE
|
|
|
|
c = get_client(options)
|
|
parameters.update(c.format_parameters(options))
|
|
result = c.create_stack(**parameters)
|
|
print result
|
|
|
|
|
|
@utils.catch_error('update')
|
|
def stack_update(options, arguments):
|
|
'''
|
|
Update an existing stack.
|
|
|
|
Usage: heat-cfn 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 = {}
|
|
try:
|
|
parameters['StackName'] = arguments.pop(0)
|
|
except IndexError:
|
|
logging.error("Please specify the stack name you wish to update")
|
|
logging.error("as the first argument")
|
|
return utils.FAILURE
|
|
|
|
templ_param = get_template_param(options)
|
|
if templ_param:
|
|
parameters.update(templ_param)
|
|
else:
|
|
logging.error('Please specify a template file or url')
|
|
return utils.FAILURE
|
|
|
|
c = get_client(options)
|
|
parameters.update(c.format_parameters(options))
|
|
result = c.update_stack(**parameters)
|
|
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-cfn delete <stack name>
|
|
'''
|
|
parameters = {}
|
|
try:
|
|
parameters['StackName'] = arguments.pop(0)
|
|
except IndexError:
|
|
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(**parameters)
|
|
print result
|
|
|
|
|
|
@utils.catch_error('describe')
|
|
def stack_describe(options, arguments):
|
|
'''
|
|
Describes an existing stack.
|
|
|
|
Usage: heat-cfn describe <stack name>
|
|
'''
|
|
parameters = {}
|
|
try:
|
|
parameters['StackName'] = arguments.pop(0)
|
|
except IndexError:
|
|
logging.info("No stack name passed, getting results for ALL stacks")
|
|
|
|
c = get_client(options)
|
|
result = c.describe_stacks(**parameters)
|
|
print c.format_stack(result)
|
|
|
|
|
|
@utils.catch_error('event-list')
|
|
def stack_events_list(options, arguments):
|
|
'''
|
|
List events associated with the given stack.
|
|
|
|
Usage: heat-cfn event-list <stack name>
|
|
'''
|
|
parameters = {}
|
|
try:
|
|
parameters['StackName'] = arguments.pop(0)
|
|
except IndexError:
|
|
pass
|
|
|
|
c = get_client(options)
|
|
result = c.list_stack_events(**parameters)
|
|
print c.format_stack_event(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 c.format_stack_resource_detail(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 c.format_stack_resource_summary(result)
|
|
|
|
|
|
@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
|
|
parameters = {
|
|
'NameOrPid': name_or_pid,
|
|
'LogicalResourceId': logical_resource_id, }
|
|
|
|
c = get_client(options)
|
|
result = c.describe_stack_resources(**parameters)
|
|
|
|
if result:
|
|
print c.format_stack_resource(result)
|
|
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-cfn list
|
|
'''
|
|
c = get_client(options)
|
|
result = c.list_stacks()
|
|
print c.format_stack_summary(result)
|
|
|
|
|
|
def get_client(options):
|
|
"""
|
|
Returns a new client object to a heat server
|
|
specified by the --host and --port options
|
|
supplied to the CLI
|
|
Note options.host is ignored for heat-boto, host must be
|
|
set in your boto config via the cfn_region_endpoint option
|
|
"""
|
|
return heat_client.get_client(host=options.host,
|
|
port=options.port,
|
|
username=options.username,
|
|
password=options.password,
|
|
tenant=options.tenant,
|
|
auth_url=options.auth_url,
|
|
auth_strategy=options.auth_strategy,
|
|
auth_token=options.auth_token,
|
|
region=options.region,
|
|
insecure=options.insecure)
|
|
|
|
|
|
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=None,
|
|
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('-o', '--template-object',
|
|
metavar="template_object", default=None,
|
|
help="URL to retrieve template object (e.g from swift)")
|
|
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.")
|
|
|
|
parser.add_option('-r', '--enable-rollback', dest="enable_rollback",
|
|
default=False, action="store_true",
|
|
help="Enable rollback on failure")
|
|
|
|
|
|
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 scriptname == 'heat-boto':
|
|
if options.host is not None:
|
|
logging.error("Use boto.cfg or ~/.boto cfn_region_endpoint")
|
|
raise ValueError("--host option not supported by heat-boto")
|
|
if options.url is not None:
|
|
logging.error("Use boto.cfg or ~/.boto cfn_region_endpoint")
|
|
raise ValueError("--url option not supported by heat-boto")
|
|
|
|
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,
|
|
'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,
|
|
'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
|
|
|
|
"""
|
|
|
|
version_string = version.version_string()
|
|
oparser = optparse.OptionParser(version=version_string,
|
|
usage=usage.strip())
|
|
create_options(oparser)
|
|
try:
|
|
(opts, cmd, args) = parse_options(oparser, sys.argv[1:])
|
|
except ValueError as ex:
|
|
logging.error("Error parsing options : %s" % str(ex))
|
|
sys.exit(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: %s" % ex)
|
|
sys.exit(1)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|