heat/bin/heat

526 lines
18 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 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)
jeos_path = '%s/heat/%s/' % (possible_topdir, "jeos")
cfntools_path = '%s/heat/%s/' % (possible_topdir, "cfntools")
else:
for p in sys.path:
if 'heat' in p:
jeos_path = '%s/heat/%s/' % (p, "jeos")
cfntools_path = '%s/heat/%s/' % (p, "cfntools")
break
gettext.install('heat', unicode=1)
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
@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 json.dumps(result, indent=2)
@utils.catch_error('gettemplate')
def get_template(options, arguments):
'''
Gets an existing stack template.
NOT YET IMPLEMENTED.
'''
pass
@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.
'''
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
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.create_stack(**parameters)
print json.dumps(result, indent=2)
@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 = {}
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
if options.template_file:
parameters['TemplateBody'] = open(options.template_file).read()
elif options.template_url:
parameters['TemplateUrl'] = options.template_url
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
c = get_client(options)
result = c.update_stack(**parameters)
print json.dumps(result, indent=2)
@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>
'''
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 json.dumps(result, indent=2)
@utils.catch_error('describe')
def stack_describe(options, arguments):
'''
Describes an existing stack.
Usage: heat describe <stack name>
'''
parameters = {}
try:
parameters['StackName'] = arguments.pop(0)
except IndexError:
logging.error("Please specify the stack name you wish to describe")
logging.error("as the first argument")
return utils.FAILURE
c = get_client(options)
result = c.describe_stacks(**parameters)
print json.dumps(result, indent=2)
@utils.catch_error('events_list')
def stack_events_list(options, arguments):
'''
List events associated with the given stack.
Usage: heat events_list <stack name>
'''
parameters = {}
try:
parameters['StackName'] = arguments.pop(0)
except IndexError:
pass
c = get_client(options)
result = c.list_stack_events(**parameters)
print json.dumps(result, indent=2)
@utils.catch_error('list')
def stack_list(options, arguments):
'''
List all running stacks.
Usage: heat list
'''
c = get_client(options)
result = c.list_stacks()
print json.dumps(result, indent=2)
@utils.catch_error('jeos_create')
def jeos_create(options, arguments):
'''
Create a new JEOS (Just Enough Operating System) image.
Usage: heat jeos_create <distribution> <architecture> <image type>
Distribution: Distribution such as 'F16', 'F17', 'U10', 'D6'.
Architecture: Architecture such as 'i386' 'i686' or 'x86_64'.
Image Type: Image type such as 'gold' or 'cfntools'.
'gold' is a basic gold JEOS.
'cfntools' contains the cfntools helper scripts.
The command must be run as root in order for libvirt to have permissions
to create virtual machines and read the raw DVDs.
'''
utils.jeos_create(options, arguments, jeos_path, cfntools_path)
def get_client(options):
"""
Returns a new client object to a heat server
specified by the --host and --port options
supplied to the CLI
"""
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="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('-t', '--template-file', metavar="template_file",
default=None, help="Path to the template. Default: None")
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
"""
if len(args) != 1:
sys.exit("Please specify a command")
parser = options.__parser
command_name = args.pop()
command = lookup_command(parser, command_name)
print command.__doc__ % {'prog': os.path.basename(sys.argv[0])}
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,
'validate': template_validate,
'gettemplate': get_template,
'describe': stack_describe,
'jeos_create': jeos_create}
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
validate Validate a template
jeos_create Create a JEOS image
events_list List events for 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()