Merge lp:~thedac/charms/precise/rabbitmq-server/enable-nrpe-external-master into lp:charms/rabbitmq-server

Proposed by David Ames
Status: Merged
Merged at revision: 45
Proposed branch: lp:~thedac/charms/precise/rabbitmq-server/enable-nrpe-external-master
Merge into: lp:charms/rabbitmq-server
Diff against target: 3480 lines (+3280/-4)
26 files modified
hooks/_pythonpath.py (+14/-0)
hooks/rabbit_utils.py (+1/-1)
hooks/rabbitmq_server_relations.py (+40/-1)
lib/charmhelpers-0.1.2.egg-info (+18/-0)
lib/charmhelpers/cli/__init__.py (+147/-0)
lib/charmhelpers/cli/commands.py (+2/-0)
lib/charmhelpers/cli/host.py (+15/-0)
lib/charmhelpers/contrib/ansible/__init__.py (+101/-0)
lib/charmhelpers/contrib/charmhelpers/__init__.py (+184/-0)
lib/charmhelpers/contrib/charmsupport/nrpe.py (+219/-0)
lib/charmhelpers/contrib/charmsupport/volumes.py (+156/-0)
lib/charmhelpers/contrib/hahelpers/apache.py (+58/-0)
lib/charmhelpers/contrib/hahelpers/cluster.py (+183/-0)
lib/charmhelpers/contrib/jujugui/utils.py (+602/-0)
lib/charmhelpers/contrib/saltstack/__init__.py (+149/-0)
lib/charmhelpers/core/hookenv.py (+395/-0)
lib/charmhelpers/core/host.py (+281/-0)
lib/charmhelpers/fetch/__init__.py (+271/-0)
lib/charmhelpers/fetch/archiveurl.py (+48/-0)
lib/charmhelpers/fetch/bzrurl.py (+49/-0)
lib/charmhelpers/payload/__init__.py (+1/-0)
lib/charmhelpers/payload/archive.py (+57/-0)
lib/charmhelpers/payload/execd.py (+50/-0)
metadata.yaml (+1/-1)
revision (+1/-1)
scripts/check_rabbitmq.py (+237/-0)
To merge this branch: bzr merge lp:~thedac/charms/precise/rabbitmq-server/enable-nrpe-external-master
Reviewer Review Type Date Requested Status
Tom Haddon Approve
Review via email: mp+196030@code.launchpad.net

Description of the change

Enable nrpe-external-master-relation.
Use charmhelpers (embedded for now)

To post a comment you must log in.
Revision history for this message
Tom Haddon (mthaddon) wrote :

I think we should change the author of the check_rabbitmq.py script, as that email address no longer works (the person in question has left Canonical). Also I wonder if we can change the NAGIOS_PLUGINS='/usr/lib/nagios/plugins' to NAGIOS_PLUGINS='/usr/local/lib/nagios/plugins' - it seems odd to install non-packaged files in /usr/lib. I think we may need to pre-create the directory, though.

review: Needs Fixing
46. By David Ames

Use /usr/local/lib/nagios/plugins for non-packaged checks

Revision history for this message
David Ames (thedac) wrote :

Use /usr/local/lib/nagios/plugins for non-packaged checks
Updated charm-helpers

Mojo tested: https://ci.admin.canonical.com/job/mojo-pes-certification/58/console

Revision history for this message
Tom Haddon (mthaddon) wrote :

