From bbd63210bca1407b19c5e8b0363876235635f61e Mon Sep 17 00:00:00 2001 From: Yolanda Robla Date: Mon, 14 Jan 2013 14:03:19 +0000 Subject: [PATCH] first hook --- config.yaml | 2 +- hooks/ceilometer-hooks | 29 +++++ hooks/install | 1 + hooks/lib/__init__.py | 1 + hooks/lib/openstack_common.py | 192 ++++++++++++++++++++++++++++++ hooks/utils.py | 216 ++++++++++++++++++++++++++++++++++ metadata.yml => metadata.yaml | 0 revision | 2 +- 8 files changed, 441 insertions(+), 2 deletions(-) create mode 100755 hooks/ceilometer-hooks create mode 120000 hooks/install create mode 100644 hooks/lib/__init__.py create mode 100644 hooks/lib/openstack_common.py create mode 100644 hooks/utils.py rename metadata.yml => metadata.yaml (100%) diff --git a/config.yaml b/config.yaml index 303590e..8b58f0e 100644 --- a/config.yaml +++ b/config.yaml @@ -8,7 +8,7 @@ options: type: string description: "Enable verbose logging" openstack-origin: - default: distro + default: cloud:precise-folsom type: string description: | Repository from which to install. May be one of the following: diff --git a/hooks/ceilometer-hooks b/hooks/ceilometer-hooks new file mode 100755 index 0000000..20eb7f2 --- /dev/null +++ b/hooks/ceilometer-hooks @@ -0,0 +1,29 @@ +#!/usr/bin/python + +import sys +import time + +from utils import * +from lib.openstack_common import * + +config = config_get() + +packages = "ceilometer" +service = "ceilometer" + +def install_hook(): + if config["openstack-origin"]!="distro": + configure_installation_source(config["openstack-origin"]) + execute("apt-get update", die=True) + execute("apt-get -y install %s" % packages, die=True, echo=True) + +hooks = { + "install": install_hook, +} + +# ceiloemter-hooks gets called by symlink corresponding to the requested relation +# hook. +arg0 = sys.argv[0].split("/").pop() +if arg0 not in hooks.keys(): + error_out("Unsupported hook: %s" % arg0) +hooks[arg0]() diff --git a/hooks/install b/hooks/install new file mode 120000 index 0000000..cb0d703 --- /dev/null +++ b/hooks/install @@ -0,0 +1 @@ +ceilometer-hooks \ No newline at end of file diff --git a/hooks/lib/__init__.py b/hooks/lib/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/hooks/lib/__init__.py @@ -0,0 +1 @@ + diff --git a/hooks/lib/openstack_common.py b/hooks/lib/openstack_common.py new file mode 100644 index 0000000..c9cbb3b --- /dev/null +++ b/hooks/lib/openstack_common.py @@ -0,0 +1,192 @@ +#!/usr/bin/python + +# Common python helper functions used for OpenStack charms. + +import subprocess + +CLOUD_ARCHIVE_URL = "http://ubuntu-cloud.archive.canonical.com/ubuntu" +CLOUD_ARCHIVE_KEY_ID = '5EDB1B62EC4926EA' + +ubuntu_openstack_release = { + 'oneiric': 'diablo', + 'precise': 'essex', + 'quantal': 'folsom', + 'raring' : 'grizzly' +} + + +openstack_codenames = { + '2011.2': 'diablo', + '2012.1': 'essex', + '2012.2': 'folsom', + '2012.3': 'grizzly' +} + + +def juju_log(msg): + subprocess.check_call(['juju-log', msg]) + + +def error_out(msg): + juju_log("FATAL ERROR: %s" % msg) + exit(1) + + +def lsb_release(): + '''Return /etc/lsb-release in a dict''' + lsb = open('/etc/lsb-release', 'r') + d = {} + for l in lsb: + k, v = l.split('=') + d[k.strip()] = v.strip() + return d + + +def get_os_codename_install_source(src): + '''Derive OpenStack release codename from a given installation source.''' + ubuntu_rel = lsb_release()['DISTRIB_CODENAME'] + + rel = '' + if src == 'distro': + try: + rel = ubuntu_openstack_release[ubuntu_rel] + except KeyError: + e = 'Code not derive openstack release for '\ + 'this Ubuntu release: %s' % rel + error_out(e) + return rel + + if src.startswith('cloud:'): + ca_rel = src.split(':')[1] + ca_rel = ca_rel.split('%s-' % ubuntu_rel)[1].split('/')[0] + return ca_rel + + # Best guess match based on deb string provided + if src.startswith('deb'): + for k, v in openstack_codenames.iteritems(): + if v in src: + return v + +def get_os_codename_version(vers): + '''Determine OpenStack codename from version number.''' + try: + return openstack_codenames[vers] + except KeyError: + e = 'Could not determine OpenStack codename for version %s' % vers + error_out(e) + + +def get_os_version_codename(codename): + '''Determine OpenStack version number from codename.''' + for k, v in openstack_codenames.iteritems(): + if v == codename: + return k + e = 'Code not derive OpenStack version for '\ + 'codename: %s' % codename + error_out(e) + + +def get_os_codename_package(pkg): + '''Derive OpenStack release codename from an installed package.''' + cmd = ['dpkg', '-l', pkg] + + try: + output = subprocess.check_output(cmd) + except subprocess.CalledProcessError: + e = 'Could not derive OpenStack version from package that is not '\ + 'installed; %s' % pkg + error_out(e) + + def _clean(line): + line = line.split(' ') + clean = [] + for c in line: + if c != '': + clean.append(c) + return clean + + vers = None + for l in output.split('\n'): + if l.startswith('ii'): + l = _clean(l) + if l[1] == pkg: + vers = l[2] + + if not vers: + e = 'Could not determine version of installed package: %s' % pkg + error_out(e) + + vers = vers[:6] + try: + return openstack_codenames[vers] + except KeyError: + e = 'Could not determine OpenStack codename for version %s' % vers + error_out(e) + + +def configure_installation_source(rel): + '''Configure apt installation source.''' + + def _import_key(id): + cmd = "apt-key adv --keyserver keyserver.ubuntu.com " \ + "--recv-keys %s" % id + try: + subprocess.check_call(cmd.split(' ')) + except: + error_out("Error importing repo key %s" % id) + + if rel == 'distro': + return + elif rel[:4] == "ppa:": + src = rel + subprocess.check_call(["add-apt-repository", "-y", src]) + elif rel[:3] == "deb": + l = len(rel.split('|')) + if l == 2: + src, key = rel.split('|') + juju_log("Importing PPA key from keyserver for %s" % src) + _import_key(key) + elif l == 1: + src = rel + else: + error_out("Invalid openstack-release: %s" % rel) + + with open('/etc/apt/sources.list.d/juju_deb.list', 'w') as f: + f.write(src) + elif rel[:6] == 'cloud:': + ubuntu_rel = lsb_release()['DISTRIB_CODENAME'] + rel = rel.split(':')[1] + u_rel = rel.split('-')[0] + ca_rel = rel.split('-')[1] + + if u_rel != ubuntu_rel: + e = 'Cannot install from Cloud Archive pocket %s on this Ubuntu '\ + 'version (%s)' % (ca_rel, ubuntu_rel) + error_out(e) + + if ca_rel == 'folsom/staging': + # staging is just a regular PPA. + cmd = 'add-apt-repository -y ppa:ubuntu-cloud-archive/folsom-staging' + subprocess.check_call(cmd.split(' ')) + return + + # map charm config options to actual archive pockets. + pockets = { + 'folsom': 'precise-updates/folsom', + 'folsom/updates': 'precise-updates/folsom', + 'folsom/proposed': 'precise-proposed/folsom' + } + + try: + pocket = pockets[ca_rel] + except KeyError: + e = 'Invalid Cloud Archive release specified: %s' % rel + error_out(e) + + src = "deb %s %s main" % (CLOUD_ARCHIVE_URL, pocket) + _import_key(CLOUD_ARCHIVE_KEY_ID) + + with open('/etc/apt/sources.list.d/cloud-archive.list', 'w') as f: + f.write(src) + else: + error_out("Invalid openstack-release specified: %s" % rel) diff --git a/hooks/utils.py b/hooks/utils.py new file mode 100644 index 0000000..bcf9cdc --- /dev/null +++ b/hooks/utils.py @@ -0,0 +1,216 @@ +#!/usr/bin/python +import subprocess +import sys +import json +import os +import time + +from lib.openstack_common import * + +ceilometer_conf = "/etc/ceilometer/ceilometer.conf" + +def execute(cmd, die=False, echo=False): + """ Executes a command + + if die=True, script will exit(1) if command does not return 0 + if echo=True, output of command will be printed to stdout + + returns a tuple: (stdout, stderr, return code) + """ + p = subprocess.Popen(cmd.split(" "), + stdout=subprocess.PIPE, + stdin=subprocess.PIPE, + stderr=subprocess.PIPE) + stdout="" + stderr="" + + def print_line(l): + if echo: + print l.strip('\n') + sys.stdout.flush() + + for l in iter(p.stdout.readline, ''): + print_line(l) + stdout += l + for l in iter(p.stderr.readline, ''): + print_line(l) + stderr += l + + p.communicate() + rc = p.returncode + + if die and rc != 0: + error_out("ERROR: command %s return non-zero.\n" % cmd) + return (stdout, stderr, rc) + + +def config_get(): + """ Obtain the units config via 'config-get' + Returns a dict representing current config. + private-address and IP of the unit is also tacked on for + convienence + """ + output = execute("config-get --format json")[0] + if output: + config = json.loads(output) + # make sure no config element is blank after config-get + for c in config.keys(): + if not config[c]: + error_out("ERROR: Config option has no paramter: %s" % c) + # tack on our private address and ip + hostname = execute("unit-get private-address")[0].strip() + config["hostname"] = execute("unit-get private-address")[0].strip() + else: + config = {} + return config + +def relation_ids(relation_name=None): + j = execute('relation-ids --format=json %s' % relation_name)[0] + return json.loads(j) + +def relation_list(relation_id=None): + cmd = 'relation-list --format=json' + if relation_id: + cmd += ' -r %s' % relation_id + j = execute(cmd)[0] + return json.loads(j) + +def relation_set(relation_data): + """ calls relation-set for all key=values in dict """ + for k in relation_data: + execute("relation-set %s=%s" % (k, relation_data[k]), die=True) + +def relation_get(relation_data): + """ Obtain all current relation data + relation_data is a list of options to query from the relation + Returns a k,v dict of the results. + Leave empty responses out of the results as they haven't yet been + set on the other end. + Caller can then "len(results.keys()) == len(relation_data)" to find out if + all relation values have been set on the other side + """ + results = {} + for r in relation_data: + result = execute("relation-get %s" % r, die=True)[0].strip('\n') + if result != "": + results[r] = result + return results + +def relation_get_dict(relation_id=None, remote_unit=None): + """Obtain all relation data as dict by way of JSON""" + cmd = 'relation-get --format=json' + if relation_id: + cmd += ' -r %s' % relation_id + if remote_unit: + remote_unit_orig = os.getenv('JUJU_REMOTE_UNIT', None) + os.environ['JUJU_REMOTE_UNIT'] = remote_unit + j = execute(cmd, die=True)[0] + if remote_unit and remote_unit_orig: + os.environ['JUJU_REMOTE_UNIT'] = remote_unit_orig + d = json.loads(j) + settings = {} + # convert unicode to strings + for k, v in d.iteritems(): + settings[str(k)] = str(v) + return settings + +def update_config_block(block, **kwargs): + """ Updates ceilometer.conf blocks given kwargs. + Can be used to update driver settings for a particular backend, + setting the sql connection, etc. + + Parses block heading as '[block]' + + If block does not exist, a new block will be created at end of file with + given kwargs + """ + f = open(ceilometer_conf, "r+") + orig = f.readlines() + new = [] + found_block = "" + heading = "[%s]\n" % block + + lines = len(orig) + ln = 0 + + def update_block(block): + for k, v in kwargs.iteritems(): + for l in block: + if l.strip().split(" ")[0] == k: + block[block.index(l)] = "%s = %s\n" % (k, v) + return + block.append('%s = %s\n' % (k, v)) + block.append('\n') + + try: + found = False + while ln < lines: + if orig[ln] != heading: + new.append(orig[ln]) + ln += 1 + else: + new.append(orig[ln]) + ln += 1 + block = [] + while orig[ln].strip() != '': + block.append(orig[ln]) + ln += 1 + update_block(block) + new += block + found = True + + if not found: + if new[(len(new) - 1)].strip() != '': + new.append('\n') + new.append('%s' % heading) + for k, v in kwargs.iteritems(): + new.append('%s = %s\n' % (k, v)) + new.append('\n') + except: + error_out('Error while attempting to update config block. '\ + 'Refusing to overwite existing config.') + + return + + # backup original config + backup = open(ceilometer_conf + '.juju-back', 'w+') + for l in orig: + backup.write(l) + backup.close() + + # update config + f.seek(0) + f.truncate() + for l in new: + f.write(l) + + +def ceilometer_conf_update(opt, val): + """ Updates ceilometer.conf values + If option exists, it is reset to new value + If it does not, it added to the top of the config file after the [DEFAULT] + heading to keep it out of the paste deploy config + """ + f = open(ceilometer_conf, "r+") + orig = f.readlines() + new = "" + found = False + for l in orig: + if l.split(' ')[0] == opt: + juju_log("Updating %s, setting %s = %s" % (keystone_conf, opt, val)) + new += "%s = %s\n" % (opt, val) + found = True + else: + new += l + new = new.split('\n') + # insert a new value at the top of the file, after the 'DEFAULT' header so + # as not to muck up paste deploy configuration later in the file + if not found: + juju_log("Adding new config option %s = %s" % (opt, val)) + header = new.index("[DEFAULT]") + new.insert((header+1), "%s = %s" % (opt, val)) + f.seek(0) + f.truncate() + for l in new: + f.write("%s\n" % l) + f.close diff --git a/metadata.yml b/metadata.yaml similarity index 100% rename from metadata.yml rename to metadata.yaml diff --git a/revision b/revision index d00491f..b8626c4 100644 --- a/revision +++ b/revision @@ -1 +1 @@ -1 +4