From 1a15f6fd8dabb22cf32c41045d08c229a4962d9e Mon Sep 17 00:00:00 2001 From: Steve Baker Date: Fri, 21 Dec 2012 09:20:49 +1300 Subject: Reorganise project to contain stand-alone heat-cfntools --- bin/cfn-get-metadata | 91 +++++++++++++ bin/cfn-hup | 119 ++++++++++++++++ bin/cfn-init | 73 ++++++++++ bin/cfn-push-stats | 259 +++++++++++++++++++++++++++++++++++ bin/cfn-signal | 92 +++++++++++++ bin/heat-jeos | 375 --------------------------------------------------- 6 files changed, 634 insertions(+), 375 deletions(-) create mode 100755 bin/cfn-get-metadata create mode 100755 bin/cfn-hup create mode 100755 bin/cfn-init create mode 100755 bin/cfn-push-stats create mode 100755 bin/cfn-signal delete mode 100755 bin/heat-jeos (limited to 'bin') diff --git a/bin/cfn-get-metadata b/bin/cfn-get-metadata new file mode 100755 index 0000000..8d3af60 --- /dev/null +++ b/bin/cfn-get-metadata @@ -0,0 +1,91 @@ +#!/usr/bin/env python +# +# 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. + +""" +Implements cfn-get-metadata CloudFormation functionality +""" +import argparse +import io +import logging +import os +import os.path +import sys + + +if os.path.exists('/opt/aws/bin'): + sys.path.insert(0, '/opt/aws/bin') + from cfn_helper import * +else: + from heat_jeos.cfntools.cfn_helper import * + +description = " " +parser = argparse.ArgumentParser(description=description) +parser.add_argument('-s', '--stack', + dest="stack_name", + help="A Heat stack name", + required=True) +parser.add_argument('-r', '--resource', + dest="logical_resource_id", + help="A Heat logical resource ID", + required=True) +parser.add_argument('--access-key', + dest="access_key", + help="A Keystone access key", + required=False) +parser.add_argument('--secret-key', + dest="secret_key", + help="A Keystone secret key", + required=False) +parser.add_argument('--region', + dest="region", + help="Openstack region", + required=False) +parser.add_argument('--credential-file', + dest="credential_file", + help="credential-file", + required=False) +parser.add_argument('-u', '--url', + dest="url", + help="service url", + required=False) +parser.add_argument('-k', '--key', + dest="key", + help="key", + required=False) +args = parser.parse_args() + +if not args.stack_name: + print 'The Stack name must not be empty.' + exit(1) + +if not args.logical_resource_id: + print 'The Resource ID must not be empty' + exit(1) + +log_format = '%(levelname)s [%(asctime)s] %(message)s' +logging.basicConfig(format=log_format, level=logging.DEBUG) + +LOG = logging.getLogger('cfntools') +log_file_name = "/var/log/cfn-get-metadata.log" +file_handler = logging.FileHandler(log_file_name) +file_handler.setFormatter(logging.Formatter(log_format)) +LOG.addHandler(file_handler) + +metadata = Metadata(args.stack_name, + args.logical_resource_id, + access_key=args.access_key, + secret_key=args.secret_key, + region=args.region) +metadata.retrieve() +LOG.debug(str(metadata)) diff --git a/bin/cfn-hup b/bin/cfn-hup new file mode 100755 index 0000000..32b4d40 --- /dev/null +++ b/bin/cfn-hup @@ -0,0 +1,119 @@ +#!/usr/bin/env python +# +# 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. + +""" +Implements cfn-hup CloudFormation functionality +""" +import argparse +import io +import logging +import os +import os.path +import sys + + +if os.path.exists('/opt/aws/bin'): + sys.path.insert(0, '/opt/aws/bin') + from cfn_helper import * +else: + from heat_jeos.cfntools.cfn_helper import * + +description = " " +parser = argparse.ArgumentParser(description=description) +parser.add_argument('-c', '--config', + dest="config_dir", + help="Hook Config Directory", + required=False, + default='/etc/cfn/hooks.d') +parser.add_argument('-f', '--no-daemon', + dest="no_deamon", + action="store_true", + help="Do not run as a deamon", + required=False) +parser.add_argument('-v', '--verbose', + action="store_true", + dest="verbose", + help="Verbose logging", + required=False) +args = parser.parse_args() + +# Setup logging +log_format = '%(levelname)s [%(asctime)s] %(message)s' +log_file_name = "/var/log/cfn-hup.log" +logging.basicConfig(filename=log_file_name, + format=log_format, + level=logging.DEBUG) +if args.verbose: + logging.basicConfig(filename=log_file_name, + format=log_format, + level=logging.DEBUG) +else: + logging.basicConfig(filename=log_file_name, + format=log_format, + level=logging.INFO) + +LOG = logging.getLogger('cfntools') + +main_conf_path = '/etc/cfn/cfn-hup.conf' +try: + main_config_file = open(main_conf_path) +except IOError as exc: + LOG.error('Could not open main configuration at %s' % main_conf_path) + exit(1) + +config_files = [] +hooks_conf_path = '/etc/cfn/hooks.conf' +if os.path.exists(hooks_conf_path): + try: + config_files.append(open(hooks_conf_path)) + except IOError as exc: + LOG.exception(exc) + +if args.config_dir and os.path.exists(args.config_dir): + try: + for f in os.listdir(args.config_dir): + config_files.append(open(os.path.join(args.config_dir, f))) + + except OSError as exc: + LOG.exception(exc) + +if not config_files: + LOG.error('No hook files found at %s or %s' % (hooks_conf_path, + args.config_dir)) + exit(1) + +try: + mainconfig = HupConfig([main_config_file] + config_files) +except Exception as ex: + LOG.error('Cannot load configuration: %s' % str(ex)) + exit(1) + +if not mainconfig.unique_resources_get(): + LOG.error('No hooks were found. Add some to %s or %s' % (hooks_conf_path, + args.config_dir)) + exit(1) + + +for r in mainconfig.unique_resources_get(): + print r + metadata = Metadata(mainconfig.stack, + r, + credentials_file=mainconfig.credential_file, + region=mainconfig.region) + metadata.retrieve() + try: + metadata.cfn_hup(mainconfig.hooks) + except Exception as e: + LOG.exception("Error processing metadata") + exit(1) diff --git a/bin/cfn-init b/bin/cfn-init new file mode 100755 index 0000000..066e5ba --- /dev/null +++ b/bin/cfn-init @@ -0,0 +1,73 @@ +#!/usr/bin/python +# +# 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. + +""" +Implements cfn-init CloudFormation functionality +""" +import argparse +import logging +import os +import sys + + +if os.path.exists('/opt/aws/bin'): + sys.path.insert(0, '/opt/aws/bin') + from cfn_helper import * +else: + from heat_jeos.cfntools.cfn_helper import * + +description = " " +parser = argparse.ArgumentParser(description=description) +parser.add_argument('-s', '--stack', + dest="stack_name", + help="A Heat stack name", + required=False) +parser.add_argument('-r', '--resource', + dest="logical_resource_id", + help="A Heat logical resource ID", + required=False) +parser.add_argument('--access-key', + dest="access_key", + help="A Keystone access key", + required=False) +parser.add_argument('--secret-key', + dest="secret_key", + help="A Keystone secret key", + required=False) +parser.add_argument('--region', + dest="region", + help="Openstack region", + required=False) +args = parser.parse_args() + +log_format = '%(levelname)s [%(asctime)s] %(message)s' +logging.basicConfig(format=log_format, level=logging.DEBUG) + +LOG = logging.getLogger('cfntools') +log_file_name = "/var/log/cfn-init.log" +file_handler = logging.FileHandler(log_file_name) +file_handler.setFormatter(logging.Formatter(log_format)) +LOG.addHandler(file_handler) + +metadata = Metadata(args.stack_name, + args.logical_resource_id, + access_key=args.access_key, + secret_key=args.secret_key, + region=args.region) +metadata.retrieve() +try: + metadata.cfn_init() +except Exception as e: + LOG.exception("Error processing metadata") + exit(1) diff --git a/bin/cfn-push-stats b/bin/cfn-push-stats new file mode 100755 index 0000000..09fe72c --- /dev/null +++ b/bin/cfn-push-stats @@ -0,0 +1,259 @@ +#!/usr/bin/env python +# +# 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. + +""" +Implements cfn-push-stats CloudFormation functionality +""" +import argparse +import logging +import os +import subprocess +import random +import sys + +# Override BOTO_CONFIG, which makes boto look only at the specified +# config file, instead of the default locations +os.environ['BOTO_CONFIG'] = '/var/lib/cloud/data/cfn-boto-cfg' +from boto.ec2.cloudwatch import CloudWatchConnection + + +log_format = '%(levelname)s [%(asctime)s] %(message)s' +log_file_name = "/var/log/cfn-push-stats.log" +logging.basicConfig(filename=log_file_name, + format=log_format, + level=logging.DEBUG) +LOG = logging.getLogger('cfntools') + +try: + import psutil +except ImportError: + LOG.warn("psutil not available. If you want process and memory " + "statistics, you need to install it.") + +if os.path.exists('/opt/aws/bin'): + sys.path.insert(0, '/opt/aws/bin') + from cfn_helper import * +else: + from heat_jeos.cfntools.cfn_helper import * + +KILO = 1024 +MEGA = 1048576 +GIGA = 1073741824 +unit_map = {'bytes': 1, + 'kilobytes': KILO, + 'megabytes': MEGA, + 'gigabytes': GIGA} + +description = " " +parser = argparse.ArgumentParser(description=description) +parser.add_argument('-v', '--verbose', action="store_true", + help="Verbose logging", required=False) +parser.add_argument('--credential-file', dest="credential_file", + help="credential-file", required=False, + default='/etc/cfn/cfn-credentials') +parser.add_argument('--service-failure', required=False, action="store_true", + help='Reports a service falure.') +parser.add_argument('--mem-util', required=False, action="store_true", + help='Reports memory utilization in percentages.') +parser.add_argument('--mem-used', required=False, action="store_true", + help='Reports memory used (excluding cache and buffers) in megabytes.') +parser.add_argument('--mem-avail', required=False, action="store_true", + help='Reports available memory (including cache and buffers) in megabytes.') +parser.add_argument('--swap-util', required=False, action="store_true", + help='Reports swap utilization in percentages.') +parser.add_argument('--swap-used', required=False, action="store_true", + help='Reports allocated swap space in megabytes.') +parser.add_argument('--disk-space-util', required=False, action="store_true", + help='Reports disk space utilization in percentages.') +parser.add_argument('--disk-space-used', required=False, action="store_true", + help='Reports allocated disk space in gigabytes.') +parser.add_argument('--disk-space-avail',required=False, action="store_true", + help='Reports available disk space in gigabytes.') +parser.add_argument('--memory-units', required=False, default='megabytes', + help='Specifies units for memory metrics.') +parser.add_argument('--disk-units', required=False, default='megabytes', + help='Specifies units for disk metrics.') +parser.add_argument('--disk-path', required=False, default='/', + help='Selects the disk by the path on which to report.') +parser.add_argument('--cpu-util', required=False, action="store_true", + help='Reports cpu utilization in percentages.') +parser.add_argument('--haproxy', required=False, action='store_true', + help='Reports HAProxy loadbalancer usage.') +parser.add_argument('--heartbeat', required=False, action='store_true', + help='Sends a Heartbeat.') +parser.add_argument('--watch', required=True, + help='the name of the watch to post to.') +args = parser.parse_args() + +LOG.debug('cfn-push-stats called %s ' % (str(args))) + +credentials = parse_creds_file(args.credential_file) + +namespace = 'system/linux' +data = {} + +# service failure +# =============== +if args.service_failure: + data['ServiceFailure'] = { + 'Value': 1, + 'Units': 'Counter'} + +# heatbeat +# ======== +if args.heartbeat: + data['Heartbeat'] = { + 'Value': 1, + 'Units': 'Counter'} + +# memory space +# ============ +if args.mem_util or args.mem_used or args.mem_avail: + mem = psutil.phymem_usage() +if args.mem_util: + data['MemoryUtilization'] = { + 'Value': mem.percent, + 'Units': 'Percent'} +if args.mem_used: + data['MemoryUsed'] = { + 'Value': mem.used / unit_map[args.memory_units], + 'Units': args.memory_units} +if args.mem_avail: + data['MemoryAvailable'] = { + 'Value': mem.free / unit_map[args.memory_units], + 'Units': args.memory_units} + +# swap space +# ========== +if args.swap_util or args.swap_used: + swap = psutil.virtmem_usage() +if args.swap_util: + data['SwapUtilization'] = { + 'Value': swap.percent, + 'Units': 'Percent'} +if args.swap_used: + data['SwapUsed'] = { + 'Value': swap.used / unit_map[args.memory_units], + 'Units': args.memory_units} + +# disk space +# ========== +if args.disk_space_util or args.disk_space_used or args.disk_space_avail: + disk = psutil.disk_usage(args.disk_path) +if args.disk_space_util: + data['DiskSpaceUtilization'] = { + 'Value': disk.percent, + 'Units': 'Percent'} +if args.disk_space_used: + data['DiskSpaceUsed'] = { + 'Value': disk.used / unit_map[args.disk_units], + 'Units': args.disk_units} +if args.disk_space_avail: + data['DiskSpaceAvailable'] = { + 'Value': disk.free / unit_map[args.disk_units], + 'Units': args.disk_units} + +# cpu utilization +# =============== +if args.cpu_util: + # blocks for 1 second. + cpu_percent = psutil.cpu_percent(interval=1) + data['CPUUtilization'] = { + 'Value': cpu_percent, + 'Units': 'Percent'} + +# HAProxy +# ======= +def parse_haproxy_unix_socket(res): + # http://docs.amazonwebservices.com/ElasticLoadBalancing/latest + # /DeveloperGuide/US_MonitoringLoadBalancerWithCW.html + + type_map = {'FRONTEND': '0', 'BACKEND': '1', 'SERVER': '2', 'SOCKET': '3'} + num_map = {'status': 17, 'svname': 1, 'check_duration': 38, 'type': 32, + 'req_tot': 48, 'hrsp_2xx': 40, 'hrsp_3xx': 41, 'hrsp_4xx': 42, + 'hrsp_5xx': 43} + + def add_stat(key, value, unit='Counter'): + res[key] = {'Value': value, + 'Units': unit} + + echo = subprocess.Popen(['echo', 'show stat'], + stdout=subprocess.PIPE) + socat = subprocess.Popen(['socat', 'stdio', '/tmp/.haproxy-stats'], + stdin=echo.stdout, + stdout=subprocess.PIPE) + end_pipe = socat.stdout + raw = [l.strip('\n').split(',') for l in end_pipe + if l[0] != '#' and len(l) > 2] + latency = 0 + up_count = 0 + down_count = 0 + for f in raw: + if f[num_map['type']] == type_map['FRONTEND']: + add_stat('RequestCount', f[num_map['req_tot']]) + add_stat('HTTPCode_ELB_4XX', f[num_map['hrsp_4xx']]) + add_stat('HTTPCode_ELB_5XX', f[num_map['hrsp_5xx']]) + elif f[num_map['type']] == type_map['BACKEND']: + add_stat('HTTPCode_Backend_2XX', f[num_map['hrsp_2xx']]) + add_stat('HTTPCode_Backend_3XX', f[num_map['hrsp_3xx']]) + add_stat('HTTPCode_Backend_4XX', f[num_map['hrsp_4xx']]) + add_stat('HTTPCode_Backend_5XX', f[num_map['hrsp_5xx']]) + else: + if f[num_map['status']] == 'UP': + up_count = up_count + 1 + else: + down_count = down_count + 1 + if f[num_map['check_duration']] != '': + latency = max(float(f[num_map['check_duration']]), latency) + + # note: haproxy's check_duration is in ms, but Latency is in seconds + add_stat('Latency', str(latency / 1000), unit='Seconds') + add_stat('HealthyHostCount', str(up_count)) + add_stat('UnHealthyHostCount', str(down_count)) + + +def send_stats(info): + + # Create boto connection, need the hard-coded port/path as boto + # can't read these from config values in BOTO_CONFIG + # FIXME : currently only http due to is_secure=False + client = CloudWatchConnection( + aws_access_key_id=credentials['AWSAccessKeyId'], + aws_secret_access_key=credentials['AWSSecretKey'], + is_secure=False, port=8003, path="/v1", debug=0) + + # Then we send the metric datapoints passed in "info", note this could + # contain multiple keys as the options parsed above are noe exclusive + # The alarm name is passed as a dimension so the metric datapoint can + # be associated with the alarm/watch in the engine + metric_dims = [{'AlarmName': args.watch}] + for key in info: + LOG.info("Sending watch %s metric %s, Units %s, Value %s" % + (args.watch, key, info[key]['Units'], info[key]['Value'])) + client.put_metric_data(namespace=namespace, + name=key, + value=info[key]['Value'], + timestamp=None, # means use "now" in the engine + unit=info[key]['Units'], + dimensions=metric_dims, + statistics=None) + + +if args.haproxy: + namespace = 'AWS/ELB' + lb_data = {} + parse_haproxy_unix_socket(lb_data) + send_stats(lb_data) +else: + send_stats(data) diff --git a/bin/cfn-signal b/bin/cfn-signal new file mode 100755 index 0000000..abafc63 --- /dev/null +++ b/bin/cfn-signal @@ -0,0 +1,92 @@ +#!/usr/bin/env python +# +# 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. + +""" +Implements cfn-signal CloudFormation functionality +""" +import argparse +import logging +import os +import sys + + +if os.path.exists('/opt/aws/bin'): + sys.path.insert(0, '/opt/aws/bin') + from cfn_helper import * +else: + from heat_jeos.cfntools.cfn_helper import * + + +description = " " +parser = argparse.ArgumentParser(description=description) +parser.add_argument('-s', '--success', + dest="success", + help="signal status to report", + default='true', + required=False) +parser.add_argument('-r', '--reason', + dest="reason", + help="The reason for the failure", + default="Configuration Complete", + required=False) +parser.add_argument('--data', + dest="data", + default="Application has completed configuration.", + help="The data to send", + required=False) +parser.add_argument('-i', '--id', + dest="unique_id", + help="the unique id to send back to the WaitCondition", + default='00000', + required=False) +parser.add_argument('-e', '--exit', + dest="exit_code", + help="The exit code from a procecc to interpret", + default=None, + required=False) +parser.add_argument('url', + help='the url to post to') +args = parser.parse_args() + +log_format = '%(levelname)s [%(asctime)s] %(message)s' +logging.basicConfig(format=log_format, level=logging.DEBUG) + +LOG = logging.getLogger('cfntools') +log_file_name = "/var/log/cfn-signal.log" +file_handler = logging.FileHandler(log_file_name) +file_handler.setFormatter(logging.Formatter(log_format)) +LOG.addHandler(file_handler) + +LOG.debug('cfn-signal called %s ' % (str(args))) + +status = 'FAILURE' +if args.exit_code: + # "exit_code" takes presedence over "success". + if args.exit_code == '0': + status = 'SUCCESS' +else: + if args.success == 'true': + status = 'SUCCESS' + +body = { + "Status" : status, + "Reason" : args.reason, + "UniqueId" : args.unique_id, + "Data" : args.data +} + +cmd_str = "curl -X PUT -H \'Content-Type:\' --data-binary \'%s\' \"%s\"" % \ + (json.dumps(body), args.url) +command = CommandRunner(cmd_str).run() +sys.exit(command.status) diff --git a/bin/heat-jeos b/bin/heat-jeos deleted file mode 100755 index d8f2d2b..0000000 --- a/bin/heat-jeos +++ /dev/null @@ -1,375 +0,0 @@ -#!/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 ConfigParser -import gettext -from glob import glob -import logging -import optparse -import os -import os.path -import sys -import time -import traceback - -from prettytable import PrettyTable - - -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_jeos')): - sys.path.insert(0, possible_topdir) - -try: - from heat_jeos import glance_clients as glance -except ImportError: - glance = None -from heat_jeos.utils import * - - -def command_list(options, arguments): - """ - List all available templates. - """ - templates = sorted(glob('%s/*.tdl' % options.jeos_dir)) - table = PrettyTable(['Name', 'OS', 'Version', 'Architecture']) - try: # prettytable version 0.5 -- on Fedora 16 and 17 - table.set_field_align('Name', 'l') - except AttributeError: # prettytable version 0.6 -- on Ubuntu 12.04 - table.align['Name'] = 'l' - for template_path in templates: - table.add_row(template_metadata(template_path)) - print table - - -register_with_glance_message = """ -Now register with Glance using: - -glance add name=%s is_public=true disk_format=qcow2 container_format=bare < %s -""" - - -def command_create(options, arguments): - """ - Create a new JEOS (Just Enough Operating System) image and (optionally) - register it. - - Usage: - heat-jeos create ( | --template-file=FILE) - - Arguments: - name Template name from `heat-jeos list` - --template-file=FILE Path to the template file to use - --iso=FILE Path to the ISO file to use as the base OS image - --register-with-glance Register the image with Glance after it's built - - The command must be run as root in order for libvirt to have permissions - to create virtual machines and read the raw DVDs. - - The image ISO must be specified in the Template file under the - `/template/os/install/iso` section or passed using the `--iso` argument. - """ - tdl_path = None - if len(arguments) == 0: - tdl_path = options.template_file - elif len(arguments) == 1: - tdl_path = find_template_by_name(options.jeos_dir, arguments[0]) - - if not tdl_path: - logging.info('You must specify a correct template name or path.') - sys.exit(1) - - if os.geteuid() != 0: - logging.error("This command must be run as root") - sys.exit(1) - - if options.register_with_glance: - if not glance: - logging.error("The Python Glance client is not installed. Please " - "install python-glance for Essex or python-glanceclient for " - "Folsom.") - sys.exit(1) - try: - client = glance.client(options) - glance.get_image(client, 'test') - except glance.NotFoundError: - pass - except glance.ConnectionError: - logging.error("Cannot connect to Glance. Please verify that it's " - "running.") - sys.exit(1) - except glance.AuthError: - logging.error("Cannot authenticate to Keystone, please check your " - "credentials.") - sys.exit(1) - - with open(tdl_path, 'r') as f: - tdl_xml = f.read() - oz_guest = get_oz_guest(tdl_xml) - dsk_path, qcow2_path, image_name = target_image_paths(oz_guest) - - should_build_jeos = True - if os.access(qcow2_path, os.R_OK): - should_build_jeos = options.yes or prompt_bool('An existing JEOS was ' - 'found on disk. Do you want to build a fresh JEOS? (y/n) ') - - if should_build_jeos: - final_tdl = create_tdl(tdl_xml, options.iso, options.cfn_dir) - - logging.info('Creating JEOS image (%s) - ' - 'this takes approximately 10 minutes.' % image_name) - build_jeos(get_oz_guest(final_tdl)) - print('\nGenerated image: %s' % qcow2_path) - - if not options.register_with_glance: - print(register_with_glance_message % (image_name, qcow2_path)) - return - - if not options.register_with_glance: - return - - logging.info('Registering JEOS image (%s) with OpenStack Glance.' % - image_name) - if not os.access(qcow2_path, os.R_OK): - logging.error('Cannot find image %s.' % qcow2_path) - sys.exit(1) - - try: - client = glance.client(options) - image = glance.find_image_by_name(client, image_name) - if image: - delete_image = options.yes or prompt_bool('Do you want to ' - 'delete the existing JEOS in glance? (y/n) ') - if delete_image: - glance.delete_image(client, image) - else: - logging.info('No action taken') - sys.exit(0) - image_id = glance.register_image(client, qcow2_path, image_name, - options.username, image) - print('\nImage %s was registered with ID %s' % (image_name, image_id)) - except glance.ConnectionError, e: - logging.error('Failed to connect to the Glance API server.') - sys.exit(1) - except Exception, e: - logging.error(" Failed to add image. Got error:") - traceback.print_exc() - logging.warning("Note: Your image metadata may still be in the " - "registry, but the image's status will likely be 'killed'.") - sys.exit(1) - - -def prompt_bool(question): - """ - Ask the user a yes/no question and return the answer as a bool. - """ - while True: - answer = raw_input(question).lower() - if answer in ('y', 'yes'): - return True - if answer in ('n', 'no'): - return False - - -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('-t', '--template-file', - default=None, - help="Path to the template file to build image from") - parser.add_option('-j', '--jeos-dir', - default=DEFAULT_JEOS_DIR, - help="Path to the JEOS templates directory") - parser.add_option('-c', '--cfn-dir', - default=DEFAULT_CFNTOOLS_DIR, - help="Path to cfntools directory") - 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('-s', '--iso', default=None, - help="Path to the ISO file to use as the base OS image") - parser.add_option('-G', '--register-with-glance', default=False, - action='store_true', help="Register the image with Glance") - 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', '--glance-host', - default=None, - help="Glance hostname") - parser.add_option('-P', '--glance-port', - default=None, - help="Glance port number") - 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) - - options.jeos_dir = os.path.join(os.getcwd(), options.jeos_dir) - options.cfn_dir = os.path.join(os.getcwd(), options.cfn_dir) - - 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): - commands = { - 'list': command_list, - 'create': command_create, - 'help': print_help, - } - - 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: - - list Prepare a template ready for Oz - - create Create a JEOS image from a template - - help Output help for one of the commands below - -""" - - 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: %s" % ex) - sys.exit(1) - - -if __name__ == '__main__': - main() -- cgit v1.2.1