Looks good, approved. Very nice to be able to see it in Mojo as well...

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== added file 'hooks/_pythonpath.py'
--- hooks/_pythonpath.py 1970-01-01 00:00:00 +0000
+++ hooks/_pythonpath.py 2013-11-21 22:43:22 +0000
@@ -0,0 +1,14 @@
1import sys
2import os
3import os.path
4
5# Make sure that charmhelpers is importable, or bail out.
6local_copy = os.path.join(
7 os.path.dirname(os.path.dirname(__file__)), "lib")
8if os.path.exists(local_copy) and os.path.isdir(local_copy):
9 sys.path.insert(0, local_copy)
10try:
11 import charmhelpers
12 _ = charmhelpers
13except ImportError:
14 sys.exit("Could not find required 'charmhelpers' library.")
015
=== added symlink 'hooks/nrpe-external-master-relation-changed'
=== target is u'rabbitmq_server_relations.py'
=== modified file 'hooks/rabbit_utils.py'
--- hooks/rabbit_utils.py 2013-05-20 17:00:03 +0000
+++ hooks/rabbit_utils.py 2013-11-21 22:43:22 +0000
@@ -7,7 +7,7 @@
7import lib.utils as utils7import lib.utils as utils
8import apt_pkg as apt8import apt_pkg as apt
99
10PACKAGES = ['pwgen', 'rabbitmq-server']10PACKAGES = ['pwgen', 'rabbitmq-server', 'python-amqplib']
1111
12RABBITMQ_CTL = '/usr/sbin/rabbitmqctl'12RABBITMQ_CTL = '/usr/sbin/rabbitmqctl'
13COOKIE_PATH = '/var/lib/rabbitmq/.erlang.cookie'13COOKIE_PATH = '/var/lib/rabbitmq/.erlang.cookie'
1414
=== modified file 'hooks/rabbitmq_server_relations.py'
--- hooks/rabbitmq_server_relations.py 2013-10-28 14:12:08 +0000
+++ hooks/rabbitmq_server_relations.py 2013-11-21 22:43:22 +0000
@@ -13,10 +13,17 @@
13import lib.ceph_utils as ceph13import lib.ceph_utils as ceph
14import lib.openstack_common as openstack14import lib.openstack_common as openstack
1515
16import _pythonpath
17_ = _pythonpath
18
19from charmhelpers.core.host import rsync
20from charmhelpers.contrib.charmsupport.nrpe import NRPE
21
1622
17SERVICE_NAME = os.getenv('JUJU_UNIT_NAME').split('/')[0]23SERVICE_NAME = os.getenv('JUJU_UNIT_NAME').split('/')[0]
18POOL_NAME = SERVICE_NAME24POOL_NAME = SERVICE_NAME
19RABBIT_DIR = '/var/lib/rabbitmq'25RABBIT_DIR = '/var/lib/rabbitmq'
26NAGIOS_PLUGINS='/usr/local/lib/nagios/plugins'
2027
2128
22def install():29def install():
@@ -237,6 +244,35 @@
237 utils.juju_log('INFO', 'Finish Ceph Relation Changed')244 utils.juju_log('INFO', 'Finish Ceph Relation Changed')
238245
239246
247def update_nrpe_checks():
248 if os.path.isdir(NAGIOS_PLUGINS):
249 rsync(os.path.join(os.getenv('CHARM_DIR'), 'scripts', 'check_rabbitmq.py'),
250 os.path.join(NAGIOS_PLUGINS, 'check_rabbitmq.py'))
251 user = 'naigos'
252 vhost = 'nagios'
253 password_file = os.path.join(RABBIT_DIR, '%s.passwd' % user)
254 if os.path.exists(password_file):
255 password = open(password_file).read().strip()
256 else:
257 cmd = ['pwgen', '64', '1']
258 password = subprocess.check_output(cmd).strip()
259 with open(password_file, 'wb') as out:
260 out.write(password)
261
262 rabbit.create_vhost(vhost)
263 rabbit.create_user(user, password)
264 rabbit.grant_permissions(user, vhost)
265
266 nrpe_compat = NRPE()
267 nrpe_compat.add_check(
268 shortname='rabbitmq',
269 description='Check RabbitMQ',
270 check_cmd='{}/check_rabbitmq.py --user {} --password {} --vhost {}'
271 ''.format(NAGIOS_PLUGINS, user, password, vhost)
272 )
273 nrpe_compat.write()
274
275
240def upgrade_charm():276def upgrade_charm():
241 pre_install_hooks()277 pre_install_hooks()
242 # Ensure older passwd files in /var/lib/juju are moved to278 # Ensure older passwd files in /var/lib/juju are moved to
@@ -281,6 +317,8 @@
281 if cluster.eligible_leader('res_rabbitmq_vip'):317 if cluster.eligible_leader('res_rabbitmq_vip'):
282 utils.restart('rabbitmq-server')318 utils.restart('rabbitmq-server')
283319
320 update_nrpe_checks()
321
284322
285def pre_install_hooks():323def pre_install_hooks():
286 for f in glob.glob('exec.d/*/charm-pre-install'):324 for f in glob.glob('exec.d/*/charm-pre-install'):
@@ -297,7 +335,8 @@
297 'ceph-relation-joined': ceph_joined,335 'ceph-relation-joined': ceph_joined,
298 'ceph-relation-changed': ceph_changed,336 'ceph-relation-changed': ceph_changed,
299 'upgrade-charm': upgrade_charm,337 'upgrade-charm': upgrade_charm,
300 'config-changed': config_changed338 'config-changed': config_changed,
339 'nrpe-external-master-relation-changed': update_nrpe_checks
301}340}
302341
303utils.do_hooks(hooks)342utils.do_hooks(hooks)
304343
=== added directory 'lib'
=== added directory 'lib/charmhelpers'
=== added file 'lib/charmhelpers-0.1.2.egg-info'
--- lib/charmhelpers-0.1.2.egg-info 1970-01-01 00:00:00 +0000
+++ lib/charmhelpers-0.1.2.egg-info 2013-11-21 22:43:22 +0000
@@ -0,0 +1,18 @@
1Metadata-Version: 1.0
2Name: charmhelpers
3Version: 0.1.2
4Summary: UNKNOWN
5Home-page: https://code.launchpad.net/charm-helpers
6Author: Ubuntu Developers
7Author-email: ubuntu-devel-discuss@lists.ubuntu.com
8License: Affero GNU Public License v3
9Description: ============
10 CharmHelpers
11 ============
12
13 CharmHelpers provides an opinionated set of tools for building Juju
14 charms that work together. In addition to basic tasks like interact-
15 ing with the charm environment and the machine it runs on, it also
16 helps keep you build hooks and establish relations effortlessly.
17
18Platform: UNKNOWN
019
=== added file 'lib/charmhelpers/__init__.py'
=== added directory 'lib/charmhelpers/cli'
=== added file 'lib/charmhelpers/cli/__init__.py'
--- lib/charmhelpers/cli/__init__.py 1970-01-01 00:00:00 +0000
+++ lib/charmhelpers/cli/__init__.py 2013-11-21 22:43:22 +0000
@@ -0,0 +1,147 @@
1import inspect
2import itertools
3import argparse
4import sys
5
6
7class OutputFormatter(object):
8 def __init__(self, outfile=sys.stdout):
9 self.formats = (
10 "raw",
11 "json",
12 "py",
13 "yaml",
14 "csv",
15 "tab",
16 )
17 self.outfile = outfile
18
19 def add_arguments(self, argument_parser):
20 formatgroup = argument_parser.add_mutually_exclusive_group()
21 choices = self.supported_formats
22 formatgroup.add_argument("--format", metavar='FMT',
23 help="Select output format for returned data, "
24 "where FMT is one of: {}".format(choices),
25 choices=choices, default='raw')
26 for fmt in self.formats:
27 fmtfunc = getattr(self, fmt)
28 formatgroup.add_argument("-{}".format(fmt[0]),
29 "--{}".format(fmt), action='store_const',
30 const=fmt, dest='format',
31 help=fmtfunc.__doc__)
32
33 @property
34 def supported_formats(self):
35 return self.formats
36
37 def raw(self, output):
38 """Output data as raw string (default)"""
39 self.outfile.write(str(output))
40
41 def py(self, output):
42 """Output data as a nicely-formatted python data structure"""
43 import pprint
44 pprint.pprint(output, stream=self.outfile)
45
46 def json(self, output):
47 """Output data in JSON format"""
48 import json
49 json.dump(output, self.outfile)
50
51 def yaml(self, output):
52 """Output data in YAML format"""
53 import yaml
54 yaml.safe_dump(output, self.outfile)
55
56 def csv(self, output):
57 """Output data as excel-compatible CSV"""
58 import csv
59 csvwriter = csv.writer(self.outfile)
60 csvwriter.writerows(output)
61
62 def tab(self, output):
63 """Output data in excel-compatible tab-delimited format"""
64 import csv
65 csvwriter = csv.writer(self.outfile, dialect=csv.excel_tab)
66 csvwriter.writerows(output)
67
68 def format_output(self, output, fmt='raw'):
69 fmtfunc = getattr(self, fmt)
70 fmtfunc(output)
71
72
73class CommandLine(object):
74 argument_parser = None
75 subparsers = None
76 formatter = None
77
78 def __init__(self):
79 if not self.argument_parser:
80 self.argument_parser = argparse.ArgumentParser(description='Perform common charm tasks')
81 if not self.formatter:
82 self.formatter = OutputFormatter()
83 self.formatter.add_arguments(self.argument_parser)
84 if not self.subparsers:
85 self.subparsers = self.argument_parser.add_subparsers(help='Commands')
86
87 def subcommand(self, command_name=None):
88 """
89 Decorate a function as a subcommand. Use its arguments as the
90 command-line arguments"""
91 def wrapper(decorated):
92 cmd_name = command_name or decorated.__name__
93 subparser = self.subparsers.add_parser(cmd_name,
94 description=decorated.__doc__)
95 for args, kwargs in describe_arguments(decorated):
96 subparser.add_argument(*args, **kwargs)
97 subparser.set_defaults(func=decorated)
98 return decorated
99 return wrapper
100
101 def subcommand_builder(self, command_name, description=None):
102 """
103 Decorate a function that builds a subcommand. Builders should accept a
104 single argument (the subparser instance) and return the function to be
105 run as the command."""
106 def wrapper(decorated):
107 subparser = self.subparsers.add_parser(command_name)
108 func = decorated(subparser)
109 subparser.set_defaults(func=func)
110 subparser.description = description or func.__doc__
111 return wrapper
112
113 def run(self):
114 "Run cli, processing arguments and executing subcommands."
115 arguments = self.argument_parser.parse_args()
116 argspec = inspect.getargspec(arguments.func)
117 vargs = []
118 kwargs = {}
119 if argspec.varargs:
120 vargs = getattr(arguments, argspec.varargs)
121 for arg in argspec.args:
122 kwargs[arg] = getattr(arguments, arg)
123 self.formatter.format_output(arguments.func(*vargs, **kwargs), arguments.format)
124
125
126cmdline = CommandLine()
127
128
129def describe_arguments(func):
130 """
131 Analyze a function's signature and return a data structure suitable for
132 passing in as arguments to an argparse parser's add_argument() method."""
133
134 argspec = inspect.getargspec(func)
135 # we should probably raise an exception somewhere if func includes **kwargs
136 if argspec.defaults:
137 positional_args = argspec.args[:-len(argspec.defaults)]
138 keyword_names = argspec.args[-len(argspec.defaults):]
139 for arg, default in itertools.izip(keyword_names, argspec.defaults):
140 yield ('--{}'.format(arg),), {'default': default}
141 else:
142 positional_args = argspec.args
143
144 for arg in positional_args:
145 yield (arg,), {}
146 if argspec.varargs:
147 yield (argspec.varargs,), {'nargs': '*'}
0148
=== added file 'lib/charmhelpers/cli/commands.py'
--- lib/charmhelpers/cli/commands.py 1970-01-01 00:00:00 +0000
+++ lib/charmhelpers/cli/commands.py 2013-11-21 22:43:22 +0000
@@ -0,0 +1,2 @@
1from . import CommandLine
2import host
03
=== added file 'lib/charmhelpers/cli/host.py'
--- lib/charmhelpers/cli/host.py 1970-01-01 00:00:00 +0000
+++ lib/charmhelpers/cli/host.py 2013-11-21 22:43:22 +0000
@@ -0,0 +1,15 @@
1from . import cmdline
2from charmhelpers.core import host
3
4
5@cmdline.subcommand()
6def mounts():
7 "List mounts"
8 return host.mounts()
9
10
11@cmdline.subcommand_builder('service', description="Control system services")
12def service(subparser):
13 subparser.add_argument("action", help="The action to perform (start, stop, etc...)")
14 subparser.add_argument("service_name", help="Name of the service to control")
15 return host.service
016
=== added directory 'lib/charmhelpers/contrib'
=== added file 'lib/charmhelpers/contrib/__init__.py'
=== added directory 'lib/charmhelpers/contrib/ansible'
=== added file 'lib/charmhelpers/contrib/ansible/__init__.py'
--- lib/charmhelpers/contrib/ansible/__init__.py 1970-01-01 00:00:00 +0000
+++ lib/charmhelpers/contrib/ansible/__init__.py 2013-11-21 22:43:22 +0000
@@ -0,0 +1,101 @@
1# Copyright 2013 Canonical Ltd.
2#
3# Authors:
4# Charm Helpers Developers <juju@lists.ubuntu.com>
5"""Charm Helpers ansible - declare the state of your machines.
6
7This helper enables you to declare your machine state, rather than
8program it procedurally (and have to test each change to your procedures).
9Your install hook can be as simple as:
10
11{{{
12import charmhelpers.contrib.ansible
13
14
15def install():
16 charmhelpers.contrib.ansible.install_ansible_support()
17 charmhelpers.contrib.ansible.apply_playbook('playbooks/install.yaml')
18}}}
19
20and won't need to change (nor will its tests) when you change the machine
21state.
22
23All of your juju config and relation-data are available as template
24variables within your playbooks and templates. An install playbook looks
25something like:
26
27{{{
28---
29- hosts: localhost
30 user: root
31
32 tasks:
33 - name: Add private repositories.
34 template:
35 src: ../templates/private-repositories.list.jinja2
36 dest: /etc/apt/sources.list.d/private.list
37
38 - name: Update the cache.
39 apt: update_cache=yes
40
41 - name: Install dependencies.
42 apt: pkg={{ item }}
43 with_items:
44 - python-mimeparse
45 - python-webob
46 - sunburnt
47
48 - name: Setup groups.
49 group: name={{ item.name }} gid={{ item.gid }}
50 with_items:
51 - { name: 'deploy_user', gid: 1800 }
52 - { name: 'service_user', gid: 1500 }
53
54 ...
55}}}
56
57Read more online about playbooks[1] and standard ansible modules[2].
58
59[1] http://www.ansibleworks.com/docs/playbooks.html
60[2] http://www.ansibleworks.com/docs/modules.html
61"""
62import os
63import subprocess
64
65import charmhelpers.contrib.saltstack
66import charmhelpers.core.host
67import charmhelpers.core.hookenv
68import charmhelpers.fetch
69
70
71charm_dir = os.environ.get('CHARM_DIR', '')
72ansible_hosts_path = '/etc/ansible/hosts'
73# Ansible will automatically include any vars in the following
74# file in its inventory when run locally.
75ansible_vars_path = '/etc/ansible/host_vars/localhost'
76
77
78def install_ansible_support(from_ppa=True):
79 """Installs the ansible package.
80
81 By default it is installed from the PPA [1] linked from
82 the ansible website [2].
83
84 [1] https://launchpad.net/~rquillo/+archive/ansible
85 [2] http://www.ansibleworks.com/docs/gettingstarted.html#ubuntu-and-debian
86
87 If from_ppa is false, you must ensure that the package is available
88 from a configured repository.
89 """
90 if from_ppa:
91 charmhelpers.fetch.add_source('ppa:rquillo/ansible')
92 charmhelpers.fetch.apt_update(fatal=True)
93 charmhelpers.fetch.apt_install('ansible')
94 with open(ansible_hosts_path, 'w+') as hosts_file:
95 hosts_file.write('localhost ansible_connection=local')
96
97
98def apply_playbook(playbook):
99 charmhelpers.contrib.saltstack.juju_state_to_yaml(
100 ansible_vars_path, namespace_separator='__')
101 subprocess.check_call(['ansible-playbook', '-c', 'local', playbook])
0102
=== added directory 'lib/charmhelpers/contrib/charmhelpers'
=== added file 'lib/charmhelpers/contrib/charmhelpers/__init__.py'
--- lib/charmhelpers/contrib/charmhelpers/__init__.py 1970-01-01 00:00:00 +0000
+++ lib/charmhelpers/contrib/charmhelpers/__init__.py 2013-11-21 22:43:22 +0000
@@ -0,0 +1,184 @@
1# Copyright 2012 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4import warnings
5warnings.warn("contrib.charmhelpers is deprecated", DeprecationWarning)
6
7"""Helper functions for writing Juju charms in Python."""
8
9__metaclass__ = type
10__all__ = [
11 #'get_config', # core.hookenv.config()
12 #'log', # core.hookenv.log()
13 #'log_entry', # core.hookenv.log()
14 #'log_exit', # core.hookenv.log()
15 #'relation_get', # core.hookenv.relation_get()
16 #'relation_set', # core.hookenv.relation_set()
17 #'relation_ids', # core.hookenv.relation_ids()
18 #'relation_list', # core.hookenv.relation_units()
19 #'config_get', # core.hookenv.config()
20 #'unit_get', # core.hookenv.unit_get()
21 #'open_port', # core.hookenv.open_port()
22 #'close_port', # core.hookenv.close_port()
23 #'service_control', # core.host.service()
24 'unit_info', # client-side, NOT IMPLEMENTED
25 'wait_for_machine', # client-side, NOT IMPLEMENTED
26 'wait_for_page_contents', # client-side, NOT IMPLEMENTED
27 'wait_for_relation', # client-side, NOT IMPLEMENTED
28 'wait_for_unit', # client-side, NOT IMPLEMENTED
29]
30
31import operator
32from shelltoolbox import (
33 command,
34)
35import tempfile
36import time
37import urllib2
38import yaml
39
40SLEEP_AMOUNT = 0.1
41# We create a juju_status Command here because it makes testing much,
42# much easier.
43juju_status = lambda: command('juju')('status')
44
45# re-implemented as charmhelpers.fetch.configure_sources()
46#def configure_source(update=False):
47# source = config_get('source')
48# if ((source.startswith('ppa:') or
49# source.startswith('cloud:') or
50# source.startswith('http:'))):
51# run('add-apt-repository', source)
52# if source.startswith("http:"):
53# run('apt-key', 'import', config_get('key'))
54# if update:
55# run('apt-get', 'update')
56
57
58# DEPRECATED: client-side only
59def make_charm_config_file(charm_config):
60 charm_config_file = tempfile.NamedTemporaryFile()
61 charm_config_file.write(yaml.dump(charm_config))
62 charm_config_file.flush()
63 # The NamedTemporaryFile instance is returned instead of just the name
64 # because we want to take advantage of garbage collection-triggered
65 # deletion of the temp file when it goes out of scope in the caller.
66 return charm_config_file
67
68
69# DEPRECATED: client-side only
70def unit_info(service_name, item_name, data=None, unit=None):
71 if data is None:
72 data = yaml.safe_load(juju_status())
73 service = data['services'].get(service_name)
74 if service is None:
75 # XXX 2012-02-08 gmb:
76 # This allows us to cope with the race condition that we
77 # have between deploying a service and having it come up in
78 # `juju status`. We could probably do with cleaning it up so
79 # that it fails a bit more noisily after a while.
80 return ''
81 units = service['units']
82 if unit is not None:
83 item = units[unit][item_name]
84 else:
85 # It might seem odd to sort the units here, but we do it to
86 # ensure that when no unit is specified, the first unit for the
87 # service (or at least the one with the lowest number) is the
88 # one whose data gets returned.
89 sorted_unit_names = sorted(units.keys())
90 item = units[sorted_unit_names[0]][item_name]
91 return item
92
93
94# DEPRECATED: client-side only
95def get_machine_data():
96 return yaml.safe_load(juju_status())['machines']
97
98
99# DEPRECATED: client-side only
100def wait_for_machine(num_machines=1, timeout=300):
101 """Wait `timeout` seconds for `num_machines` machines to come up.
102
103 This wait_for... function can be called by other wait_for functions
104 whose timeouts might be too short in situations where only a bare
105 Juju setup has been bootstrapped.
106
107 :return: A tuple of (num_machines, time_taken). This is used for
108 testing.
109 """
110 # You may think this is a hack, and you'd be right. The easiest way
111 # to tell what environment we're working in (LXC vs EC2) is to check
112 # the dns-name of the first machine. If it's localhost we're in LXC
113 # and we can just return here.
114 if get_machine_data()[0]['dns-name'] == 'localhost':
115 return 1, 0
116 start_time = time.time()
117 while True:
118 # Drop the first machine, since it's the Zookeeper and that's
119 # not a machine that we need to wait for. This will only work
120 # for EC2 environments, which is why we return early above if
121 # we're in LXC.
122 machine_data = get_machine_data()
123 non_zookeeper_machines = [
124 machine_data[key] for key in machine_data.keys()[1:]]
125 if len(non_zookeeper_machines) >= num_machines:
126 all_machines_running = True
127 for machine in non_zookeeper_machines:
128 if machine.get('instance-state') != 'running':
129 all_machines_running = False
130 break
131 if all_machines_running:
132 break
133 if time.time() - start_time >= timeout:
134 raise RuntimeError('timeout waiting for service to start')
135 time.sleep(SLEEP_AMOUNT)
136 return num_machines, time.time() - start_time
137
138
139# DEPRECATED: client-side only
140def wait_for_unit(service_name, timeout=480):
141 """Wait `timeout` seconds for a given service name to come up."""
142 wait_for_machine(num_machines=1)
143 start_time = time.time()
144 while True:
145 state = unit_info(service_name, 'agent-state')
146 if 'error' in state or state == 'started':
147 break
148 if time.time() - start_time >= timeout:
149 raise RuntimeError('timeout waiting for service to start')
150 time.sleep(SLEEP_AMOUNT)
151 if state != 'started':
152 raise RuntimeError('unit did not start, agent-state: ' + state)
153
154
155# DEPRECATED: client-side only
156def wait_for_relation(service_name, relation_name, timeout=120):
157 """Wait `timeout` seconds for a given relation to come up."""
158 start_time = time.time()
159 while True:
160 relation = unit_info(service_name, 'relations').get(relation_name)
161 if relation is not None and relation['state'] == 'up':
162 break
163 if time.time() - start_time >= timeout:
164 raise RuntimeError('timeout waiting for relation to be up')
165 time.sleep(SLEEP_AMOUNT)
166
167
168# DEPRECATED: client-side only
169def wait_for_page_contents(url, contents, timeout=120, validate=None):
170 if validate is None:
171 validate = operator.contains
172 start_time = time.time()
173 while True:
174 try:
175 stream = urllib2.urlopen(url)
176 except (urllib2.HTTPError, urllib2.URLError):
177 pass
178 else:
179 page = stream.read()
180 if validate(page, contents):
181 return page
182 if time.time() - start_time >= timeout:
183 raise RuntimeError('timeout waiting for contents of ' + url)
184 time.sleep(SLEEP_AMOUNT)
0185
=== added directory 'lib/charmhelpers/contrib/charmsupport'
=== added file 'lib/charmhelpers/contrib/charmsupport/__init__.py'
=== added file 'lib/charmhelpers/contrib/charmsupport/nrpe.py'
--- lib/charmhelpers/contrib/charmsupport/nrpe.py 1970-01-01 00:00:00 +0000
+++ lib/charmhelpers/contrib/charmsupport/nrpe.py 2013-11-21 22:43:22 +0000
@@ -0,0 +1,219 @@
1"""Compatibility with the nrpe-external-master charm"""
2# Copyright 2012 Canonical Ltd.
3#
4# Authors:
5# Matthew Wedgwood <matthew.wedgwood@canonical.com>
6
7import subprocess
8import pwd
9import grp
10import os
11import re
12import shlex
13import yaml
14
15from charmhelpers.core.hookenv import (
16 config,
17 local_unit,
18 log,
19 relation_ids,
20 relation_set,
21)
22
23from charmhelpers.core.host import service
24
25# This module adds compatibility with the nrpe-external-master and plain nrpe
26# subordinate charms. To use it in your charm:
27#
28# 1. Update metadata.yaml
29#
30# provides:
31# (...)
32# nrpe-external-master:
33# interface: nrpe-external-master
34# scope: container
35#
36# and/or
37#
38# provides:
39# (...)
40# local-monitors:
41# interface: local-monitors
42# scope: container
43
44#
45# 2. Add the following to config.yaml
46#
47# nagios_context:
48# default: "juju"
49# type: string
50# description: |
51# Used by the nrpe subordinate charms.
52# A string that will be prepended to instance name to set the host name
53# in nagios. So for instance the hostname would be something like:
54# juju-myservice-0
55# If you're running multiple environments with the same services in them
56# this allows you to differentiate between them.
57#
58# 3. Add custom checks (Nagios plugins) to files/nrpe-external-master
59#
60# 4. Update your hooks.py with something like this:
61#
62# from charmsupport.nrpe import NRPE
63# (...)
64# def update_nrpe_config():
65# nrpe_compat = NRPE()
66# nrpe_compat.add_check(
67# shortname = "myservice",
68# description = "Check MyService",
69# check_cmd = "check_http -w 2 -c 10 http://localhost"
70# )
71# nrpe_compat.add_check(
72# "myservice_other",
73# "Check for widget failures",
74# check_cmd = "/srv/myapp/scripts/widget_check"
75# )
76# nrpe_compat.write()
77#
78# def config_changed():
79# (...)
80# update_nrpe_config()
81#
82# def nrpe_external_master_relation_changed():
83# update_nrpe_config()
84#
85# def local_monitors_relation_changed():
86# update_nrpe_config()
87#
88# 5. ln -s hooks.py nrpe-external-master-relation-changed
89# ln -s hooks.py local-monitors-relation-changed
90
91
92class CheckException(Exception):
93 pass
94
95
96class Check(object):
97 shortname_re = '[A-Za-z0-9-_]+$'
98 service_template = ("""
99#---------------------------------------------------
100# This file is Juju managed
101#---------------------------------------------------
102define service {{
103 use active-service
104 host_name {nagios_hostname}
105 service_description {nagios_hostname}[{shortname}] """
106 """{description}
107 check_command check_nrpe!{command}
108 servicegroups {nagios_servicegroup}
109}}
110""")
111
112 def __init__(self, shortname, description, check_cmd):
113 super(Check, self).__init__()
114 # XXX: could be better to calculate this from the service name
115 if not re.match(self.shortname_re, shortname):
116 raise CheckException("shortname must match {}".format(
117 Check.shortname_re))
118 self.shortname = shortname
119 self.command = "check_{}".format(shortname)
120 # Note: a set of invalid characters is defined by the
121 # Nagios server config
122 # The default is: illegal_object_name_chars=`~!$%^&*"|'<>?,()=
123 self.description = description
124 self.check_cmd = self._locate_cmd(check_cmd)
125
126 def _locate_cmd(self, check_cmd):
127 search_path = (
128 '/',
129 os.path.join(os.environ['CHARM_DIR'],
130 'files/nrpe-external-master'),
131 '/usr/lib/nagios/plugins',
132 '/usr/local/lib/nagios/plugins',
133 )
134 parts = shlex.split(check_cmd)
135 for path in search_path:
136 if os.path.exists(os.path.join(path, parts[0])):
137 command = os.path.join(path, parts[0])
138 if len(parts) > 1:
139 command += " " + " ".join(parts[1:])
140 return command
141 log('Check command not found: {}'.format(parts[0]))
142 return ''
143
144 def write(self, nagios_context, hostname):
145 nrpe_check_file = '/etc/nagios/nrpe.d/{}.cfg'.format(
146 self.command)
147 with open(nrpe_check_file, 'w') as nrpe_check_config:
148 nrpe_check_config.write("# check {}\n".format(self.shortname))
149 nrpe_check_config.write("command[{}]={}\n".format(
150 self.command, self.check_cmd))
151
152 if not os.path.exists(NRPE.nagios_exportdir):
153 log('Not writing service config as {} is not accessible'.format(
154 NRPE.nagios_exportdir))
155 else:
156 self.write_service_config(nagios_context, hostname)
157
158 def write_service_config(self, nagios_context, hostname):
159 for f in os.listdir(NRPE.nagios_exportdir):
160 if re.search('.*{}.cfg'.format(self.command), f):
161 os.remove(os.path.join(NRPE.nagios_exportdir, f))
162
163 templ_vars = {
164 'nagios_hostname': hostname,
165 'nagios_servicegroup': nagios_context,
166 'description': self.description,
167 'shortname': self.shortname,
168 'command': self.command,
169 }
170 nrpe_service_text = Check.service_template.format(**templ_vars)
171 nrpe_service_file = '{}/service__{}_{}.cfg'.format(
172 NRPE.nagios_exportdir, hostname, self.command)
173 with open(nrpe_service_file, 'w') as nrpe_service_config:
174 nrpe_service_config.write(str(nrpe_service_text))
175
176 def run(self):
177 subprocess.call(self.check_cmd)
178
179
180class NRPE(object):
181 nagios_logdir = '/var/log/nagios'
182 nagios_exportdir = '/var/lib/nagios/export'
183 nrpe_confdir = '/etc/nagios/nrpe.d'
184
185 def __init__(self):
186 super(NRPE, self).__init__()
187 self.config = config()
188 self.nagios_context = self.config['nagios_context']
189 self.unit_name = local_unit().replace('/', '-')
190 self.hostname = "{}-{}".format(self.nagios_context, self.unit_name)
191 self.checks = []
192
193 def add_check(self, *args, **kwargs):
194 self.checks.append(Check(*args, **kwargs))
195
196 def write(self):
197 try:
198 nagios_uid = pwd.getpwnam('nagios').pw_uid
199 nagios_gid = grp.getgrnam('nagios').gr_gid
200 except:
201 log("Nagios user not set up, nrpe checks not updated")
202 return
203
204 if not os.path.exists(NRPE.nagios_logdir):
205 os.mkdir(NRPE.nagios_logdir)
206 os.chown(NRPE.nagios_logdir, nagios_uid, nagios_gid)
207
208 nrpe_monitors = {}
209 monitors = {"monitors": {"remote": {"nrpe": nrpe_monitors}}}
210 for nrpecheck in self.checks:
211 nrpecheck.write(self.nagios_context, self.hostname)
212 nrpe_monitors[nrpecheck.shortname] = {
213 "command": nrpecheck.command,
214 }
215
216 service('restart', 'nagios-nrpe-server')
217
218 for rid in relation_ids("local-monitors"):
219 relation_set(relation_id=rid, monitors=yaml.dump(monitors))
0220
=== added file 'lib/charmhelpers/contrib/charmsupport/volumes.py'
--- lib/charmhelpers/contrib/charmsupport/volumes.py 1970-01-01 00:00:00 +0000
+++ lib/charmhelpers/contrib/charmsupport/volumes.py 2013-11-21 22:43:22 +0000
@@ -0,0 +1,156 @@
1'''
2Functions for managing volumes in juju units. One volume is supported per unit.
3Subordinates may have their own storage, provided it is on its own partition.
4
5Configuration stanzas:
6 volume-ephemeral:
7 type: boolean
8 default: true
9 description: >
10 If false, a volume is mounted as sepecified in "volume-map"
11 If true, ephemeral storage will be used, meaning that log data
12 will only exist as long as the machine. YOU HAVE BEEN WARNED.
13 volume-map:
14 type: string
15 default: {}
16 description: >
17 YAML map of units to device names, e.g:
18 "{ rsyslog/0: /dev/vdb, rsyslog/1: /dev/vdb }"
19 Service units will raise a configure-error if volume-ephemeral
20 is 'true' and no volume-map value is set. Use 'juju set' to set a
21 value and 'juju resolved' to complete configuration.
22
23Usage:
24 from charmsupport.volumes import configure_volume, VolumeConfigurationError
25 from charmsupport.hookenv import log, ERROR
26 def post_mount_hook():
27 stop_service('myservice')
28 def post_mount_hook():
29 start_service('myservice')
30
31 if __name__ == '__main__':
32 try:
33 configure_volume(before_change=pre_mount_hook,
34 after_change=post_mount_hook)
35 except VolumeConfigurationError:
36 log('Storage could not be configured', ERROR)
37'''
38
39# XXX: Known limitations
40# - fstab is neither consulted nor updated
41
42import os
43from charmhelpers.core import hookenv
44from charmhelpers.core import host
45import yaml
46
47
48MOUNT_BASE = '/srv/juju/volumes'
49
50
51class VolumeConfigurationError(Exception):
52 '''Volume configuration data is missing or invalid'''
53 pass
54
55
56def get_config():
57 '''Gather and sanity-check volume configuration data'''
58 volume_config = {}
59 config = hookenv.config()
60
61 errors = False
62
63 if config.get('volume-ephemeral') in (True, 'True', 'true', 'Yes', 'yes'):
64 volume_config['ephemeral'] = True
65 else:
66 volume_config['ephemeral'] = False
67
68 try:
69 volume_map = yaml.safe_load(config.get('volume-map', '{}'))
70 except yaml.YAMLError as e:
71 hookenv.log("Error parsing YAML volume-map: {}".format(e),
72 hookenv.ERROR)
73 errors = True
74 if volume_map is None:
75 # probably an empty string
76 volume_map = {}
77 elif not isinstance(volume_map, dict):
78 hookenv.log("Volume-map should be a dictionary, not {}".format(
79 type(volume_map)))
80 errors = True
81
82 volume_config['device'] = volume_map.get(os.environ['JUJU_UNIT_NAME'])
83 if volume_config['device'] and volume_config['ephemeral']:
84 # asked for ephemeral storage but also defined a volume ID
85 hookenv.log('A volume is defined for this unit, but ephemeral '
86 'storage was requested', hookenv.ERROR)
87 errors = True
88 elif not volume_config['device'] and not volume_config['ephemeral']:
89 # asked for permanent storage but did not define volume ID
90 hookenv.log('Ephemeral storage was requested, but there is no volume '
91 'defined for this unit.', hookenv.ERROR)
92 errors = True
93
94 unit_mount_name = hookenv.local_unit().replace('/', '-')
95 volume_config['mountpoint'] = os.path.join(MOUNT_BASE, unit_mount_name)
96
97 if errors:
98 return None
99 return volume_config
100
101
102def mount_volume(config):
103 if os.path.exists(config['mountpoint']):
104 if not os.path.isdir(config['mountpoint']):
105 hookenv.log('Not a directory: {}'.format(config['mountpoint']))
106 raise VolumeConfigurationError()
107 else:
108 host.mkdir(config['mountpoint'])
109 if os.path.ismount(config['mountpoint']):
110 unmount_volume(config)
111 if not host.mount(config['device'], config['mountpoint'], persist=True):
112 raise VolumeConfigurationError()
113
114
115def unmount_volume(config):
116 if os.path.ismount(config['mountpoint']):
117 if not host.umount(config['mountpoint'], persist=True):
118 raise VolumeConfigurationError()
119
120
121def managed_mounts():
122 '''List of all mounted managed volumes'''
123 return filter(lambda mount: mount[0].startswith(MOUNT_BASE), host.mounts())
124
125
126def configure_volume(before_change=lambda: None, after_change=lambda: None):
127 '''Set up storage (or don't) according to the charm's volume configuration.
128 Returns the mount point or "ephemeral". before_change and after_change
129 are optional functions to be called if the volume configuration changes.
130 '''
131
132 config = get_config()
133 if not config:
134 hookenv.log('Failed to read volume configuration', hookenv.CRITICAL)
135 raise VolumeConfigurationError()
136
137 if config['ephemeral']:
138 if os.path.ismount(config['mountpoint']):
139 before_change()
140 unmount_volume(config)
141 after_change()
142 return 'ephemeral'
143 else:
144 # persistent storage
145 if os.path.ismount(config['mountpoint']):
146 mounts = dict(managed_mounts())
147 if mounts.get(config['mountpoint']) != config['device']:
148 before_change()
149 unmount_volume(config)
150 mount_volume(config)
151 after_change()
152 else:
153 before_change()
154 mount_volume(config)
155 after_change()
156 return config['mountpoint']
0157
=== added directory 'lib/charmhelpers/contrib/hahelpers'
=== added file 'lib/charmhelpers/contrib/hahelpers/__init__.py'
=== added file 'lib/charmhelpers/contrib/hahelpers/apache.py'
--- lib/charmhelpers/contrib/hahelpers/apache.py 1970-01-01 00:00:00 +0000
+++ lib/charmhelpers/contrib/hahelpers/apache.py 2013-11-21 22:43:22 +0000
@@ -0,0 +1,58 @@
1#
2# Copyright 2012 Canonical Ltd.
3#
4# This file is sourced from lp:openstack-charm-helpers
5#
6# Authors:
7# James Page <james.page@ubuntu.com>
8# Adam Gandelman <adamg@ubuntu.com>
9#
10
11import subprocess
12
13from charmhelpers.core.hookenv import (
14 config as config_get,
15 relation_get,
16 relation_ids,
17 related_units as relation_list,
18 log,
19 INFO,
20)
21
22
23def get_cert():
24 cert = config_get('ssl_cert')
25 key = config_get('ssl_key')
26 if not (cert and key):
27 log("Inspecting identity-service relations for SSL certificate.",
28 level=INFO)
29 cert = key = None
30 for r_id in relation_ids('identity-service'):
31 for unit in relation_list(r_id):
32 if not cert:
33 cert = relation_get('ssl_cert',
34 rid=r_id, unit=unit)
35 if not key:
36 key = relation_get('ssl_key',
37 rid=r_id, unit=unit)
38 return (cert, key)
39
40
41def get_ca_cert():
42 ca_cert = None
43 log("Inspecting identity-service relations for CA SSL certificate.",
44 level=INFO)
45 for r_id in relation_ids('identity-service'):
46 for unit in relation_list(r_id):
47 if not ca_cert:
48 ca_cert = relation_get('ca_cert',
49 rid=r_id, unit=unit)
50 return ca_cert
51
52
53def install_ca_cert(ca_cert):
54 if ca_cert:
55 with open('/usr/local/share/ca-certificates/keystone_juju_ca_cert.crt',
56 'w') as crt:
57 crt.write(ca_cert)
58 subprocess.check_call(['update-ca-certificates', '--fresh'])
059
=== added file 'lib/charmhelpers/contrib/hahelpers/cluster.py'
--- lib/charmhelpers/contrib/hahelpers/cluster.py 1970-01-01 00:00:00 +0000
+++ lib/charmhelpers/contrib/hahelpers/cluster.py 2013-11-21 22:43:22 +0000
@@ -0,0 +1,183 @@
1#
2# Copyright 2012 Canonical Ltd.
3#
4# Authors:
5# James Page <james.page@ubuntu.com>
6# Adam Gandelman <adamg@ubuntu.com>
7#
8
9import subprocess
10import os
11
12from socket import gethostname as get_unit_hostname
13
14from charmhelpers.core.hookenv import (
15 log,
16 relation_ids,
17 related_units as relation_list,
18 relation_get,
19 config as config_get,
20 INFO,
21 ERROR,
22 unit_get,
23)
24
25
26class HAIncompleteConfig(Exception):
27 pass
28
29
30def is_clustered():
31 for r_id in (relation_ids('ha') or []):
32 for unit in (relation_list(r_id) or []):
33 clustered = relation_get('clustered',
34 rid=r_id,
35 unit=unit)
36 if clustered:
37 return True
38 return False
39
40
41def is_leader(resource):
42 cmd = [
43 "crm", "resource",
44 "show", resource
45 ]
46 try:
47 status = subprocess.check_output(cmd)
48 except subprocess.CalledProcessError:
49 return False
50 else:
51 if get_unit_hostname() in status:
52 return True
53 else:
54 return False
55
56
57def peer_units():
58 peers = []
59 for r_id in (relation_ids('cluster') or []):
60 for unit in (relation_list(r_id) or []):
61 peers.append(unit)
62 return peers
63
64
65def oldest_peer(peers):
66 local_unit_no = int(os.getenv('JUJU_UNIT_NAME').split('/')[1])
67 for peer in peers:
68 remote_unit_no = int(peer.split('/')[1])
69 if remote_unit_no < local_unit_no:
70 return False
71 return True
72
73
74def eligible_leader(resource):
75 if is_clustered():
76 if not is_leader(resource):
77 log('Deferring action to CRM leader.', level=INFO)
78 return False
79 else:
80 peers = peer_units()
81 if peers and not oldest_peer(peers):
82 log('Deferring action to oldest service unit.', level=INFO)
83 return False
84 return True
85
86
87def https():
88 '''
89 Determines whether enough data has been provided in configuration
90 or relation data to configure HTTPS
91 .
92 returns: boolean
93 '''
94 if config_get('use-https') == "yes":
95 return True
96 if config_get('ssl_cert') and config_get('ssl_key'):
97 return True
98 for r_id in relation_ids('identity-service'):
99 for unit in relation_list(r_id):
100 rel_state = [
101 relation_get('https_keystone', rid=r_id, unit=unit),
102 relation_get('ssl_cert', rid=r_id, unit=unit),
103 relation_get('ssl_key', rid=r_id, unit=unit),
104 relation_get('ca_cert', rid=r_id, unit=unit),
105 ]
106 # NOTE: works around (LP: #1203241)
107 if (None not in rel_state) and ('' not in rel_state):
108 return True
109 return False
110
111
112def determine_api_port(public_port):
113 '''
114 Determine correct API server listening port based on
115 existence of HTTPS reverse proxy and/or haproxy.
116
117 public_port: int: standard public port for given service
118
119 returns: int: the correct listening port for the API service
120 '''
121 i = 0
122 if len(peer_units()) > 0 or is_clustered():
123 i += 1
124 if https():
125 i += 1
126 return public_port - (i * 10)
127
128
129def determine_haproxy_port(public_port):
130 '''
131 Description: Determine correct proxy listening port based on public IP +
132 existence of HTTPS reverse proxy.
133
134 public_port: int: standard public port for given service
135
136 returns: int: the correct listening port for the HAProxy service
137 '''
138 i = 0
139 if https():
140 i += 1
141 return public_port - (i * 10)
142
143
144def get_hacluster_config():
145 '''
146 Obtains all relevant configuration from charm configuration required
147 for initiating a relation to hacluster:
148
149 ha-bindiface, ha-mcastport, vip, vip_iface, vip_cidr
150
151 returns: dict: A dict containing settings keyed by setting name.
152 raises: HAIncompleteConfig if settings are missing.
153 '''
154 settings = ['ha-bindiface', 'ha-mcastport', 'vip', 'vip_iface', 'vip_cidr']
155 conf = {}
156 for setting in settings:
157 conf[setting] = config_get(setting)
158 missing = []
159 [missing.append(s) for s, v in conf.iteritems() if v is None]
160 if missing:
161 log('Insufficient config data to configure hacluster.', level=ERROR)
162 raise HAIncompleteConfig
163 return conf
164
165
166def canonical_url(configs, vip_setting='vip'):
167 '''
168 Returns the correct HTTP URL to this host given the state of HTTPS
169 configuration and hacluster.
170
171 :configs : OSTemplateRenderer: A config tempating object to inspect for
172 a complete https context.
173 :vip_setting: str: Setting in charm config that specifies
174 VIP address.
175 '''
176 scheme = 'http'
177 if 'https' in configs.complete_contexts():
178 scheme = 'https'
179 if is_clustered():
180 addr = config_get(vip_setting)
181 else:
182 addr = unit_get('private-address')
183 return '%s://%s' % (scheme, addr)
0184
=== added directory 'lib/charmhelpers/contrib/jujugui'
=== added file 'lib/charmhelpers/contrib/jujugui/__init__.py'
=== added file 'lib/charmhelpers/contrib/jujugui/utils.py'
--- lib/charmhelpers/contrib/jujugui/utils.py 1970-01-01 00:00:00 +0000
+++ lib/charmhelpers/contrib/jujugui/utils.py 2013-11-21 22:43:22 +0000
@@ -0,0 +1,602 @@
1"""Juju GUI charm utilities."""
2
3__all__ = [
4 'AGENT',
5 'APACHE',
6 'API_PORT',
7 'CURRENT_DIR',
8 'HAPROXY',
9 'IMPROV',
10 'JUJU_DIR',
11 'JUJU_GUI_DIR',
12 'JUJU_GUI_SITE',
13 'JUJU_PEM',
14 'WEB_PORT',
15 'bzr_checkout',
16 'chain',
17 'cmd_log',
18 'fetch_api',
19 'fetch_gui',
20 'find_missing_packages',
21 'first_path_in_dir',
22 'get_api_address',
23 'get_npm_cache_archive_url',
24 'get_release_file_url',
25 'get_staging_dependencies',
26 'get_zookeeper_address',
27 'legacy_juju',
28 'log_hook',
29 'merge',
30 'parse_source',
31 'prime_npm_cache',
32 'render_to_file',
33 'save_or_create_certificates',
34 'setup_apache',
35 'setup_gui',
36 'start_agent',
37 'start_gui',
38 'start_improv',
39 'write_apache_config',
40]
41
42from contextlib import contextmanager
43import errno
44import json
45import os
46import logging
47import shutil
48from subprocess import CalledProcessError
49import tempfile
50from urlparse import urlparse
51
52import apt
53import tempita
54
55from launchpadlib.launchpad import Launchpad
56from shelltoolbox import (
57 Serializer,
58 apt_get_install,
59 command,
60 environ,
61 install_extra_repositories,
62 run,
63 script_name,
64 search_file,
65 su,
66)
67from charmhelpers.core.host import (
68 service_start,
69)
70from charmhelpers.core.hookenv import (
71 log,
72 config,
73 unit_get,
74)
75
76
77AGENT = 'juju-api-agent'
78APACHE = 'apache2'
79IMPROV = 'juju-api-improv'
80HAPROXY = 'haproxy'
81
82API_PORT = 8080
83WEB_PORT = 8000
84
85CURRENT_DIR = os.getcwd()
86JUJU_DIR = os.path.join(CURRENT_DIR, 'juju')
87JUJU_GUI_DIR = os.path.join(CURRENT_DIR, 'juju-gui')
88JUJU_GUI_SITE = '/etc/apache2/sites-available/juju-gui'
89JUJU_GUI_PORTS = '/etc/apache2/ports.conf'
90JUJU_PEM = 'juju.includes-private-key.pem'
91BUILD_REPOSITORIES = ('ppa:chris-lea/node.js-legacy',)
92DEB_BUILD_DEPENDENCIES = (
93 'bzr', 'imagemagick', 'make', 'nodejs', 'npm',
94)
95DEB_STAGE_DEPENDENCIES = (
96 'zookeeper',
97)
98
99
100# Store the configuration from on invocation to the next.
101config_json = Serializer('/tmp/config.json')
102# Bazaar checkout command.
103bzr_checkout = command('bzr', 'co', '--lightweight')
104# Whether or not the charm is deployed using juju-core.
105# If juju-core has been used to deploy the charm, an agent.conf file must
106# be present in the charm parent directory.
107legacy_juju = lambda: not os.path.exists(
108 os.path.join(CURRENT_DIR, '..', 'agent.conf'))
109
110
111def _get_build_dependencies():
112 """Install deb dependencies for building."""
113 log('Installing build dependencies.')
114 cmd_log(install_extra_repositories(*BUILD_REPOSITORIES))
115 cmd_log(apt_get_install(*DEB_BUILD_DEPENDENCIES))
116
117
118def get_api_address(unit_dir):
119 """Return the Juju API address stored in the uniter agent.conf file."""
120 import yaml # python-yaml is only installed if juju-core is used.
121 # XXX 2013-03-27 frankban bug=1161443:
122 # currently the uniter agent.conf file does not include the API
123 # address. For now retrieve it from the machine agent file.
124 base_dir = os.path.abspath(os.path.join(unit_dir, '..'))
125 for dirname in os.listdir(base_dir):
126 if dirname.startswith('machine-'):
127 agent_conf = os.path.join(base_dir, dirname, 'agent.conf')
128 break
129 else:
130 raise IOError('Juju agent configuration file not found.')
131 contents = yaml.load(open(agent_conf))
132 return contents['apiinfo']['addrs'][0]
133
134
135def get_staging_dependencies():
136 """Install deb dependencies for the stage (improv) environment."""
137 log('Installing stage dependencies.')
138 cmd_log(apt_get_install(*DEB_STAGE_DEPENDENCIES))
139
140
141def first_path_in_dir(directory):
142 """Return the full path of the first file/dir in *directory*."""
143 return os.path.join(directory, os.listdir(directory)[0])
144
145
146def _get_by_attr(collection, attr, value):
147 """Return the first item in collection having attr == value.
148
149 Return None if the item is not found.
150 """
151 for item in collection:
152 if getattr(item, attr) == value:
153 return item
154
155
156def get_release_file_url(project, series_name, release_version):
157 """Return the URL of the release file hosted in Launchpad.
158
159 The returned URL points to a release file for the given project, series
160 name and release version.
161 The argument *project* is a project object as returned by launchpadlib.
162 The arguments *series_name* and *release_version* are strings. If
163 *release_version* is None, the URL of the latest release will be returned.
164 """
165 series = _get_by_attr(project.series, 'name', series_name)
166 if series is None:
167 raise ValueError('%r: series not found' % series_name)
168 # Releases are returned by Launchpad in reverse date order.
169 releases = list(series.releases)
170 if not releases:
171 raise ValueError('%r: series does not contain releases' % series_name)
172 if release_version is not None:
173 release = _get_by_attr(releases, 'version', release_version)
174 if release is None:
175 raise ValueError('%r: release not found' % release_version)
176 releases = [release]
177 for release in releases:
178 for file_ in release.files:
179 if str(file_).endswith('.tgz'):
180 return file_.file_link
181 raise ValueError('%r: file not found' % release_version)
182
183
184def get_zookeeper_address(agent_file_path):
185 """Retrieve the Zookeeper address contained in the given *agent_file_path*.
186
187 The *agent_file_path* is a path to a file containing a line similar to the
188 following::
189
190 env JUJU_ZOOKEEPER="address"
191 """
192 line = search_file('JUJU_ZOOKEEPER', agent_file_path).strip()
193 return line.split('=')[1].strip('"')
194
195
196@contextmanager
197def log_hook():
198 """Log when a hook starts and stops its execution.
199
200 Also log to stdout possible CalledProcessError exceptions raised executing
201 the hook.
202 """
203 script = script_name()
204 log(">>> Entering {}".format(script))
205 try:
206 yield
207 except CalledProcessError as err:
208 log('Exception caught:')
209 log(err.output)
210 raise
211 finally:
212 log("<<< Exiting {}".format(script))
213
214
215def parse_source(source):
216 """Parse the ``juju-gui-source`` option.
217
218 Return a tuple of two elements representing info on how to deploy Juju GUI.
219 Examples:
220 - ('stable', None): latest stable release;
221 - ('stable', '0.1.0'): stable release v0.1.0;
222 - ('trunk', None): latest trunk release;
223 - ('trunk', '0.1.0+build.1'): trunk release v0.1.0 bzr revision 1;
224 - ('branch', 'lp:juju-gui'): release is made from a branch;
225 - ('url', 'http://example.com/gui'): release from a downloaded file.
226 """
227 if source.startswith('url:'):
228 source = source[4:]
229 # Support file paths, including relative paths.
230 if urlparse(source).scheme == '':
231 if not source.startswith('/'):
232 source = os.path.join(os.path.abspath(CURRENT_DIR), source)
233 source = "file://%s" % source
234 return 'url', source
235 if source in ('stable', 'trunk'):
236 return source, None
237 if source.startswith('lp:') or source.startswith('http://'):
238 return 'branch', source
239 if 'build' in source:
240 return 'trunk', source
241 return 'stable', source
242
243
244def render_to_file(template_name, context, destination):
245 """Render the given *template_name* into *destination* using *context*.
246
247 The tempita template language is used to render contents
248 (see http://pythonpaste.org/tempita/).
249 The argument *template_name* is the name or path of the template file:
250 it may be either a path relative to ``../config`` or an absolute path.
251 The argument *destination* is a file path.
252 The argument *context* is a dict-like object.
253 """
254 template_path = os.path.abspath(template_name)
255 template = tempita.Template.from_filename(template_path)
256 with open(destination, 'w') as stream:
257 stream.write(template.substitute(context))
258
259
260results_log = None
261
262
263def _setupLogging():
264 global results_log
265 if results_log is not None:
266 return
267 cfg = config()
268 logging.basicConfig(
269 filename=cfg['command-log-file'],
270 level=logging.INFO,
271 format="%(asctime)s: %(name)s@%(levelname)s %(message)s")
272 results_log = logging.getLogger('juju-gui')
273
274
275def cmd_log(results):
276 global results_log
277 if not results:
278 return
279 if results_log is None:
280 _setupLogging()
281 # Since 'results' may be multi-line output, start it on a separate line
282 # from the logger timestamp, etc.
283 results_log.info('\n' + results)
284
285
286def start_improv(staging_env, ssl_cert_path,
287 config_path='/etc/init/juju-api-improv.conf'):
288 """Start a simulated juju environment using ``improv.py``."""
289 log('Setting up staging start up script.')
290 context = {
291 'juju_dir': JUJU_DIR,
292 'keys': ssl_cert_path,
293 'port': API_PORT,
294 'staging_env': staging_env,
295 }
296 render_to_file('config/juju-api-improv.conf.template', context, config_path)
297 log('Starting the staging backend.')
298 with su('root'):
299 service_start(IMPROV)
300
301
302def start_agent(
303 ssl_cert_path, config_path='/etc/init/juju-api-agent.conf',
304 read_only=False):
305 """Start the Juju agent and connect to the current environment."""
306 # Retrieve the Zookeeper address from the start up script.
307 unit_dir = os.path.realpath(os.path.join(CURRENT_DIR, '..'))
308 agent_file = '/etc/init/juju-{0}.conf'.format(os.path.basename(unit_dir))
309 zookeeper = get_zookeeper_address(agent_file)
310 log('Setting up API agent start up script.')
311 context = {
312 'juju_dir': JUJU_DIR,
313 'keys': ssl_cert_path,
314 'port': API_PORT,
315 'zookeeper': zookeeper,
316 'read_only': read_only
317 }
318 render_to_file('config/juju-api-agent.conf.template', context, config_path)
319 log('Starting API agent.')
320 with su('root'):
321 service_start(AGENT)
322
323
324def start_gui(
325 console_enabled, login_help, readonly, in_staging, ssl_cert_path,
326 charmworld_url, serve_tests, haproxy_path='/etc/haproxy/haproxy.cfg',
327 config_js_path=None, secure=True, sandbox=False):
328 """Set up and start the Juju GUI server."""
329 with su('root'):
330 run('chown', '-R', 'ubuntu:', JUJU_GUI_DIR)
331 # XXX 2013-02-05 frankban bug=1116320:
332 # External insecure resources are still loaded when testing in the
333 # debug environment. For now, switch to the production environment if
334 # the charm is configured to serve tests.
335 if in_staging and not serve_tests:
336 build_dirname = 'build-debug'
337 else:
338 build_dirname = 'build-prod'
339 build_dir = os.path.join(JUJU_GUI_DIR, build_dirname)
340 log('Generating the Juju GUI configuration file.')
341 is_legacy_juju = legacy_juju()
342 user, password = None, None
343 if (is_legacy_juju and in_staging) or sandbox:
344 user, password = 'admin', 'admin'
345 else:
346 user, password = None, None
347
348 api_backend = 'python' if is_legacy_juju else 'go'
349 if secure:
350 protocol = 'wss'
351 else:
352 log('Running in insecure mode! Port 80 will serve unencrypted.')
353 protocol = 'ws'
354
355 context = {
356 'raw_protocol': protocol,
357 'address': unit_get('public-address'),
358 'console_enabled': json.dumps(console_enabled),
359 'login_help': json.dumps(login_help),
360 'password': json.dumps(password),
361 'api_backend': json.dumps(api_backend),
362 'readonly': json.dumps(readonly),
363 'user': json.dumps(user),
364 'protocol': json.dumps(protocol),
365 'sandbox': json.dumps(sandbox),
366 'charmworld_url': json.dumps(charmworld_url),
367 }
368 if config_js_path is None:
369 config_js_path = os.path.join(
370 build_dir, 'juju-ui', 'assets', 'config.js')
371 render_to_file('config/config.js.template', context, config_js_path)
372
373 write_apache_config(build_dir, serve_tests)
374
375 log('Generating haproxy configuration file.')
376 if is_legacy_juju:
377 # The PyJuju API agent is listening on localhost.
378 api_address = '127.0.0.1:{0}'.format(API_PORT)
379 else:
380 # Retrieve the juju-core API server address.
381 api_address = get_api_address(os.path.join(CURRENT_DIR, '..'))
382 context = {
383 'api_address': api_address,
384 'api_pem': JUJU_PEM,
385 'legacy_juju': is_legacy_juju,
386 'ssl_cert_path': ssl_cert_path,
387 # In PyJuju environments, use the same certificate for both HTTPS and
388 # WebSocket connections. In juju-core the system already has the proper
389 # certificate installed.
390 'web_pem': JUJU_PEM,
391 'web_port': WEB_PORT,
392 'secure': secure
393 }
394 render_to_file('config/haproxy.cfg.template', context, haproxy_path)
395 log('Starting Juju GUI.')
396
397
398def write_apache_config(build_dir, serve_tests=False):
399 log('Generating the apache site configuration file.')
400 context = {
401 'port': WEB_PORT,
402 'serve_tests': serve_tests,
403 'server_root': build_dir,
404 'tests_root': os.path.join(JUJU_GUI_DIR, 'test', ''),
405 }
406 render_to_file('config/apache-ports.template', context, JUJU_GUI_PORTS)
407 render_to_file('config/apache-site.template', context, JUJU_GUI_SITE)
408
409
410def get_npm_cache_archive_url(Launchpad=Launchpad):
411 """Figure out the URL of the most recent NPM cache archive on Launchpad."""
412 launchpad = Launchpad.login_anonymously('Juju GUI charm', 'production')
413 project = launchpad.projects['juju-gui']
414 # Find the URL of the most recently created NPM cache archive.
415 npm_cache_url = get_release_file_url(project, 'npm-cache', None)
416 return npm_cache_url
417
418
419def prime_npm_cache(npm_cache_url):
420 """Download NPM cache archive and prime the NPM cache with it."""
421 # Download the cache archive and then uncompress it into the NPM cache.
422 npm_cache_archive = os.path.join(CURRENT_DIR, 'npm-cache.tgz')
423 cmd_log(run('curl', '-L', '-o', npm_cache_archive, npm_cache_url))
424 npm_cache_dir = os.path.expanduser('~/.npm')
425 # The NPM cache directory probably does not exist, so make it if not.
426 try:
427 os.mkdir(npm_cache_dir)
428 except OSError, e:
429 # If the directory already exists then ignore the error.
430 if e.errno != errno.EEXIST: # File exists.
431 raise
432 uncompress = command('tar', '-x', '-z', '-C', npm_cache_dir, '-f')
433 cmd_log(uncompress(npm_cache_archive))
434
435
436def fetch_gui(juju_gui_source, logpath):
437 """Retrieve the Juju GUI release/branch."""
438 # Retrieve a Juju GUI release.
439 origin, version_or_branch = parse_source(juju_gui_source)
440 if origin == 'branch':
441 # Make sure we have the dependencies necessary for us to actually make
442 # a build.
443 _get_build_dependencies()
444 # Create a release starting from a branch.
445 juju_gui_source_dir = os.path.join(CURRENT_DIR, 'juju-gui-source')
446 log('Retrieving Juju GUI source checkout from %s.' % version_or_branch)
447 cmd_log(run('rm', '-rf', juju_gui_source_dir))
448 cmd_log(bzr_checkout(version_or_branch, juju_gui_source_dir))
449 log('Preparing a Juju GUI release.')
450 logdir = os.path.dirname(logpath)
451 fd, name = tempfile.mkstemp(prefix='make-distfile-', dir=logdir)
452 log('Output from "make distfile" sent to %s' % name)
453 with environ(NO_BZR='1'):
454 run('make', '-C', juju_gui_source_dir, 'distfile',
455 stdout=fd, stderr=fd)
456 release_tarball = first_path_in_dir(
457 os.path.join(juju_gui_source_dir, 'releases'))
458 else:
459 log('Retrieving Juju GUI release.')
460 if origin == 'url':
461 file_url = version_or_branch
462 else:
463 # Retrieve a release from Launchpad.
464 launchpad = Launchpad.login_anonymously(
465 'Juju GUI charm', 'production')
466 project = launchpad.projects['juju-gui']
467 file_url = get_release_file_url(project, origin, version_or_branch)
468 log('Downloading release file from %s.' % file_url)
469 release_tarball = os.path.join(CURRENT_DIR, 'release.tgz')
470 cmd_log(run('curl', '-L', '-o', release_tarball, file_url))
471 return release_tarball
472
473
474def fetch_api(juju_api_branch):
475 """Retrieve the Juju branch."""
476 # Retrieve Juju API source checkout.
477 log('Retrieving Juju API source checkout.')
478 cmd_log(run('rm', '-rf', JUJU_DIR))
479 cmd_log(bzr_checkout(juju_api_branch, JUJU_DIR))
480
481
482def setup_gui(release_tarball):
483 """Set up Juju GUI."""
484 # Uncompress the release tarball.
485 log('Installing Juju GUI.')
486 release_dir = os.path.join(CURRENT_DIR, 'release')
487 cmd_log(run('rm', '-rf', release_dir))
488 os.mkdir(release_dir)
489 uncompress = command('tar', '-x', '-z', '-C', release_dir, '-f')
490 cmd_log(uncompress(release_tarball))
491 # Link the Juju GUI dir to the contents of the release tarball.
492 cmd_log(run('ln', '-sf', first_path_in_dir(release_dir), JUJU_GUI_DIR))
493
494
495def setup_apache():
496 """Set up apache."""
497 log('Setting up apache.')
498 if not os.path.exists(JUJU_GUI_SITE):
499 cmd_log(run('touch', JUJU_GUI_SITE))
500 cmd_log(run('chown', 'ubuntu:', JUJU_GUI_SITE))
501 cmd_log(
502 run('ln', '-s', JUJU_GUI_SITE,
503 '/etc/apache2/sites-enabled/juju-gui'))
504
505 if not os.path.exists(JUJU_GUI_PORTS):
506 cmd_log(run('touch', JUJU_GUI_PORTS))
507 cmd_log(run('chown', 'ubuntu:', JUJU_GUI_PORTS))
508
509 with su('root'):
510 run('a2dissite', 'default')
511 run('a2ensite', 'juju-gui')
512
513
514def save_or_create_certificates(
515 ssl_cert_path, ssl_cert_contents, ssl_key_contents):
516 """Generate the SSL certificates.
517
518 If both *ssl_cert_contents* and *ssl_key_contents* are provided, use them
519 as certificates; otherwise, generate them.
520
521 Also create a pem file, suitable for use in the haproxy configuration,
522 concatenating the key and the certificate files.
523 """
524 crt_path = os.path.join(ssl_cert_path, 'juju.crt')
525 key_path = os.path.join(ssl_cert_path, 'juju.key')
526 if not os.path.exists(ssl_cert_path):
527 os.makedirs(ssl_cert_path)
528 if ssl_cert_contents and ssl_key_contents:
529 # Save the provided certificates.
530 with open(crt_path, 'w') as cert_file:
531 cert_file.write(ssl_cert_contents)
532 with open(key_path, 'w') as key_file:
533 key_file.write(ssl_key_contents)
534 else:
535 # Generate certificates.
536 # See http://superuser.com/questions/226192/openssl-without-prompt
537 cmd_log(run(
538 'openssl', 'req', '-new', '-newkey', 'rsa:4096',
539 '-days', '365', '-nodes', '-x509', '-subj',
540 # These are arbitrary test values for the certificate.
541 '/C=GB/ST=Juju/L=GUI/O=Ubuntu/CN=juju.ubuntu.com',
542 '-keyout', key_path, '-out', crt_path))
543 # Generate the pem file.
544 pem_path = os.path.join(ssl_cert_path, JUJU_PEM)
545 if os.path.exists(pem_path):
546 os.remove(pem_path)
547 with open(pem_path, 'w') as pem_file:
548 shutil.copyfileobj(open(key_path), pem_file)
549 shutil.copyfileobj(open(crt_path), pem_file)
550
551
552def find_missing_packages(*packages):
553 """Given a list of packages, return the packages which are not installed.
554 """
555 cache = apt.Cache()
556 missing = set()
557 for pkg_name in packages:
558 try:
559 pkg = cache[pkg_name]
560 except KeyError:
561 missing.add(pkg_name)
562 continue
563 if pkg.is_installed:
564 continue
565 missing.add(pkg_name)
566 return missing
567
568
569## Backend support decorators
570
571def chain(name):
572 """Helper method to compose a set of mixin objects into a callable.
573
574 Each method is called in the context of its mixin instance, and its
575 argument is the Backend instance.
576 """
577 # Chain method calls through all implementing mixins.
578 def method(self):
579 for mixin in self.mixins:
580 a_callable = getattr(type(mixin), name, None)
581 if a_callable:
582 a_callable(mixin, self)
583
584 method.__name__ = name
585 return method
586
587
588def merge(name):
589 """Helper to merge a property from a set of strategy objects
590 into a unified set.
591 """
592 # Return merged property from every providing mixin as a set.
593 @property
594 def method(self):
595 result = set()
596 for mixin in self.mixins:
597 segment = getattr(type(mixin), name, None)
598 if segment and isinstance(segment, (list, tuple, set)):
599 result |= set(segment)
600
601 return result
602 return method
0603
=== added directory 'lib/charmhelpers/contrib/saltstack'
=== added file 'lib/charmhelpers/contrib/saltstack/__init__.py'
--- lib/charmhelpers/contrib/saltstack/__init__.py 1970-01-01 00:00:00 +0000
+++ lib/charmhelpers/contrib/saltstack/__init__.py 2013-11-21 22:43:22 +0000
@@ -0,0 +1,149 @@
1"""Charm Helpers saltstack - declare the state of your machines.
2
3This helper enables you to declare your machine state, rather than
4program it procedurally (and have to test each change to your procedures).
5Your install hook can be as simple as:
6
7{{{
8from charmhelpers.contrib.saltstack import (
9 install_salt_support,
10 update_machine_state,
11)
12
13
14def install():
15 install_salt_support()
16 update_machine_state('machine_states/dependencies.yaml')
17 update_machine_state('machine_states/installed.yaml')
18}}}
19
20and won't need to change (nor will its tests) when you change the machine
21state.
22
23It's using a python package called salt-minion which allows various formats for
24specifying resources, such as:
25
26{{{
27/srv/{{ basedir }}:
28 file.directory:
29 - group: ubunet
30 - user: ubunet
31 - require:
32 - user: ubunet
33 - recurse:
34 - user
35 - group
36
37ubunet:
38 group.present:
39 - gid: 1500
40 user.present:
41 - uid: 1500
42 - gid: 1500
43 - createhome: False
44 - require:
45 - group: ubunet
46}}}
47
48The docs for all the different state definitions are at:
49 http://docs.saltstack.com/ref/states/all/
50
51
52TODO:
53 * Add test helpers which will ensure that machine state definitions
54 are functionally (but not necessarily logically) correct (ie. getting
55 salt to parse all state defs.
56 * Add a link to a public bootstrap charm example / blogpost.
57 * Find a way to obviate the need to use the grains['charm_dir'] syntax
58 in templates.
59"""
60# Copyright 2013 Canonical Ltd.
61#
62# Authors:
63# Charm Helpers Developers <juju@lists.ubuntu.com>
64import os
65import subprocess
66import yaml
67
68import charmhelpers.core.host
69import charmhelpers.core.hookenv
70
71
72charm_dir = os.environ.get('CHARM_DIR', '')
73salt_grains_path = '/etc/salt/grains'
74
75
76def install_salt_support(from_ppa=True):
77 """Installs the salt-minion helper for machine state.
78
79 By default the salt-minion package is installed from
80 the saltstack PPA. If from_ppa is False you must ensure
81 that the salt-minion package is available in the apt cache.
82 """
83 if from_ppa:
84 subprocess.check_call([
85 '/usr/bin/add-apt-repository',
86 '--yes',
87 'ppa:saltstack/salt',
88 ])
89 subprocess.check_call(['/usr/bin/apt-get', 'update'])
90 # We install salt-common as salt-minion would run the salt-minion
91 # daemon.
92 charmhelpers.fetch.apt_install('salt-common')
93
94
95def update_machine_state(state_path):
96 """Update the machine state using the provided state declaration."""
97 juju_state_to_yaml(salt_grains_path)
98 subprocess.check_call([
99 'salt-call',
100 '--local',
101 'state.template',
102 state_path,
103 ])
104
105
106def juju_state_to_yaml(yaml_path, namespace_separator=':'):
107 """Update the juju config and state in a yaml file.
108
109 This includes any current relation-get data, and the charm
110 directory.
111 """
112 config = charmhelpers.core.hookenv.config()
113
114 # Add the charm_dir which we will need to refer to charm
115 # file resources etc.
116 config['charm_dir'] = charm_dir
117 config['local_unit'] = charmhelpers.core.hookenv.local_unit()
118
119 # Add any relation data prefixed with the relation type.
120 relation_type = charmhelpers.core.hookenv.relation_type()
121 if relation_type is not None:
122 relation_data = charmhelpers.core.hookenv.relation_get()
123 relation_data = dict(
124 ("{relation_type}{namespace_separator}{key}".format(
125 relation_type=relation_type.replace('-', '_'),
126 key=key,
127 namespace_separator=namespace_separator), val)
128 for key, val in relation_data.items())
129 config.update(relation_data)
130
131 # Don't use non-standard tags for unicode which will not
132 # work when salt uses yaml.load_safe.
133 yaml.add_representer(unicode, lambda dumper,
134 value: dumper.represent_scalar(
135 u'tag:yaml.org,2002:str', value))
136
137 yaml_dir = os.path.dirname(yaml_path)
138 if not os.path.exists(yaml_dir):
139 os.makedirs(yaml_dir)
140
141 if os.path.exists(yaml_path):
142 with open(yaml_path, "r") as existing_vars_file:
143 existing_vars = yaml.load(existing_vars_file.read())
144 else:
145 existing_vars = {}
146
147 existing_vars.update(config)
148 with open(yaml_path, "w+") as fp:
149 fp.write(yaml.dump(existing_vars))
0150
=== added directory 'lib/charmhelpers/core'
=== added file 'lib/charmhelpers/core/__init__.py'
=== added file 'lib/charmhelpers/core/hookenv.py'
--- lib/charmhelpers/core/hookenv.py 1970-01-01 00:00:00 +0000
+++ lib/charmhelpers/core/hookenv.py 2013-11-21 22:43:22 +0000
@@ -0,0 +1,395 @@
1"Interactions with the Juju environment"
2# Copyright 2013 Canonical Ltd.
3#
4# Authors:
5# Charm Helpers Developers <juju@lists.ubuntu.com>
6
7import os
8import json
9import yaml
10import subprocess
11import UserDict
12from subprocess import CalledProcessError
13
14CRITICAL = "CRITICAL"
15ERROR = "ERROR"
16WARNING = "WARNING"
17INFO = "INFO"
18DEBUG = "DEBUG"
19MARKER = object()
20
21cache = {}
22
23
24def cached(func):
25 """Cache return values for multiple executions of func + args
26
27 For example:
28
29 @cached
30 def unit_get(attribute):
31 pass
32
33 unit_get('test')
34
35 will cache the result of unit_get + 'test' for future calls.
36 """
37 def wrapper(*args, **kwargs):
38 global cache
39 key = str((func, args, kwargs))
40 try:
41 return cache[key]
42 except KeyError:
43 res = func(*args, **kwargs)
44 cache[key] = res
45 return res
46 return wrapper
47
48
49def flush(key):
50 """Flushes any entries from function cache where the
51 key is found in the function+args """
52 flush_list = []
53 for item in cache:
54 if key in item:
55 flush_list.append(item)
56 for item in flush_list:
57 del cache[item]
58
59
60def log(message, level=None):
61 """Write a message to the juju log"""
62 command = ['juju-log']
63 if level:
64 command += ['-l', level]
65 command += [message]
66 subprocess.call(command)
67
68
69class Serializable(UserDict.IterableUserDict):
70 """Wrapper, an object that can be serialized to yaml or json"""
71
72 def __init__(self, obj):
73 # wrap the object
74 UserDict.IterableUserDict.__init__(self)
75 self.data = obj
76
77 def __getattr__(self, attr):
78 # See if this object has attribute.
79 if attr in ("json", "yaml", "data"):
80 return self.__dict__[attr]
81 # Check for attribute in wrapped object.
82 got = getattr(self.data, attr, MARKER)
83 if got is not MARKER:
84 return got
85 # Proxy to the wrapped object via dict interface.
86 try:
87 return self.data[attr]
88 except KeyError:
89 raise AttributeError(attr)
90
91 def __getstate__(self):
92 # Pickle as a standard dictionary.
93 return self.data
94
95 def __setstate__(self, state):
96 # Unpickle into our wrapper.
97 self.data = state
98
99 def json(self):
100 """Serialize the object to json"""
101 return json.dumps(self.data)
102
103 def yaml(self):
104 """Serialize the object to yaml"""
105 return yaml.dump(self.data)
106
107
108def execution_environment():
109 """A convenient bundling of the current execution context"""
110 context = {}
111 context['conf'] = config()
112 if relation_id():
113 context['reltype'] = relation_type()
114 context['relid'] = relation_id()
115 context['rel'] = relation_get()
116 context['unit'] = local_unit()
117 context['rels'] = relations()
118 context['env'] = os.environ
119 return context
120
121
122def in_relation_hook():
123 """Determine whether we're running in a relation hook"""
124 return 'JUJU_RELATION' in os.environ
125
126
127def relation_type():
128 """The scope for the current relation hook"""
129 return os.environ.get('JUJU_RELATION', None)
130
131
132def relation_id():
133 """The relation ID for the current relation hook"""
134 return os.environ.get('JUJU_RELATION_ID', None)
135
136
137def local_unit():
138 """Local unit ID"""
139 return os.environ['JUJU_UNIT_NAME']
140
141
142def remote_unit():
143 """The remote unit for the current relation hook"""
144 return os.environ['JUJU_REMOTE_UNIT']
145
146
147def service_name():
148 """The name service group this unit belongs to"""
149 return local_unit().split('/')[0]
150
151
152@cached
153def config(scope=None):
154 """Juju charm configuration"""
155 config_cmd_line = ['config-get']
156 if scope is not None:
157 config_cmd_line.append(scope)
158 config_cmd_line.append('--format=json')
159 try:
160 return json.loads(subprocess.check_output(config_cmd_line))
161 except ValueError:
162 return None
163
164
165@cached
166def relation_get(attribute=None, unit=None, rid=None):
167 """Get relation information"""
168 _args = ['relation-get', '--format=json']
169 if rid:
170 _args.append('-r')
171 _args.append(rid)
172 _args.append(attribute or '-')
173 if unit:
174 _args.append(unit)
175 try:
176 return json.loads(subprocess.check_output(_args))
177 except ValueError:
178 return None
179 except CalledProcessError, e:
180 if e.returncode == 2:
181 return None
182 raise
183
184
185def relation_set(relation_id=None, relation_settings={}, **kwargs):
186 """Set relation information for the current unit"""
187 relation_cmd_line = ['relation-set']
188 if relation_id is not None:
189 relation_cmd_line.extend(('-r', relation_id))
190 for k, v in (relation_settings.items() + kwargs.items()):
191 if v is None:
192 relation_cmd_line.append('{}='.format(k))
193 else:
194 relation_cmd_line.append('{}={}'.format(k, v))
195 subprocess.check_call(relation_cmd_line)
196 # Flush cache of any relation-gets for local unit
197 flush(local_unit())
198
199
200@cached
201def relation_ids(reltype=None):
202 """A list of relation_ids"""
203 reltype = reltype or relation_type()
204 relid_cmd_line = ['relation-ids', '--format=json']
205 if reltype is not None:
206 relid_cmd_line.append(reltype)
207 return json.loads(subprocess.check_output(relid_cmd_line)) or []
208 return []
209
210
211@cached
212def related_units(relid=None):
213 """A list of related units"""
214 relid = relid or relation_id()
215 units_cmd_line = ['relation-list', '--format=json']
216 if relid is not None:
217 units_cmd_line.extend(('-r', relid))
218 return json.loads(subprocess.check_output(units_cmd_line)) or []
219
220
221@cached
222def relation_for_unit(unit=None, rid=None):
223 """Get the json represenation of a unit's relation"""
224 unit = unit or remote_unit()
225 relation = relation_get(unit=unit, rid=rid)
226 for key in relation:
227 if key.endswith('-list'):
228 relation[key] = relation[key].split()
229 relation['__unit__'] = unit
230 return relation
231
232
233@cached
234def relations_for_id(relid=None):
235 """Get relations of a specific relation ID"""
236 relation_data = []
237 relid = relid or relation_ids()
238 for unit in related_units(relid):
239 unit_data = relation_for_unit(unit, relid)
240 unit_data['__relid__'] = relid
241 relation_data.append(unit_data)
242 return relation_data
243
244
245@cached
246def relations_of_type(reltype=None):
247 """Get relations of a specific type"""
248 relation_data = []
249 reltype = reltype or relation_type()
250 for relid in relation_ids(reltype):
251 for relation in relations_for_id(relid):
252 relation['__relid__'] = relid
253 relation_data.append(relation)
254 return relation_data
255
256
257@cached
258def relation_types():
259 """Get a list of relation types supported by this charm"""
260 charmdir = os.environ.get('CHARM_DIR', '')
261 mdf = open(os.path.join(charmdir, 'metadata.yaml'))
262 md = yaml.safe_load(mdf)
263 rel_types = []
264 for key in ('provides', 'requires', 'peers'):
265 section = md.get(key)
266 if section:
267 rel_types.extend(section.keys())
268 mdf.close()
269 return rel_types
270
271
272@cached
273def relations():
274 """Get a nested dictionary of relation data for all related units"""
275 rels = {}
276 for reltype in relation_types():
277 relids = {}
278 for relid in relation_ids(reltype):
279 units = {local_unit(): relation_get(unit=local_unit(), rid=relid)}
280 for unit in related_units(relid):
281 reldata = relation_get(unit=unit, rid=relid)
282 units[unit] = reldata
283 relids[relid] = units
284 rels[reltype] = relids
285 return rels
286
287
288@cached
289def is_relation_made(relation, keys='private-address'):
290 '''
291 Determine whether a relation is established by checking for
292 presence of key(s). If a list of keys is provided, they
293 must all be present for the relation to be identified as made
294 '''
295 if isinstance(keys, str):
296 keys = [keys]
297 for r_id in relation_ids(relation):
298 for unit in related_units(r_id):
299 context = {}
300 for k in keys:
301 context[k] = relation_get(k, rid=r_id,
302 unit=unit)
303 if None not in context.values():
304 return True
305 return False
306
307
308def open_port(port, protocol="TCP"):
309 """Open a service network port"""
310 _args = ['open-port']
311 _args.append('{}/{}'.format(port, protocol))
312 subprocess.check_call(_args)
313
314
315def close_port(port, protocol="TCP"):
316 """Close a service network port"""
317 _args = ['close-port']
318 _args.append('{}/{}'.format(port, protocol))
319 subprocess.check_call(_args)
320
321
322@cached
323def unit_get(attribute):
324 """Get the unit ID for the remote unit"""
325 _args = ['unit-get', '--format=json', attribute]
326 try:
327 return json.loads(subprocess.check_output(_args))
328 except ValueError:
329 return None
330
331
332def unit_private_ip():
333 """Get this unit's private IP address"""
334 return unit_get('private-address')
335
336
337class UnregisteredHookError(Exception):
338 """Raised when an undefined hook is called"""
339 pass
340
341
342class Hooks(object):
343 """A convenient handler for hook functions.
344
345 Example:
346 hooks = Hooks()
347
348 # register a hook, taking its name from the function name
349 @hooks.hook()
350 def install():
351 ...
352
353 # register a hook, providing a custom hook name
354 @hooks.hook("config-changed")
355 def config_changed():
356 ...
357
358 if __name__ == "__main__":
359 # execute a hook based on the name the program is called by
360 hooks.execute(sys.argv)
361 """
362
363 def __init__(self):
364 super(Hooks, self).__init__()
365 self._hooks = {}
366
367 def register(self, name, function):
368 """Register a hook"""
369 self._hooks[name] = function
370
371 def execute(self, args):
372 """Execute a registered hook based on args[0]"""
373 hook_name = os.path.basename(args[0])
374 if hook_name in self._hooks:
375 self._hooks[hook_name]()
376 else:
377 raise UnregisteredHookError(hook_name)
378
379 def hook(self, *hook_names):
380 """Decorator, registering them as hooks"""
381 def wrapper(decorated):
382 for hook_name in hook_names:
383 self.register(hook_name, decorated)
384 else:
385 self.register(decorated.__name__, decorated)
386 if '_' in decorated.__name__:
387 self.register(
388 decorated.__name__.replace('_', '-'), decorated)
389 return decorated
390 return wrapper
391
392
393def charm_dir():
394 """Return the root directory of the current charm"""
395 return os.environ.get('CHARM_DIR')
0396
=== added file 'lib/charmhelpers/core/host.py'
--- lib/charmhelpers/core/host.py 1970-01-01 00:00:00 +0000
+++ lib/charmhelpers/core/host.py 2013-11-21 22:43:22 +0000
@@ -0,0 +1,281 @@
1"""Tools for working with the host system"""
2# Copyright 2012 Canonical Ltd.
3#
4# Authors:
5# Nick Moffitt <nick.moffitt@canonical.com>
6# Matthew Wedgwood <matthew.wedgwood@canonical.com>
7
8import os
9import pwd
10import grp
11import random
12import string
13import subprocess
14import hashlib
15
16from collections import OrderedDict
17
18from hookenv import log
19
20
21def service_start(service_name):
22 """Start a system service"""
23 return service('start', service_name)
24
25
26def service_stop(service_name):
27 """Stop a system service"""
28 return service('stop', service_name)
29
30
31def service_restart(service_name):
32 """Restart a system service"""
33 return service('restart', service_name)
34
35
36def service_reload(service_name, restart_on_failure=False):
37 """Reload a system service, optionally falling back to restart if reload fails"""
38 service_result = service('reload', service_name)
39 if not service_result and restart_on_failure:
40 service_result = service('restart', service_name)
41 return service_result
42
43
44def service(action, service_name):
45 """Control a system service"""
46 cmd = ['service', service_name, action]
47 return subprocess.call(cmd) == 0
48
49
50def service_running(service):
51 """Determine whether a system service is running"""
52 try:
53 output = subprocess.check_output(['service', service, 'status'])
54 except subprocess.CalledProcessError:
55 return False
56 else:
57 if ("start/running" in output or "is running" in output):
58 return True
59 else:
60 return False
61
62
63def adduser(username, password=None, shell='/bin/bash', system_user=False):
64 """Add a user to the system"""
65 try:
66 user_info = pwd.getpwnam(username)
67 log('user {0} already exists!'.format(username))
68 except KeyError:
69 log('creating user {0}'.format(username))
70 cmd = ['useradd']
71 if system_user or password is None:
72 cmd.append('--system')
73 else:
74 cmd.extend([
75 '--create-home',
76 '--shell', shell,
77 '--password', password,
78 ])
79 cmd.append(username)
80 subprocess.check_call(cmd)
81 user_info = pwd.getpwnam(username)
82 return user_info
83
84
85def add_user_to_group(username, group):
86 """Add a user to a group"""
87 cmd = [
88 'gpasswd', '-a',
89 username,
90 group
91 ]
92 log("Adding user {} to group {}".format(username, group))
93 subprocess.check_call(cmd)
94
95
96def rsync(from_path, to_path, flags='-r', options=None):
97 """Replicate the contents of a path"""
98 options = options or ['--delete', '--executability']
99 cmd = ['/usr/bin/rsync', flags]
100 cmd.extend(options)
101 cmd.append(from_path)
102 cmd.append(to_path)
103 log(" ".join(cmd))
104 return subprocess.check_output(cmd).strip()
105
106
107def symlink(source, destination):
108 """Create a symbolic link"""
109 log("Symlinking {} as {}".format(source, destination))
110 cmd = [
111 'ln',
112 '-sf',
113 source,
114 destination,
115 ]
116 subprocess.check_call(cmd)
117
118
119def mkdir(path, owner='root', group='root', perms=0555, force=False):
120 """Create a directory"""
121 log("Making dir {} {}:{} {:o}".format(path, owner, group,
122 perms))
123 uid = pwd.getpwnam(owner).pw_uid
124 gid = grp.getgrnam(group).gr_gid
125 realpath = os.path.abspath(path)
126 if os.path.exists(realpath):
127 if force and not os.path.isdir(realpath):
128 log("Removing non-directory file {} prior to mkdir()".format(path))
129 os.unlink(realpath)
130 else:
131 os.makedirs(realpath, perms)
132 os.chown(realpath, uid, gid)
133
134
135def write_file(path, content, owner='root', group='root', perms=0444):
136 """Create or overwrite a file with the contents of a string"""
137 log("Writing file {} {}:{} {:o}".format(path, owner, group, perms))
138 uid = pwd.getpwnam(owner).pw_uid
139 gid = grp.getgrnam(group).gr_gid
140 with open(path, 'w') as target:
141 os.fchown(target.fileno(), uid, gid)
142 os.fchmod(target.fileno(), perms)
143 target.write(content)
144
145
146def mount(device, mountpoint, options=None, persist=False):
147 """Mount a filesystem at a particular mountpoint"""
148 cmd_args = ['mount']
149 if options is not None:
150 cmd_args.extend(['-o', options])
151 cmd_args.extend([device, mountpoint])
152 try:
153 subprocess.check_output(cmd_args)
154 except subprocess.CalledProcessError, e:
155 log('Error mounting {} at {}\n{}'.format(device, mountpoint, e.output))
156 return False
157 if persist:
158 # TODO: update fstab
159 pass
160 return True
161
162
163def umount(mountpoint, persist=False):
164 """Unmount a filesystem"""
165 cmd_args = ['umount', mountpoint]
166 try:
167 subprocess.check_output(cmd_args)
168 except subprocess.CalledProcessError, e:
169 log('Error unmounting {}\n{}'.format(mountpoint, e.output))
170 return False
171 if persist:
172 # TODO: update fstab
173 pass
174 return True
175
176
177def mounts():
178 """Get a list of all mounted volumes as [[mountpoint,device],[...]]"""
179 with open('/proc/mounts') as f:
180 # [['/mount/point','/dev/path'],[...]]
181 system_mounts = [m[1::-1] for m in [l.strip().split()
182 for l in f.readlines()]]
183 return system_mounts
184
185
186def file_hash(path):
187 """Generate a md5 hash of the contents of 'path' or None if not found """
188 if os.path.exists(path):
189 h = hashlib.md5()
190 with open(path, 'r') as source:
191 h.update(source.read()) # IGNORE:E1101 - it does have update
192 return h.hexdigest()
193 else:
194 return None
195
196
197def restart_on_change(restart_map):
198 """Restart services based on configuration files changing
199
200 This function is used a decorator, for example
201
202 @restart_on_change({
203 '/etc/ceph/ceph.conf': [ 'cinder-api', 'cinder-volume' ]
204 })
205 def ceph_client_changed():
206 ...
207
208 In this example, the cinder-api and cinder-volume services
209 would be restarted if /etc/ceph/ceph.conf is changed by the
210 ceph_client_changed function.
211 """
212 def wrap(f):
213 def wrapped_f(*args):
214 checksums = {}
215 for path in restart_map:
216 checksums[path] = file_hash(path)
217 f(*args)
218 restarts = []
219 for path in restart_map:
220 if checksums[path] != file_hash(path):
221 restarts += restart_map[path]
222 for service_name in list(OrderedDict.fromkeys(restarts)):
223 service('restart', service_name)
224 return wrapped_f
225 return wrap
226
227
228def lsb_release():
229 """Return /etc/lsb-release in a dict"""
230 d = {}
231 with open('/etc/lsb-release', 'r') as lsb:
232 for l in lsb:
233 k, v = l.split('=')
234 d[k.strip()] = v.strip()
235 return d
236
237
238def pwgen(length=None):
239 """Generate a random pasword."""
240 if length is None:
241 length = random.choice(range(35, 45))
242 alphanumeric_chars = [
243 l for l in (string.letters + string.digits)
244 if l not in 'l0QD1vAEIOUaeiou']
245 random_chars = [
246 random.choice(alphanumeric_chars) for _ in range(length)]
247 return(''.join(random_chars))
248
249
250def list_nics(nic_type):
251 '''Return a list of nics of given type(s)'''
252 if isinstance(nic_type, basestring):
253 int_types = [nic_type]
254 else:
255 int_types = nic_type
256 interfaces = []
257 for int_type in int_types:
258 cmd = ['ip', 'addr', 'show', 'label', int_type + '*']
259 ip_output = subprocess.check_output(cmd).split('\n')
260 ip_output = (line for line in ip_output if line)
261 for line in ip_output:
262 if line.split()[1].startswith(int_type):
263 interfaces.append(line.split()[1].replace(":", ""))
264 return interfaces
265
266
267def set_nic_mtu(nic, mtu):
268 '''Set MTU on a network interface'''
269 cmd = ['ip', 'link', 'set', nic, 'mtu', mtu]
270 subprocess.check_call(cmd)
271
272
273def get_nic_mtu(nic):
274 cmd = ['ip', 'addr', 'show', nic]
275 ip_output = subprocess.check_output(cmd).split('\n')
276 mtu = ""
277 for line in ip_output:
278 words = line.split()
279 if 'mtu' in words:
280 mtu = words[words.index("mtu") + 1]
281 return mtu
0282
=== added directory 'lib/charmhelpers/fetch'
=== added file 'lib/charmhelpers/fetch/__init__.py'
--- lib/charmhelpers/fetch/__init__.py 1970-01-01 00:00:00 +0000
+++ lib/charmhelpers/fetch/__init__.py 2013-11-21 22:43:22 +0000
@@ -0,0 +1,271 @@
1import importlib
2from yaml import safe_load
3from charmhelpers.core.host import (
4 lsb_release
5)
6from urlparse import (
7 urlparse,
8 urlunparse,
9)
10import subprocess
11from charmhelpers.core.hookenv import (
12 config,
13 log,
14)
15import apt_pkg
16import os
17
18CLOUD_ARCHIVE = """# Ubuntu Cloud Archive
19deb http://ubuntu-cloud.archive.canonical.com/ubuntu {} main
20"""
21PROPOSED_POCKET = """# Proposed
22deb http://archive.ubuntu.com/ubuntu {}-proposed main universe multiverse restricted
23"""
24CLOUD_ARCHIVE_POCKETS = {
25 # Folsom
26 'folsom': 'precise-updates/folsom',
27 'precise-folsom': 'precise-updates/folsom',
28 'precise-folsom/updates': 'precise-updates/folsom',
29 'precise-updates/folsom': 'precise-updates/folsom',
30 'folsom/proposed': 'precise-proposed/folsom',
31 'precise-folsom/proposed': 'precise-proposed/folsom',
32 'precise-proposed/folsom': 'precise-proposed/folsom',
33 # Grizzly
34 'grizzly': 'precise-updates/grizzly',
35 'precise-grizzly': 'precise-updates/grizzly',
36 'precise-grizzly/updates': 'precise-updates/grizzly',
37 'precise-updates/grizzly': 'precise-updates/grizzly',
38 'grizzly/proposed': 'precise-proposed/grizzly',
39 'precise-grizzly/proposed': 'precise-proposed/grizzly',
40 'precise-proposed/grizzly': 'precise-proposed/grizzly',
41 # Havana
42 'havana': 'precise-updates/havana',
43 'precise-havana': 'precise-updates/havana',
44 'precise-havana/updates': 'precise-updates/havana',
45 'precise-updates/havana': 'precise-updates/havana',
46 'havana/proposed': 'precise-proposed/havana',
47 'precies-havana/proposed': 'precise-proposed/havana',
48 'precise-proposed/havana': 'precise-proposed/havana',
49}
50
51
52def filter_installed_packages(packages):
53 """Returns a list of packages that require installation"""
54 apt_pkg.init()
55 cache = apt_pkg.Cache()
56 _pkgs = []
57 for package in packages:
58 try:
59 p = cache[package]
60 p.current_ver or _pkgs.append(package)
61 except KeyError:
62 log('Package {} has no installation candidate.'.format(package),
63 level='WARNING')
64 _pkgs.append(package)
65 return _pkgs
66
67
68def apt_install(packages, options=None, fatal=False):
69 """Install one or more packages"""
70 if options is None:
71 options = ['--option=Dpkg::Options::=--force-confold']
72
73 cmd = ['apt-get', '--assume-yes']
74 cmd.extend(options)
75 cmd.append('install')
76 if isinstance(packages, basestring):
77 cmd.append(packages)
78 else:
79 cmd.extend(packages)
80 log("Installing {} with options: {}".format(packages,
81 options))
82 env = os.environ.copy()
83 if 'DEBIAN_FRONTEND' not in env:
84 env['DEBIAN_FRONTEND'] = 'noninteractive'
85
86 if fatal:
87 subprocess.check_call(cmd, env=env)
88 else:
89 subprocess.call(cmd, env=env)
90
91
92def apt_update(fatal=False):
93 """Update local apt cache"""
94 cmd = ['apt-get', 'update']
95 if fatal:
96 subprocess.check_call(cmd)
97 else:
98 subprocess.call(cmd)
99
100
101def apt_purge(packages, fatal=False):
102 """Purge one or more packages"""
103 cmd = ['apt-get', '--assume-yes', 'purge']
104 if isinstance(packages, basestring):
105 cmd.append(packages)
106 else:
107 cmd.extend(packages)
108 log("Purging {}".format(packages))
109 if fatal:
110 subprocess.check_call(cmd)
111 else:
112 subprocess.call(cmd)
113
114
115def apt_hold(packages, fatal=False):
116 """Hold one or more packages"""
117 cmd = ['apt-mark', 'hold']
118 if isinstance(packages, basestring):
119 cmd.append(packages)
120 else:
121 cmd.extend(packages)
122 log("Holding {}".format(packages))
123 if fatal:
124 subprocess.check_call(cmd)
125 else:
126 subprocess.call(cmd)
127
128
129def add_source(source, key=None):
130 if (source.startswith('ppa:') or
131 source.startswith('http:') or
132 source.startswith('deb ') or
133 source.startswith('cloud-archive:')):
134 subprocess.check_call(['add-apt-repository', '--yes', source])
135 elif source.startswith('cloud:'):
136 apt_install(filter_installed_packages(['ubuntu-cloud-keyring']),
137 fatal=True)
138 pocket = source.split(':')[-1]
139 if pocket not in CLOUD_ARCHIVE_POCKETS:
140 raise SourceConfigError(
141 'Unsupported cloud: source option %s' %
142 pocket)
143 actual_pocket = CLOUD_ARCHIVE_POCKETS[pocket]
144 with open('/etc/apt/sources.list.d/cloud-archive.list', 'w') as apt:
145 apt.write(CLOUD_ARCHIVE.format(actual_pocket))
146 elif source == 'proposed':
147 release = lsb_release()['DISTRIB_CODENAME']
148 with open('/etc/apt/sources.list.d/proposed.list', 'w') as apt:
149 apt.write(PROPOSED_POCKET.format(release))
150 if key:
151 subprocess.check_call(['apt-key', 'import', key])
152
153
154class SourceConfigError(Exception):
155 pass
156
157
158def configure_sources(update=False,
159 sources_var='install_sources',
160 keys_var='install_keys'):
161 """
162 Configure multiple sources from charm configuration
163
164 Example config:
165 install_sources:
166 - "ppa:foo"
167 - "http://example.com/repo precise main"
168 install_keys:
169 - null
170 - "a1b2c3d4"
171
172 Note that 'null' (a.k.a. None) should not be quoted.
173 """
174 sources = safe_load(config(sources_var))
175 keys = config(keys_var)
176 if keys is not None:
177 keys = safe_load(keys)
178 if isinstance(sources, basestring) and (
179 keys is None or isinstance(keys, basestring)):
180 add_source(sources, keys)
181 else:
182 if not len(sources) == len(keys):
183 msg = 'Install sources and keys lists are different lengths'
184 raise SourceConfigError(msg)
185 for src_num in range(len(sources)):
186 add_source(sources[src_num], keys[src_num])
187 if update:
188 apt_update(fatal=True)
189
190# The order of this list is very important. Handlers should be listed in from
191# least- to most-specific URL matching.
192FETCH_HANDLERS = (
193 'charmhelpers.fetch.archiveurl.ArchiveUrlFetchHandler',
194 'charmhelpers.fetch.bzrurl.BzrUrlFetchHandler',
195)
196
197
198class UnhandledSource(Exception):
199 pass
200
201
202def install_remote(source):
203 """
204 Install a file tree from a remote source
205
206 The specified source should be a url of the form:
207 scheme://[host]/path[#[option=value][&...]]
208
209 Schemes supported are based on this modules submodules
210 Options supported are submodule-specific"""
211 # We ONLY check for True here because can_handle may return a string
212 # explaining why it can't handle a given source.
213 handlers = [h for h in plugins() if h.can_handle(source) is True]
214 installed_to = None
215 for handler in handlers:
216 try:
217 installed_to = handler.install(source)
218 except UnhandledSource:
219 pass
220 if not installed_to:
221 raise UnhandledSource("No handler found for source {}".format(source))
222 return installed_to
223
224
225def install_from_config(config_var_name):
226 charm_config = config()
227 source = charm_config[config_var_name]
228 return install_remote(source)
229
230
231class BaseFetchHandler(object):
232
233 """Base class for FetchHandler implementations in fetch plugins"""
234
235 def can_handle(self, source):
236 """Returns True if the source can be handled. Otherwise returns
237 a string explaining why it cannot"""
238 return "Wrong source type"
239
240 def install(self, source):
241 """Try to download and unpack the source. Return the path to the
242 unpacked files or raise UnhandledSource."""
243 raise UnhandledSource("Wrong source type {}".format(source))
244
245 def parse_url(self, url):
246 return urlparse(url)
247
248 def base_url(self, url):
249 """Return url without querystring or fragment"""
250 parts = list(self.parse_url(url))
251 parts[4:] = ['' for i in parts[4:]]
252 return urlunparse(parts)
253
254
255def plugins(fetch_handlers=None):
256 if not fetch_handlers:
257 fetch_handlers = FETCH_HANDLERS
258 plugin_list = []
259 for handler_name in fetch_handlers:
260 package, classname = handler_name.rsplit('.', 1)
261 try:
262 handler_class = getattr(
263 importlib.import_module(package),
264 classname)
265 plugin_list.append(handler_class())
266 except (ImportError, AttributeError):
267 # Skip missing plugins so that they can be ommitted from
268 # installation if desired
269 log("FetchHandler {} not found, skipping plugin".format(
270 handler_name))
271 return plugin_list
0272
=== added file 'lib/charmhelpers/fetch/archiveurl.py'
--- lib/charmhelpers/fetch/archiveurl.py 1970-01-01 00:00:00 +0000
+++ lib/charmhelpers/fetch/archiveurl.py 2013-11-21 22:43:22 +0000
@@ -0,0 +1,48 @@
1import os
2import urllib2
3from charmhelpers.fetch import (
4 BaseFetchHandler,
5 UnhandledSource
6)
7from charmhelpers.payload.archive import (
8 get_archive_handler,
9 extract,
10)
11from charmhelpers.core.host import mkdir
12
13
14class ArchiveUrlFetchHandler(BaseFetchHandler):
15 """Handler for archives via generic URLs"""
16 def can_handle(self, source):
17 url_parts = self.parse_url(source)
18 if url_parts.scheme not in ('http', 'https', 'ftp', 'file'):
19 return "Wrong source type"
20 if get_archive_handler(self.base_url(source)):
21 return True
22 return False
23
24 def download(self, source, dest):
25 # propogate all exceptions
26 # URLError, OSError, etc
27 response = urllib2.urlopen(source)
28 try:
29 with open(dest, 'w') as dest_file:
30 dest_file.write(response.read())
31 except Exception as e:
32 if os.path.isfile(dest):
33 os.unlink(dest)
34 raise e
35
36 def install(self, source):
37 url_parts = self.parse_url(source)
38 dest_dir = os.path.join(os.environ.get('CHARM_DIR'), 'fetched')
39 if not os.path.exists(dest_dir):
40 mkdir(dest_dir, perms=0755)
41 dld_file = os.path.join(dest_dir, os.path.basename(url_parts.path))
42 try:
43 self.download(source, dld_file)
44 except urllib2.URLError as e:
45 raise UnhandledSource(e.reason)
46 except OSError as e:
47 raise UnhandledSource(e.strerror)
48 return extract(dld_file)
049
=== added file 'lib/charmhelpers/fetch/bzrurl.py'
--- lib/charmhelpers/fetch/bzrurl.py 1970-01-01 00:00:00 +0000
+++ lib/charmhelpers/fetch/bzrurl.py 2013-11-21 22:43:22 +0000
@@ -0,0 +1,49 @@
1import os
2from charmhelpers.fetch import (
3 BaseFetchHandler,
4 UnhandledSource
5)
6from charmhelpers.core.host import mkdir
7
8try:
9 from bzrlib.branch import Branch
10except ImportError:
11 from charmhelpers.fetch import apt_install
12 apt_install("python-bzrlib")
13 from bzrlib.branch import Branch
14
15
16class BzrUrlFetchHandler(BaseFetchHandler):
17 """Handler for bazaar branches via generic and lp URLs"""
18 def can_handle(self, source):
19 url_parts = self.parse_url(source)
20 if url_parts.scheme not in ('bzr+ssh', 'lp'):
21 return False
22 else:
23 return True
24
25 def branch(self, source, dest):
26 url_parts = self.parse_url(source)
27 # If we use lp:branchname scheme we need to load plugins
28 if not self.can_handle(source):
29 raise UnhandledSource("Cannot handle {}".format(source))
30 if url_parts.scheme == "lp":
31 from bzrlib.plugin import load_plugins
32 load_plugins()
33 try:
34 remote_branch = Branch.open(source)
35 remote_branch.bzrdir.sprout(dest).open_branch()
36 except Exception as e:
37 raise e
38
39 def install(self, source):
40 url_parts = self.parse_url(source)
41 branch_name = url_parts.path.strip("/").split("/")[-1]
42 dest_dir = os.path.join(os.environ.get('CHARM_DIR'), "fetched", branch_name)
43 if not os.path.exists(dest_dir):
44 mkdir(dest_dir, perms=0755)
45 try:
46 self.branch(source, dest_dir)
47 except OSError as e:
48 raise UnhandledSource(e.strerror)
49 return dest_dir
050
=== added directory 'lib/charmhelpers/payload'
=== added file 'lib/charmhelpers/payload/__init__.py'
--- lib/charmhelpers/payload/__init__.py 1970-01-01 00:00:00 +0000
+++ lib/charmhelpers/payload/__init__.py 2013-11-21 22:43:22 +0000
@@ -0,0 +1,1 @@
1"Tools for working with files injected into a charm just before deployment."
02
=== added file 'lib/charmhelpers/payload/archive.py'
--- lib/charmhelpers/payload/archive.py 1970-01-01 00:00:00 +0000
+++ lib/charmhelpers/payload/archive.py 2013-11-21 22:43:22 +0000
@@ -0,0 +1,57 @@
1import os
2import tarfile
3import zipfile
4from charmhelpers.core import (
5 host,
6 hookenv,
7)
8
9
10class ArchiveError(Exception):
11 pass
12
13
14def get_archive_handler(archive_name):
15 if os.path.isfile(archive_name):
16 if tarfile.is_tarfile(archive_name):
17 return extract_tarfile
18 elif zipfile.is_zipfile(archive_name):
19 return extract_zipfile
20 else:
21 # look at the file name
22 for ext in ('.tar', '.tar.gz', '.tgz', 'tar.bz2', '.tbz2', '.tbz'):
23 if archive_name.endswith(ext):
24 return extract_tarfile
25 for ext in ('.zip', '.jar'):
26 if archive_name.endswith(ext):
27 return extract_zipfile
28
29
30def archive_dest_default(archive_name):
31 archive_file = os.path.basename(archive_name)
32 return os.path.join(hookenv.charm_dir(), "archives", archive_file)
33
34
35def extract(archive_name, destpath=None):
36 handler = get_archive_handler(archive_name)
37 if handler:
38 if not destpath:
39 destpath = archive_dest_default(archive_name)
40 if not os.path.isdir(destpath):
41 host.mkdir(destpath)
42 handler(archive_name, destpath)
43 return destpath
44 else:
45 raise ArchiveError("No handler for archive")
46
47
48def extract_tarfile(archive_name, destpath):
49 "Unpack a tar archive, optionally compressed"
50 archive = tarfile.open(archive_name)
51 archive.extractall(destpath)
52
53
54def extract_zipfile(archive_name, destpath):
55 "Unpack a zip file"
56 archive = zipfile.ZipFile(archive_name)
57 archive.extractall(destpath)
058
=== added file 'lib/charmhelpers/payload/execd.py'
--- lib/charmhelpers/payload/execd.py 1970-01-01 00:00:00 +0000
+++ lib/charmhelpers/payload/execd.py 2013-11-21 22:43:22 +0000
@@ -0,0 +1,50 @@
1#!/usr/bin/env python
2
3import os
4import sys
5import subprocess
6from charmhelpers.core import hookenv
7
8
9def default_execd_dir():
10 return os.path.join(os.environ['CHARM_DIR'], 'exec.d')
11
12
13def execd_module_paths(execd_dir=None):
14 """Generate a list of full paths to modules within execd_dir."""
15 if not execd_dir:
16 execd_dir = default_execd_dir()
17
18 if not os.path.exists(execd_dir):
19 return
20
21 for subpath in os.listdir(execd_dir):
22 module = os.path.join(execd_dir, subpath)
23 if os.path.isdir(module):
24 yield module
25
26
27def execd_submodule_paths(command, execd_dir=None):
28 """Generate a list of full paths to the specified command within exec_dir.
29 """
30 for module_path in execd_module_paths(execd_dir):
31 path = os.path.join(module_path, command)
32 if os.access(path, os.X_OK) and os.path.isfile(path):
33 yield path
34
35
36def execd_run(command, execd_dir=None, die_on_error=False, stderr=None):
37 """Run command for each module within execd_dir which defines it."""
38 for submodule_path in execd_submodule_paths(command, execd_dir):
39 try:
40 subprocess.check_call(submodule_path, shell=True, stderr=stderr)
41 except subprocess.CalledProcessError as e:
42 hookenv.log("Error ({}) running {}. Output: {}".format(
43 e.returncode, e.cmd, e.output))
44 if die_on_error:
45 sys.exit(e.returncode)
46
47
48def execd_preinstall(execd_dir=None):
49 """Run charm-pre-install for each module within execd_dir."""
50 execd_run('charm-pre-install', execd_dir=execd_dir)
051
=== modified file 'metadata.yaml'
--- metadata.yaml 2013-05-20 13:19:13 +0000
+++ metadata.yaml 2013-11-21 22:43:22 +0000
@@ -9,10 +9,10 @@
9provides:9provides:
10 amqp:10 amqp:
11 interface: rabbitmq11 interface: rabbitmq
12requires:
13 nrpe-external-master:12 nrpe-external-master:
14 interface: nrpe-external-master13 interface: nrpe-external-master
15 scope: container14 scope: container
15requires:
16 ha:16 ha:
17 interface: hacluster17 interface: hacluster
18 scope: container18 scope: container
1919
=== modified file 'revision'
--- revision 2013-08-08 21:22:39 +0000
+++ revision 2013-11-21 22:43:22 +0000
@@ -1,1 +1,1 @@
197198
22
=== added file 'scripts/check_rabbitmq.py'
--- scripts/check_rabbitmq.py 1970-01-01 00:00:00 +0000
+++ scripts/check_rabbitmq.py 2013-11-21 22:43:22 +0000
@@ -0,0 +1,237 @@
1#!/usr/bin/python
2#
3# #
4# # # # # # #
5# # # # # # #
6# # # # # # #
7# # # # # # # #
8# # # # # # # # #
9# ##### #### #### ####
10
11# This file is managed by juju. Do not make local changes.
12
13# Copyright (C) 2009, 2012 Canonical
14# All Rights Reserved
15#
16# tests RabbitMQ operation
17
18""" test rabbitmq functionality """
19
20import os
21import sys
22import signal
23import socket
24
25try:
26 from amqplib import client_0_8 as amqp
27except ImportError:
28 print "CRITICAL: amqplib not found"
29 sys.exit(2)
30
31from optparse import OptionParser
32
33ROUTE_KEY = "test_mq"
34
35
36def alarm_handler(signum, frame):
37 print "TIMEOUT waiting for all queued messages to be delivered"
38 os._exit(1)
39
40
41def get_connection(host_port, user, password, vhost):
42 """ connect to the amqp service """
43 if options.verbose:
44 print "Connection to %s requested" % host_port
45 try:
46 ret = amqp.Connection(host=host_port, userid=user,
47 password=password, virtual_host=vhost,
48 insist=False)
49 except (socket.error, TypeError), e:
50 print "ERROR: Could not connect to RabbitMQ server %s:%d" % (
51 options.host, options.port)
52 if options.verbose:
53 print e
54 raise
55 sys.exit(2)
56 except:
57 print "ERROR: Unknown error connecting to RabbitMQ server %s:%d" % (
58 options.host, options.port)
59 if options.verbose:
60 raise
61 sys.exit(3)
62 return ret
63
64
65def setup_exchange(conn, exchange_name, exchange_type):
66 """ create an exchange """
67 # see if we already have the exchange
68 must_create = False
69 chan = conn.channel()
70 try:
71 chan.exchange_declare(exchange=exchange_name, type=exchange_type,
72 passive=True)
73 except (amqp.AMQPConnectionException, amqp.AMQPChannelException), e:
74 if e.amqp_reply_code == 404:
75 must_create = True
76 # amqplib kills the channel on error.... we dispose of it too
77 chan.close()
78 chan = conn.channel()
79 else:
80 raise
81 # now create the exchange if needed
82 if must_create:
83 chan.exchange_declare(exchange=exchange_name, type=exchange_type,
84 durable=False, auto_delete=False,)
85 if options.verbose:
86 print "Created new exchange %s (%s)" % (
87 exchange_name, exchange_type)
88 else:
89 if options.verbose:
90 print "Exchange %s (%s) is already declared" % (
91 exchange_name, exchange_type)
92 chan.close()
93 return must_create
94
95
96class Consumer(object):
97 """ message consumer class """
98 _quit = False
99
100 def __init__(self, conn, exname):
101 self.exname = exname
102 self.connection = conn
103 self.name = "%s_queue" % exname
104
105 def setup(self):
106 """ sets up the queue and links it to the exchange """
107 if options.verbose:
108 print self.name, "setup"
109 chan = self.connection.channel()
110 # setup the queue
111 chan.queue_declare(queue=self.name, durable=False,
112 exclusive=False, auto_delete=False)
113 chan.queue_bind(queue=self.name, exchange=self.exname,
114 routing_key=ROUTE_KEY)
115 chan.queue_purge(self.name)
116 chan.close()
117
118 def check_end(self, msg):
119 """ checks if this is an end request """
120 return msg.body.startswith("QUIT")
121
122 def loop(self, timeout=5):
123 """ main loop for the consumer client """
124 consumer_tag = "callback_%s" % self.name
125 chan = self.connection.channel()
126
127 def callback(msg):
128 """ callback for message received """
129 if options.verbose:
130 print "Client %s saw this message: '%s'" % (self.name, msg.body)
131 if self.check_end(msg): # we have been asked to quit
132 self._quit = True
133 chan.basic_consume(queue=self.name, no_ack=True, callback=callback,
134 consumer_tag=consumer_tag)
135 signal.signal(signal.SIGALRM, alarm_handler)
136 signal.alarm(timeout)
137 while True:
138 chan.wait()
139 if self._quit:
140 break
141 # cancel alarm for receive wait
142 signal.alarm(0)
143 chan.basic_cancel(consumer_tag)
144 chan.close()
145 return self._quit
146
147
148def send_message(chan, exname, counter=None, message=None):
149 """ publish a message on the exchange """
150 if not message:
151 message = "This is test message %d" % counter
152 msg = amqp.Message(message)
153 chan.basic_publish(msg, exchange=exname, routing_key=ROUTE_KEY)
154 if options.verbose:
155 print "Sent message: %s" % message
156
157
158def main_loop(conn, exname):
159 """ demo code to send/receive a few messages """
160 # first, set up a few consumers
161 # setup the queue that would collect the messages
162 consumer = Consumer(conn, exname)
163 consumer.setup()
164 # open up our own connection and start sending messages
165 chan = conn.channel()
166 # loop a few messages
167 for i in range(options.messages):
168 send_message(chan, exname, i)
169 # signal end of test
170 send_message(chan, exname, message="QUIT")
171 chan.close()
172
173 # loop around for a while waiting for messages to be picked up
174 return consumer.loop(timeout=options.timeout)
175
176
177def main(host, port, exname, extype, user, password, vhost):
178 """ setup the connection and the communication channel """
179 sys.stdout = os.fdopen(os.dup(1), "w", 0)
180 host_port = "%s:%s" % (host, port)
181 conn = get_connection(host_port, user, password, vhost)
182 chan = conn.channel()
183 if setup_exchange(conn, exname, extype):
184 if options.verbose:
185 print "Created %s exchange of type %s" % (exname, extype)
186 else:
187 if options.verbose:
188 print "Reusing existing exchange %s of type %s" % (exname, extype)
189 ret = main_loop(conn, exname)
190 chan.close()
191 conn.close()
192 return ret
193
194if __name__ == '__main__':
195 parser = OptionParser()
196 parser.add_option("--host", dest="host",
197 help="RabbitMQ host [default=%default]",
198 metavar="HOST", default="localhost")
199 parser.add_option("--port", dest="port", type="int",
200 help="port RabbitMQ is running on [default=%default]",
201 metavar="PORT", default=5672)
202 parser.add_option("--exchange", dest="exchange",
203 help="Exchange name to use [default=%default]",
204 default="test_exchange", metavar="EXCHANGE")
205 parser.add_option("--type", dest="type",
206 help="EXCHANGE type [default=%default]",
207 metavar="TYPE", default="fanout")
208 parser.add_option("-v", "--verbose", default=False, action="store_true",
209 help="verbose run")
210 parser.add_option("-m", "--messages", dest="messages", type="int",
211 help="send NUM messages for testing [default=%default]",
212 metavar="NUM", default=10)
213 parser.add_option("-t", "--timeout", dest="timeout", type="int",
214 help="wait TIMEOUT sec for loop test [default=%default]",
215 metavar="TIMEOUT", default=5)
216 parser.add_option("-u", "--user", dest="user", default="guest",
217 help="RabbitMQ user [default=%default]",
218 metavar="USER")
219 parser.add_option("-p", "--password", dest="password", default="guest",
220 help="RabbitMQ password [default=%default]",
221 metavar="PASSWORD")
222 parser.add_option("--vhost", dest="vhost", default="/",
223 help="RabbitMQ vhost [default=%default]",
224 metavar="VHOST")
225
226 (options, args) = parser.parse_args()
227 if options.verbose:
228 print """
229Using AMQP setup: host:port=%s:%d exchange_name=%s exchange_type=%s
230""" % (options.host, options.port, options.exchange, options.type)
231 ret = main(options.host, options.port, options.exchange, options.type,
232 options.user, options.password, options.vhost)
233 if ret:
234 print "Ok: sent and received %d test messages" % options.messages
235 sys.exit(0)
236 print "ERROR: Could not send/receive test messages"
237 sys.exit(3)

Subscribers

People subscribed via source and target branches