631 lines
22 KiB
Python
Executable File
631 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:
|
|
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 glance import client as glance_client
|
|
from heat import client as heat_client
|
|
from heat import version
|
|
from heat.common import config
|
|
from heat.common import exception
|
|
|
|
|
|
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()
|