heat/bin/heat

629 lines
22 KiB
Python
Executable File

#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
"""
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 functools
import gettext
import optparse
import os
import sys
import time
import json
import base64
import libxml2
import re
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:
jeos_path = '%s/heat/%s/' % (sys.path[1], "jeos")
cfntools_path = '%s/heat/%s/' % (sys.path[1], "cfntools")
gettext.install('heat', unicode=1)
from heat import client as heat_client
from heat.common import exception
from heat.common import config
from heat import version
from glance import client as glance_client
from distutils.sysconfig import get_python_lib
SUCCESS = 0
FAILURE = 1
def catch_error(action):
"""Decorator to provide sensible default error handling for actions."""
def wrap(func):
@functools.wraps(func)
def wrapper(*arguments, **kwargs):
try:
ret = func(*arguments, **kwargs)
return SUCCESS if ret is None else ret
except exception.NotAuthorized:
print "Not authorized to make this request. Check "\
"your credentials (OS_USERNAME, OS_PASSWORD, "\
"OS_TENANT_NAME, OS_AUTH_URL and OS_AUTH_STRATEGY)."
return FAILURE
except exception.ClientConfigurationError:
raise
except Exception, e:
options = arguments[0]
if options.debug:
raise
print "Failed to %s. Got error:" % action
pieces = unicode(e).split('\n')
for piece in pieces:
print piece
return FAILURE
return wrapper
return wrap
@catch_error('validate')
def template_validate(options, arguments):
'''
'''
parameters = {}
if options.template_file:
parameters['TemplateBody'] = open(options.template_file).read()
elif options.template_url:
parameters['TemplateUrl'] = options.template_url
else:
print 'Please specify a template file or url'
return FAILURE
client = get_client(options)
result = client.validate_template(**parameters)
print json.dumps(result, indent=2)
@catch_error('gettemplate')
def get_template(options, arguments):
'''
'''
pass
@catch_error('create')
def stack_create(options, arguments):
'''
'''
parameters = {}
try:
parameters['StackName'] = arguments.pop(0)
except IndexError:
print "Please specify the stack name you wish to create "
print "as the first argument"
return 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:
print 'Please specify a template file or url'
return FAILURE
c = get_client(options)
result = c.create_stack(**parameters)
print json.dumps(result, indent=2)
@catch_error('update')
def stack_update(options, arguments):
'''
'''
parameters = {}
try:
parameters['StackName'] = arguments.pop(0)
except IndexError:
print "Please specify the stack name you wish to update "
print "as the first argument"
return 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)
@catch_error('delete')
def stack_delete(options, arguments):
'''
'''
parameters = {}
try:
parameters['StackName'] = arguments.pop(0)
except IndexError:
print "Please specify the stack name you wish to delete "
print "as the first argument"
return FAILURE
c = get_client(options)
result = c.delete_stack(**parameters)
print json.dumps(result, indent=2)
@catch_error('describe')
def stack_describe(options, arguments):
'''
'''
parameters = {}
try:
parameters['StackName'] = arguments.pop(0)
except IndexError:
print "Please specify the stack name you wish to describe "
print "as the first argument"
return FAILURE
c = get_client(options)
result = c.describe_stacks(**parameters)
print json.dumps(result, indent=2)
@catch_error('events_list')
def stack_events_list(options, arguments):
'''
'''
parameters = {}
try:
parameters['StackName'] = arguments.pop(0)
except IndexError:
print "Please specify the stack name "
print "as the first argument"
return FAILURE
c = get_client(options)
result = c.list_stack_events(**parameters)
print json.dumps(result, indent=2)
@catch_error('list')
def stack_list(options, arguments):
'''
'''
c = get_client(options)
result = c.list_stacks()
print json.dumps(result, indent=2)
@catch_error('jeos_create')
def jeos_create(options, arguments):
'''
The jeos_create option must be run as root. This command takes three
arguments that are popped from the arguments parameter.
arg0 -> Distribution such as 'F16', 'F17', 'U10', 'D6'
arg1 -> Architecture such as 'i386' 'i686' or 'x86_64'
arg2 -> 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.
'''
global jeos_path
global cfntools_path
# if not running as root, return EPERM to command line
if os.geteuid() != 0:
print "jeos_create must be run as root"
sys.exit(1)
if len(arguments) < 3:
print '\n Please provide the distro, arch, and instance type.'
print ' Usage:'
print ' heat jeos_create <distro> <arch> <instancetype>'
print ' instance type can be:'
print ' gold builds a base image where userdata is used to initialize the instance'
print ' cfntools builds a base image where AWS CloudFormations tools are present'
sys.exit(1)
distro = arguments.pop(0)
arch = arguments.pop(0)
instance_type = arguments.pop(0)
arches = ('x86_64', 'i386', 'amd64')
arches_str = " | ".join(arches)
instance_types = ('gold', 'cfntools')
instances_str = " | ".join(instance_types)
if not arch in arches:
print 'arch %s not supported' % arch
print 'try: heat jeos_create %s [ %s ]' % (distro, arches_str)
sys.exit(1)
if not instance_type in instance_types:
print 'A JEOS instance type of %s not supported' % instance_type
print 'try: heat jeos_create %s %s [ %s ]' % (distro, arch, instances_str)
sys.exit(1)
fedora_match = re.match('F(1[6-7])', distro)
if fedora_match:
version = fedora_match.group(1)
iso = '/var/lib/libvirt/images/Fedora-%s-%s-DVD.iso' % (version, arch)
elif distro == 'U10':
iso = '/var/lib/libvirt/images/ubuntu-10.04.3-server-%s.iso' % arch
else:
print 'distro %s not supported' % distro
print 'try: F16, F17 or U10'
sys.exit(1)
if not os.access(iso, os.R_OK):
print '*** %s does not exist.' % (iso)
sys.exit(1)
tdl_path = '%s%s-%s-%s-jeos.tdl' % (jeos_path, distro, arch, instance_type)
# Load the cfntools into the cfntool image by encoding them in base64
# and injecting them into the TDL at the appropriate place
if instance_type == 'cfntools':
tdl_xml = libxml2.parseFile(tdl_path)
for cfnname in ['cfn-init', 'cfn-hup', 'cfn-signal']:
f = open('%s/%s' % (cfntools_path, cfnname), 'r')
cfscript_e64 = base64.b64encode(f.read())
f.close()
tdl_xml.xpathEval("/template/files/file[@name='/opt/aws/bin/%s']" % cfnname)[0].setContent(cfscript_e64)
# TODO(sdake) INSECURE
tdl_xml.saveFormatFile('/tmp/tdl', format=1)
tdl_path = '/tmp/tdl'
dsk_filename = '/var/lib/libvirt/images/%s-%s-%s-jeos.dsk' % (distro, arch, instance_type)
qcow2_filename = '/var/lib/libvirt/images/%s-%s-%s-jeos.qcow2' % (distro, arch, instance_type)
image_name = '%s-%s-%s' % (distro, arch, instance_type)
if not os.access(tdl_path, os.R_OK):
print 'The tdl for that disto/arch is not available'
sys.exit(1)
creds = dict(username=options.username,
password=options.password,
tenant=options.tenant,
auth_url=options.auth_url,
strategy=options.auth_strategy)
client = glance_client.Client(host="0.0.0.0", port=9292,
use_ssl=False, auth_tok=None, creds=creds)
parameters = {
"filters": {},
"limit": 10,
}
images = client.get_images(**parameters)
image_registered = False
for image in images:
if image['name'] == distro + '-' + arch + '-' + instance_type:
image_registered = True
#print ' *** image already in glance: %s > %s' % (image['name'], image['id'])
runoz = None
if os.access(qcow2_filename, os.R_OK):
while runoz not in ('y', 'n'):
runoz = raw_input('An existing JEOS was found on disk. Do you want to build a fresh JEOS? (y/n) ').lower()
if runoz == 'y':
os.remove(qcow2_filename)
os.remove(dsk_filename)
if image_registered:
client.delete_image(image['id'])
elif runoz == 'n':
answer = None
while answer not in ('y', 'n'):
answer = raw_input('Do you want to register your existing JEOS file with glance? (y/n) ').lower()
if answer == 'n':
print 'No action taken'
sys.exit(0)
elif answer == 'y' and image_registered:
answer = None
while answer not in ('y', 'n'):
answer = raw_input('Do you want to delete the existing JEOS in glance? (y/n) ').lower()
if answer == 'n':
print 'No action taken'
sys.exit(0)
elif answer == 'y':
client.delete_image(image['id'])
if runoz == None or runoz == 'y':
print 'Creating JEOS image (%s) - this takes approximately 10 minutes.' % image_name
extra_opts = ' '
if options.debug:
extra_opts = ' -d 3 '
res = os.system("oz-install %s -t 50000 -u %s -x /dev/null" % (extra_opts, tdl_path))
if res == 256:
sys.exit(1)
if not os.access(dsk_filename, os.R_OK):
print 'oz-install did not create the image, check your oz installation.'
sys.exit(1)
print 'Converting raw disk image to a qcow2 image.'
os.system("qemu-img convert -O qcow2 %s %s" % (dsk_filename, qcow2_filename))
print 'Registering JEOS image (%s) with OpenStack Glance.' % image_name
image_meta = {'name': image_name,
'is_public': True,
'disk_format': 'qcow2',
'min_disk': 0,
'min_ram': 0,
'owner': options.username,
'container_format': 'bare'}
try:
with open(qcow2_filename) as ifile:
image_meta = client.add_image(image_meta, ifile)
image_id = image_meta['id']
print " Added new image with ID: %s" % image_id
print " Returned the following metadata for the new image:"
for k, v in sorted(image_meta.items()):
print " %(k)30s => %(v)s" % locals()
except exception.ClientConnectionError, e:
print (" Failed to connect to the Glance API server."
" Is the server running?" % locals())
pieces = unicode(e).split('\n')
for piece in pieces:
print piece
sys.exit(1)
except Exception, e:
print " Failed to add image. Got error:"
pieces = unicode(e).split('\n')
for piece in pieces:
print piece
print (" Note: Your image metadata may still be in the registry, "
"but the image's status will likely be 'killed'.")
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('-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)
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()
if opts.verbose:
print "Completed in %-0.4f sec." % (end_time - start_time)
sys.exit(result)
except (RuntimeError,
NotImplementedError,
exception.ClientConfigurationError), ex:
oparser.print_usage()
print >> sys.stderr, "ERROR: ", ex
sys.exit(1)
if __name__ == '__main__':
main()