summaryrefslogtreecommitdiff
path: root/bin
diff options
context:
space:
mode:
authorSteve Baker <sbaker@redhat.com>2012-12-21 09:20:49 +1300
committerSteve Baker <sbaker@redhat.com>2012-12-21 09:20:49 +1300
commit1a15f6fd8dabb22cf32c41045d08c229a4962d9e (patch)
treeda4d109952f6c0ecaab69f18366e4feb2a1e3bc4 /bin
parent305f6753abdc9459305cc9c8d21cc17d2d39778b (diff)
downloadheat-cfntools-1a15f6fd8dabb22cf32c41045d08c229a4962d9e.tar.gz
Reorganise project to contain stand-alone heat-cfntools
Diffstat (limited to 'bin')
-rwxr-xr-xbin/cfn-get-metadata91
-rwxr-xr-xbin/cfn-hup119
-rwxr-xr-xbin/cfn-init73
-rwxr-xr-xbin/cfn-push-stats259
-rwxr-xr-xbin/cfn-signal92
-rwxr-xr-xbin/heat-jeos375
6 files changed, 634 insertions, 375 deletions
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 (<name> | --template-file=FILE) <options>
-
- 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 <command> [options] [args]
-
-Commands:
-
- list Prepare a template ready for Oz
-
- create Create a JEOS image from a template
-
- help <command> 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()