Initial standalone heat-jeos bin
This commit is contained in:
439
bin/heat-jeos
Executable file
439
bin/heat-jeos
Executable file
@@ -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> <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.
|
||||
'''
|
||||
# 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 <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 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 <command> [options] [args]
|
||||
|
||||
Commands:
|
||||
|
||||
help <command> 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()
|
||||
Reference in New Issue
Block a user