diff --git a/bin/heat-jeos b/bin/heat-jeos new file mode 100755 index 0000000..4af68d4 --- /dev/null +++ b/bin/heat-jeos @@ -0,0 +1,439 @@ +#!/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. + +""" +Tools to generate JEOS TDLs, build the images and register them in Glance. +""" + +import base64 +import gettext +import json +import logging +from lxml import etree +import optparse +import os +import os.path +import re +import sys +import time + + +from glance.common import exception +from glance import client as glance_client + + +# TODO(shadower): fix the cmdline options +# TODO(shadower): split the options to TDL/image/glance +# TODO(shadower): use Oz as a library, don't shell out +# TODO(shadower): remove jeos-create from the old binary, lib +# TODO(shadower): update the getting started guide +# TODO(shadower): update the tests + +# TODO(shadower): once this is in a separate repo/package, prolly needs fixing +possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]), + '..')) + +jeos_path = '' +cfntools_path = '' +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") + jeos_path = os.path.join(possible_topdir, 'jeos') + cfntools_path = os.path.join(possible_topdir, 'cfntools') +else: + for p in sys.path: + jeos_path = os.path.join(p, 'heat', 'jeos') + cfntools_path = os.path.join(p, 'heat', 'cfntools') + if os.access(jeos_path, os.R_OK): + break + + +def jeos_create(options, arguments): + ''' + Create a new JEOS (Just Enough Operating System) image. + + Usage: heat jeos-create + + 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. + ''' + # if not running as root, return EPERM to command line + if os.geteuid() != 0: + logging.error("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 ' + 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 CloudFormation' \ + ' tools are present' + sys.exit(1) + + distro = arguments.pop(0) + arch = arguments.pop(0) + instance_type = arguments.pop(0) + images_dir = '/var/lib/libvirt/images' + + arches = ('x86_64', 'i386', 'amd64') + arches_str = " | ".join(arches) + instance_types = ('gold', 'cfntools') + instances_str = " | ".join(instance_types) + + if not arch in arches: + logging.error('arch %s not supported' % arch) + logging.error('try: heat jeos-create %s [ %s ]' % (distro, arches_str)) + sys.exit(1) + + if not instance_type in instance_types: + logging.error('A JEOS instance type of %s not supported' %\ + instance_type) + logging.error('try: heat jeos-create %s %s [ %s ]' %\ + (distro, arch, instances_str)) + sys.exit(1) + + src_arch = 'i386' + fedora_match = re.match('F(1[6-7])', distro) + if fedora_match: + if arch == 'x86_64': + src_arch = 'x86_64' + version = fedora_match.group(1) + iso = '%s/Fedora-%s-%s-DVD.iso' % (images_dir, version, arch) + elif distro == 'U10': + if arch == 'amd64': + src_arch = 'x86_64' + iso = '%s/ubuntu-10.04.3-server-%s.iso' % (images_dir, arch) + else: + logging.error('distro %s not supported' % distro) + logging.error('try: F16, F17 or U10') + sys.exit(1) + + if not os.access(iso, os.R_OK): + logging.error('*** %s does not exist.' % (iso)) + sys.exit(1) + + tdl_file = '%s-%s-%s-jeos.tdl' % (distro, arch, instance_type) + tdl_path = os.path.join(jeos_path, tdl_file) + if options.debug: + print "Using tdl: %s" % tdl_path + + # 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 = etree.parse(tdl_path) + cfn_tools = ['cfn-init', 'cfn-hup', 'cfn-signal', + 'cfn-get-metadata', 'cfn_helper.py', 'cfn-push-stats'] + for cfnname in cfn_tools: + f = open('%s/%s' % (cfntools_path, cfnname), 'r') + cfscript_e64 = base64.b64encode(f.read()) + f.close() + cfnpath = "/template/files/file[@name='/opt/aws/bin/%s']" % cfnname + tdl_xml.xpath(cfnpath)[0].text = cfscript_e64 + + # TODO(sdake) INSECURE + tdl_xml.write('/tmp/tdl', xml_declaration=True) + tdl_path = '/tmp/tdl' + + dsk_filename = '%s/%s-%s-%s-jeos.dsk' % (images_dir, distro, + src_arch, instance_type) + qcow2_filename = '%s/%s-%s-%s-jeos.qcow2' % (images_dir, distro, + arch, instance_type) + image_name = '%s-%s-%s' % (distro, arch, instance_type) + + if not os.access(tdl_path, os.R_OK): + logging.error('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 + + runoz = options.yes and 'y' or 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': + logging.info('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': + logging.info('No action taken') + sys.exit(0) + elif answer == 'y': + client.delete_image(image['id']) + + if runoz == None or runoz == 'y': + logging.info('Creating JEOS image (%s) - '\ + 'this takes approximately 10 minutes.' % image_name) + extra_opts = ' ' + if options.debug: + extra_opts = ' -d 3 ' + + ozcmd = "oz-install %s -t 50000 -u %s -x /dev/null" % (extra_opts, + tdl_path) + logging.debug("Running : %s" % ozcmd) + res = os.system(ozcmd) + if res == 256: + sys.exit(1) + if not os.access(dsk_filename, os.R_OK): + logging.error('oz-install did not create the image,' \ + ' check your oz installation.') + sys.exit(1) + + logging.info('Converting raw disk image to a qcow2 image.') + os.system("qemu-img convert -c -O qcow2 %s %s" % (dsk_filename, + qcow2_filename)) + + logging.info('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'] + logging.debug(" Added new image with ID: %s" % image_id) + logging.debug(" Returned the following metadata for the new image:") + for k, v in sorted(image_meta.items()): + logging.debug(" %(k)30s => %(v)s" % locals()) + except exception.ClientConnectionError, e: + logging.error((" Failed to connect to the Glance API server." +\ + " Is the server running?" % locals())) + pieces = unicode(e).split('\n') + for piece in pieces: + logging.error(piece) + sys.exit(1) + except Exception, e: + logging.error(" Failed to add image. Got error:") + pieces = unicode(e).split('\n') + for piece in pieces: + logging.error(piece) + logging.warning(" Note: Your image metadata may still be in the " +\ + "registry, but the image's status will likely be 'killed'.") + + +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('-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)") + + +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 not options.auth_strategy: + options.auth_strategy = 'noauth' + + # 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 = { + '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 [options] [args] + +Commands: + + help Output help for one of the commands below + + jeos-create Create a JEOS image + +""" + + oparser = optparse.OptionParser(version='%%prog %s' + % '0.0.1', + 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), ex: + oparser.print_usage() + logging.error("ERROR: " % ex) + sys.exit(1) + + +if __name__ == '__main__': + main()