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
1=== added file 'hooks/_pythonpath.py'
2--- hooks/_pythonpath.py 1970-01-01 00:00:00 +0000
3+++ hooks/_pythonpath.py 2013-11-21 22:43:22 +0000
4@@ -0,0 +1,14 @@
5+import sys
6+import os
7+import os.path
8+
9+# Make sure that charmhelpers is importable, or bail out.
10+local_copy = os.path.join(
11+ os.path.dirname(os.path.dirname(__file__)), "lib")
12+if os.path.exists(local_copy) and os.path.isdir(local_copy):
13+ sys.path.insert(0, local_copy)
14+try:
15+ import charmhelpers
16+ _ = charmhelpers
17+except ImportError:
18+ sys.exit("Could not find required 'charmhelpers' library.")
19
20=== added symlink 'hooks/nrpe-external-master-relation-changed'
21=== target is u'rabbitmq_server_relations.py'
22=== modified file 'hooks/rabbit_utils.py'
23--- hooks/rabbit_utils.py 2013-05-20 17:00:03 +0000
24+++ hooks/rabbit_utils.py 2013-11-21 22:43:22 +0000
25@@ -7,7 +7,7 @@
26 import lib.utils as utils
27 import apt_pkg as apt
28
29-PACKAGES = ['pwgen', 'rabbitmq-server']
30+PACKAGES = ['pwgen', 'rabbitmq-server', 'python-amqplib']
31
32 RABBITMQ_CTL = '/usr/sbin/rabbitmqctl'
33 COOKIE_PATH = '/var/lib/rabbitmq/.erlang.cookie'
34
35=== modified file 'hooks/rabbitmq_server_relations.py'
36--- hooks/rabbitmq_server_relations.py 2013-10-28 14:12:08 +0000
37+++ hooks/rabbitmq_server_relations.py 2013-11-21 22:43:22 +0000
38@@ -13,10 +13,17 @@
39 import lib.ceph_utils as ceph
40 import lib.openstack_common as openstack
41
42+import _pythonpath
43+_ = _pythonpath
44+
45+from charmhelpers.core.host import rsync
46+from charmhelpers.contrib.charmsupport.nrpe import NRPE
47+
48
49 SERVICE_NAME = os.getenv('JUJU_UNIT_NAME').split('/')[0]
50 POOL_NAME = SERVICE_NAME
51 RABBIT_DIR = '/var/lib/rabbitmq'
52+NAGIOS_PLUGINS='/usr/local/lib/nagios/plugins'
53
54
55 def install():
56@@ -237,6 +244,35 @@
57 utils.juju_log('INFO', 'Finish Ceph Relation Changed')
58
59
60+def update_nrpe_checks():
61+ if os.path.isdir(NAGIOS_PLUGINS):
62+ rsync(os.path.join(os.getenv('CHARM_DIR'), 'scripts', 'check_rabbitmq.py'),
63+ os.path.join(NAGIOS_PLUGINS, 'check_rabbitmq.py'))
64+ user = 'naigos'
65+ vhost = 'nagios'
66+ password_file = os.path.join(RABBIT_DIR, '%s.passwd' % user)
67+ if os.path.exists(password_file):
68+ password = open(password_file).read().strip()
69+ else:
70+ cmd = ['pwgen', '64', '1']
71+ password = subprocess.check_output(cmd).strip()
72+ with open(password_file, 'wb') as out:
73+ out.write(password)
74+
75+ rabbit.create_vhost(vhost)
76+ rabbit.create_user(user, password)
77+ rabbit.grant_permissions(user, vhost)
78+
79+ nrpe_compat = NRPE()
80+ nrpe_compat.add_check(
81+ shortname='rabbitmq',
82+ description='Check RabbitMQ',
83+ check_cmd='{}/check_rabbitmq.py --user {} --password {} --vhost {}'
84+ ''.format(NAGIOS_PLUGINS, user, password, vhost)
85+ )
86+ nrpe_compat.write()
87+
88+
89 def upgrade_charm():
90 pre_install_hooks()
91 # Ensure older passwd files in /var/lib/juju are moved to
92@@ -281,6 +317,8 @@
93 if cluster.eligible_leader('res_rabbitmq_vip'):
94 utils.restart('rabbitmq-server')
95
96+ update_nrpe_checks()
97+
98
99 def pre_install_hooks():
100 for f in glob.glob('exec.d/*/charm-pre-install'):
101@@ -297,7 +335,8 @@
102 'ceph-relation-joined': ceph_joined,
103 'ceph-relation-changed': ceph_changed,
104 'upgrade-charm': upgrade_charm,
105- 'config-changed': config_changed
106+ 'config-changed': config_changed,
107+ 'nrpe-external-master-relation-changed': update_nrpe_checks
108 }
109
110 utils.do_hooks(hooks)
111
112=== added directory 'lib'
113=== added directory 'lib/charmhelpers'
114=== added file 'lib/charmhelpers-0.1.2.egg-info'
115--- lib/charmhelpers-0.1.2.egg-info 1970-01-01 00:00:00 +0000
116+++ lib/charmhelpers-0.1.2.egg-info 2013-11-21 22:43:22 +0000
117@@ -0,0 +1,18 @@
118+Metadata-Version: 1.0
119+Name: charmhelpers
120+Version: 0.1.2
121+Summary: UNKNOWN
122+Home-page: https://code.launchpad.net/charm-helpers
123+Author: Ubuntu Developers
124+Author-email: ubuntu-devel-discuss@lists.ubuntu.com
125+License: Affero GNU Public License v3
126+Description: ============
127+ CharmHelpers
128+ ============
129+
130+ CharmHelpers provides an opinionated set of tools for building Juju
131+ charms that work together. In addition to basic tasks like interact-
132+ ing with the charm environment and the machine it runs on, it also
133+ helps keep you build hooks and establish relations effortlessly.
134+
135+Platform: UNKNOWN
136
137=== added file 'lib/charmhelpers/__init__.py'
138=== added directory 'lib/charmhelpers/cli'
139=== added file 'lib/charmhelpers/cli/__init__.py'
140--- lib/charmhelpers/cli/__init__.py 1970-01-01 00:00:00 +0000
141+++ lib/charmhelpers/cli/__init__.py 2013-11-21 22:43:22 +0000
142@@ -0,0 +1,147 @@
143+import inspect
144+import itertools
145+import argparse
146+import sys
147+
148+
149+class OutputFormatter(object):
150+ def __init__(self, outfile=sys.stdout):
151+ self.formats = (
152+ "raw",
153+ "json",
154+ "py",
155+ "yaml",
156+ "csv",
157+ "tab",
158+ )
159+ self.outfile = outfile
160+
161+ def add_arguments(self, argument_parser):
162+ formatgroup = argument_parser.add_mutually_exclusive_group()
163+ choices = self.supported_formats
164+ formatgroup.add_argument("--format", metavar='FMT',
165+ help="Select output format for returned data, "
166+ "where FMT is one of: {}".format(choices),
167+ choices=choices, default='raw')
168+ for fmt in self.formats:
169+ fmtfunc = getattr(self, fmt)
170+ formatgroup.add_argument("-{}".format(fmt[0]),
171+ "--{}".format(fmt), action='store_const',
172+ const=fmt, dest='format',
173+ help=fmtfunc.__doc__)
174+
175+ @property
176+ def supported_formats(self):
177+ return self.formats
178+
179+ def raw(self, output):
180+ """Output data as raw string (default)"""
181+ self.outfile.write(str(output))
182+
183+ def py(self, output):
184+ """Output data as a nicely-formatted python data structure"""
185+ import pprint
186+ pprint.pprint(output, stream=self.outfile)
187+
188+ def json(self, output):
189+ """Output data in JSON format"""
190+ import json
191+ json.dump(output, self.outfile)
192+
193+ def yaml(self, output):
194+ """Output data in YAML format"""
195+ import yaml
196+ yaml.safe_dump(output, self.outfile)
197+
198+ def csv(self, output):
199+ """Output data as excel-compatible CSV"""
200+ import csv
201+ csvwriter = csv.writer(self.outfile)
202+ csvwriter.writerows(output)
203+
204+ def tab(self, output):
205+ """Output data in excel-compatible tab-delimited format"""
206+ import csv
207+ csvwriter = csv.writer(self.outfile, dialect=csv.excel_tab)
208+ csvwriter.writerows(output)
209+
210+ def format_output(self, output, fmt='raw'):
211+ fmtfunc = getattr(self, fmt)
212+ fmtfunc(output)
213+
214+
215+class CommandLine(object):
216+ argument_parser = None
217+ subparsers = None
218+ formatter = None
219+
220+ def __init__(self):
221+ if not self.argument_parser:
222+ self.argument_parser = argparse.ArgumentParser(description='Perform common charm tasks')
223+ if not self.formatter:
224+ self.formatter = OutputFormatter()
225+ self.formatter.add_arguments(self.argument_parser)
226+ if not self.subparsers:
227+ self.subparsers = self.argument_parser.add_subparsers(help='Commands')
228+
229+ def subcommand(self, command_name=None):
230+ """
231+ Decorate a function as a subcommand. Use its arguments as the
232+ command-line arguments"""
233+ def wrapper(decorated):
234+ cmd_name = command_name or decorated.__name__
235+ subparser = self.subparsers.add_parser(cmd_name,
236+ description=decorated.__doc__)
237+ for args, kwargs in describe_arguments(decorated):
238+ subparser.add_argument(*args, **kwargs)
239+ subparser.set_defaults(func=decorated)
240+ return decorated
241+ return wrapper
242+
243+ def subcommand_builder(self, command_name, description=None):
244+ """
245+ Decorate a function that builds a subcommand. Builders should accept a
246+ single argument (the subparser instance) and return the function to be
247+ run as the command."""
248+ def wrapper(decorated):
249+ subparser = self.subparsers.add_parser(command_name)
250+ func = decorated(subparser)
251+ subparser.set_defaults(func=func)
252+ subparser.description = description or func.__doc__
253+ return wrapper
254+
255+ def run(self):
256+ "Run cli, processing arguments and executing subcommands."
257+ arguments = self.argument_parser.parse_args()
258+ argspec = inspect.getargspec(arguments.func)
259+ vargs = []
260+ kwargs = {}
261+ if argspec.varargs:
262+ vargs = getattr(arguments, argspec.varargs)
263+ for arg in argspec.args:
264+ kwargs[arg] = getattr(arguments, arg)
265+ self.formatter.format_output(arguments.func(*vargs, **kwargs), arguments.format)
266+
267+
268+cmdline = CommandLine()
269+
270+
271+def describe_arguments(func):
272+ """
273+ Analyze a function's signature and return a data structure suitable for
274+ passing in as arguments to an argparse parser's add_argument() method."""
275+
276+ argspec = inspect.getargspec(func)
277+ # we should probably raise an exception somewhere if func includes **kwargs
278+ if argspec.defaults:
279+ positional_args = argspec.args[:-len(argspec.defaults)]
280+ keyword_names = argspec.args[-len(argspec.defaults):]
281+ for arg, default in itertools.izip(keyword_names, argspec.defaults):
282+ yield ('--{}'.format(arg),), {'default': default}
283+ else:
284+ positional_args = argspec.args
285+
286+ for arg in positional_args:
287+ yield (arg,), {}
288+ if argspec.varargs:
289+ yield (argspec.varargs,), {'nargs': '*'}
290
291=== added file 'lib/charmhelpers/cli/commands.py'
292--- lib/charmhelpers/cli/commands.py 1970-01-01 00:00:00 +0000
293+++ lib/charmhelpers/cli/commands.py 2013-11-21 22:43:22 +0000
294@@ -0,0 +1,2 @@
295+from . import CommandLine
296+import host
297
298=== added file 'lib/charmhelpers/cli/host.py'
299--- lib/charmhelpers/cli/host.py 1970-01-01 00:00:00 +0000
300+++ lib/charmhelpers/cli/host.py 2013-11-21 22:43:22 +0000
301@@ -0,0 +1,15 @@
302+from . import cmdline
303+from charmhelpers.core import host
304+
305+
306+@cmdline.subcommand()
307+def mounts():
308+ "List mounts"
309+ return host.mounts()
310+
311+
312+@cmdline.subcommand_builder('service', description="Control system services")
313+def service(subparser):
314+ subparser.add_argument("action", help="The action to perform (start, stop, etc...)")
315+ subparser.add_argument("service_name", help="Name of the service to control")
316+ return host.service
317
318=== added directory 'lib/charmhelpers/contrib'
319=== added file 'lib/charmhelpers/contrib/__init__.py'
320=== added directory 'lib/charmhelpers/contrib/ansible'
321=== added file 'lib/charmhelpers/contrib/ansible/__init__.py'
322--- lib/charmhelpers/contrib/ansible/__init__.py 1970-01-01 00:00:00 +0000
323+++ lib/charmhelpers/contrib/ansible/__init__.py 2013-11-21 22:43:22 +0000
324@@ -0,0 +1,101 @@
325+# Copyright 2013 Canonical Ltd.
326+#
327+# Authors:
328+# Charm Helpers Developers <juju@lists.ubuntu.com>
329+"""Charm Helpers ansible - declare the state of your machines.
330+
331+This helper enables you to declare your machine state, rather than
332+program it procedurally (and have to test each change to your procedures).
333+Your install hook can be as simple as:
334+
335+{{{
336+import charmhelpers.contrib.ansible
337+
338+
339+def install():
340+ charmhelpers.contrib.ansible.install_ansible_support()
341+ charmhelpers.contrib.ansible.apply_playbook('playbooks/install.yaml')
342+}}}
343+
344+and won't need to change (nor will its tests) when you change the machine
345+state.
346+
347+All of your juju config and relation-data are available as template
348+variables within your playbooks and templates. An install playbook looks
349+something like:
350+
351+{{{
352+---
353+- hosts: localhost
354+ user: root
355+
356+ tasks:
357+ - name: Add private repositories.
358+ template:
359+ src: ../templates/private-repositories.list.jinja2
360+ dest: /etc/apt/sources.list.d/private.list
361+
362+ - name: Update the cache.
363+ apt: update_cache=yes
364+
365+ - name: Install dependencies.
366+ apt: pkg={{ item }}
367+ with_items:
368+ - python-mimeparse
369+ - python-webob
370+ - sunburnt
371+
372+ - name: Setup groups.
373+ group: name={{ item.name }} gid={{ item.gid }}
374+ with_items:
375+ - { name: 'deploy_user', gid: 1800 }
376+ - { name: 'service_user', gid: 1500 }
377+
378+ ...
379+}}}
380+
381+Read more online about playbooks[1] and standard ansible modules[2].
382+
383+[1] http://www.ansibleworks.com/docs/playbooks.html
384+[2] http://www.ansibleworks.com/docs/modules.html
385+"""
386+import os
387+import subprocess
388+
389+import charmhelpers.contrib.saltstack
390+import charmhelpers.core.host
391+import charmhelpers.core.hookenv
392+import charmhelpers.fetch
393+
394+
395+charm_dir = os.environ.get('CHARM_DIR', '')
396+ansible_hosts_path = '/etc/ansible/hosts'
397+# Ansible will automatically include any vars in the following
398+# file in its inventory when run locally.
399+ansible_vars_path = '/etc/ansible/host_vars/localhost'
400+
401+
402+def install_ansible_support(from_ppa=True):
403+ """Installs the ansible package.
404+
405+ By default it is installed from the PPA [1] linked from
406+ the ansible website [2].
407+
408+ [1] https://launchpad.net/~rquillo/+archive/ansible
409+ [2] http://www.ansibleworks.com/docs/gettingstarted.html#ubuntu-and-debian
410+
411+ If from_ppa is false, you must ensure that the package is available
412+ from a configured repository.
413+ """
414+ if from_ppa:
415+ charmhelpers.fetch.add_source('ppa:rquillo/ansible')
416+ charmhelpers.fetch.apt_update(fatal=True)
417+ charmhelpers.fetch.apt_install('ansible')
418+ with open(ansible_hosts_path, 'w+') as hosts_file:
419+ hosts_file.write('localhost ansible_connection=local')
420+
421+
422+def apply_playbook(playbook):
423+ charmhelpers.contrib.saltstack.juju_state_to_yaml(
424+ ansible_vars_path, namespace_separator='__')
425+ subprocess.check_call(['ansible-playbook', '-c', 'local', playbook])
426
427=== added directory 'lib/charmhelpers/contrib/charmhelpers'
428=== added file 'lib/charmhelpers/contrib/charmhelpers/__init__.py'
429--- lib/charmhelpers/contrib/charmhelpers/__init__.py 1970-01-01 00:00:00 +0000
430+++ lib/charmhelpers/contrib/charmhelpers/__init__.py 2013-11-21 22:43:22 +0000
431@@ -0,0 +1,184 @@
432+# Copyright 2012 Canonical Ltd. This software is licensed under the
433+# GNU Affero General Public License version 3 (see the file LICENSE).
434+
435+import warnings
436+warnings.warn("contrib.charmhelpers is deprecated", DeprecationWarning)
437+
438+"""Helper functions for writing Juju charms in Python."""
439+
440+__metaclass__ = type
441+__all__ = [
442+ #'get_config', # core.hookenv.config()
443+ #'log', # core.hookenv.log()
444+ #'log_entry', # core.hookenv.log()
445+ #'log_exit', # core.hookenv.log()
446+ #'relation_get', # core.hookenv.relation_get()
447+ #'relation_set', # core.hookenv.relation_set()
448+ #'relation_ids', # core.hookenv.relation_ids()
449+ #'relation_list', # core.hookenv.relation_units()
450+ #'config_get', # core.hookenv.config()
451+ #'unit_get', # core.hookenv.unit_get()
452+ #'open_port', # core.hookenv.open_port()
453+ #'close_port', # core.hookenv.close_port()
454+ #'service_control', # core.host.service()
455+ 'unit_info', # client-side, NOT IMPLEMENTED
456+ 'wait_for_machine', # client-side, NOT IMPLEMENTED
457+ 'wait_for_page_contents', # client-side, NOT IMPLEMENTED
458+ 'wait_for_relation', # client-side, NOT IMPLEMENTED
459+ 'wait_for_unit', # client-side, NOT IMPLEMENTED
460+]
461+
462+import operator
463+from shelltoolbox import (
464+ command,
465+)
466+import tempfile
467+import time
468+import urllib2
469+import yaml
470+
471+SLEEP_AMOUNT = 0.1
472+# We create a juju_status Command here because it makes testing much,
473+# much easier.
474+juju_status = lambda: command('juju')('status')
475+
476+# re-implemented as charmhelpers.fetch.configure_sources()
477+#def configure_source(update=False):
478+# source = config_get('source')
479+# if ((source.startswith('ppa:') or
480+# source.startswith('cloud:') or
481+# source.startswith('http:'))):
482+# run('add-apt-repository', source)
483+# if source.startswith("http:"):
484+# run('apt-key', 'import', config_get('key'))
485+# if update:
486+# run('apt-get', 'update')
487+
488+
489+# DEPRECATED: client-side only
490+def make_charm_config_file(charm_config):
491+ charm_config_file = tempfile.NamedTemporaryFile()
492+ charm_config_file.write(yaml.dump(charm_config))
493+ charm_config_file.flush()
494+ # The NamedTemporaryFile instance is returned instead of just the name
495+ # because we want to take advantage of garbage collection-triggered
496+ # deletion of the temp file when it goes out of scope in the caller.
497+ return charm_config_file
498+
499+
500+# DEPRECATED: client-side only
501+def unit_info(service_name, item_name, data=None, unit=None):
502+ if data is None:
503+ data = yaml.safe_load(juju_status())
504+ service = data['services'].get(service_name)
505+ if service is None:
506+ # XXX 2012-02-08 gmb:
507+ # This allows us to cope with the race condition that we
508+ # have between deploying a service and having it come up in
509+ # `juju status`. We could probably do with cleaning it up so
510+ # that it fails a bit more noisily after a while.
511+ return ''
512+ units = service['units']
513+ if unit is not None:
514+ item = units[unit][item_name]
515+ else:
516+ # It might seem odd to sort the units here, but we do it to
517+ # ensure that when no unit is specified, the first unit for the
518+ # service (or at least the one with the lowest number) is the
519+ # one whose data gets returned.
520+ sorted_unit_names = sorted(units.keys())
521+ item = units[sorted_unit_names[0]][item_name]
522+ return item
523+
524+
525+# DEPRECATED: client-side only
526+def get_machine_data():
527+ return yaml.safe_load(juju_status())['machines']
528+
529+
530+# DEPRECATED: client-side only
531+def wait_for_machine(num_machines=1, timeout=300):
532+ """Wait `timeout` seconds for `num_machines` machines to come up.
533+
534+ This wait_for... function can be called by other wait_for functions
535+ whose timeouts might be too short in situations where only a bare
536+ Juju setup has been bootstrapped.
537+
538+ :return: A tuple of (num_machines, time_taken). This is used for
539+ testing.
540+ """
541+ # You may think this is a hack, and you'd be right. The easiest way
542+ # to tell what environment we're working in (LXC vs EC2) is to check
543+ # the dns-name of the first machine. If it's localhost we're in LXC
544+ # and we can just return here.
545+ if get_machine_data()[0]['dns-name'] == 'localhost':
546+ return 1, 0
547+ start_time = time.time()
548+ while True:
549+ # Drop the first machine, since it's the Zookeeper and that's
550+ # not a machine that we need to wait for. This will only work
551+ # for EC2 environments, which is why we return early above if
552+ # we're in LXC.
553+ machine_data = get_machine_data()
554+ non_zookeeper_machines = [
555+ machine_data[key] for key in machine_data.keys()[1:]]
556+ if len(non_zookeeper_machines) >= num_machines:
557+ all_machines_running = True
558+ for machine in non_zookeeper_machines:
559+ if machine.get('instance-state') != 'running':
560+ all_machines_running = False
561+ break
562+ if all_machines_running:
563+ break
564+ if time.time() - start_time >= timeout:
565+ raise RuntimeError('timeout waiting for service to start')
566+ time.sleep(SLEEP_AMOUNT)
567+ return num_machines, time.time() - start_time
568+
569+
570+# DEPRECATED: client-side only
571+def wait_for_unit(service_name, timeout=480):
572+ """Wait `timeout` seconds for a given service name to come up."""
573+ wait_for_machine(num_machines=1)
574+ start_time = time.time()
575+ while True:
576+ state = unit_info(service_name, 'agent-state')
577+ if 'error' in state or state == 'started':
578+ break
579+ if time.time() - start_time >= timeout:
580+ raise RuntimeError('timeout waiting for service to start')
581+ time.sleep(SLEEP_AMOUNT)
582+ if state != 'started':
583+ raise RuntimeError('unit did not start, agent-state: ' + state)
584+
585+
586+# DEPRECATED: client-side only
587+def wait_for_relation(service_name, relation_name, timeout=120):
588+ """Wait `timeout` seconds for a given relation to come up."""
589+ start_time = time.time()
590+ while True:
591+ relation = unit_info(service_name, 'relations').get(relation_name)
592+ if relation is not None and relation['state'] == 'up':
593+ break
594+ if time.time() - start_time >= timeout:
595+ raise RuntimeError('timeout waiting for relation to be up')
596+ time.sleep(SLEEP_AMOUNT)
597+
598+
599+# DEPRECATED: client-side only
600+def wait_for_page_contents(url, contents, timeout=120, validate=None):
601+ if validate is None:
602+ validate = operator.contains
603+ start_time = time.time()
604+ while True:
605+ try:
606+ stream = urllib2.urlopen(url)
607+ except (urllib2.HTTPError, urllib2.URLError):
608+ pass
609+ else:
610+ page = stream.read()
611+ if validate(page, contents):
612+ return page
613+ if time.time() - start_time >= timeout:
614+ raise RuntimeError('timeout waiting for contents of ' + url)
615+ time.sleep(SLEEP_AMOUNT)
616
617=== added directory 'lib/charmhelpers/contrib/charmsupport'
618=== added file 'lib/charmhelpers/contrib/charmsupport/__init__.py'
619=== added file 'lib/charmhelpers/contrib/charmsupport/nrpe.py'
620--- lib/charmhelpers/contrib/charmsupport/nrpe.py 1970-01-01 00:00:00 +0000
621+++ lib/charmhelpers/contrib/charmsupport/nrpe.py 2013-11-21 22:43:22 +0000
622@@ -0,0 +1,219 @@
623+"""Compatibility with the nrpe-external-master charm"""
624+# Copyright 2012 Canonical Ltd.
625+#
626+# Authors:
627+# Matthew Wedgwood <matthew.wedgwood@canonical.com>
628+
629+import subprocess
630+import pwd
631+import grp
632+import os
633+import re
634+import shlex
635+import yaml
636+
637+from charmhelpers.core.hookenv import (
638+ config,
639+ local_unit,
640+ log,
641+ relation_ids,
642+ relation_set,
643+)
644+
645+from charmhelpers.core.host import service
646+
647+# This module adds compatibility with the nrpe-external-master and plain nrpe
648+# subordinate charms. To use it in your charm:
649+#
650+# 1. Update metadata.yaml
651+#
652+# provides:
653+# (...)
654+# nrpe-external-master:
655+# interface: nrpe-external-master
656+# scope: container
657+#
658+# and/or
659+#
660+# provides:
661+# (...)
662+# local-monitors:
663+# interface: local-monitors
664+# scope: container
665+
666+#
667+# 2. Add the following to config.yaml
668+#
669+# nagios_context:
670+# default: "juju"
671+# type: string
672+# description: |
673+# Used by the nrpe subordinate charms.
674+# A string that will be prepended to instance name to set the host name
675+# in nagios. So for instance the hostname would be something like:
676+# juju-myservice-0
677+# If you're running multiple environments with the same services in them
678+# this allows you to differentiate between them.
679+#
680+# 3. Add custom checks (Nagios plugins) to files/nrpe-external-master
681+#
682+# 4. Update your hooks.py with something like this:
683+#
684+# from charmsupport.nrpe import NRPE
685+# (...)
686+# def update_nrpe_config():
687+# nrpe_compat = NRPE()
688+# nrpe_compat.add_check(
689+# shortname = "myservice",
690+# description = "Check MyService",
691+# check_cmd = "check_http -w 2 -c 10 http://localhost"
692+# )
693+# nrpe_compat.add_check(
694+# "myservice_other",
695+# "Check for widget failures",
696+# check_cmd = "/srv/myapp/scripts/widget_check"
697+# )
698+# nrpe_compat.write()
699+#
700+# def config_changed():
701+# (...)
702+# update_nrpe_config()
703+#
704+# def nrpe_external_master_relation_changed():
705+# update_nrpe_config()
706+#
707+# def local_monitors_relation_changed():
708+# update_nrpe_config()
709+#
710+# 5. ln -s hooks.py nrpe-external-master-relation-changed
711+# ln -s hooks.py local-monitors-relation-changed
712+
713+
714+class CheckException(Exception):
715+ pass
716+
717+
718+class Check(object):
719+ shortname_re = '[A-Za-z0-9-_]+$'
720+ service_template = ("""
721+#---------------------------------------------------
722+# This file is Juju managed
723+#---------------------------------------------------
724+define service {{
725+ use active-service
726+ host_name {nagios_hostname}
727+ service_description {nagios_hostname}[{shortname}] """
728+ """{description}
729+ check_command check_nrpe!{command}
730+ servicegroups {nagios_servicegroup}
731+}}
732+""")
733+
734+ def __init__(self, shortname, description, check_cmd):
735+ super(Check, self).__init__()
736+ # XXX: could be better to calculate this from the service name
737+ if not re.match(self.shortname_re, shortname):
738+ raise CheckException("shortname must match {}".format(
739+ Check.shortname_re))
740+ self.shortname = shortname
741+ self.command = "check_{}".format(shortname)
742+ # Note: a set of invalid characters is defined by the
743+ # Nagios server config
744+ # The default is: illegal_object_name_chars=`~!$%^&*"|'<>?,()=
745+ self.description = description
746+ self.check_cmd = self._locate_cmd(check_cmd)
747+
748+ def _locate_cmd(self, check_cmd):
749+ search_path = (
750+ '/',
751+ os.path.join(os.environ['CHARM_DIR'],
752+ 'files/nrpe-external-master'),
753+ '/usr/lib/nagios/plugins',
754+ '/usr/local/lib/nagios/plugins',
755+ )
756+ parts = shlex.split(check_cmd)
757+ for path in search_path:
758+ if os.path.exists(os.path.join(path, parts[0])):
759+ command = os.path.join(path, parts[0])
760+ if len(parts) > 1:
761+ command += " " + " ".join(parts[1:])
762+ return command
763+ log('Check command not found: {}'.format(parts[0]))
764+ return ''
765+
766+ def write(self, nagios_context, hostname):
767+ nrpe_check_file = '/etc/nagios/nrpe.d/{}.cfg'.format(
768+ self.command)
769+ with open(nrpe_check_file, 'w') as nrpe_check_config:
770+ nrpe_check_config.write("# check {}\n".format(self.shortname))
771+ nrpe_check_config.write("command[{}]={}\n".format(
772+ self.command, self.check_cmd))
773+
774+ if not os.path.exists(NRPE.nagios_exportdir):
775+ log('Not writing service config as {} is not accessible'.format(
776+ NRPE.nagios_exportdir))
777+ else:
778+ self.write_service_config(nagios_context, hostname)
779+
780+ def write_service_config(self, nagios_context, hostname):
781+ for f in os.listdir(NRPE.nagios_exportdir):
782+ if re.search('.*{}.cfg'.format(self.command), f):
783+ os.remove(os.path.join(NRPE.nagios_exportdir, f))
784+
785+ templ_vars = {
786+ 'nagios_hostname': hostname,
787+ 'nagios_servicegroup': nagios_context,
788+ 'description': self.description,
789+ 'shortname': self.shortname,
790+ 'command': self.command,
791+ }
792+ nrpe_service_text = Check.service_template.format(**templ_vars)
793+ nrpe_service_file = '{}/service__{}_{}.cfg'.format(
794+ NRPE.nagios_exportdir, hostname, self.command)
795+ with open(nrpe_service_file, 'w') as nrpe_service_config:
796+ nrpe_service_config.write(str(nrpe_service_text))
797+
798+ def run(self):
799+ subprocess.call(self.check_cmd)
800+
801+
802+class NRPE(object):
803+ nagios_logdir = '/var/log/nagios'
804+ nagios_exportdir = '/var/lib/nagios/export'
805+ nrpe_confdir = '/etc/nagios/nrpe.d'
806+
807+ def __init__(self):
808+ super(NRPE, self).__init__()
809+ self.config = config()
810+ self.nagios_context = self.config['nagios_context']
811+ self.unit_name = local_unit().replace('/', '-')
812+ self.hostname = "{}-{}".format(self.nagios_context, self.unit_name)
813+ self.checks = []
814+
815+ def add_check(self, *args, **kwargs):
816+ self.checks.append(Check(*args, **kwargs))
817+
818+ def write(self):
819+ try:
820+ nagios_uid = pwd.getpwnam('nagios').pw_uid
821+ nagios_gid = grp.getgrnam('nagios').gr_gid
822+ except:
823+ log("Nagios user not set up, nrpe checks not updated")
824+ return
825+
826+ if not os.path.exists(NRPE.nagios_logdir):
827+ os.mkdir(NRPE.nagios_logdir)
828+ os.chown(NRPE.nagios_logdir, nagios_uid, nagios_gid)
829+
830+ nrpe_monitors = {}
831+ monitors = {"monitors": {"remote": {"nrpe": nrpe_monitors}}}
832+ for nrpecheck in self.checks:
833+ nrpecheck.write(self.nagios_context, self.hostname)
834+ nrpe_monitors[nrpecheck.shortname] = {
835+ "command": nrpecheck.command,
836+ }
837+
838+ service('restart', 'nagios-nrpe-server')
839+
840+ for rid in relation_ids("local-monitors"):
841+ relation_set(relation_id=rid, monitors=yaml.dump(monitors))
842
843=== added file 'lib/charmhelpers/contrib/charmsupport/volumes.py'
844--- lib/charmhelpers/contrib/charmsupport/volumes.py 1970-01-01 00:00:00 +0000
845+++ lib/charmhelpers/contrib/charmsupport/volumes.py 2013-11-21 22:43:22 +0000
846@@ -0,0 +1,156 @@
847+'''
848+Functions for managing volumes in juju units. One volume is supported per unit.
849+Subordinates may have their own storage, provided it is on its own partition.
850+
851+Configuration stanzas:
852+ volume-ephemeral:
853+ type: boolean
854+ default: true
855+ description: >
856+ If false, a volume is mounted as sepecified in "volume-map"
857+ If true, ephemeral storage will be used, meaning that log data
858+ will only exist as long as the machine. YOU HAVE BEEN WARNED.
859+ volume-map:
860+ type: string
861+ default: {}
862+ description: >
863+ YAML map of units to device names, e.g:
864+ "{ rsyslog/0: /dev/vdb, rsyslog/1: /dev/vdb }"
865+ Service units will raise a configure-error if volume-ephemeral
866+ is 'true' and no volume-map value is set. Use 'juju set' to set a
867+ value and 'juju resolved' to complete configuration.
868+
869+Usage:
870+ from charmsupport.volumes import configure_volume, VolumeConfigurationError
871+ from charmsupport.hookenv import log, ERROR
872+ def post_mount_hook():
873+ stop_service('myservice')
874+ def post_mount_hook():
875+ start_service('myservice')
876+
877+ if __name__ == '__main__':
878+ try:
879+ configure_volume(before_change=pre_mount_hook,
880+ after_change=post_mount_hook)
881+ except VolumeConfigurationError:
882+ log('Storage could not be configured', ERROR)
883+'''
884+
885+# XXX: Known limitations
886+# - fstab is neither consulted nor updated
887+
888+import os
889+from charmhelpers.core import hookenv
890+from charmhelpers.core import host
891+import yaml
892+
893+
894+MOUNT_BASE = '/srv/juju/volumes'
895+
896+
897+class VolumeConfigurationError(Exception):
898+ '''Volume configuration data is missing or invalid'''
899+ pass
900+
901+
902+def get_config():
903+ '''Gather and sanity-check volume configuration data'''
904+ volume_config = {}
905+ config = hookenv.config()
906+
907+ errors = False
908+
909+ if config.get('volume-ephemeral') in (True, 'True', 'true', 'Yes', 'yes'):
910+ volume_config['ephemeral'] = True
911+ else:
912+ volume_config['ephemeral'] = False
913+
914+ try:
915+ volume_map = yaml.safe_load(config.get('volume-map', '{}'))
916+ except yaml.YAMLError as e:
917+ hookenv.log("Error parsing YAML volume-map: {}".format(e),
918+ hookenv.ERROR)
919+ errors = True
920+ if volume_map is None:
921+ # probably an empty string
922+ volume_map = {}
923+ elif not isinstance(volume_map, dict):
924+ hookenv.log("Volume-map should be a dictionary, not {}".format(
925+ type(volume_map)))
926+ errors = True
927+
928+ volume_config['device'] = volume_map.get(os.environ['JUJU_UNIT_NAME'])
929+ if volume_config['device'] and volume_config['ephemeral']:
930+ # asked for ephemeral storage but also defined a volume ID
931+ hookenv.log('A volume is defined for this unit, but ephemeral '
932+ 'storage was requested', hookenv.ERROR)
933+ errors = True
934+ elif not volume_config['device'] and not volume_config['ephemeral']:
935+ # asked for permanent storage but did not define volume ID
936+ hookenv.log('Ephemeral storage was requested, but there is no volume '
937+ 'defined for this unit.', hookenv.ERROR)
938+ errors = True
939+
940+ unit_mount_name = hookenv.local_unit().replace('/', '-')
941+ volume_config['mountpoint'] = os.path.join(MOUNT_BASE, unit_mount_name)
942+
943+ if errors:
944+ return None
945+ return volume_config
946+
947+
948+def mount_volume(config):
949+ if os.path.exists(config['mountpoint']):
950+ if not os.path.isdir(config['mountpoint']):
951+ hookenv.log('Not a directory: {}'.format(config['mountpoint']))
952+ raise VolumeConfigurationError()
953+ else:
954+ host.mkdir(config['mountpoint'])
955+ if os.path.ismount(config['mountpoint']):
956+ unmount_volume(config)
957+ if not host.mount(config['device'], config['mountpoint'], persist=True):
958+ raise VolumeConfigurationError()
959+
960+
961+def unmount_volume(config):
962+ if os.path.ismount(config['mountpoint']):
963+ if not host.umount(config['mountpoint'], persist=True):
964+ raise VolumeConfigurationError()
965+
966+
967+def managed_mounts():
968+ '''List of all mounted managed volumes'''
969+ return filter(lambda mount: mount[0].startswith(MOUNT_BASE), host.mounts())
970+
971+
972+def configure_volume(before_change=lambda: None, after_change=lambda: None):
973+ '''Set up storage (or don't) according to the charm's volume configuration.
974+ Returns the mount point or "ephemeral". before_change and after_change
975+ are optional functions to be called if the volume configuration changes.
976+ '''
977+
978+ config = get_config()
979+ if not config:
980+ hookenv.log('Failed to read volume configuration', hookenv.CRITICAL)
981+ raise VolumeConfigurationError()
982+
983+ if config['ephemeral']:
984+ if os.path.ismount(config['mountpoint']):
985+ before_change()
986+ unmount_volume(config)
987+ after_change()
988+ return 'ephemeral'
989+ else:
990+ # persistent storage
991+ if os.path.ismount(config['mountpoint']):
992+ mounts = dict(managed_mounts())
993+ if mounts.get(config['mountpoint']) != config['device']:
994+ before_change()
995+ unmount_volume(config)
996+ mount_volume(config)
997+ after_change()
998+ else:
999+ before_change()
1000+ mount_volume(config)
1001+ after_change()
1002+ return config['mountpoint']
1003
1004=== added directory 'lib/charmhelpers/contrib/hahelpers'
1005=== added file 'lib/charmhelpers/contrib/hahelpers/__init__.py'
1006=== added file 'lib/charmhelpers/contrib/hahelpers/apache.py'
1007--- lib/charmhelpers/contrib/hahelpers/apache.py 1970-01-01 00:00:00 +0000
1008+++ lib/charmhelpers/contrib/hahelpers/apache.py 2013-11-21 22:43:22 +0000
1009@@ -0,0 +1,58 @@
1010+#
1011+# Copyright 2012 Canonical Ltd.
1012+#
1013+# This file is sourced from lp:openstack-charm-helpers
1014+#
1015+# Authors:
1016+# James Page <james.page@ubuntu.com>
1017+# Adam Gandelman <adamg@ubuntu.com>
1018+#
1019+
1020+import subprocess
1021+
1022+from charmhelpers.core.hookenv import (
1023+ config as config_get,
1024+ relation_get,
1025+ relation_ids,
1026+ related_units as relation_list,
1027+ log,
1028+ INFO,
1029+)
1030+
1031+
1032+def get_cert():
1033+ cert = config_get('ssl_cert')
1034+ key = config_get('ssl_key')
1035+ if not (cert and key):
1036+ log("Inspecting identity-service relations for SSL certificate.",
1037+ level=INFO)
1038+ cert = key = None
1039+ for r_id in relation_ids('identity-service'):
1040+ for unit in relation_list(r_id):
1041+ if not cert:
1042+ cert = relation_get('ssl_cert',
1043+ rid=r_id, unit=unit)
1044+ if not key:
1045+ key = relation_get('ssl_key',
1046+ rid=r_id, unit=unit)
1047+ return (cert, key)
1048+
1049+
1050+def get_ca_cert():
1051+ ca_cert = None
1052+ log("Inspecting identity-service relations for CA SSL certificate.",
1053+ level=INFO)
1054+ for r_id in relation_ids('identity-service'):
1055+ for unit in relation_list(r_id):
1056+ if not ca_cert:
1057+ ca_cert = relation_get('ca_cert',
1058+ rid=r_id, unit=unit)
1059+ return ca_cert
1060+
1061+
1062+def install_ca_cert(ca_cert):
1063+ if ca_cert:
1064+ with open('/usr/local/share/ca-certificates/keystone_juju_ca_cert.crt',
1065+ 'w') as crt:
1066+ crt.write(ca_cert)
1067+ subprocess.check_call(['update-ca-certificates', '--fresh'])
1068
1069=== added file 'lib/charmhelpers/contrib/hahelpers/cluster.py'
1070--- lib/charmhelpers/contrib/hahelpers/cluster.py 1970-01-01 00:00:00 +0000
1071+++ lib/charmhelpers/contrib/hahelpers/cluster.py 2013-11-21 22:43:22 +0000
1072@@ -0,0 +1,183 @@
1073+#
1074+# Copyright 2012 Canonical Ltd.
1075+#
1076+# Authors:
1077+# James Page <james.page@ubuntu.com>
1078+# Adam Gandelman <adamg@ubuntu.com>
1079+#
1080+
1081+import subprocess
1082+import os
1083+
1084+from socket import gethostname as get_unit_hostname
1085+
1086+from charmhelpers.core.hookenv import (
1087+ log,
1088+ relation_ids,
1089+ related_units as relation_list,
1090+ relation_get,
1091+ config as config_get,
1092+ INFO,
1093+ ERROR,
1094+ unit_get,
1095+)
1096+
1097+
1098+class HAIncompleteConfig(Exception):
1099+ pass
1100+
1101+
1102+def is_clustered():
1103+ for r_id in (relation_ids('ha') or []):
1104+ for unit in (relation_list(r_id) or []):
1105+ clustered = relation_get('clustered',
1106+ rid=r_id,
1107+ unit=unit)
1108+ if clustered:
1109+ return True
1110+ return False
1111+
1112+
1113+def is_leader(resource):
1114+ cmd = [
1115+ "crm", "resource",
1116+ "show", resource
1117+ ]
1118+ try:
1119+ status = subprocess.check_output(cmd)
1120+ except subprocess.CalledProcessError:
1121+ return False
1122+ else:
1123+ if get_unit_hostname() in status:
1124+ return True
1125+ else:
1126+ return False
1127+
1128+
1129+def peer_units():
1130+ peers = []
1131+ for r_id in (relation_ids('cluster') or []):
1132+ for unit in (relation_list(r_id) or []):
1133+ peers.append(unit)
1134+ return peers
1135+
1136+
1137+def oldest_peer(peers):
1138+ local_unit_no = int(os.getenv('JUJU_UNIT_NAME').split('/')[1])
1139+ for peer in peers:
1140+ remote_unit_no = int(peer.split('/')[1])
1141+ if remote_unit_no < local_unit_no:
1142+ return False
1143+ return True
1144+
1145+
1146+def eligible_leader(resource):
1147+ if is_clustered():
1148+ if not is_leader(resource):
1149+ log('Deferring action to CRM leader.', level=INFO)
1150+ return False
1151+ else:
1152+ peers = peer_units()
1153+ if peers and not oldest_peer(peers):
1154+ log('Deferring action to oldest service unit.', level=INFO)
1155+ return False
1156+ return True
1157+
1158+
1159+def https():
1160+ '''
1161+ Determines whether enough data has been provided in configuration
1162+ or relation data to configure HTTPS
1163+ .
1164+ returns: boolean
1165+ '''
1166+ if config_get('use-https') == "yes":
1167+ return True
1168+ if config_get('ssl_cert') and config_get('ssl_key'):
1169+ return True
1170+ for r_id in relation_ids('identity-service'):
1171+ for unit in relation_list(r_id):
1172+ rel_state = [
1173+ relation_get('https_keystone', rid=r_id, unit=unit),
1174+ relation_get('ssl_cert', rid=r_id, unit=unit),
1175+ relation_get('ssl_key', rid=r_id, unit=unit),
1176+ relation_get('ca_cert', rid=r_id, unit=unit),
1177+ ]
1178+ # NOTE: works around (LP: #1203241)
1179+ if (None not in rel_state) and ('' not in rel_state):
1180+ return True
1181+ return False
1182+
1183+
1184+def determine_api_port(public_port):
1185+ '''
1186+ Determine correct API server listening port based on
1187+ existence of HTTPS reverse proxy and/or haproxy.
1188+
1189+ public_port: int: standard public port for given service
1190+
1191+ returns: int: the correct listening port for the API service
1192+ '''
1193+ i = 0
1194+ if len(peer_units()) > 0 or is_clustered():
1195+ i += 1
1196+ if https():
1197+ i += 1
1198+ return public_port - (i * 10)
1199+
1200+
1201+def determine_haproxy_port(public_port):
1202+ '''
1203+ Description: Determine correct proxy listening port based on public IP +
1204+ existence of HTTPS reverse proxy.
1205+
1206+ public_port: int: standard public port for given service
1207+
1208+ returns: int: the correct listening port for the HAProxy service
1209+ '''
1210+ i = 0
1211+ if https():
1212+ i += 1
1213+ return public_port - (i * 10)
1214+
1215+
1216+def get_hacluster_config():
1217+ '''
1218+ Obtains all relevant configuration from charm configuration required
1219+ for initiating a relation to hacluster:
1220+
1221+ ha-bindiface, ha-mcastport, vip, vip_iface, vip_cidr
1222+
1223+ returns: dict: A dict containing settings keyed by setting name.
1224+ raises: HAIncompleteConfig if settings are missing.
1225+ '''
1226+ settings = ['ha-bindiface', 'ha-mcastport', 'vip', 'vip_iface', 'vip_cidr']
1227+ conf = {}
1228+ for setting in settings:
1229+ conf[setting] = config_get(setting)
1230+ missing = []
1231+ [missing.append(s) for s, v in conf.iteritems() if v is None]
1232+ if missing:
1233+ log('Insufficient config data to configure hacluster.', level=ERROR)
1234+ raise HAIncompleteConfig
1235+ return conf
1236+
1237+
1238+def canonical_url(configs, vip_setting='vip'):
1239+ '''
1240+ Returns the correct HTTP URL to this host given the state of HTTPS
1241+ configuration and hacluster.
1242+
1243+ :configs : OSTemplateRenderer: A config tempating object to inspect for
1244+ a complete https context.
1245+ :vip_setting: str: Setting in charm config that specifies
1246+ VIP address.
1247+ '''
1248+ scheme = 'http'
1249+ if 'https' in configs.complete_contexts():
1250+ scheme = 'https'
1251+ if is_clustered():
1252+ addr = config_get(vip_setting)
1253+ else:
1254+ addr = unit_get('private-address')
1255+ return '%s://%s' % (scheme, addr)
1256
1257=== added directory 'lib/charmhelpers/contrib/jujugui'
1258=== added file 'lib/charmhelpers/contrib/jujugui/__init__.py'
1259=== added file 'lib/charmhelpers/contrib/jujugui/utils.py'
1260--- lib/charmhelpers/contrib/jujugui/utils.py 1970-01-01 00:00:00 +0000
1261+++ lib/charmhelpers/contrib/jujugui/utils.py 2013-11-21 22:43:22 +0000
1262@@ -0,0 +1,602 @@
1263+"""Juju GUI charm utilities."""
1264+
1265+__all__ = [
1266+ 'AGENT',
1267+ 'APACHE',
1268+ 'API_PORT',
1269+ 'CURRENT_DIR',
1270+ 'HAPROXY',
1271+ 'IMPROV',
1272+ 'JUJU_DIR',
1273+ 'JUJU_GUI_DIR',
1274+ 'JUJU_GUI_SITE',
1275+ 'JUJU_PEM',
1276+ 'WEB_PORT',
1277+ 'bzr_checkout',
1278+ 'chain',
1279+ 'cmd_log',
1280+ 'fetch_api',
1281+ 'fetch_gui',
1282+ 'find_missing_packages',
1283+ 'first_path_in_dir',
1284+ 'get_api_address',
1285+ 'get_npm_cache_archive_url',
1286+ 'get_release_file_url',
1287+ 'get_staging_dependencies',
1288+ 'get_zookeeper_address',
1289+ 'legacy_juju',
1290+ 'log_hook',
1291+ 'merge',
1292+ 'parse_source',
1293+ 'prime_npm_cache',
1294+ 'render_to_file',
1295+ 'save_or_create_certificates',
1296+ 'setup_apache',
1297+ 'setup_gui',
1298+ 'start_agent',
1299+ 'start_gui',
1300+ 'start_improv',
1301+ 'write_apache_config',
1302+]
1303+
1304+from contextlib import contextmanager
1305+import errno
1306+import json
1307+import os
1308+import logging
1309+import shutil
1310+from subprocess import CalledProcessError
1311+import tempfile
1312+from urlparse import urlparse
1313+
1314+import apt
1315+import tempita
1316+
1317+from launchpadlib.launchpad import Launchpad
1318+from shelltoolbox import (
1319+ Serializer,
1320+ apt_get_install,
1321+ command,
1322+ environ,
1323+ install_extra_repositories,
1324+ run,
1325+ script_name,
1326+ search_file,
1327+ su,
1328+)
1329+from charmhelpers.core.host import (
1330+ service_start,
1331+)
1332+from charmhelpers.core.hookenv import (
1333+ log,
1334+ config,
1335+ unit_get,
1336+)
1337+
1338+
1339+AGENT = 'juju-api-agent'
1340+APACHE = 'apache2'
1341+IMPROV = 'juju-api-improv'
1342+HAPROXY = 'haproxy'
1343+
1344+API_PORT = 8080
1345+WEB_PORT = 8000
1346+
1347+CURRENT_DIR = os.getcwd()
1348+JUJU_DIR = os.path.join(CURRENT_DIR, 'juju')
1349+JUJU_GUI_DIR = os.path.join(CURRENT_DIR, 'juju-gui')
1350+JUJU_GUI_SITE = '/etc/apache2/sites-available/juju-gui'
1351+JUJU_GUI_PORTS = '/etc/apache2/ports.conf'
1352+JUJU_PEM = 'juju.includes-private-key.pem'
1353+BUILD_REPOSITORIES = ('ppa:chris-lea/node.js-legacy',)
1354+DEB_BUILD_DEPENDENCIES = (
1355+ 'bzr', 'imagemagick', 'make', 'nodejs', 'npm',
1356+)
1357+DEB_STAGE_DEPENDENCIES = (
1358+ 'zookeeper',
1359+)
1360+
1361+
1362+# Store the configuration from on invocation to the next.
1363+config_json = Serializer('/tmp/config.json')
1364+# Bazaar checkout command.
1365+bzr_checkout = command('bzr', 'co', '--lightweight')
1366+# Whether or not the charm is deployed using juju-core.
1367+# If juju-core has been used to deploy the charm, an agent.conf file must
1368+# be present in the charm parent directory.
1369+legacy_juju = lambda: not os.path.exists(
1370+ os.path.join(CURRENT_DIR, '..', 'agent.conf'))
1371+
1372+
1373+def _get_build_dependencies():
1374+ """Install deb dependencies for building."""
1375+ log('Installing build dependencies.')
1376+ cmd_log(install_extra_repositories(*BUILD_REPOSITORIES))
1377+ cmd_log(apt_get_install(*DEB_BUILD_DEPENDENCIES))
1378+
1379+
1380+def get_api_address(unit_dir):
1381+ """Return the Juju API address stored in the uniter agent.conf file."""
1382+ import yaml # python-yaml is only installed if juju-core is used.
1383+ # XXX 2013-03-27 frankban bug=1161443:
1384+ # currently the uniter agent.conf file does not include the API
1385+ # address. For now retrieve it from the machine agent file.
1386+ base_dir = os.path.abspath(os.path.join(unit_dir, '..'))
1387+ for dirname in os.listdir(base_dir):
1388+ if dirname.startswith('machine-'):
1389+ agent_conf = os.path.join(base_dir, dirname, 'agent.conf')
1390+ break
1391+ else:
1392+ raise IOError('Juju agent configuration file not found.')
1393+ contents = yaml.load(open(agent_conf))
1394+ return contents['apiinfo']['addrs'][0]
1395+
1396+
1397+def get_staging_dependencies():
1398+ """Install deb dependencies for the stage (improv) environment."""
1399+ log('Installing stage dependencies.')
1400+ cmd_log(apt_get_install(*DEB_STAGE_DEPENDENCIES))
1401+
1402+
1403+def first_path_in_dir(directory):
1404+ """Return the full path of the first file/dir in *directory*."""
1405+ return os.path.join(directory, os.listdir(directory)[0])
1406+
1407+
1408+def _get_by_attr(collection, attr, value):
1409+ """Return the first item in collection having attr == value.
1410+
1411+ Return None if the item is not found.
1412+ """
1413+ for item in collection:
1414+ if getattr(item, attr) == value:
1415+ return item
1416+
1417+
1418+def get_release_file_url(project, series_name, release_version):
1419+ """Return the URL of the release file hosted in Launchpad.
1420+
1421+ The returned URL points to a release file for the given project, series
1422+ name and release version.
1423+ The argument *project* is a project object as returned by launchpadlib.
1424+ The arguments *series_name* and *release_version* are strings. If
1425+ *release_version* is None, the URL of the latest release will be returned.
1426+ """
1427+ series = _get_by_attr(project.series, 'name', series_name)
1428+ if series is None:
1429+ raise ValueError('%r: series not found' % series_name)
1430+ # Releases are returned by Launchpad in reverse date order.
1431+ releases = list(series.releases)
1432+ if not releases:
1433+ raise ValueError('%r: series does not contain releases' % series_name)
1434+ if release_version is not None:
1435+ release = _get_by_attr(releases, 'version', release_version)
1436+ if release is None:
1437+ raise ValueError('%r: release not found' % release_version)
1438+ releases = [release]
1439+ for release in releases:
1440+ for file_ in release.files:
1441+ if str(file_).endswith('.tgz'):
1442+ return file_.file_link
1443+ raise ValueError('%r: file not found' % release_version)
1444+
1445+
1446+def get_zookeeper_address(agent_file_path):
1447+ """Retrieve the Zookeeper address contained in the given *agent_file_path*.
1448+
1449+ The *agent_file_path* is a path to a file containing a line similar to the
1450+ following::
1451+
1452+ env JUJU_ZOOKEEPER="address"
1453+ """
1454+ line = search_file('JUJU_ZOOKEEPER', agent_file_path).strip()
1455+ return line.split('=')[1].strip('"')
1456+
1457+
1458+@contextmanager
1459+def log_hook():
1460+ """Log when a hook starts and stops its execution.
1461+
1462+ Also log to stdout possible CalledProcessError exceptions raised executing
1463+ the hook.
1464+ """
1465+ script = script_name()
1466+ log(">>> Entering {}".format(script))
1467+ try:
1468+ yield
1469+ except CalledProcessError as err:
1470+ log('Exception caught:')
1471+ log(err.output)
1472+ raise
1473+ finally:
1474+ log("<<< Exiting {}".format(script))
1475+
1476+
1477+def parse_source(source):
1478+ """Parse the ``juju-gui-source`` option.
1479+
1480+ Return a tuple of two elements representing info on how to deploy Juju GUI.
1481+ Examples:
1482+ - ('stable', None): latest stable release;
1483+ - ('stable', '0.1.0'): stable release v0.1.0;
1484+ - ('trunk', None): latest trunk release;
1485+ - ('trunk', '0.1.0+build.1'): trunk release v0.1.0 bzr revision 1;
1486+ - ('branch', 'lp:juju-gui'): release is made from a branch;
1487+ - ('url', 'http://example.com/gui'): release from a downloaded file.
1488+ """
1489+ if source.startswith('url:'):
1490+ source = source[4:]
1491+ # Support file paths, including relative paths.
1492+ if urlparse(source).scheme == '':
1493+ if not source.startswith('/'):
1494+ source = os.path.join(os.path.abspath(CURRENT_DIR), source)
1495+ source = "file://%s" % source
1496+ return 'url', source
1497+ if source in ('stable', 'trunk'):
1498+ return source, None
1499+ if source.startswith('lp:') or source.startswith('http://'):
1500+ return 'branch', source
1501+ if 'build' in source:
1502+ return 'trunk', source
1503+ return 'stable', source
1504+
1505+
1506+def render_to_file(template_name, context, destination):
1507+ """Render the given *template_name* into *destination* using *context*.
1508+
1509+ The tempita template language is used to render contents
1510+ (see http://pythonpaste.org/tempita/).
1511+ The argument *template_name* is the name or path of the template file:
1512+ it may be either a path relative to ``../config`` or an absolute path.
1513+ The argument *destination* is a file path.
1514+ The argument *context* is a dict-like object.
1515+ """
1516+ template_path = os.path.abspath(template_name)
1517+ template = tempita.Template.from_filename(template_path)
1518+ with open(destination, 'w') as stream:
1519+ stream.write(template.substitute(context))
1520+
1521+
1522+results_log = None
1523+
1524+
1525+def _setupLogging():
1526+ global results_log
1527+ if results_log is not None:
1528+ return
1529+ cfg = config()
1530+ logging.basicConfig(
1531+ filename=cfg['command-log-file'],
1532+ level=logging.INFO,
1533+ format="%(asctime)s: %(name)s@%(levelname)s %(message)s")
1534+ results_log = logging.getLogger('juju-gui')
1535+
1536+
1537+def cmd_log(results):
1538+ global results_log
1539+ if not results:
1540+ return
1541+ if results_log is None:
1542+ _setupLogging()
1543+ # Since 'results' may be multi-line output, start it on a separate line
1544+ # from the logger timestamp, etc.
1545+ results_log.info('\n' + results)
1546+
1547+
1548+def start_improv(staging_env, ssl_cert_path,
1549+ config_path='/etc/init/juju-api-improv.conf'):
1550+ """Start a simulated juju environment using ``improv.py``."""
1551+ log('Setting up staging start up script.')
1552+ context = {
1553+ 'juju_dir': JUJU_DIR,
1554+ 'keys': ssl_cert_path,
1555+ 'port': API_PORT,
1556+ 'staging_env': staging_env,
1557+ }
1558+ render_to_file('config/juju-api-improv.conf.template', context, config_path)
1559+ log('Starting the staging backend.')
1560+ with su('root'):
1561+ service_start(IMPROV)
1562+
1563+
1564+def start_agent(
1565+ ssl_cert_path, config_path='/etc/init/juju-api-agent.conf',
1566+ read_only=False):
1567+ """Start the Juju agent and connect to the current environment."""
1568+ # Retrieve the Zookeeper address from the start up script.
1569+ unit_dir = os.path.realpath(os.path.join(CURRENT_DIR, '..'))
1570+ agent_file = '/etc/init/juju-{0}.conf'.format(os.path.basename(unit_dir))
1571+ zookeeper = get_zookeeper_address(agent_file)
1572+ log('Setting up API agent start up script.')
1573+ context = {
1574+ 'juju_dir': JUJU_DIR,
1575+ 'keys': ssl_cert_path,
1576+ 'port': API_PORT,
1577+ 'zookeeper': zookeeper,
1578+ 'read_only': read_only
1579+ }
1580+ render_to_file('config/juju-api-agent.conf.template', context, config_path)
1581+ log('Starting API agent.')
1582+ with su('root'):
1583+ service_start(AGENT)
1584+
1585+
1586+def start_gui(
1587+ console_enabled, login_help, readonly, in_staging, ssl_cert_path,
1588+ charmworld_url, serve_tests, haproxy_path='/etc/haproxy/haproxy.cfg',
1589+ config_js_path=None, secure=True, sandbox=False):
1590+ """Set up and start the Juju GUI server."""
1591+ with su('root'):
1592+ run('chown', '-R', 'ubuntu:', JUJU_GUI_DIR)
1593+ # XXX 2013-02-05 frankban bug=1116320:
1594+ # External insecure resources are still loaded when testing in the
1595+ # debug environment. For now, switch to the production environment if
1596+ # the charm is configured to serve tests.
1597+ if in_staging and not serve_tests:
1598+ build_dirname = 'build-debug'
1599+ else:
1600+ build_dirname = 'build-prod'
1601+ build_dir = os.path.join(JUJU_GUI_DIR, build_dirname)
1602+ log('Generating the Juju GUI configuration file.')
1603+ is_legacy_juju = legacy_juju()
1604+ user, password = None, None
1605+ if (is_legacy_juju and in_staging) or sandbox:
1606+ user, password = 'admin', 'admin'
1607+ else:
1608+ user, password = None, None
1609+
1610+ api_backend = 'python' if is_legacy_juju else 'go'
1611+ if secure:
1612+ protocol = 'wss'
1613+ else:
1614+ log('Running in insecure mode! Port 80 will serve unencrypted.')
1615+ protocol = 'ws'
1616+
1617+ context = {
1618+ 'raw_protocol': protocol,
1619+ 'address': unit_get('public-address'),
1620+ 'console_enabled': json.dumps(console_enabled),
1621+ 'login_help': json.dumps(login_help),
1622+ 'password': json.dumps(password),
1623+ 'api_backend': json.dumps(api_backend),
1624+ 'readonly': json.dumps(readonly),
1625+ 'user': json.dumps(user),
1626+ 'protocol': json.dumps(protocol),
1627+ 'sandbox': json.dumps(sandbox),
1628+ 'charmworld_url': json.dumps(charmworld_url),
1629+ }
1630+ if config_js_path is None:
1631+ config_js_path = os.path.join(
1632+ build_dir, 'juju-ui', 'assets', 'config.js')
1633+ render_to_file('config/config.js.template', context, config_js_path)
1634+
1635+ write_apache_config(build_dir, serve_tests)
1636+
1637+ log('Generating haproxy configuration file.')
1638+ if is_legacy_juju:
1639+ # The PyJuju API agent is listening on localhost.
1640+ api_address = '127.0.0.1:{0}'.format(API_PORT)
1641+ else:
1642+ # Retrieve the juju-core API server address.
1643+ api_address = get_api_address(os.path.join(CURRENT_DIR, '..'))
1644+ context = {
1645+ 'api_address': api_address,
1646+ 'api_pem': JUJU_PEM,
1647+ 'legacy_juju': is_legacy_juju,
1648+ 'ssl_cert_path': ssl_cert_path,
1649+ # In PyJuju environments, use the same certificate for both HTTPS and
1650+ # WebSocket connections. In juju-core the system already has the proper
1651+ # certificate installed.
1652+ 'web_pem': JUJU_PEM,
1653+ 'web_port': WEB_PORT,
1654+ 'secure': secure
1655+ }
1656+ render_to_file('config/haproxy.cfg.template', context, haproxy_path)
1657+ log('Starting Juju GUI.')
1658+
1659+
1660+def write_apache_config(build_dir, serve_tests=False):
1661+ log('Generating the apache site configuration file.')
1662+ context = {
1663+ 'port': WEB_PORT,
1664+ 'serve_tests': serve_tests,
1665+ 'server_root': build_dir,
1666+ 'tests_root': os.path.join(JUJU_GUI_DIR, 'test', ''),
1667+ }
1668+ render_to_file('config/apache-ports.template', context, JUJU_GUI_PORTS)
1669+ render_to_file('config/apache-site.template', context, JUJU_GUI_SITE)
1670+
1671+
1672+def get_npm_cache_archive_url(Launchpad=Launchpad):
1673+ """Figure out the URL of the most recent NPM cache archive on Launchpad."""
1674+ launchpad = Launchpad.login_anonymously('Juju GUI charm', 'production')
1675+ project = launchpad.projects['juju-gui']
1676+ # Find the URL of the most recently created NPM cache archive.
1677+ npm_cache_url = get_release_file_url(project, 'npm-cache', None)
1678+ return npm_cache_url
1679+
1680+
1681+def prime_npm_cache(npm_cache_url):
1682+ """Download NPM cache archive and prime the NPM cache with it."""
1683+ # Download the cache archive and then uncompress it into the NPM cache.
1684+ npm_cache_archive = os.path.join(CURRENT_DIR, 'npm-cache.tgz')
1685+ cmd_log(run('curl', '-L', '-o', npm_cache_archive, npm_cache_url))
1686+ npm_cache_dir = os.path.expanduser('~/.npm')
1687+ # The NPM cache directory probably does not exist, so make it if not.
1688+ try:
1689+ os.mkdir(npm_cache_dir)
1690+ except OSError, e:
1691+ # If the directory already exists then ignore the error.
1692+ if e.errno != errno.EEXIST: # File exists.
1693+ raise
1694+ uncompress = command('tar', '-x', '-z', '-C', npm_cache_dir, '-f')
1695+ cmd_log(uncompress(npm_cache_archive))
1696+
1697+
1698+def fetch_gui(juju_gui_source, logpath):
1699+ """Retrieve the Juju GUI release/branch."""
1700+ # Retrieve a Juju GUI release.
1701+ origin, version_or_branch = parse_source(juju_gui_source)
1702+ if origin == 'branch':
1703+ # Make sure we have the dependencies necessary for us to actually make
1704+ # a build.
1705+ _get_build_dependencies()
1706+ # Create a release starting from a branch.
1707+ juju_gui_source_dir = os.path.join(CURRENT_DIR, 'juju-gui-source')
1708+ log('Retrieving Juju GUI source checkout from %s.' % version_or_branch)
1709+ cmd_log(run('rm', '-rf', juju_gui_source_dir))
1710+ cmd_log(bzr_checkout(version_or_branch, juju_gui_source_dir))
1711+ log('Preparing a Juju GUI release.')
1712+ logdir = os.path.dirname(logpath)
1713+ fd, name = tempfile.mkstemp(prefix='make-distfile-', dir=logdir)
1714+ log('Output from "make distfile" sent to %s' % name)
1715+ with environ(NO_BZR='1'):
1716+ run('make', '-C', juju_gui_source_dir, 'distfile',
1717+ stdout=fd, stderr=fd)
1718+ release_tarball = first_path_in_dir(
1719+ os.path.join(juju_gui_source_dir, 'releases'))
1720+ else:
1721+ log('Retrieving Juju GUI release.')
1722+ if origin == 'url':
1723+ file_url = version_or_branch
1724+ else:
1725+ # Retrieve a release from Launchpad.
1726+ launchpad = Launchpad.login_anonymously(
1727+ 'Juju GUI charm', 'production')
1728+ project = launchpad.projects['juju-gui']
1729+ file_url = get_release_file_url(project, origin, version_or_branch)
1730+ log('Downloading release file from %s.' % file_url)
1731+ release_tarball = os.path.join(CURRENT_DIR, 'release.tgz')
1732+ cmd_log(run('curl', '-L', '-o', release_tarball, file_url))
1733+ return release_tarball
1734+
1735+
1736+def fetch_api(juju_api_branch):
1737+ """Retrieve the Juju branch."""
1738+ # Retrieve Juju API source checkout.
1739+ log('Retrieving Juju API source checkout.')
1740+ cmd_log(run('rm', '-rf', JUJU_DIR))
1741+ cmd_log(bzr_checkout(juju_api_branch, JUJU_DIR))
1742+
1743+
1744+def setup_gui(release_tarball):
1745+ """Set up Juju GUI."""
1746+ # Uncompress the release tarball.
1747+ log('Installing Juju GUI.')
1748+ release_dir = os.path.join(CURRENT_DIR, 'release')
1749+ cmd_log(run('rm', '-rf', release_dir))
1750+ os.mkdir(release_dir)
1751+ uncompress = command('tar', '-x', '-z', '-C', release_dir, '-f')
1752+ cmd_log(uncompress(release_tarball))
1753+ # Link the Juju GUI dir to the contents of the release tarball.
1754+ cmd_log(run('ln', '-sf', first_path_in_dir(release_dir), JUJU_GUI_DIR))
1755+
1756+
1757+def setup_apache():
1758+ """Set up apache."""
1759+ log('Setting up apache.')
1760+ if not os.path.exists(JUJU_GUI_SITE):
1761+ cmd_log(run('touch', JUJU_GUI_SITE))
1762+ cmd_log(run('chown', 'ubuntu:', JUJU_GUI_SITE))
1763+ cmd_log(
1764+ run('ln', '-s', JUJU_GUI_SITE,
1765+ '/etc/apache2/sites-enabled/juju-gui'))
1766+
1767+ if not os.path.exists(JUJU_GUI_PORTS):
1768+ cmd_log(run('touch', JUJU_GUI_PORTS))
1769+ cmd_log(run('chown', 'ubuntu:', JUJU_GUI_PORTS))
1770+
1771+ with su('root'):
1772+ run('a2dissite', 'default')
1773+ run('a2ensite', 'juju-gui')
1774+
1775+
1776+def save_or_create_certificates(
1777+ ssl_cert_path, ssl_cert_contents, ssl_key_contents):
1778+ """Generate the SSL certificates.
1779+
1780+ If both *ssl_cert_contents* and *ssl_key_contents* are provided, use them
1781+ as certificates; otherwise, generate them.
1782+
1783+ Also create a pem file, suitable for use in the haproxy configuration,
1784+ concatenating the key and the certificate files.
1785+ """
1786+ crt_path = os.path.join(ssl_cert_path, 'juju.crt')
1787+ key_path = os.path.join(ssl_cert_path, 'juju.key')
1788+ if not os.path.exists(ssl_cert_path):
1789+ os.makedirs(ssl_cert_path)
1790+ if ssl_cert_contents and ssl_key_contents:
1791+ # Save the provided certificates.
1792+ with open(crt_path, 'w') as cert_file:
1793+ cert_file.write(ssl_cert_contents)
1794+ with open(key_path, 'w') as key_file:
1795+ key_file.write(ssl_key_contents)
1796+ else:
1797+ # Generate certificates.
1798+ # See http://superuser.com/questions/226192/openssl-without-prompt
1799+ cmd_log(run(
1800+ 'openssl', 'req', '-new', '-newkey', 'rsa:4096',
1801+ '-days', '365', '-nodes', '-x509', '-subj',
1802+ # These are arbitrary test values for the certificate.
1803+ '/C=GB/ST=Juju/L=GUI/O=Ubuntu/CN=juju.ubuntu.com',
1804+ '-keyout', key_path, '-out', crt_path))
1805+ # Generate the pem file.
1806+ pem_path = os.path.join(ssl_cert_path, JUJU_PEM)
1807+ if os.path.exists(pem_path):
1808+ os.remove(pem_path)
1809+ with open(pem_path, 'w') as pem_file:
1810+ shutil.copyfileobj(open(key_path), pem_file)
1811+ shutil.copyfileobj(open(crt_path), pem_file)
1812+
1813+
1814+def find_missing_packages(*packages):
1815+ """Given a list of packages, return the packages which are not installed.
1816+ """
1817+ cache = apt.Cache()
1818+ missing = set()
1819+ for pkg_name in packages:
1820+ try:
1821+ pkg = cache[pkg_name]
1822+ except KeyError:
1823+ missing.add(pkg_name)
1824+ continue
1825+ if pkg.is_installed:
1826+ continue
1827+ missing.add(pkg_name)
1828+ return missing
1829+
1830+
1831+## Backend support decorators
1832+
1833+def chain(name):
1834+ """Helper method to compose a set of mixin objects into a callable.
1835+
1836+ Each method is called in the context of its mixin instance, and its
1837+ argument is the Backend instance.
1838+ """
1839+ # Chain method calls through all implementing mixins.
1840+ def method(self):
1841+ for mixin in self.mixins:
1842+ a_callable = getattr(type(mixin), name, None)
1843+ if a_callable:
1844+ a_callable(mixin, self)
1845+
1846+ method.__name__ = name
1847+ return method
1848+
1849+
1850+def merge(name):
1851+ """Helper to merge a property from a set of strategy objects
1852+ into a unified set.
1853+ """
1854+ # Return merged property from every providing mixin as a set.
1855+ @property
1856+ def method(self):
1857+ result = set()
1858+ for mixin in self.mixins:
1859+ segment = getattr(type(mixin), name, None)
1860+ if segment and isinstance(segment, (list, tuple, set)):
1861+ result |= set(segment)
1862+
1863+ return result
1864+ return method
1865
1866=== added directory 'lib/charmhelpers/contrib/saltstack'
1867=== added file 'lib/charmhelpers/contrib/saltstack/__init__.py'
1868--- lib/charmhelpers/contrib/saltstack/__init__.py 1970-01-01 00:00:00 +0000
1869+++ lib/charmhelpers/contrib/saltstack/__init__.py 2013-11-21 22:43:22 +0000
1870@@ -0,0 +1,149 @@
1871+"""Charm Helpers saltstack - declare the state of your machines.
1872+
1873+This helper enables you to declare your machine state, rather than
1874+program it procedurally (and have to test each change to your procedures).
1875+Your install hook can be as simple as:
1876+
1877+{{{
1878+from charmhelpers.contrib.saltstack import (
1879+ install_salt_support,
1880+ update_machine_state,
1881+)
1882+
1883+
1884+def install():
1885+ install_salt_support()
1886+ update_machine_state('machine_states/dependencies.yaml')
1887+ update_machine_state('machine_states/installed.yaml')
1888+}}}
1889+
1890+and won't need to change (nor will its tests) when you change the machine
1891+state.
1892+
1893+It's using a python package called salt-minion which allows various formats for
1894+specifying resources, such as:
1895+
1896+{{{
1897+/srv/{{ basedir }}:
1898+ file.directory:
1899+ - group: ubunet
1900+ - user: ubunet
1901+ - require:
1902+ - user: ubunet
1903+ - recurse:
1904+ - user
1905+ - group
1906+
1907+ubunet:
1908+ group.present:
1909+ - gid: 1500
1910+ user.present:
1911+ - uid: 1500
1912+ - gid: 1500
1913+ - createhome: False
1914+ - require:
1915+ - group: ubunet
1916+}}}
1917+
1918+The docs for all the different state definitions are at:
1919+ http://docs.saltstack.com/ref/states/all/
1920+
1921+
1922+TODO:
1923+ * Add test helpers which will ensure that machine state definitions
1924+ are functionally (but not necessarily logically) correct (ie. getting
1925+ salt to parse all state defs.
1926+ * Add a link to a public bootstrap charm example / blogpost.
1927+ * Find a way to obviate the need to use the grains['charm_dir'] syntax
1928+ in templates.
1929+"""
1930+# Copyright 2013 Canonical Ltd.
1931+#
1932+# Authors:
1933+# Charm Helpers Developers <juju@lists.ubuntu.com>
1934+import os
1935+import subprocess
1936+import yaml
1937+
1938+import charmhelpers.core.host
1939+import charmhelpers.core.hookenv
1940+
1941+
1942+charm_dir = os.environ.get('CHARM_DIR', '')
1943+salt_grains_path = '/etc/salt/grains'
1944+
1945+
1946+def install_salt_support(from_ppa=True):
1947+ """Installs the salt-minion helper for machine state.
1948+
1949+ By default the salt-minion package is installed from
1950+ the saltstack PPA. If from_ppa is False you must ensure
1951+ that the salt-minion package is available in the apt cache.
1952+ """
1953+ if from_ppa:
1954+ subprocess.check_call([
1955+ '/usr/bin/add-apt-repository',
1956+ '--yes',
1957+ 'ppa:saltstack/salt',
1958+ ])
1959+ subprocess.check_call(['/usr/bin/apt-get', 'update'])
1960+ # We install salt-common as salt-minion would run the salt-minion
1961+ # daemon.
1962+ charmhelpers.fetch.apt_install('salt-common')
1963+
1964+
1965+def update_machine_state(state_path):
1966+ """Update the machine state using the provided state declaration."""
1967+ juju_state_to_yaml(salt_grains_path)
1968+ subprocess.check_call([
1969+ 'salt-call',
1970+ '--local',
1971+ 'state.template',
1972+ state_path,
1973+ ])
1974+
1975+
1976+def juju_state_to_yaml(yaml_path, namespace_separator=':'):
1977+ """Update the juju config and state in a yaml file.
1978+
1979+ This includes any current relation-get data, and the charm
1980+ directory.
1981+ """
1982+ config = charmhelpers.core.hookenv.config()
1983+
1984+ # Add the charm_dir which we will need to refer to charm
1985+ # file resources etc.
1986+ config['charm_dir'] = charm_dir
1987+ config['local_unit'] = charmhelpers.core.hookenv.local_unit()
1988+
1989+ # Add any relation data prefixed with the relation type.
1990+ relation_type = charmhelpers.core.hookenv.relation_type()
1991+ if relation_type is not None:
1992+ relation_data = charmhelpers.core.hookenv.relation_get()
1993+ relation_data = dict(
1994+ ("{relation_type}{namespace_separator}{key}".format(
1995+ relation_type=relation_type.replace('-', '_'),
1996+ key=key,
1997+ namespace_separator=namespace_separator), val)
1998+ for key, val in relation_data.items())
1999+ config.update(relation_data)
2000+
2001+ # Don't use non-standard tags for unicode which will not
2002+ # work when salt uses yaml.load_safe.
2003+ yaml.add_representer(unicode, lambda dumper,
2004+ value: dumper.represent_scalar(
2005+ u'tag:yaml.org,2002:str', value))
2006+
2007+ yaml_dir = os.path.dirname(yaml_path)
2008+ if not os.path.exists(yaml_dir):
2009+ os.makedirs(yaml_dir)
2010+
2011+ if os.path.exists(yaml_path):
2012+ with open(yaml_path, "r") as existing_vars_file:
2013+ existing_vars = yaml.load(existing_vars_file.read())
2014+ else:
2015+ existing_vars = {}
2016+
2017+ existing_vars.update(config)
2018+ with open(yaml_path, "w+") as fp:
2019+ fp.write(yaml.dump(existing_vars))
2020
2021=== added directory 'lib/charmhelpers/core'
2022=== added file 'lib/charmhelpers/core/__init__.py'
2023=== added file 'lib/charmhelpers/core/hookenv.py'
2024--- lib/charmhelpers/core/hookenv.py 1970-01-01 00:00:00 +0000
2025+++ lib/charmhelpers/core/hookenv.py 2013-11-21 22:43:22 +0000
2026@@ -0,0 +1,395 @@
2027+"Interactions with the Juju environment"
2028+# Copyright 2013 Canonical Ltd.
2029+#
2030+# Authors:
2031+# Charm Helpers Developers <juju@lists.ubuntu.com>
2032+
2033+import os
2034+import json
2035+import yaml
2036+import subprocess
2037+import UserDict
2038+from subprocess import CalledProcessError
2039+
2040+CRITICAL = "CRITICAL"
2041+ERROR = "ERROR"
2042+WARNING = "WARNING"
2043+INFO = "INFO"
2044+DEBUG = "DEBUG"
2045+MARKER = object()
2046+
2047+cache = {}
2048+
2049+
2050+def cached(func):
2051+ """Cache return values for multiple executions of func + args
2052+
2053+ For example:
2054+
2055+ @cached
2056+ def unit_get(attribute):
2057+ pass
2058+
2059+ unit_get('test')
2060+
2061+ will cache the result of unit_get + 'test' for future calls.
2062+ """
2063+ def wrapper(*args, **kwargs):
2064+ global cache
2065+ key = str((func, args, kwargs))
2066+ try:
2067+ return cache[key]
2068+ except KeyError:
2069+ res = func(*args, **kwargs)
2070+ cache[key] = res
2071+ return res
2072+ return wrapper
2073+
2074+
2075+def flush(key):
2076+ """Flushes any entries from function cache where the
2077+ key is found in the function+args """
2078+ flush_list = []
2079+ for item in cache:
2080+ if key in item:
2081+ flush_list.append(item)
2082+ for item in flush_list:
2083+ del cache[item]
2084+
2085+
2086+def log(message, level=None):
2087+ """Write a message to the juju log"""
2088+ command = ['juju-log']
2089+ if level:
2090+ command += ['-l', level]
2091+ command += [message]
2092+ subprocess.call(command)
2093+
2094+
2095+class Serializable(UserDict.IterableUserDict):
2096+ """Wrapper, an object that can be serialized to yaml or json"""
2097+
2098+ def __init__(self, obj):
2099+ # wrap the object
2100+ UserDict.IterableUserDict.__init__(self)
2101+ self.data = obj
2102+
2103+ def __getattr__(self, attr):
2104+ # See if this object has attribute.
2105+ if attr in ("json", "yaml", "data"):
2106+ return self.__dict__[attr]
2107+ # Check for attribute in wrapped object.
2108+ got = getattr(self.data, attr, MARKER)
2109+ if got is not MARKER:
2110+ return got
2111+ # Proxy to the wrapped object via dict interface.
2112+ try:
2113+ return self.data[attr]
2114+ except KeyError:
2115+ raise AttributeError(attr)
2116+
2117+ def __getstate__(self):
2118+ # Pickle as a standard dictionary.
2119+ return self.data
2120+
2121+ def __setstate__(self, state):
2122+ # Unpickle into our wrapper.
2123+ self.data = state
2124+
2125+ def json(self):
2126+ """Serialize the object to json"""
2127+ return json.dumps(self.data)
2128+
2129+ def yaml(self):
2130+ """Serialize the object to yaml"""
2131+ return yaml.dump(self.data)
2132+
2133+
2134+def execution_environment():
2135+ """A convenient bundling of the current execution context"""
2136+ context = {}
2137+ context['conf'] = config()
2138+ if relation_id():
2139+ context['reltype'] = relation_type()
2140+ context['relid'] = relation_id()
2141+ context['rel'] = relation_get()
2142+ context['unit'] = local_unit()
2143+ context['rels'] = relations()
2144+ context['env'] = os.environ
2145+ return context
2146+
2147+
2148+def in_relation_hook():
2149+ """Determine whether we're running in a relation hook"""
2150+ return 'JUJU_RELATION' in os.environ
2151+
2152+
2153+def relation_type():
2154+ """The scope for the current relation hook"""
2155+ return os.environ.get('JUJU_RELATION', None)
2156+
2157+
2158+def relation_id():
2159+ """The relation ID for the current relation hook"""
2160+ return os.environ.get('JUJU_RELATION_ID', None)
2161+
2162+
2163+def local_unit():
2164+ """Local unit ID"""
2165+ return os.environ['JUJU_UNIT_NAME']
2166+
2167+
2168+def remote_unit():
2169+ """The remote unit for the current relation hook"""
2170+ return os.environ['JUJU_REMOTE_UNIT']
2171+
2172+
2173+def service_name():
2174+ """The name service group this unit belongs to"""
2175+ return local_unit().split('/')[0]
2176+
2177+
2178+@cached
2179+def config(scope=None):
2180+ """Juju charm configuration"""
2181+ config_cmd_line = ['config-get']
2182+ if scope is not None:
2183+ config_cmd_line.append(scope)
2184+ config_cmd_line.append('--format=json')
2185+ try:
2186+ return json.loads(subprocess.check_output(config_cmd_line))
2187+ except ValueError:
2188+ return None
2189+
2190+
2191+@cached
2192+def relation_get(attribute=None, unit=None, rid=None):
2193+ """Get relation information"""
2194+ _args = ['relation-get', '--format=json']
2195+ if rid:
2196+ _args.append('-r')
2197+ _args.append(rid)
2198+ _args.append(attribute or '-')
2199+ if unit:
2200+ _args.append(unit)
2201+ try:
2202+ return json.loads(subprocess.check_output(_args))
2203+ except ValueError:
2204+ return None
2205+ except CalledProcessError, e:
2206+ if e.returncode == 2:
2207+ return None
2208+ raise
2209+
2210+
2211+def relation_set(relation_id=None, relation_settings={}, **kwargs):
2212+ """Set relation information for the current unit"""
2213+ relation_cmd_line = ['relation-set']
2214+ if relation_id is not None:
2215+ relation_cmd_line.extend(('-r', relation_id))
2216+ for k, v in (relation_settings.items() + kwargs.items()):
2217+ if v is None:
2218+ relation_cmd_line.append('{}='.format(k))
2219+ else:
2220+ relation_cmd_line.append('{}={}'.format(k, v))
2221+ subprocess.check_call(relation_cmd_line)
2222+ # Flush cache of any relation-gets for local unit
2223+ flush(local_unit())
2224+
2225+
2226+@cached
2227+def relation_ids(reltype=None):
2228+ """A list of relation_ids"""
2229+ reltype = reltype or relation_type()
2230+ relid_cmd_line = ['relation-ids', '--format=json']
2231+ if reltype is not None:
2232+ relid_cmd_line.append(reltype)
2233+ return json.loads(subprocess.check_output(relid_cmd_line)) or []
2234+ return []
2235+
2236+
2237+@cached
2238+def related_units(relid=None):
2239+ """A list of related units"""
2240+ relid = relid or relation_id()
2241+ units_cmd_line = ['relation-list', '--format=json']
2242+ if relid is not None:
2243+ units_cmd_line.extend(('-r', relid))
2244+ return json.loads(subprocess.check_output(units_cmd_line)) or []
2245+
2246+
2247+@cached
2248+def relation_for_unit(unit=None, rid=None):
2249+ """Get the json represenation of a unit's relation"""
2250+ unit = unit or remote_unit()
2251+ relation = relation_get(unit=unit, rid=rid)
2252+ for key in relation:
2253+ if key.endswith('-list'):
2254+ relation[key] = relation[key].split()
2255+ relation['__unit__'] = unit
2256+ return relation
2257+
2258+
2259+@cached
2260+def relations_for_id(relid=None):
2261+ """Get relations of a specific relation ID"""
2262+ relation_data = []
2263+ relid = relid or relation_ids()
2264+ for unit in related_units(relid):
2265+ unit_data = relation_for_unit(unit, relid)
2266+ unit_data['__relid__'] = relid
2267+ relation_data.append(unit_data)
2268+ return relation_data
2269+
2270+
2271+@cached
2272+def relations_of_type(reltype=None):
2273+ """Get relations of a specific type"""
2274+ relation_data = []
2275+ reltype = reltype or relation_type()
2276+ for relid in relation_ids(reltype):
2277+ for relation in relations_for_id(relid):
2278+ relation['__relid__'] = relid
2279+ relation_data.append(relation)
2280+ return relation_data
2281+
2282+
2283+@cached
2284+def relation_types():
2285+ """Get a list of relation types supported by this charm"""
2286+ charmdir = os.environ.get('CHARM_DIR', '')
2287+ mdf = open(os.path.join(charmdir, 'metadata.yaml'))
2288+ md = yaml.safe_load(mdf)
2289+ rel_types = []
2290+ for key in ('provides', 'requires', 'peers'):
2291+ section = md.get(key)
2292+ if section:
2293+ rel_types.extend(section.keys())
2294+ mdf.close()
2295+ return rel_types
2296+
2297+
2298+@cached
2299+def relations():
2300+ """Get a nested dictionary of relation data for all related units"""
2301+ rels = {}
2302+ for reltype in relation_types():
2303+ relids = {}
2304+ for relid in relation_ids(reltype):
2305+ units = {local_unit(): relation_get(unit=local_unit(), rid=relid)}
2306+ for unit in related_units(relid):
2307+ reldata = relation_get(unit=unit, rid=relid)
2308+ units[unit] = reldata
2309+ relids[relid] = units
2310+ rels[reltype] = relids
2311+ return rels
2312+
2313+
2314+@cached
2315+def is_relation_made(relation, keys='private-address'):
2316+ '''
2317+ Determine whether a relation is established by checking for
2318+ presence of key(s). If a list of keys is provided, they
2319+ must all be present for the relation to be identified as made
2320+ '''
2321+ if isinstance(keys, str):
2322+ keys = [keys]
2323+ for r_id in relation_ids(relation):
2324+ for unit in related_units(r_id):
2325+ context = {}
2326+ for k in keys:
2327+ context[k] = relation_get(k, rid=r_id,
2328+ unit=unit)
2329+ if None not in context.values():
2330+ return True
2331+ return False
2332+
2333+
2334+def open_port(port, protocol="TCP"):
2335+ """Open a service network port"""
2336+ _args = ['open-port']
2337+ _args.append('{}/{}'.format(port, protocol))
2338+ subprocess.check_call(_args)
2339+
2340+
2341+def close_port(port, protocol="TCP"):
2342+ """Close a service network port"""
2343+ _args = ['close-port']
2344+ _args.append('{}/{}'.format(port, protocol))
2345+ subprocess.check_call(_args)
2346+
2347+
2348+@cached
2349+def unit_get(attribute):
2350+ """Get the unit ID for the remote unit"""
2351+ _args = ['unit-get', '--format=json', attribute]
2352+ try:
2353+ return json.loads(subprocess.check_output(_args))
2354+ except ValueError:
2355+ return None
2356+
2357+
2358+def unit_private_ip():
2359+ """Get this unit's private IP address"""
2360+ return unit_get('private-address')
2361+
2362+
2363+class UnregisteredHookError(Exception):
2364+ """Raised when an undefined hook is called"""
2365+ pass
2366+
2367+
2368+class Hooks(object):
2369+ """A convenient handler for hook functions.
2370+
2371+ Example:
2372+ hooks = Hooks()
2373+
2374+ # register a hook, taking its name from the function name
2375+ @hooks.hook()
2376+ def install():
2377+ ...
2378+
2379+ # register a hook, providing a custom hook name
2380+ @hooks.hook("config-changed")
2381+ def config_changed():
2382+ ...
2383+
2384+ if __name__ == "__main__":
2385+ # execute a hook based on the name the program is called by
2386+ hooks.execute(sys.argv)
2387+ """
2388+
2389+ def __init__(self):
2390+ super(Hooks, self).__init__()
2391+ self._hooks = {}
2392+
2393+ def register(self, name, function):
2394+ """Register a hook"""
2395+ self._hooks[name] = function
2396+
2397+ def execute(self, args):
2398+ """Execute a registered hook based on args[0]"""
2399+ hook_name = os.path.basename(args[0])
2400+ if hook_name in self._hooks:
2401+ self._hooks[hook_name]()
2402+ else:
2403+ raise UnregisteredHookError(hook_name)
2404+
2405+ def hook(self, *hook_names):
2406+ """Decorator, registering them as hooks"""
2407+ def wrapper(decorated):
2408+ for hook_name in hook_names:
2409+ self.register(hook_name, decorated)
2410+ else:
2411+ self.register(decorated.__name__, decorated)
2412+ if '_' in decorated.__name__:
2413+ self.register(
2414+ decorated.__name__.replace('_', '-'), decorated)
2415+ return decorated
2416+ return wrapper
2417+
2418+
2419+def charm_dir():
2420+ """Return the root directory of the current charm"""
2421+ return os.environ.get('CHARM_DIR')
2422
2423=== added file 'lib/charmhelpers/core/host.py'
2424--- lib/charmhelpers/core/host.py 1970-01-01 00:00:00 +0000
2425+++ lib/charmhelpers/core/host.py 2013-11-21 22:43:22 +0000
2426@@ -0,0 +1,281 @@
2427+"""Tools for working with the host system"""
2428+# Copyright 2012 Canonical Ltd.
2429+#
2430+# Authors:
2431+# Nick Moffitt <nick.moffitt@canonical.com>
2432+# Matthew Wedgwood <matthew.wedgwood@canonical.com>
2433+
2434+import os
2435+import pwd
2436+import grp
2437+import random
2438+import string
2439+import subprocess
2440+import hashlib
2441+
2442+from collections import OrderedDict
2443+
2444+from hookenv import log
2445+
2446+
2447+def service_start(service_name):
2448+ """Start a system service"""
2449+ return service('start', service_name)
2450+
2451+
2452+def service_stop(service_name):
2453+ """Stop a system service"""
2454+ return service('stop', service_name)
2455+
2456+
2457+def service_restart(service_name):
2458+ """Restart a system service"""
2459+ return service('restart', service_name)
2460+
2461+
2462+def service_reload(service_name, restart_on_failure=False):
2463+ """Reload a system service, optionally falling back to restart if reload fails"""
2464+ service_result = service('reload', service_name)
2465+ if not service_result and restart_on_failure:
2466+ service_result = service('restart', service_name)
2467+ return service_result
2468+
2469+
2470+def service(action, service_name):
2471+ """Control a system service"""
2472+ cmd = ['service', service_name, action]
2473+ return subprocess.call(cmd) == 0
2474+
2475+
2476+def service_running(service):
2477+ """Determine whether a system service is running"""
2478+ try:
2479+ output = subprocess.check_output(['service', service, 'status'])
2480+ except subprocess.CalledProcessError:
2481+ return False
2482+ else:
2483+ if ("start/running" in output or "is running" in output):
2484+ return True
2485+ else:
2486+ return False
2487+
2488+
2489+def adduser(username, password=None, shell='/bin/bash', system_user=False):
2490+ """Add a user to the system"""
2491+ try:
2492+ user_info = pwd.getpwnam(username)
2493+ log('user {0} already exists!'.format(username))
2494+ except KeyError:
2495+ log('creating user {0}'.format(username))
2496+ cmd = ['useradd']
2497+ if system_user or password is None:
2498+ cmd.append('--system')
2499+ else:
2500+ cmd.extend([
2501+ '--create-home',
2502+ '--shell', shell,
2503+ '--password', password,
2504+ ])
2505+ cmd.append(username)
2506+ subprocess.check_call(cmd)
2507+ user_info = pwd.getpwnam(username)
2508+ return user_info
2509+
2510+
2511+def add_user_to_group(username, group):
2512+ """Add a user to a group"""
2513+ cmd = [
2514+ 'gpasswd', '-a',
2515+ username,
2516+ group
2517+ ]
2518+ log("Adding user {} to group {}".format(username, group))
2519+ subprocess.check_call(cmd)
2520+
2521+
2522+def rsync(from_path, to_path, flags='-r', options=None):
2523+ """Replicate the contents of a path"""
2524+ options = options or ['--delete', '--executability']
2525+ cmd = ['/usr/bin/rsync', flags]
2526+ cmd.extend(options)
2527+ cmd.append(from_path)
2528+ cmd.append(to_path)
2529+ log(" ".join(cmd))
2530+ return subprocess.check_output(cmd).strip()
2531+
2532+
2533+def symlink(source, destination):
2534+ """Create a symbolic link"""
2535+ log("Symlinking {} as {}".format(source, destination))
2536+ cmd = [
2537+ 'ln',
2538+ '-sf',
2539+ source,
2540+ destination,
2541+ ]
2542+ subprocess.check_call(cmd)
2543+
2544+
2545+def mkdir(path, owner='root', group='root', perms=0555, force=False):
2546+ """Create a directory"""
2547+ log("Making dir {} {}:{} {:o}".format(path, owner, group,
2548+ perms))
2549+ uid = pwd.getpwnam(owner).pw_uid
2550+ gid = grp.getgrnam(group).gr_gid
2551+ realpath = os.path.abspath(path)
2552+ if os.path.exists(realpath):
2553+ if force and not os.path.isdir(realpath):
2554+ log("Removing non-directory file {} prior to mkdir()".format(path))
2555+ os.unlink(realpath)
2556+ else:
2557+ os.makedirs(realpath, perms)
2558+ os.chown(realpath, uid, gid)
2559+
2560+
2561+def write_file(path, content, owner='root', group='root', perms=0444):
2562+ """Create or overwrite a file with the contents of a string"""
2563+ log("Writing file {} {}:{} {:o}".format(path, owner, group, perms))
2564+ uid = pwd.getpwnam(owner).pw_uid
2565+ gid = grp.getgrnam(group).gr_gid
2566+ with open(path, 'w') as target:
2567+ os.fchown(target.fileno(), uid, gid)
2568+ os.fchmod(target.fileno(), perms)
2569+ target.write(content)
2570+
2571+
2572+def mount(device, mountpoint, options=None, persist=False):
2573+ """Mount a filesystem at a particular mountpoint"""
2574+ cmd_args = ['mount']
2575+ if options is not None:
2576+ cmd_args.extend(['-o', options])
2577+ cmd_args.extend([device, mountpoint])
2578+ try:
2579+ subprocess.check_output(cmd_args)
2580+ except subprocess.CalledProcessError, e:
2581+ log('Error mounting {} at {}\n{}'.format(device, mountpoint, e.output))
2582+ return False
2583+ if persist:
2584+ # TODO: update fstab
2585+ pass
2586+ return True
2587+
2588+
2589+def umount(mountpoint, persist=False):
2590+ """Unmount a filesystem"""
2591+ cmd_args = ['umount', mountpoint]
2592+ try:
2593+ subprocess.check_output(cmd_args)
2594+ except subprocess.CalledProcessError, e:
2595+ log('Error unmounting {}\n{}'.format(mountpoint, e.output))
2596+ return False
2597+ if persist:
2598+ # TODO: update fstab
2599+ pass
2600+ return True
2601+
2602+
2603+def mounts():
2604+ """Get a list of all mounted volumes as [[mountpoint,device],[...]]"""
2605+ with open('/proc/mounts') as f:
2606+ # [['/mount/point','/dev/path'],[...]]
2607+ system_mounts = [m[1::-1] for m in [l.strip().split()
2608+ for l in f.readlines()]]
2609+ return system_mounts
2610+
2611+
2612+def file_hash(path):
2613+ """Generate a md5 hash of the contents of 'path' or None if not found """
2614+ if os.path.exists(path):
2615+ h = hashlib.md5()
2616+ with open(path, 'r') as source:
2617+ h.update(source.read()) # IGNORE:E1101 - it does have update
2618+ return h.hexdigest()
2619+ else:
2620+ return None
2621+
2622+
2623+def restart_on_change(restart_map):
2624+ """Restart services based on configuration files changing
2625+
2626+ This function is used a decorator, for example
2627+
2628+ @restart_on_change({
2629+ '/etc/ceph/ceph.conf': [ 'cinder-api', 'cinder-volume' ]
2630+ })
2631+ def ceph_client_changed():
2632+ ...
2633+
2634+ In this example, the cinder-api and cinder-volume services
2635+ would be restarted if /etc/ceph/ceph.conf is changed by the
2636+ ceph_client_changed function.
2637+ """
2638+ def wrap(f):
2639+ def wrapped_f(*args):
2640+ checksums = {}
2641+ for path in restart_map:
2642+ checksums[path] = file_hash(path)
2643+ f(*args)
2644+ restarts = []
2645+ for path in restart_map:
2646+ if checksums[path] != file_hash(path):
2647+ restarts += restart_map[path]
2648+ for service_name in list(OrderedDict.fromkeys(restarts)):
2649+ service('restart', service_name)
2650+ return wrapped_f
2651+ return wrap
2652+
2653+
2654+def lsb_release():
2655+ """Return /etc/lsb-release in a dict"""
2656+ d = {}
2657+ with open('/etc/lsb-release', 'r') as lsb:
2658+ for l in lsb:
2659+ k, v = l.split('=')
2660+ d[k.strip()] = v.strip()
2661+ return d
2662+
2663+
2664+def pwgen(length=None):
2665+ """Generate a random pasword."""
2666+ if length is None:
2667+ length = random.choice(range(35, 45))
2668+ alphanumeric_chars = [
2669+ l for l in (string.letters + string.digits)
2670+ if l not in 'l0QD1vAEIOUaeiou']
2671+ random_chars = [
2672+ random.choice(alphanumeric_chars) for _ in range(length)]
2673+ return(''.join(random_chars))
2674+
2675+
2676+def list_nics(nic_type):
2677+ '''Return a list of nics of given type(s)'''
2678+ if isinstance(nic_type, basestring):
2679+ int_types = [nic_type]
2680+ else:
2681+ int_types = nic_type
2682+ interfaces = []
2683+ for int_type in int_types:
2684+ cmd = ['ip', 'addr', 'show', 'label', int_type + '*']
2685+ ip_output = subprocess.check_output(cmd).split('\n')
2686+ ip_output = (line for line in ip_output if line)
2687+ for line in ip_output:
2688+ if line.split()[1].startswith(int_type):
2689+ interfaces.append(line.split()[1].replace(":", ""))
2690+ return interfaces
2691+
2692+
2693+def set_nic_mtu(nic, mtu):
2694+ '''Set MTU on a network interface'''
2695+ cmd = ['ip', 'link', 'set', nic, 'mtu', mtu]
2696+ subprocess.check_call(cmd)
2697+
2698+
2699+def get_nic_mtu(nic):
2700+ cmd = ['ip', 'addr', 'show', nic]
2701+ ip_output = subprocess.check_output(cmd).split('\n')
2702+ mtu = ""
2703+ for line in ip_output:
2704+ words = line.split()
2705+ if 'mtu' in words:
2706+ mtu = words[words.index("mtu") + 1]
2707+ return mtu
2708
2709=== added directory 'lib/charmhelpers/fetch'
2710=== added file 'lib/charmhelpers/fetch/__init__.py'
2711--- lib/charmhelpers/fetch/__init__.py 1970-01-01 00:00:00 +0000
2712+++ lib/charmhelpers/fetch/__init__.py 2013-11-21 22:43:22 +0000
2713@@ -0,0 +1,271 @@
2714+import importlib
2715+from yaml import safe_load
2716+from charmhelpers.core.host import (
2717+ lsb_release
2718+)
2719+from urlparse import (
2720+ urlparse,
2721+ urlunparse,
2722+)
2723+import subprocess
2724+from charmhelpers.core.hookenv import (
2725+ config,
2726+ log,
2727+)
2728+import apt_pkg
2729+import os
2730+
2731+CLOUD_ARCHIVE = """# Ubuntu Cloud Archive
2732+deb http://ubuntu-cloud.archive.canonical.com/ubuntu {} main
2733+"""
2734+PROPOSED_POCKET = """# Proposed
2735+deb http://archive.ubuntu.com/ubuntu {}-proposed main universe multiverse restricted
2736+"""
2737+CLOUD_ARCHIVE_POCKETS = {
2738+ # Folsom
2739+ 'folsom': 'precise-updates/folsom',
2740+ 'precise-folsom': 'precise-updates/folsom',
2741+ 'precise-folsom/updates': 'precise-updates/folsom',
2742+ 'precise-updates/folsom': 'precise-updates/folsom',
2743+ 'folsom/proposed': 'precise-proposed/folsom',
2744+ 'precise-folsom/proposed': 'precise-proposed/folsom',
2745+ 'precise-proposed/folsom': 'precise-proposed/folsom',
2746+ # Grizzly
2747+ 'grizzly': 'precise-updates/grizzly',
2748+ 'precise-grizzly': 'precise-updates/grizzly',
2749+ 'precise-grizzly/updates': 'precise-updates/grizzly',
2750+ 'precise-updates/grizzly': 'precise-updates/grizzly',
2751+ 'grizzly/proposed': 'precise-proposed/grizzly',
2752+ 'precise-grizzly/proposed': 'precise-proposed/grizzly',
2753+ 'precise-proposed/grizzly': 'precise-proposed/grizzly',
2754+ # Havana
2755+ 'havana': 'precise-updates/havana',
2756+ 'precise-havana': 'precise-updates/havana',
2757+ 'precise-havana/updates': 'precise-updates/havana',
2758+ 'precise-updates/havana': 'precise-updates/havana',
2759+ 'havana/proposed': 'precise-proposed/havana',
2760+ 'precies-havana/proposed': 'precise-proposed/havana',
2761+ 'precise-proposed/havana': 'precise-proposed/havana',
2762+}
2763+
2764+
2765+def filter_installed_packages(packages):
2766+ """Returns a list of packages that require installation"""
2767+ apt_pkg.init()
2768+ cache = apt_pkg.Cache()
2769+ _pkgs = []
2770+ for package in packages:
2771+ try:
2772+ p = cache[package]
2773+ p.current_ver or _pkgs.append(package)
2774+ except KeyError:
2775+ log('Package {} has no installation candidate.'.format(package),
2776+ level='WARNING')
2777+ _pkgs.append(package)
2778+ return _pkgs
2779+
2780+
2781+def apt_install(packages, options=None, fatal=False):
2782+ """Install one or more packages"""
2783+ if options is None:
2784+ options = ['--option=Dpkg::Options::=--force-confold']
2785+
2786+ cmd = ['apt-get', '--assume-yes']
2787+ cmd.extend(options)
2788+ cmd.append('install')
2789+ if isinstance(packages, basestring):
2790+ cmd.append(packages)
2791+ else:
2792+ cmd.extend(packages)
2793+ log("Installing {} with options: {}".format(packages,
2794+ options))
2795+ env = os.environ.copy()
2796+ if 'DEBIAN_FRONTEND' not in env:
2797+ env['DEBIAN_FRONTEND'] = 'noninteractive'
2798+
2799+ if fatal:
2800+ subprocess.check_call(cmd, env=env)
2801+ else:
2802+ subprocess.call(cmd, env=env)
2803+
2804+
2805+def apt_update(fatal=False):
2806+ """Update local apt cache"""
2807+ cmd = ['apt-get', 'update']
2808+ if fatal:
2809+ subprocess.check_call(cmd)
2810+ else:
2811+ subprocess.call(cmd)
2812+
2813+
2814+def apt_purge(packages, fatal=False):
2815+ """Purge one or more packages"""
2816+ cmd = ['apt-get', '--assume-yes', 'purge']
2817+ if isinstance(packages, basestring):
2818+ cmd.append(packages)
2819+ else:
2820+ cmd.extend(packages)
2821+ log("Purging {}".format(packages))
2822+ if fatal:
2823+ subprocess.check_call(cmd)
2824+ else:
2825+ subprocess.call(cmd)
2826+
2827+
2828+def apt_hold(packages, fatal=False):
2829+ """Hold one or more packages"""
2830+ cmd = ['apt-mark', 'hold']
2831+ if isinstance(packages, basestring):
2832+ cmd.append(packages)
2833+ else:
2834+ cmd.extend(packages)
2835+ log("Holding {}".format(packages))
2836+ if fatal:
2837+ subprocess.check_call(cmd)
2838+ else:
2839+ subprocess.call(cmd)
2840+
2841+
2842+def add_source(source, key=None):
2843+ if (source.startswith('ppa:') or
2844+ source.startswith('http:') or
2845+ source.startswith('deb ') or
2846+ source.startswith('cloud-archive:')):
2847+ subprocess.check_call(['add-apt-repository', '--yes', source])
2848+ elif source.startswith('cloud:'):
2849+ apt_install(filter_installed_packages(['ubuntu-cloud-keyring']),
2850+ fatal=True)
2851+ pocket = source.split(':')[-1]
2852+ if pocket not in CLOUD_ARCHIVE_POCKETS:
2853+ raise SourceConfigError(
2854+ 'Unsupported cloud: source option %s' %
2855+ pocket)
2856+ actual_pocket = CLOUD_ARCHIVE_POCKETS[pocket]
2857+ with open('/etc/apt/sources.list.d/cloud-archive.list', 'w') as apt:
2858+ apt.write(CLOUD_ARCHIVE.format(actual_pocket))
2859+ elif source == 'proposed':
2860+ release = lsb_release()['DISTRIB_CODENAME']
2861+ with open('/etc/apt/sources.list.d/proposed.list', 'w') as apt:
2862+ apt.write(PROPOSED_POCKET.format(release))
2863+ if key:
2864+ subprocess.check_call(['apt-key', 'import', key])
2865+
2866+
2867+class SourceConfigError(Exception):
2868+ pass
2869+
2870+
2871+def configure_sources(update=False,
2872+ sources_var='install_sources',
2873+ keys_var='install_keys'):
2874+ """
2875+ Configure multiple sources from charm configuration
2876+
2877+ Example config:
2878+ install_sources:
2879+ - "ppa:foo"
2880+ - "http://example.com/repo precise main"
2881+ install_keys:
2882+ - null
2883+ - "a1b2c3d4"
2884+
2885+ Note that 'null' (a.k.a. None) should not be quoted.
2886+ """
2887+ sources = safe_load(config(sources_var))
2888+ keys = config(keys_var)
2889+ if keys is not None:
2890+ keys = safe_load(keys)
2891+ if isinstance(sources, basestring) and (
2892+ keys is None or isinstance(keys, basestring)):
2893+ add_source(sources, keys)
2894+ else:
2895+ if not len(sources) == len(keys):
2896+ msg = 'Install sources and keys lists are different lengths'
2897+ raise SourceConfigError(msg)
2898+ for src_num in range(len(sources)):
2899+ add_source(sources[src_num], keys[src_num])
2900+ if update:
2901+ apt_update(fatal=True)
2902+
2903+# The order of this list is very important. Handlers should be listed in from
2904+# least- to most-specific URL matching.
2905+FETCH_HANDLERS = (
2906+ 'charmhelpers.fetch.archiveurl.ArchiveUrlFetchHandler',
2907+ 'charmhelpers.fetch.bzrurl.BzrUrlFetchHandler',
2908+)
2909+
2910+
2911+class UnhandledSource(Exception):
2912+ pass
2913+
2914+
2915+def install_remote(source):
2916+ """
2917+ Install a file tree from a remote source
2918+
2919+ The specified source should be a url of the form:
2920+ scheme://[host]/path[#[option=value][&...]]
2921+
2922+ Schemes supported are based on this modules submodules
2923+ Options supported are submodule-specific"""
2924+ # We ONLY check for True here because can_handle may return a string
2925+ # explaining why it can't handle a given source.
2926+ handlers = [h for h in plugins() if h.can_handle(source) is True]
2927+ installed_to = None
2928+ for handler in handlers:
2929+ try:
2930+ installed_to = handler.install(source)
2931+ except UnhandledSource:
2932+ pass
2933+ if not installed_to:
2934+ raise UnhandledSource("No handler found for source {}".format(source))
2935+ return installed_to
2936+
2937+
2938+def install_from_config(config_var_name):
2939+ charm_config = config()
2940+ source = charm_config[config_var_name]
2941+ return install_remote(source)
2942+
2943+
2944+class BaseFetchHandler(object):
2945+
2946+ """Base class for FetchHandler implementations in fetch plugins"""
2947+
2948+ def can_handle(self, source):
2949+ """Returns True if the source can be handled. Otherwise returns
2950+ a string explaining why it cannot"""
2951+ return "Wrong source type"
2952+
2953+ def install(self, source):
2954+ """Try to download and unpack the source. Return the path to the
2955+ unpacked files or raise UnhandledSource."""
2956+ raise UnhandledSource("Wrong source type {}".format(source))
2957+
2958+ def parse_url(self, url):
2959+ return urlparse(url)
2960+
2961+ def base_url(self, url):
2962+ """Return url without querystring or fragment"""
2963+ parts = list(self.parse_url(url))
2964+ parts[4:] = ['' for i in parts[4:]]
2965+ return urlunparse(parts)
2966+
2967+
2968+def plugins(fetch_handlers=None):
2969+ if not fetch_handlers:
2970+ fetch_handlers = FETCH_HANDLERS
2971+ plugin_list = []
2972+ for handler_name in fetch_handlers:
2973+ package, classname = handler_name.rsplit('.', 1)
2974+ try:
2975+ handler_class = getattr(
2976+ importlib.import_module(package),
2977+ classname)
2978+ plugin_list.append(handler_class())
2979+ except (ImportError, AttributeError):
2980+ # Skip missing plugins so that they can be ommitted from
2981+ # installation if desired
2982+ log("FetchHandler {} not found, skipping plugin".format(
2983+ handler_name))
2984+ return plugin_list
2985
2986=== added file 'lib/charmhelpers/fetch/archiveurl.py'
2987--- lib/charmhelpers/fetch/archiveurl.py 1970-01-01 00:00:00 +0000
2988+++ lib/charmhelpers/fetch/archiveurl.py 2013-11-21 22:43:22 +0000
2989@@ -0,0 +1,48 @@
2990+import os
2991+import urllib2
2992+from charmhelpers.fetch import (
2993+ BaseFetchHandler,
2994+ UnhandledSource
2995+)
2996+from charmhelpers.payload.archive import (
2997+ get_archive_handler,
2998+ extract,
2999+)
3000+from charmhelpers.core.host import mkdir
3001+
3002+
3003+class ArchiveUrlFetchHandler(BaseFetchHandler):
3004+ """Handler for archives via generic URLs"""
3005+ def can_handle(self, source):
3006+ url_parts = self.parse_url(source)
3007+ if url_parts.scheme not in ('http', 'https', 'ftp', 'file'):
3008+ return "Wrong source type"
3009+ if get_archive_handler(self.base_url(source)):
3010+ return True
3011+ return False
3012+
3013+ def download(self, source, dest):
3014+ # propogate all exceptions
3015+ # URLError, OSError, etc
3016+ response = urllib2.urlopen(source)
3017+ try:
3018+ with open(dest, 'w') as dest_file:
3019+ dest_file.write(response.read())
3020+ except Exception as e:
3021+ if os.path.isfile(dest):
3022+ os.unlink(dest)
3023+ raise e
3024+
3025+ def install(self, source):
3026+ url_parts = self.parse_url(source)
3027+ dest_dir = os.path.join(os.environ.get('CHARM_DIR'), 'fetched')
3028+ if not os.path.exists(dest_dir):
3029+ mkdir(dest_dir, perms=0755)
3030+ dld_file = os.path.join(dest_dir, os.path.basename(url_parts.path))
3031+ try:
3032+ self.download(source, dld_file)
3033+ except urllib2.URLError as e:
3034+ raise UnhandledSource(e.reason)
3035+ except OSError as e:
3036+ raise UnhandledSource(e.strerror)
3037+ return extract(dld_file)
3038
3039=== added file 'lib/charmhelpers/fetch/bzrurl.py'
3040--- lib/charmhelpers/fetch/bzrurl.py 1970-01-01 00:00:00 +0000
3041+++ lib/charmhelpers/fetch/bzrurl.py 2013-11-21 22:43:22 +0000
3042@@ -0,0 +1,49 @@
3043+import os
3044+from charmhelpers.fetch import (
3045+ BaseFetchHandler,
3046+ UnhandledSource
3047+)
3048+from charmhelpers.core.host import mkdir
3049+
3050+try:
3051+ from bzrlib.branch import Branch
3052+except ImportError:
3053+ from charmhelpers.fetch import apt_install
3054+ apt_install("python-bzrlib")
3055+ from bzrlib.branch import Branch
3056+
3057+
3058+class BzrUrlFetchHandler(BaseFetchHandler):
3059+ """Handler for bazaar branches via generic and lp URLs"""
3060+ def can_handle(self, source):
3061+ url_parts = self.parse_url(source)
3062+ if url_parts.scheme not in ('bzr+ssh', 'lp'):
3063+ return False
3064+ else:
3065+ return True
3066+
3067+ def branch(self, source, dest):
3068+ url_parts = self.parse_url(source)
3069+ # If we use lp:branchname scheme we need to load plugins
3070+ if not self.can_handle(source):
3071+ raise UnhandledSource("Cannot handle {}".format(source))
3072+ if url_parts.scheme == "lp":
3073+ from bzrlib.plugin import load_plugins
3074+ load_plugins()
3075+ try:
3076+ remote_branch = Branch.open(source)
3077+ remote_branch.bzrdir.sprout(dest).open_branch()
3078+ except Exception as e:
3079+ raise e
3080+
3081+ def install(self, source):
3082+ url_parts = self.parse_url(source)
3083+ branch_name = url_parts.path.strip("/").split("/")[-1]
3084+ dest_dir = os.path.join(os.environ.get('CHARM_DIR'), "fetched", branch_name)
3085+ if not os.path.exists(dest_dir):
3086+ mkdir(dest_dir, perms=0755)
3087+ try:
3088+ self.branch(source, dest_dir)
3089+ except OSError as e:
3090+ raise UnhandledSource(e.strerror)
3091+ return dest_dir
3092
3093=== added directory 'lib/charmhelpers/payload'
3094=== added file 'lib/charmhelpers/payload/__init__.py'
3095--- lib/charmhelpers/payload/__init__.py 1970-01-01 00:00:00 +0000
3096+++ lib/charmhelpers/payload/__init__.py 2013-11-21 22:43:22 +0000
3097@@ -0,0 +1,1 @@
3098+"Tools for working with files injected into a charm just before deployment."
3099
3100=== added file 'lib/charmhelpers/payload/archive.py'
3101--- lib/charmhelpers/payload/archive.py 1970-01-01 00:00:00 +0000
3102+++ lib/charmhelpers/payload/archive.py 2013-11-21 22:43:22 +0000
3103@@ -0,0 +1,57 @@
3104+import os
3105+import tarfile
3106+import zipfile
3107+from charmhelpers.core import (
3108+ host,
3109+ hookenv,
3110+)
3111+
3112+
3113+class ArchiveError(Exception):
3114+ pass
3115+
3116+
3117+def get_archive_handler(archive_name):
3118+ if os.path.isfile(archive_name):
3119+ if tarfile.is_tarfile(archive_name):
3120+ return extract_tarfile
3121+ elif zipfile.is_zipfile(archive_name):
3122+ return extract_zipfile
3123+ else:
3124+ # look at the file name
3125+ for ext in ('.tar', '.tar.gz', '.tgz', 'tar.bz2', '.tbz2', '.tbz'):
3126+ if archive_name.endswith(ext):
3127+ return extract_tarfile
3128+ for ext in ('.zip', '.jar'):
3129+ if archive_name.endswith(ext):
3130+ return extract_zipfile
3131+
3132+
3133+def archive_dest_default(archive_name):
3134+ archive_file = os.path.basename(archive_name)
3135+ return os.path.join(hookenv.charm_dir(), "archives", archive_file)
3136+
3137+
3138+def extract(archive_name, destpath=None):
3139+ handler = get_archive_handler(archive_name)
3140+ if handler:
3141+ if not destpath:
3142+ destpath = archive_dest_default(archive_name)
3143+ if not os.path.isdir(destpath):
3144+ host.mkdir(destpath)
3145+ handler(archive_name, destpath)
3146+ return destpath
3147+ else:
3148+ raise ArchiveError("No handler for archive")
3149+
3150+
3151+def extract_tarfile(archive_name, destpath):
3152+ "Unpack a tar archive, optionally compressed"
3153+ archive = tarfile.open(archive_name)
3154+ archive.extractall(destpath)
3155+
3156+
3157+def extract_zipfile(archive_name, destpath):
3158+ "Unpack a zip file"
3159+ archive = zipfile.ZipFile(archive_name)
3160+ archive.extractall(destpath)
3161
3162=== added file 'lib/charmhelpers/payload/execd.py'
3163--- lib/charmhelpers/payload/execd.py 1970-01-01 00:00:00 +0000
3164+++ lib/charmhelpers/payload/execd.py 2013-11-21 22:43:22 +0000
3165@@ -0,0 +1,50 @@
3166+#!/usr/bin/env python
3167+
3168+import os
3169+import sys
3170+import subprocess
3171+from charmhelpers.core import hookenv
3172+
3173+
3174+def default_execd_dir():
3175+ return os.path.join(os.environ['CHARM_DIR'], 'exec.d')
3176+
3177+
3178+def execd_module_paths(execd_dir=None):
3179+ """Generate a list of full paths to modules within execd_dir."""
3180+ if not execd_dir:
3181+ execd_dir = default_execd_dir()
3182+
3183+ if not os.path.exists(execd_dir):
3184+ return
3185+
3186+ for subpath in os.listdir(execd_dir):
3187+ module = os.path.join(execd_dir, subpath)
3188+ if os.path.isdir(module):
3189+ yield module
3190+
3191+
3192+def execd_submodule_paths(command, execd_dir=None):
3193+ """Generate a list of full paths to the specified command within exec_dir.
3194+ """
3195+ for module_path in execd_module_paths(execd_dir):
3196+ path = os.path.join(module_path, command)
3197+ if os.access(path, os.X_OK) and os.path.isfile(path):
3198+ yield path
3199+
3200+
3201+def execd_run(command, execd_dir=None, die_on_error=False, stderr=None):
3202+ """Run command for each module within execd_dir which defines it."""
3203+ for submodule_path in execd_submodule_paths(command, execd_dir):
3204+ try:
3205+ subprocess.check_call(submodule_path, shell=True, stderr=stderr)
3206+ except subprocess.CalledProcessError as e:
3207+ hookenv.log("Error ({}) running {}. Output: {}".format(
3208+ e.returncode, e.cmd, e.output))
3209+ if die_on_error:
3210+ sys.exit(e.returncode)
3211+
3212+
3213+def execd_preinstall(execd_dir=None):
3214+ """Run charm-pre-install for each module within execd_dir."""
3215+ execd_run('charm-pre-install', execd_dir=execd_dir)
3216
3217=== modified file 'metadata.yaml'
3218--- metadata.yaml 2013-05-20 13:19:13 +0000
3219+++ metadata.yaml 2013-11-21 22:43:22 +0000
3220@@ -9,10 +9,10 @@
3221 provides:
3222 amqp:
3223 interface: rabbitmq
3224-requires:
3225 nrpe-external-master:
3226 interface: nrpe-external-master
3227 scope: container
3228+requires:
3229 ha:
3230 interface: hacluster
3231 scope: container
3232
3233=== modified file 'revision'
3234--- revision 2013-08-08 21:22:39 +0000
3235+++ revision 2013-11-21 22:43:22 +0000
3236@@ -1,1 +1,1 @@
3237-97
3238+98
3239
3240=== added file 'scripts/check_rabbitmq.py'
3241--- scripts/check_rabbitmq.py 1970-01-01 00:00:00 +0000
3242+++ scripts/check_rabbitmq.py 2013-11-21 22:43:22 +0000
3243@@ -0,0 +1,237 @@
3244+#!/usr/bin/python
3245+#
3246+# #
3247+# # # # # # #
3248+# # # # # # #
3249+# # # # # # #
3250+# # # # # # # #
3251+# # # # # # # # #
3252+# ##### #### #### ####
3253+
3254+# This file is managed by juju. Do not make local changes.
3255+
3256+# Copyright (C) 2009, 2012 Canonical
3257+# All Rights Reserved
3258+#
3259+# tests RabbitMQ operation
3260+
3261+""" test rabbitmq functionality """
3262+
3263+import os
3264+import sys
3265+import signal
3266+import socket
3267+
3268+try:
3269+ from amqplib import client_0_8 as amqp
3270+except ImportError:
3271+ print "CRITICAL: amqplib not found"
3272+ sys.exit(2)
3273+
3274+from optparse import OptionParser
3275+
3276+ROUTE_KEY = "test_mq"
3277+
3278+
3279+def alarm_handler(signum, frame):
3280+ print "TIMEOUT waiting for all queued messages to be delivered"
3281+ os._exit(1)
3282+
3283+
3284+def get_connection(host_port, user, password, vhost):
3285+ """ connect to the amqp service """
3286+ if options.verbose:
3287+ print "Connection to %s requested" % host_port
3288+ try:
3289+ ret = amqp.Connection(host=host_port, userid=user,
3290+ password=password, virtual_host=vhost,
3291+ insist=False)
3292+ except (socket.error, TypeError), e:
3293+ print "ERROR: Could not connect to RabbitMQ server %s:%d" % (
3294+ options.host, options.port)
3295+ if options.verbose:
3296+ print e
3297+ raise
3298+ sys.exit(2)
3299+ except:
3300+ print "ERROR: Unknown error connecting to RabbitMQ server %s:%d" % (
3301+ options.host, options.port)
3302+ if options.verbose:
3303+ raise
3304+ sys.exit(3)
3305+ return ret
3306+
3307+
3308+def setup_exchange(conn, exchange_name, exchange_type):
3309+ """ create an exchange """
3310+ # see if we already have the exchange
3311+ must_create = False
3312+ chan = conn.channel()
3313+ try:
3314+ chan.exchange_declare(exchange=exchange_name, type=exchange_type,
3315+ passive=True)
3316+ except (amqp.AMQPConnectionException, amqp.AMQPChannelException), e:
3317+ if e.amqp_reply_code == 404:
3318+ must_create = True
3319+ # amqplib kills the channel on error.... we dispose of it too
3320+ chan.close()
3321+ chan = conn.channel()
3322+ else:
3323+ raise
3324+ # now create the exchange if needed
3325+ if must_create:
3326+ chan.exchange_declare(exchange=exchange_name, type=exchange_type,
3327+ durable=False, auto_delete=False,)
3328+ if options.verbose:
3329+ print "Created new exchange %s (%s)" % (
3330+ exchange_name, exchange_type)
3331+ else:
3332+ if options.verbose:
3333+ print "Exchange %s (%s) is already declared" % (
3334+ exchange_name, exchange_type)
3335+ chan.close()
3336+ return must_create
3337+
3338+
3339+class Consumer(object):
3340+ """ message consumer class """
3341+ _quit = False
3342+
3343+ def __init__(self, conn, exname):
3344+ self.exname = exname
3345+ self.connection = conn
3346+ self.name = "%s_queue" % exname
3347+
3348+ def setup(self):
3349+ """ sets up the queue and links it to the exchange """
3350+ if options.verbose:
3351+ print self.name, "setup"
3352+ chan = self.connection.channel()
3353+ # setup the queue
3354+ chan.queue_declare(queue=self.name, durable=False,
3355+ exclusive=False, auto_delete=False)
3356+ chan.queue_bind(queue=self.name, exchange=self.exname,
3357+ routing_key=ROUTE_KEY)
3358+ chan.queue_purge(self.name)
3359+ chan.close()
3360+
3361+ def check_end(self, msg):
3362+ """ checks if this is an end request """
3363+ return msg.body.startswith("QUIT")
3364+
3365+ def loop(self, timeout=5):
3366+ """ main loop for the consumer client """
3367+ consumer_tag = "callback_%s" % self.name
3368+ chan = self.connection.channel()
3369+
3370+ def callback(msg):
3371+ """ callback for message received """
3372+ if options.verbose:
3373+ print "Client %s saw this message: '%s'" % (self.name, msg.body)
3374+ if self.check_end(msg): # we have been asked to quit
3375+ self._quit = True
3376+ chan.basic_consume(queue=self.name, no_ack=True, callback=callback,
3377+ consumer_tag=consumer_tag)
3378+ signal.signal(signal.SIGALRM, alarm_handler)
3379+ signal.alarm(timeout)
3380+ while True:
3381+ chan.wait()
3382+ if self._quit:
3383+ break
3384+ # cancel alarm for receive wait
3385+ signal.alarm(0)
3386+ chan.basic_cancel(consumer_tag)
3387+ chan.close()
3388+ return self._quit
3389+
3390+
3391+def send_message(chan, exname, counter=None, message=None):
3392+ """ publish a message on the exchange """
3393+ if not message:
3394+ message = "This is test message %d" % counter
3395+ msg = amqp.Message(message)
3396+ chan.basic_publish(msg, exchange=exname, routing_key=ROUTE_KEY)
3397+ if options.verbose:
3398+ print "Sent message: %s" % message
3399+
3400+
3401+def main_loop(conn, exname):
3402+ """ demo code to send/receive a few messages """
3403+ # first, set up a few consumers
3404+ # setup the queue that would collect the messages
3405+ consumer = Consumer(conn, exname)
3406+ consumer.setup()
3407+ # open up our own connection and start sending messages
3408+ chan = conn.channel()
3409+ # loop a few messages
3410+ for i in range(options.messages):
3411+ send_message(chan, exname, i)
3412+ # signal end of test
3413+ send_message(chan, exname, message="QUIT")
3414+ chan.close()
3415+
3416+ # loop around for a while waiting for messages to be picked up
3417+ return consumer.loop(timeout=options.timeout)
3418+
3419+
3420+def main(host, port, exname, extype, user, password, vhost):
3421+ """ setup the connection and the communication channel """
3422+ sys.stdout = os.fdopen(os.dup(1), "w", 0)
3423+ host_port = "%s:%s" % (host, port)
3424+ conn = get_connection(host_port, user, password, vhost)
3425+ chan = conn.channel()
3426+ if setup_exchange(conn, exname, extype):
3427+ if options.verbose:
3428+ print "Created %s exchange of type %s" % (exname, extype)
3429+ else:
3430+ if options.verbose:
3431+ print "Reusing existing exchange %s of type %s" % (exname, extype)
3432+ ret = main_loop(conn, exname)
3433+ chan.close()
3434+ conn.close()
3435+ return ret
3436+
3437+if __name__ == '__main__':
3438+ parser = OptionParser()
3439+ parser.add_option("--host", dest="host",
3440+ help="RabbitMQ host [default=%default]",
3441+ metavar="HOST", default="localhost")
3442+ parser.add_option("--port", dest="port", type="int",
3443+ help="port RabbitMQ is running on [default=%default]",
3444+ metavar="PORT", default=5672)
3445+ parser.add_option("--exchange", dest="exchange",
3446+ help="Exchange name to use [default=%default]",
3447+ default="test_exchange", metavar="EXCHANGE")
3448+ parser.add_option("--type", dest="type",
3449+ help="EXCHANGE type [default=%default]",
3450+ metavar="TYPE", default="fanout")
3451+ parser.add_option("-v", "--verbose", default=False, action="store_true",
3452+ help="verbose run")
3453+ parser.add_option("-m", "--messages", dest="messages", type="int",
3454+ help="send NUM messages for testing [default=%default]",
3455+ metavar="NUM", default=10)
3456+ parser.add_option("-t", "--timeout", dest="timeout", type="int",
3457+ help="wait TIMEOUT sec for loop test [default=%default]",
3458+ metavar="TIMEOUT", default=5)
3459+ parser.add_option("-u", "--user", dest="user", default="guest",
3460+ help="RabbitMQ user [default=%default]",
3461+ metavar="USER")
3462+ parser.add_option("-p", "--password", dest="password", default="guest",
3463+ help="RabbitMQ password [default=%default]",
3464+ metavar="PASSWORD")
3465+ parser.add_option("--vhost", dest="vhost", default="/",
3466+ help="RabbitMQ vhost [default=%default]",
3467+ metavar="VHOST")
3468+
3469+ (options, args) = parser.parse_args()
3470+ if options.verbose:
3471+ print """
3472+Using AMQP setup: host:port=%s:%d exchange_name=%s exchange_type=%s
3473+""" % (options.host, options.port, options.exchange, options.type)
3474+ ret = main(options.host, options.port, options.exchange, options.type,
3475+ options.user, options.password, options.vhost)
3476+ if ret:
3477+ print "Ok: sent and received %d test messages" % options.messages
3478+ sys.exit(0)
3479+ print "ERROR: Could not send/receive test messages"
3480+ sys.exit(3)

Subscribers

People subscribed via source and target branches