Merge lp:~opnfv-team/charms/trusty/percona-cluster/power8 into lp:charms/trusty/percona-cluster

Proposed by Narinder Gupta
Status: Needs review
Proposed branch: lp:~opnfv-team/charms/trusty/percona-cluster/power8
Merge into: lp:charms/trusty/percona-cluster
Diff against target: 14145 lines (+13604/-0) (has conflicts)
89 files modified
.coveragerc (+6/-0)
.gitignore (+10/-0)
.gitreview (+4/-0)
.testr.conf (+8/-0)
Makefile (+29/-0)
README.md (+71/-0)
actions.yaml (+20/-0)
actions/actions.py (+99/-0)
charm-helpers-hooks.yaml (+12/-0)
charm-helpers-tests.yaml (+6/-0)
charmhelpers/__init__.py (+38/-0)
charmhelpers/cli/__init__.py (+191/-0)
charmhelpers/cli/benchmark.py (+36/-0)
charmhelpers/cli/commands.py (+32/-0)
charmhelpers/cli/hookenv.py (+23/-0)
charmhelpers/cli/host.py (+31/-0)
charmhelpers/cli/unitdata.py (+39/-0)
charmhelpers/contrib/__init__.py (+15/-0)
charmhelpers/contrib/charmsupport/__init__.py (+15/-0)
charmhelpers/contrib/charmsupport/nrpe.py (+398/-0)
charmhelpers/contrib/charmsupport/volumes.py (+175/-0)
charmhelpers/contrib/database/mysql.py (+415/-0)
charmhelpers/contrib/hahelpers/__init__.py (+15/-0)
charmhelpers/contrib/hahelpers/cluster.py (+316/-0)
charmhelpers/contrib/network/__init__.py (+15/-0)
charmhelpers/contrib/network/ip.py (+458/-0)
charmhelpers/contrib/peerstorage/__init__.py (+269/-0)
charmhelpers/core/__init__.py (+15/-0)
charmhelpers/core/decorators.py (+57/-0)
charmhelpers/core/files.py (+45/-0)
charmhelpers/core/fstab.py (+134/-0)
charmhelpers/core/hookenv.py (+978/-0)
charmhelpers/core/host.py (+659/-0)
charmhelpers/core/hugepage.py (+71/-0)
charmhelpers/core/kernel.py (+68/-0)
charmhelpers/core/services/__init__.py (+18/-0)
charmhelpers/core/services/base.py (+353/-0)
charmhelpers/core/services/helpers.py (+292/-0)
charmhelpers/core/strutils.py (+72/-0)
charmhelpers/core/sysctl.py (+56/-0)
charmhelpers/core/templating.py (+81/-0)
charmhelpers/core/unitdata.py (+521/-0)
charmhelpers/fetch/__init__.py (+464/-0)
charmhelpers/fetch/archiveurl.py (+167/-0)
charmhelpers/fetch/bzrurl.py (+68/-0)
charmhelpers/fetch/giturl.py (+68/-0)
charmhelpers/payload/__init__.py (+17/-0)
charmhelpers/payload/execd.py (+66/-0)
config.yaml (+154/-0)
copyright (+29/-0)
hooks/install (+20/-0)
hooks/percona_hooks.py (+644/-0)
hooks/percona_utils.py (+419/-0)
keys/repo.percona.com (+30/-0)
metadata.yaml (+26/-0)
ocf/percona/mysql_monitor (+636/-0)
requirements.txt (+12/-0)
revision (+1/-0)
setup.cfg (+6/-0)
templates/my.cnf (+90/-0)
templates/mysqld.cnf (+116/-0)
test-requirements.txt (+8/-0)
tests/10-deploy_test.py (+29/-0)
tests/20-broken-mysqld.py (+38/-0)
tests/30-kill-9-mysqld.py (+38/-0)
tests/31-test-pause-and-resume.py (+38/-0)
tests/40-test-bootstrap-single.py (+17/-0)
tests/41-test-bootstrap-multi-notmin.py (+41/-0)
tests/42-test-bootstrap-multi-min.py (+43/-0)
tests/README (+113/-0)
tests/basic_deployment.py (+193/-0)
tests/charmhelpers/__init__.py (+38/-0)
tests/charmhelpers/contrib/__init__.py (+15/-0)
tests/charmhelpers/contrib/amulet/__init__.py (+15/-0)
tests/charmhelpers/contrib/amulet/deployment.py (+95/-0)
tests/charmhelpers/contrib/amulet/utils.py (+818/-0)
tests/charmhelpers/contrib/openstack/__init__.py (+15/-0)
tests/charmhelpers/contrib/openstack/amulet/__init__.py (+15/-0)
tests/charmhelpers/contrib/openstack/amulet/deployment.py (+301/-0)
tests/charmhelpers/contrib/openstack/amulet/utils.py (+985/-0)
tests/charmhelpers/core/__init__.py (+15/-0)
tests/charmhelpers/core/hookenv.py (+978/-0)
tests/setup/00-setup (+18/-0)
tests/tests.yaml (+21/-0)
tox.ini (+30/-0)
unit_tests/__init__.py (+3/-0)
unit_tests/test_percona_hooks.py (+131/-0)
unit_tests/test_percona_utils.py (+233/-0)
unit_tests/test_utils.py (+121/-0)
Conflict adding file .coveragerc.  Moved existing file to .coveragerc.moved.
Conflict adding file .gitignore.  Moved existing file to .gitignore.moved.
Conflict adding file .gitreview.  Moved existing file to .gitreview.moved.
Conflict adding file Makefile.  Moved existing file to Makefile.moved.
Conflict adding file README.md.  Moved existing file to README.md.moved.
Conflict adding file actions.  Moved existing file to actions.moved.
Conflict adding file actions.yaml.  Moved existing file to actions.yaml.moved.
Conflict adding file charm-helpers-hooks.yaml.  Moved existing file to charm-helpers-hooks.yaml.moved.
Conflict adding file charm-helpers-tests.yaml.  Moved existing file to charm-helpers-tests.yaml.moved.
Conflict adding file charmhelpers.  Moved existing file to charmhelpers.moved.
Conflict adding file config.yaml.  Moved existing file to config.yaml.moved.
Conflict adding file copyright.  Moved existing file to copyright.moved.
Conflict adding file hooks.  Moved existing file to hooks.moved.
Conflict adding file keys.  Moved existing file to keys.moved.
Conflict adding file metadata.yaml.  Moved existing file to metadata.yaml.moved.
Conflict adding file ocf.  Moved existing file to ocf.moved.
Conflict adding file revision.  Moved existing file to revision.moved.
Conflict adding file setup.cfg.  Moved existing file to setup.cfg.moved.
Conflict adding file templates.  Moved existing file to templates.moved.
Conflict adding file tests.  Moved existing file to tests.moved.
Conflict adding file unit_tests.  Moved existing file to unit_tests.moved.
To merge this branch: bzr merge lp:~opnfv-team/charms/trusty/percona-cluster/power8
Reviewer Review Type Date Requested Status
David Ames Pending
OpenStack Charmers Pending
Review via email: mp+288722@code.launchpad.net

Description of the change

this merge proposal is from IBM where percona-cluster charm was failing to install on power8 system. It seems there was no root-password was set during install and even config option did not help. But after doing this modification we are able to install this on Power8 system.

To post a comment you must log in.

Unmerged revisions

90. By Narinder Gupta

modified to include the power8 changes.

89. By Jill Rouleau

Add backup action

Add a new action to backup the pxc database on a unit.

Backups by default are stored in /opt/backups/mysql and can
optionally be compressed and done as incremental backups.

Change-Id: I5c6ab9fd8be7cb6cdb2a26e849ec0b22d8d4f9a6

88. By James Page

Add tox support for check/gate

This charm was missed pre-migration to git/gerrit.

Add support for executing pep8 and unit tests using tox.

Change-Id: I5518e870c572ccc292d6fe4e9b7c910c7f3f0260

87. By uosci-testing-bot

Adapt imports and metadata for github move

86. By James Page

Add gitreview prior to migration to openstack

85. By Liam Young

[thedac, r=gnuoy] Charm helpers sync. Make the service_running status check cover percona-cluster's unique status message.

84. By David Ames

[jamespage, r=thedac] Fix Bug:1481362 Make datadir dynamic depending on ubuntu version

83. By Liam Young

Update test combo definitions, remove Vivid deprecated release tests, update bundletester testplan yaml, update tests README.

82. By Liam Young

[hopem, r=gnuoy]
Improve shared-db rel logging and tidy get_db_host
Closes-Bug: 1512293

81. By James Page

Update maintainer

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file '.coveragerc'
2--- .coveragerc 1970-01-01 00:00:00 +0000
3+++ .coveragerc 2016-03-10 22:55:31 +0000
4@@ -0,0 +1,6 @@
5+[report]
6+# Regexes for lines to exclude from consideration
7+exclude_lines =
8+ if __name__ == .__main__.:
9+include=
10+ hooks/percona*
11
12=== renamed file '.coveragerc' => '.coveragerc.moved'
13=== added file '.gitignore'
14--- .gitignore 1970-01-01 00:00:00 +0000
15+++ .gitignore 2016-03-10 22:55:31 +0000
16@@ -0,0 +1,10 @@
17+bin
18+.coverage
19+.pydevproject
20+.project
21+*.pyc
22+*.pyo
23+__pycache__
24+*.sw[nop]
25+.testrepository
26+.tox
27
28=== renamed file '.gitignore' => '.gitignore.moved'
29=== added file '.gitreview'
30--- .gitreview 1970-01-01 00:00:00 +0000
31+++ .gitreview 2016-03-10 22:55:31 +0000
32@@ -0,0 +1,4 @@
33+[gerrit]
34+host=review.openstack.org
35+port=29418
36+project=openstack/charm-percona-cluster.git
37
38=== renamed file '.gitreview' => '.gitreview.moved'
39=== added file '.testr.conf'
40--- .testr.conf 1970-01-01 00:00:00 +0000
41+++ .testr.conf 2016-03-10 22:55:31 +0000
42@@ -0,0 +1,8 @@
43+[DEFAULT]
44+test_command=OS_STDOUT_CAPTURE=${OS_STDOUT_CAPTURE:-1} \
45+ OS_STDERR_CAPTURE=${OS_STDERR_CAPTURE:-1} \
46+ OS_TEST_TIMEOUT=${OS_TEST_TIMEOUT:-60} \
47+ ${PYTHON:-python} -m subunit.run discover -t ./ ./unit_tests $LISTOPT $IDOPTION
48+
49+test_id_option=--load-list $IDFILE
50+test_list_option=--list
51
52=== added file 'Makefile'
53--- Makefile 1970-01-01 00:00:00 +0000
54+++ Makefile 2016-03-10 22:55:31 +0000
55@@ -0,0 +1,29 @@
56+#!/usr/bin/make
57+PYTHON := /usr/bin/env python
58+export PYTHONPATH := hooks
59+
60+lint:
61+ @flake8 --exclude hooks/charmhelpers,tests/charmhelpers \
62+ actions hooks unit_tests tests
63+ @charm proof
64+
65+test:
66+ @# Bundletester expects unit tests here.
67+ @$(PYTHON) /usr/bin/nosetests -v --nologcapture --with-coverage unit_tests
68+
69+functional_test:
70+ @echo Starting amulet tests...
71+ @tests/setup/00-setup
72+ @juju test -v -p AMULET_HTTP_PROXY,AMULET_OS_VIP --timeout 2700
73+
74+bin/charm_helpers_sync.py:
75+ @mkdir -p bin
76+ @bzr cat lp:charm-helpers/tools/charm_helpers_sync/charm_helpers_sync.py \
77+ > bin/charm_helpers_sync.py
78+
79+sync: bin/charm_helpers_sync.py
80+ @$(PYTHON) bin/charm_helpers_sync.py -c charm-helpers-hooks.yaml
81+ @$(PYTHON) bin/charm_helpers_sync.py -c charm-helpers-tests.yaml
82+
83+publish: lint test
84+ bzr push lp:charms/trusty/percona-cluster
85
86=== renamed file 'Makefile' => 'Makefile.moved'
87=== added file 'README.md'
88--- README.md 1970-01-01 00:00:00 +0000
89+++ README.md 2016-03-10 22:55:31 +0000
90@@ -0,0 +1,71 @@
91+Overview
92+========
93+
94+Percona XtraDB Cluster is a high availability and high scalability solution for
95+MySQL clustering. Percona XtraDB Cluster integrates Percona Server with the
96+Galera library of MySQL high availability solutions in a single product package
97+which enables you to create a cost-effective MySQL cluster.
98+
99+This charm deploys Percona XtraDB Cluster onto Ubuntu.
100+
101+Usage
102+=====
103+
104+WARNING: Its critical that you follow the bootstrap process detailed in this
105+document in order to end up with a running Active/Active Percona Cluster.
106+
107+Proxy Configuration
108+-------------------
109+
110+If you are deploying this charm on MAAS or in an environment without direct
111+access to the internet, you will need to allow access to repo.percona.com
112+as the charm installs packages direct from the Percona respositories. If you
113+are using squid-deb-proxy, follow the steps below:
114+
115+ echo "repo.percona.com" | sudo tee /etc/squid-deb-proxy/mirror-dstdomain.acl.d/40-percona
116+ sudo service squid-deb-proxy restart
117+
118+Deployment
119+----------
120+
121+The first service unit deployed acts as the seed node for the rest of the
122+cluster; in order for the cluster to function correctly, the same MySQL passwords
123+must be used across all nodes:
124+
125+ cat > percona.yaml << EOF
126+ percona-cluster:
127+ root-password: my-root-password
128+ sst-password: my-sst-password
129+ EOF
130+
131+Once you have created this file, you can deploy the first seed unit:
132+
133+ juju deploy --config percona.yaml percona-cluster
134+
135+Once this node is full operational, you can add extra units one at a time to the
136+deployment:
137+
138+ juju add-unit percona-cluster
139+
140+A minimium cluster size of three units is recommended.
141+
142+In order to access the cluster, use the hacluster charm to provide a single IP
143+address:
144+
145+ juju set percona-cluster vip=10.0.3.200
146+ juju deploy hacluster
147+ juju add-relation hacluster percona-cluster
148+
149+Clients can then access using the vip provided. This vip will be passed to
150+related services:
151+
152+ juju add-relation keystone percona-cluster
153+
154+
155+Limitiations
156+============
157+
158+Note that Percona XtraDB Cluster is not a 'scale-out' MySQL solution; reads
159+and writes are channelled through a single service unit and synchronously
160+replicated to other nodes in the cluster; reads/writes are as slow as the
161+slowest node you have in your deployment.
162
163=== renamed file 'README.md' => 'README.md.moved'
164=== added directory 'actions'
165=== renamed directory 'actions' => 'actions.moved'
166=== added file 'actions.yaml'
167--- actions.yaml 1970-01-01 00:00:00 +0000
168+++ actions.yaml 2016-03-10 22:55:31 +0000
169@@ -0,0 +1,20 @@
170+pause:
171+ description: Pause the MySQL service.
172+resume:
173+ description: Resume the MySQL service.
174+backup:
175+ description: Full database backup
176+ params:
177+ basedir:
178+ type: string
179+ default: "/opt/backups/mysql"
180+ description: The base directory for backups
181+ compress:
182+ type: boolean
183+ default: false
184+ description: Whether or not to compress the backup
185+ incremental:
186+ type: boolean
187+ default: false
188+ description: Make an incremental database backup
189+
190
191=== renamed file 'actions.yaml' => 'actions.yaml.moved'
192=== added file 'actions/actions.py'
193--- actions/actions.py 1970-01-01 00:00:00 +0000
194+++ actions/actions.py 2016-03-10 22:55:31 +0000
195@@ -0,0 +1,99 @@
196+#!/usr/bin/python
197+
198+import os
199+import sys
200+import subprocess
201+import traceback
202+from time import gmtime, strftime
203+
204+from charmhelpers.core.host import service_pause, service_resume
205+from charmhelpers.core.hookenv import (
206+ action_get,
207+ action_set,
208+ action_fail,
209+ status_set,
210+ config,
211+)
212+
213+from percona_utils import assess_status
214+
215+MYSQL_SERVICE = "mysql"
216+
217+
218+def pause(args):
219+ """Pause the MySQL service.
220+
221+ @raises Exception should the service fail to stop.
222+ """
223+ if not service_pause(MYSQL_SERVICE):
224+ raise Exception("Failed to pause MySQL service.")
225+ status_set(
226+ "maintenance",
227+ "Unit paused - use 'resume' action to resume normal service")
228+
229+
230+def resume(args):
231+ """Resume the MySQL service.
232+
233+ @raises Exception should the service fail to start."""
234+ if not service_resume(MYSQL_SERVICE):
235+ raise Exception("Failed to resume MySQL service.")
236+ assess_status()
237+
238+
239+def backup():
240+ basedir = (action_get("basedir")).lower()
241+ compress = (action_get("compress"))
242+ incremental = (action_get("incremental"))
243+ sstpw = config("sst-password")
244+ optionlist = []
245+
246+ # innobackupex will not create recursive dirs that do not already exist,
247+ # so help it along
248+ if not os.path.exists(basedir):
249+ os.makedirs(basedir)
250+
251+ # Build a list of options to pass to innobackupex
252+ if compress is "true":
253+ optionlist.append("--compress")
254+
255+ if incremental is "true":
256+ optionlist.append("--incremental")
257+
258+ try:
259+ subprocess.check_call(
260+ ['innobackupex', '--compact', '--galera-info', '--rsync',
261+ basedir, '--user=sstuser', '--password=' + sstpw] + optionlist)
262+ action_set({
263+ 'time-completed': (strftime("%Y-%m-%d %H:%M:%S", gmtime())),
264+ 'outcome': 'Success'}
265+ )
266+ except subprocess.CalledProcessError as e:
267+ action_set({
268+ 'time-completed': (strftime("%Y-%m-%d %H:%M:%S", gmtime())),
269+ 'output': e.output,
270+ 'return-code': e.returncode,
271+ 'traceback': traceback.format_exc()})
272+ action_fail("innobackupex failed, you should log on to the unit"
273+ "and check the status of the database")
274+
275+# A dictionary of all the defined actions to callables (which take
276+# parsed arguments).
277+ACTIONS = {"pause": pause, "resume": resume, "backup": backup}
278+
279+
280+def main(args):
281+ action_name = os.path.basename(args[0])
282+ try:
283+ action = ACTIONS[action_name]
284+ except KeyError:
285+ return "Action %s undefined" % action_name
286+ else:
287+ try:
288+ action(args)
289+ except Exception as e:
290+ action_fail(str(e))
291+
292+
293+if __name__ == "__main__":
294+ sys.exit(main(sys.argv))
295
296=== added symlink 'actions/charmhelpers'
297=== target is u'../charmhelpers'
298=== added symlink 'actions/pause'
299=== target is u'actions.py'
300=== added symlink 'actions/percona_utils.py'
301=== target is u'../hooks/percona_utils.py'
302=== added symlink 'actions/resume'
303=== target is u'actions.py'
304=== added file 'charm-helpers-hooks.yaml'
305--- charm-helpers-hooks.yaml 1970-01-01 00:00:00 +0000
306+++ charm-helpers-hooks.yaml 2016-03-10 22:55:31 +0000
307@@ -0,0 +1,12 @@
308+branch: lp:charm-helpers
309+destination: hooks/charmhelpers
310+include:
311+ - core
312+ - cli
313+ - fetch
314+ - contrib.hahelpers.cluster
315+ - contrib.peerstorage
316+ - payload.execd
317+ - contrib.network.ip
318+ - contrib.database
319+ - contrib.charmsupport
320
321=== renamed file 'charm-helpers-hooks.yaml' => 'charm-helpers-hooks.yaml.moved'
322=== added file 'charm-helpers-tests.yaml'
323--- charm-helpers-tests.yaml 1970-01-01 00:00:00 +0000
324+++ charm-helpers-tests.yaml 2016-03-10 22:55:31 +0000
325@@ -0,0 +1,6 @@
326+branch: lp:charm-helpers
327+destination: tests/charmhelpers
328+include:
329+ - contrib.amulet
330+ - contrib.openstack.amulet
331+ - core.hookenv
332
333=== renamed file 'charm-helpers-tests.yaml' => 'charm-helpers-tests.yaml.moved'
334=== added directory 'charmhelpers'
335=== renamed directory 'charmhelpers' => 'charmhelpers.moved'
336=== added file 'charmhelpers/__init__.py'
337--- charmhelpers/__init__.py 1970-01-01 00:00:00 +0000
338+++ charmhelpers/__init__.py 2016-03-10 22:55:31 +0000
339@@ -0,0 +1,38 @@
340+# Copyright 2014-2015 Canonical Limited.
341+#
342+# This file is part of charm-helpers.
343+#
344+# charm-helpers is free software: you can redistribute it and/or modify
345+# it under the terms of the GNU Lesser General Public License version 3 as
346+# published by the Free Software Foundation.
347+#
348+# charm-helpers is distributed in the hope that it will be useful,
349+# but WITHOUT ANY WARRANTY; without even the implied warranty of
350+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
351+# GNU Lesser General Public License for more details.
352+#
353+# You should have received a copy of the GNU Lesser General Public License
354+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
355+
356+# Bootstrap charm-helpers, installing its dependencies if necessary using
357+# only standard libraries.
358+import subprocess
359+import sys
360+
361+try:
362+ import six # flake8: noqa
363+except ImportError:
364+ if sys.version_info.major == 2:
365+ subprocess.check_call(['apt-get', 'install', '-y', 'python-six'])
366+ else:
367+ subprocess.check_call(['apt-get', 'install', '-y', 'python3-six'])
368+ import six # flake8: noqa
369+
370+try:
371+ import yaml # flake8: noqa
372+except ImportError:
373+ if sys.version_info.major == 2:
374+ subprocess.check_call(['apt-get', 'install', '-y', 'python-yaml'])
375+ else:
376+ subprocess.check_call(['apt-get', 'install', '-y', 'python3-yaml'])
377+ import yaml # flake8: noqa
378
379=== added directory 'charmhelpers/cli'
380=== added file 'charmhelpers/cli/__init__.py'
381--- charmhelpers/cli/__init__.py 1970-01-01 00:00:00 +0000
382+++ charmhelpers/cli/__init__.py 2016-03-10 22:55:31 +0000
383@@ -0,0 +1,191 @@
384+# Copyright 2014-2015 Canonical Limited.
385+#
386+# This file is part of charm-helpers.
387+#
388+# charm-helpers is free software: you can redistribute it and/or modify
389+# it under the terms of the GNU Lesser General Public License version 3 as
390+# published by the Free Software Foundation.
391+#
392+# charm-helpers is distributed in the hope that it will be useful,
393+# but WITHOUT ANY WARRANTY; without even the implied warranty of
394+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
395+# GNU Lesser General Public License for more details.
396+#
397+# You should have received a copy of the GNU Lesser General Public License
398+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
399+
400+import inspect
401+import argparse
402+import sys
403+
404+from six.moves import zip
405+
406+import charmhelpers.core.unitdata
407+
408+
409+class OutputFormatter(object):
410+ def __init__(self, outfile=sys.stdout):
411+ self.formats = (
412+ "raw",
413+ "json",
414+ "py",
415+ "yaml",
416+ "csv",
417+ "tab",
418+ )
419+ self.outfile = outfile
420+
421+ def add_arguments(self, argument_parser):
422+ formatgroup = argument_parser.add_mutually_exclusive_group()
423+ choices = self.supported_formats
424+ formatgroup.add_argument("--format", metavar='FMT',
425+ help="Select output format for returned data, "
426+ "where FMT is one of: {}".format(choices),
427+ choices=choices, default='raw')
428+ for fmt in self.formats:
429+ fmtfunc = getattr(self, fmt)
430+ formatgroup.add_argument("-{}".format(fmt[0]),
431+ "--{}".format(fmt), action='store_const',
432+ const=fmt, dest='format',
433+ help=fmtfunc.__doc__)
434+
435+ @property
436+ def supported_formats(self):
437+ return self.formats
438+
439+ def raw(self, output):
440+ """Output data as raw string (default)"""
441+ if isinstance(output, (list, tuple)):
442+ output = '\n'.join(map(str, output))
443+ self.outfile.write(str(output))
444+
445+ def py(self, output):
446+ """Output data as a nicely-formatted python data structure"""
447+ import pprint
448+ pprint.pprint(output, stream=self.outfile)
449+
450+ def json(self, output):
451+ """Output data in JSON format"""
452+ import json
453+ json.dump(output, self.outfile)
454+
455+ def yaml(self, output):
456+ """Output data in YAML format"""
457+ import yaml
458+ yaml.safe_dump(output, self.outfile)
459+
460+ def csv(self, output):
461+ """Output data as excel-compatible CSV"""
462+ import csv
463+ csvwriter = csv.writer(self.outfile)
464+ csvwriter.writerows(output)
465+
466+ def tab(self, output):
467+ """Output data in excel-compatible tab-delimited format"""
468+ import csv
469+ csvwriter = csv.writer(self.outfile, dialect=csv.excel_tab)
470+ csvwriter.writerows(output)
471+
472+ def format_output(self, output, fmt='raw'):
473+ fmtfunc = getattr(self, fmt)
474+ fmtfunc(output)
475+
476+
477+class CommandLine(object):
478+ argument_parser = None
479+ subparsers = None
480+ formatter = None
481+ exit_code = 0
482+
483+ def __init__(self):
484+ if not self.argument_parser:
485+ self.argument_parser = argparse.ArgumentParser(description='Perform common charm tasks')
486+ if not self.formatter:
487+ self.formatter = OutputFormatter()
488+ self.formatter.add_arguments(self.argument_parser)
489+ if not self.subparsers:
490+ self.subparsers = self.argument_parser.add_subparsers(help='Commands')
491+
492+ def subcommand(self, command_name=None):
493+ """
494+ Decorate a function as a subcommand. Use its arguments as the
495+ command-line arguments"""
496+ def wrapper(decorated):
497+ cmd_name = command_name or decorated.__name__
498+ subparser = self.subparsers.add_parser(cmd_name,
499+ description=decorated.__doc__)
500+ for args, kwargs in describe_arguments(decorated):
501+ subparser.add_argument(*args, **kwargs)
502+ subparser.set_defaults(func=decorated)
503+ return decorated
504+ return wrapper
505+
506+ def test_command(self, decorated):
507+ """
508+ Subcommand is a boolean test function, so bool return values should be
509+ converted to a 0/1 exit code.
510+ """
511+ decorated._cli_test_command = True
512+ return decorated
513+
514+ def no_output(self, decorated):
515+ """
516+ Subcommand is not expected to return a value, so don't print a spurious None.
517+ """
518+ decorated._cli_no_output = True
519+ return decorated
520+
521+ def subcommand_builder(self, command_name, description=None):
522+ """
523+ Decorate a function that builds a subcommand. Builders should accept a
524+ single argument (the subparser instance) and return the function to be
525+ run as the command."""
526+ def wrapper(decorated):
527+ subparser = self.subparsers.add_parser(command_name)
528+ func = decorated(subparser)
529+ subparser.set_defaults(func=func)
530+ subparser.description = description or func.__doc__
531+ return wrapper
532+
533+ def run(self):
534+ "Run cli, processing arguments and executing subcommands."
535+ arguments = self.argument_parser.parse_args()
536+ argspec = inspect.getargspec(arguments.func)
537+ vargs = []
538+ for arg in argspec.args:
539+ vargs.append(getattr(arguments, arg))
540+ if argspec.varargs:
541+ vargs.extend(getattr(arguments, argspec.varargs))
542+ output = arguments.func(*vargs)
543+ if getattr(arguments.func, '_cli_test_command', False):
544+ self.exit_code = 0 if output else 1
545+ output = ''
546+ if getattr(arguments.func, '_cli_no_output', False):
547+ output = ''
548+ self.formatter.format_output(output, arguments.format)
549+ if charmhelpers.core.unitdata._KV:
550+ charmhelpers.core.unitdata._KV.flush()
551+
552+
553+cmdline = CommandLine()
554+
555+
556+def describe_arguments(func):
557+ """
558+ Analyze a function's signature and return a data structure suitable for
559+ passing in as arguments to an argparse parser's add_argument() method."""
560+
561+ argspec = inspect.getargspec(func)
562+ # we should probably raise an exception somewhere if func includes **kwargs
563+ if argspec.defaults:
564+ positional_args = argspec.args[:-len(argspec.defaults)]
565+ keyword_names = argspec.args[-len(argspec.defaults):]
566+ for arg, default in zip(keyword_names, argspec.defaults):
567+ yield ('--{}'.format(arg),), {'default': default}
568+ else:
569+ positional_args = argspec.args
570+
571+ for arg in positional_args:
572+ yield (arg,), {}
573+ if argspec.varargs:
574+ yield (argspec.varargs,), {'nargs': '*'}
575
576=== added file 'charmhelpers/cli/benchmark.py'
577--- charmhelpers/cli/benchmark.py 1970-01-01 00:00:00 +0000
578+++ charmhelpers/cli/benchmark.py 2016-03-10 22:55:31 +0000
579@@ -0,0 +1,36 @@
580+# Copyright 2014-2015 Canonical Limited.
581+#
582+# This file is part of charm-helpers.
583+#
584+# charm-helpers is free software: you can redistribute it and/or modify
585+# it under the terms of the GNU Lesser General Public License version 3 as
586+# published by the Free Software Foundation.
587+#
588+# charm-helpers is distributed in the hope that it will be useful,
589+# but WITHOUT ANY WARRANTY; without even the implied warranty of
590+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
591+# GNU Lesser General Public License for more details.
592+#
593+# You should have received a copy of the GNU Lesser General Public License
594+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
595+
596+from . import cmdline
597+from charmhelpers.contrib.benchmark import Benchmark
598+
599+
600+@cmdline.subcommand(command_name='benchmark-start')
601+def start():
602+ Benchmark.start()
603+
604+
605+@cmdline.subcommand(command_name='benchmark-finish')
606+def finish():
607+ Benchmark.finish()
608+
609+
610+@cmdline.subcommand_builder('benchmark-composite', description="Set the benchmark composite score")
611+def service(subparser):
612+ subparser.add_argument("value", help="The composite score.")
613+ subparser.add_argument("units", help="The units the composite score represents, i.e., 'reads/sec'.")
614+ subparser.add_argument("direction", help="'asc' if a lower score is better, 'desc' if a higher score is better.")
615+ return Benchmark.set_composite_score
616
617=== added file 'charmhelpers/cli/commands.py'
618--- charmhelpers/cli/commands.py 1970-01-01 00:00:00 +0000
619+++ charmhelpers/cli/commands.py 2016-03-10 22:55:31 +0000
620@@ -0,0 +1,32 @@
621+# Copyright 2014-2015 Canonical Limited.
622+#
623+# This file is part of charm-helpers.
624+#
625+# charm-helpers is free software: you can redistribute it and/or modify
626+# it under the terms of the GNU Lesser General Public License version 3 as
627+# published by the Free Software Foundation.
628+#
629+# charm-helpers is distributed in the hope that it will be useful,
630+# but WITHOUT ANY WARRANTY; without even the implied warranty of
631+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
632+# GNU Lesser General Public License for more details.
633+#
634+# You should have received a copy of the GNU Lesser General Public License
635+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
636+
637+"""
638+This module loads sub-modules into the python runtime so they can be
639+discovered via the inspect module. In order to prevent flake8 from (rightfully)
640+telling us these are unused modules, throw a ' # noqa' at the end of each import
641+so that the warning is suppressed.
642+"""
643+
644+from . import CommandLine # noqa
645+
646+"""
647+Import the sub-modules which have decorated subcommands to register with chlp.
648+"""
649+from . import host # noqa
650+from . import benchmark # noqa
651+from . import unitdata # noqa
652+from . import hookenv # noqa
653
654=== added file 'charmhelpers/cli/hookenv.py'
655--- charmhelpers/cli/hookenv.py 1970-01-01 00:00:00 +0000
656+++ charmhelpers/cli/hookenv.py 2016-03-10 22:55:31 +0000
657@@ -0,0 +1,23 @@
658+# Copyright 2014-2015 Canonical Limited.
659+#
660+# This file is part of charm-helpers.
661+#
662+# charm-helpers is free software: you can redistribute it and/or modify
663+# it under the terms of the GNU Lesser General Public License version 3 as
664+# published by the Free Software Foundation.
665+#
666+# charm-helpers is distributed in the hope that it will be useful,
667+# but WITHOUT ANY WARRANTY; without even the implied warranty of
668+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
669+# GNU Lesser General Public License for more details.
670+#
671+# You should have received a copy of the GNU Lesser General Public License
672+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
673+
674+from . import cmdline
675+from charmhelpers.core import hookenv
676+
677+
678+cmdline.subcommand('relation-id')(hookenv.relation_id._wrapped)
679+cmdline.subcommand('service-name')(hookenv.service_name)
680+cmdline.subcommand('remote-service-name')(hookenv.remote_service_name._wrapped)
681
682=== added file 'charmhelpers/cli/host.py'
683--- charmhelpers/cli/host.py 1970-01-01 00:00:00 +0000
684+++ charmhelpers/cli/host.py 2016-03-10 22:55:31 +0000
685@@ -0,0 +1,31 @@
686+# Copyright 2014-2015 Canonical Limited.
687+#
688+# This file is part of charm-helpers.
689+#
690+# charm-helpers is free software: you can redistribute it and/or modify
691+# it under the terms of the GNU Lesser General Public License version 3 as
692+# published by the Free Software Foundation.
693+#
694+# charm-helpers is distributed in the hope that it will be useful,
695+# but WITHOUT ANY WARRANTY; without even the implied warranty of
696+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
697+# GNU Lesser General Public License for more details.
698+#
699+# You should have received a copy of the GNU Lesser General Public License
700+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
701+
702+from . import cmdline
703+from charmhelpers.core import host
704+
705+
706+@cmdline.subcommand()
707+def mounts():
708+ "List mounts"
709+ return host.mounts()
710+
711+
712+@cmdline.subcommand_builder('service', description="Control system services")
713+def service(subparser):
714+ subparser.add_argument("action", help="The action to perform (start, stop, etc...)")
715+ subparser.add_argument("service_name", help="Name of the service to control")
716+ return host.service
717
718=== added file 'charmhelpers/cli/unitdata.py'
719--- charmhelpers/cli/unitdata.py 1970-01-01 00:00:00 +0000
720+++ charmhelpers/cli/unitdata.py 2016-03-10 22:55:31 +0000
721@@ -0,0 +1,39 @@
722+# Copyright 2014-2015 Canonical Limited.
723+#
724+# This file is part of charm-helpers.
725+#
726+# charm-helpers is free software: you can redistribute it and/or modify
727+# it under the terms of the GNU Lesser General Public License version 3 as
728+# published by the Free Software Foundation.
729+#
730+# charm-helpers is distributed in the hope that it will be useful,
731+# but WITHOUT ANY WARRANTY; without even the implied warranty of
732+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
733+# GNU Lesser General Public License for more details.
734+#
735+# You should have received a copy of the GNU Lesser General Public License
736+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
737+
738+from . import cmdline
739+from charmhelpers.core import unitdata
740+
741+
742+@cmdline.subcommand_builder('unitdata', description="Store and retrieve data")
743+def unitdata_cmd(subparser):
744+ nested = subparser.add_subparsers()
745+ get_cmd = nested.add_parser('get', help='Retrieve data')
746+ get_cmd.add_argument('key', help='Key to retrieve the value of')
747+ get_cmd.set_defaults(action='get', value=None)
748+ set_cmd = nested.add_parser('set', help='Store data')
749+ set_cmd.add_argument('key', help='Key to set')
750+ set_cmd.add_argument('value', help='Value to store')
751+ set_cmd.set_defaults(action='set')
752+
753+ def _unitdata_cmd(action, key, value):
754+ if action == 'get':
755+ return unitdata.kv().get(key)
756+ elif action == 'set':
757+ unitdata.kv().set(key, value)
758+ unitdata.kv().flush()
759+ return ''
760+ return _unitdata_cmd
761
762=== added directory 'charmhelpers/contrib'
763=== added file 'charmhelpers/contrib/__init__.py'
764--- charmhelpers/contrib/__init__.py 1970-01-01 00:00:00 +0000
765+++ charmhelpers/contrib/__init__.py 2016-03-10 22:55:31 +0000
766@@ -0,0 +1,15 @@
767+# Copyright 2014-2015 Canonical Limited.
768+#
769+# This file is part of charm-helpers.
770+#
771+# charm-helpers is free software: you can redistribute it and/or modify
772+# it under the terms of the GNU Lesser General Public License version 3 as
773+# published by the Free Software Foundation.
774+#
775+# charm-helpers is distributed in the hope that it will be useful,
776+# but WITHOUT ANY WARRANTY; without even the implied warranty of
777+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
778+# GNU Lesser General Public License for more details.
779+#
780+# You should have received a copy of the GNU Lesser General Public License
781+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
782
783=== added directory 'charmhelpers/contrib/charmsupport'
784=== added file 'charmhelpers/contrib/charmsupport/__init__.py'
785--- charmhelpers/contrib/charmsupport/__init__.py 1970-01-01 00:00:00 +0000
786+++ charmhelpers/contrib/charmsupport/__init__.py 2016-03-10 22:55:31 +0000
787@@ -0,0 +1,15 @@
788+# Copyright 2014-2015 Canonical Limited.
789+#
790+# This file is part of charm-helpers.
791+#
792+# charm-helpers is free software: you can redistribute it and/or modify
793+# it under the terms of the GNU Lesser General Public License version 3 as
794+# published by the Free Software Foundation.
795+#
796+# charm-helpers is distributed in the hope that it will be useful,
797+# but WITHOUT ANY WARRANTY; without even the implied warranty of
798+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
799+# GNU Lesser General Public License for more details.
800+#
801+# You should have received a copy of the GNU Lesser General Public License
802+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
803
804=== added file 'charmhelpers/contrib/charmsupport/nrpe.py'
805--- charmhelpers/contrib/charmsupport/nrpe.py 1970-01-01 00:00:00 +0000
806+++ charmhelpers/contrib/charmsupport/nrpe.py 2016-03-10 22:55:31 +0000
807@@ -0,0 +1,398 @@
808+# Copyright 2014-2015 Canonical Limited.
809+#
810+# This file is part of charm-helpers.
811+#
812+# charm-helpers is free software: you can redistribute it and/or modify
813+# it under the terms of the GNU Lesser General Public License version 3 as
814+# published by the Free Software Foundation.
815+#
816+# charm-helpers is distributed in the hope that it will be useful,
817+# but WITHOUT ANY WARRANTY; without even the implied warranty of
818+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
819+# GNU Lesser General Public License for more details.
820+#
821+# You should have received a copy of the GNU Lesser General Public License
822+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
823+
824+"""Compatibility with the nrpe-external-master charm"""
825+# Copyright 2012 Canonical Ltd.
826+#
827+# Authors:
828+# Matthew Wedgwood <matthew.wedgwood@canonical.com>
829+
830+import subprocess
831+import pwd
832+import grp
833+import os
834+import glob
835+import shutil
836+import re
837+import shlex
838+import yaml
839+
840+from charmhelpers.core.hookenv import (
841+ config,
842+ local_unit,
843+ log,
844+ relation_ids,
845+ relation_set,
846+ relations_of_type,
847+)
848+
849+from charmhelpers.core.host import service
850+
851+# This module adds compatibility with the nrpe-external-master and plain nrpe
852+# subordinate charms. To use it in your charm:
853+#
854+# 1. Update metadata.yaml
855+#
856+# provides:
857+# (...)
858+# nrpe-external-master:
859+# interface: nrpe-external-master
860+# scope: container
861+#
862+# and/or
863+#
864+# provides:
865+# (...)
866+# local-monitors:
867+# interface: local-monitors
868+# scope: container
869+
870+#
871+# 2. Add the following to config.yaml
872+#
873+# nagios_context:
874+# default: "juju"
875+# type: string
876+# description: |
877+# Used by the nrpe subordinate charms.
878+# A string that will be prepended to instance name to set the host name
879+# in nagios. So for instance the hostname would be something like:
880+# juju-myservice-0
881+# If you're running multiple environments with the same services in them
882+# this allows you to differentiate between them.
883+# nagios_servicegroups:
884+# default: ""
885+# type: string
886+# description: |
887+# A comma-separated list of nagios servicegroups.
888+# If left empty, the nagios_context will be used as the servicegroup
889+#
890+# 3. Add custom checks (Nagios plugins) to files/nrpe-external-master
891+#
892+# 4. Update your hooks.py with something like this:
893+#
894+# from charmsupport.nrpe import NRPE
895+# (...)
896+# def update_nrpe_config():
897+# nrpe_compat = NRPE()
898+# nrpe_compat.add_check(
899+# shortname = "myservice",
900+# description = "Check MyService",
901+# check_cmd = "check_http -w 2 -c 10 http://localhost"
902+# )
903+# nrpe_compat.add_check(
904+# "myservice_other",
905+# "Check for widget failures",
906+# check_cmd = "/srv/myapp/scripts/widget_check"
907+# )
908+# nrpe_compat.write()
909+#
910+# def config_changed():
911+# (...)
912+# update_nrpe_config()
913+#
914+# def nrpe_external_master_relation_changed():
915+# update_nrpe_config()
916+#
917+# def local_monitors_relation_changed():
918+# update_nrpe_config()
919+#
920+# 5. ln -s hooks.py nrpe-external-master-relation-changed
921+# ln -s hooks.py local-monitors-relation-changed
922+
923+
924+class CheckException(Exception):
925+ pass
926+
927+
928+class Check(object):
929+ shortname_re = '[A-Za-z0-9-_]+$'
930+ service_template = ("""
931+#---------------------------------------------------
932+# This file is Juju managed
933+#---------------------------------------------------
934+define service {{
935+ use active-service
936+ host_name {nagios_hostname}
937+ service_description {nagios_hostname}[{shortname}] """
938+ """{description}
939+ check_command check_nrpe!{command}
940+ servicegroups {nagios_servicegroup}
941+}}
942+""")
943+
944+ def __init__(self, shortname, description, check_cmd):
945+ super(Check, self).__init__()
946+ # XXX: could be better to calculate this from the service name
947+ if not re.match(self.shortname_re, shortname):
948+ raise CheckException("shortname must match {}".format(
949+ Check.shortname_re))
950+ self.shortname = shortname
951+ self.command = "check_{}".format(shortname)
952+ # Note: a set of invalid characters is defined by the
953+ # Nagios server config
954+ # The default is: illegal_object_name_chars=`~!$%^&*"|'<>?,()=
955+ self.description = description
956+ self.check_cmd = self._locate_cmd(check_cmd)
957+
958+ def _get_check_filename(self):
959+ return os.path.join(NRPE.nrpe_confdir, '{}.cfg'.format(self.command))
960+
961+ def _get_service_filename(self, hostname):
962+ return os.path.join(NRPE.nagios_exportdir,
963+ 'service__{}_{}.cfg'.format(hostname, self.command))
964+
965+ def _locate_cmd(self, check_cmd):
966+ search_path = (
967+ '/usr/lib/nagios/plugins',
968+ '/usr/local/lib/nagios/plugins',
969+ )
970+ parts = shlex.split(check_cmd)
971+ for path in search_path:
972+ if os.path.exists(os.path.join(path, parts[0])):
973+ command = os.path.join(path, parts[0])
974+ if len(parts) > 1:
975+ command += " " + " ".join(parts[1:])
976+ return command
977+ log('Check command not found: {}'.format(parts[0]))
978+ return ''
979+
980+ def _remove_service_files(self):
981+ if not os.path.exists(NRPE.nagios_exportdir):
982+ return
983+ for f in os.listdir(NRPE.nagios_exportdir):
984+ if f.endswith('_{}.cfg'.format(self.command)):
985+ os.remove(os.path.join(NRPE.nagios_exportdir, f))
986+
987+ def remove(self, hostname):
988+ nrpe_check_file = self._get_check_filename()
989+ if os.path.exists(nrpe_check_file):
990+ os.remove(nrpe_check_file)
991+ self._remove_service_files()
992+
993+ def write(self, nagios_context, hostname, nagios_servicegroups):
994+ nrpe_check_file = self._get_check_filename()
995+ with open(nrpe_check_file, 'w') as nrpe_check_config:
996+ nrpe_check_config.write("# check {}\n".format(self.shortname))
997+ nrpe_check_config.write("command[{}]={}\n".format(
998+ self.command, self.check_cmd))
999+
1000+ if not os.path.exists(NRPE.nagios_exportdir):
1001+ log('Not writing service config as {} is not accessible'.format(
1002+ NRPE.nagios_exportdir))
1003+ else:
1004+ self.write_service_config(nagios_context, hostname,
1005+ nagios_servicegroups)
1006+
1007+ def write_service_config(self, nagios_context, hostname,
1008+ nagios_servicegroups):
1009+ self._remove_service_files()
1010+
1011+ templ_vars = {
1012+ 'nagios_hostname': hostname,
1013+ 'nagios_servicegroup': nagios_servicegroups,
1014+ 'description': self.description,
1015+ 'shortname': self.shortname,
1016+ 'command': self.command,
1017+ }
1018+ nrpe_service_text = Check.service_template.format(**templ_vars)
1019+ nrpe_service_file = self._get_service_filename(hostname)
1020+ with open(nrpe_service_file, 'w') as nrpe_service_config:
1021+ nrpe_service_config.write(str(nrpe_service_text))
1022+
1023+ def run(self):
1024+ subprocess.call(self.check_cmd)
1025+
1026+
1027+class NRPE(object):
1028+ nagios_logdir = '/var/log/nagios'
1029+ nagios_exportdir = '/var/lib/nagios/export'
1030+ nrpe_confdir = '/etc/nagios/nrpe.d'
1031+
1032+ def __init__(self, hostname=None):
1033+ super(NRPE, self).__init__()
1034+ self.config = config()
1035+ self.nagios_context = self.config['nagios_context']
1036+ if 'nagios_servicegroups' in self.config and self.config['nagios_servicegroups']:
1037+ self.nagios_servicegroups = self.config['nagios_servicegroups']
1038+ else:
1039+ self.nagios_servicegroups = self.nagios_context
1040+ self.unit_name = local_unit().replace('/', '-')
1041+ if hostname:
1042+ self.hostname = hostname
1043+ else:
1044+ nagios_hostname = get_nagios_hostname()
1045+ if nagios_hostname:
1046+ self.hostname = nagios_hostname
1047+ else:
1048+ self.hostname = "{}-{}".format(self.nagios_context, self.unit_name)
1049+ self.checks = []
1050+
1051+ def add_check(self, *args, **kwargs):
1052+ self.checks.append(Check(*args, **kwargs))
1053+
1054+ def remove_check(self, *args, **kwargs):
1055+ if kwargs.get('shortname') is None:
1056+ raise ValueError('shortname of check must be specified')
1057+
1058+ # Use sensible defaults if they're not specified - these are not
1059+ # actually used during removal, but they're required for constructing
1060+ # the Check object; check_disk is chosen because it's part of the
1061+ # nagios-plugins-basic package.
1062+ if kwargs.get('check_cmd') is None:
1063+ kwargs['check_cmd'] = 'check_disk'
1064+ if kwargs.get('description') is None:
1065+ kwargs['description'] = ''
1066+
1067+ check = Check(*args, **kwargs)
1068+ check.remove(self.hostname)
1069+
1070+ def write(self):
1071+ try:
1072+ nagios_uid = pwd.getpwnam('nagios').pw_uid
1073+ nagios_gid = grp.getgrnam('nagios').gr_gid
1074+ except:
1075+ log("Nagios user not set up, nrpe checks not updated")
1076+ return
1077+
1078+ if not os.path.exists(NRPE.nagios_logdir):
1079+ os.mkdir(NRPE.nagios_logdir)
1080+ os.chown(NRPE.nagios_logdir, nagios_uid, nagios_gid)
1081+
1082+ nrpe_monitors = {}
1083+ monitors = {"monitors": {"remote": {"nrpe": nrpe_monitors}}}
1084+ for nrpecheck in self.checks:
1085+ nrpecheck.write(self.nagios_context, self.hostname,
1086+ self.nagios_servicegroups)
1087+ nrpe_monitors[nrpecheck.shortname] = {
1088+ "command": nrpecheck.command,
1089+ }
1090+
1091+ service('restart', 'nagios-nrpe-server')
1092+
1093+ monitor_ids = relation_ids("local-monitors") + \
1094+ relation_ids("nrpe-external-master")
1095+ for rid in monitor_ids:
1096+ relation_set(relation_id=rid, monitors=yaml.dump(monitors))
1097+
1098+
1099+def get_nagios_hostcontext(relation_name='nrpe-external-master'):
1100+ """
1101+ Query relation with nrpe subordinate, return the nagios_host_context
1102+
1103+ :param str relation_name: Name of relation nrpe sub joined to
1104+ """
1105+ for rel in relations_of_type(relation_name):
1106+ if 'nagios_host_context' in rel:
1107+ return rel['nagios_host_context']
1108+
1109+
1110+def get_nagios_hostname(relation_name='nrpe-external-master'):
1111+ """
1112+ Query relation with nrpe subordinate, return the nagios_hostname
1113+
1114+ :param str relation_name: Name of relation nrpe sub joined to
1115+ """
1116+ for rel in relations_of_type(relation_name):
1117+ if 'nagios_hostname' in rel:
1118+ return rel['nagios_hostname']
1119+
1120+
1121+def get_nagios_unit_name(relation_name='nrpe-external-master'):
1122+ """
1123+ Return the nagios unit name prepended with host_context if needed
1124+
1125+ :param str relation_name: Name of relation nrpe sub joined to
1126+ """
1127+ host_context = get_nagios_hostcontext(relation_name)
1128+ if host_context:
1129+ unit = "%s:%s" % (host_context, local_unit())
1130+ else:
1131+ unit = local_unit()
1132+ return unit
1133+
1134+
1135+def add_init_service_checks(nrpe, services, unit_name):
1136+ """
1137+ Add checks for each service in list
1138+
1139+ :param NRPE nrpe: NRPE object to add check to
1140+ :param list services: List of services to check
1141+ :param str unit_name: Unit name to use in check description
1142+ """
1143+ for svc in services:
1144+ upstart_init = '/etc/init/%s.conf' % svc
1145+ sysv_init = '/etc/init.d/%s' % svc
1146+ if os.path.exists(upstart_init):
1147+ # Don't add a check for these services from neutron-gateway
1148+ if svc not in ['ext-port', 'os-charm-phy-nic-mtu']:
1149+ nrpe.add_check(
1150+ shortname=svc,
1151+ description='process check {%s}' % unit_name,
1152+ check_cmd='check_upstart_job %s' % svc
1153+ )
1154+ elif os.path.exists(sysv_init):
1155+ cronpath = '/etc/cron.d/nagios-service-check-%s' % svc
1156+ cron_file = ('*/5 * * * * root '
1157+ '/usr/local/lib/nagios/plugins/check_exit_status.pl '
1158+ '-s /etc/init.d/%s status > '
1159+ '/var/lib/nagios/service-check-%s.txt\n' % (svc,
1160+ svc)
1161+ )
1162+ f = open(cronpath, 'w')
1163+ f.write(cron_file)
1164+ f.close()
1165+ nrpe.add_check(
1166+ shortname=svc,
1167+ description='process check {%s}' % unit_name,
1168+ check_cmd='check_status_file.py -f '
1169+ '/var/lib/nagios/service-check-%s.txt' % svc,
1170+ )
1171+
1172+
1173+def copy_nrpe_checks():
1174+ """
1175+ Copy the nrpe checks into place
1176+
1177+ """
1178+ NAGIOS_PLUGINS = '/usr/local/lib/nagios/plugins'
1179+ nrpe_files_dir = os.path.join(os.getenv('CHARM_DIR'), 'hooks',
1180+ 'charmhelpers', 'contrib', 'openstack',
1181+ 'files')
1182+
1183+ if not os.path.exists(NAGIOS_PLUGINS):
1184+ os.makedirs(NAGIOS_PLUGINS)
1185+ for fname in glob.glob(os.path.join(nrpe_files_dir, "check_*")):
1186+ if os.path.isfile(fname):
1187+ shutil.copy2(fname,
1188+ os.path.join(NAGIOS_PLUGINS, os.path.basename(fname)))
1189+
1190+
1191+def add_haproxy_checks(nrpe, unit_name):
1192+ """
1193+ Add checks for each service in list
1194+
1195+ :param NRPE nrpe: NRPE object to add check to
1196+ :param str unit_name: Unit name to use in check description
1197+ """
1198+ nrpe.add_check(
1199+ shortname='haproxy_servers',
1200+ description='Check HAProxy {%s}' % unit_name,
1201+ check_cmd='check_haproxy.sh')
1202+ nrpe.add_check(
1203+ shortname='haproxy_queue',
1204+ description='Check HAProxy queue depth {%s}' % unit_name,
1205+ check_cmd='check_haproxy_queue_depth.sh')
1206
1207=== added file 'charmhelpers/contrib/charmsupport/volumes.py'
1208--- charmhelpers/contrib/charmsupport/volumes.py 1970-01-01 00:00:00 +0000
1209+++ charmhelpers/contrib/charmsupport/volumes.py 2016-03-10 22:55:31 +0000
1210@@ -0,0 +1,175 @@
1211+# Copyright 2014-2015 Canonical Limited.
1212+#
1213+# This file is part of charm-helpers.
1214+#
1215+# charm-helpers is free software: you can redistribute it and/or modify
1216+# it under the terms of the GNU Lesser General Public License version 3 as
1217+# published by the Free Software Foundation.
1218+#
1219+# charm-helpers is distributed in the hope that it will be useful,
1220+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1221+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1222+# GNU Lesser General Public License for more details.
1223+#
1224+# You should have received a copy of the GNU Lesser General Public License
1225+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1226+
1227+'''
1228+Functions for managing volumes in juju units. One volume is supported per unit.
1229+Subordinates may have their own storage, provided it is on its own partition.
1230+
1231+Configuration stanzas::
1232+
1233+ volume-ephemeral:
1234+ type: boolean
1235+ default: true
1236+ description: >
1237+ If false, a volume is mounted as sepecified in "volume-map"
1238+ If true, ephemeral storage will be used, meaning that log data
1239+ will only exist as long as the machine. YOU HAVE BEEN WARNED.
1240+ volume-map:
1241+ type: string
1242+ default: {}
1243+ description: >
1244+ YAML map of units to device names, e.g:
1245+ "{ rsyslog/0: /dev/vdb, rsyslog/1: /dev/vdb }"
1246+ Service units will raise a configure-error if volume-ephemeral
1247+ is 'true' and no volume-map value is set. Use 'juju set' to set a
1248+ value and 'juju resolved' to complete configuration.
1249+
1250+Usage::
1251+
1252+ from charmsupport.volumes import configure_volume, VolumeConfigurationError
1253+ from charmsupport.hookenv import log, ERROR
1254+ def post_mount_hook():
1255+ stop_service('myservice')
1256+ def post_mount_hook():
1257+ start_service('myservice')
1258+
1259+ if __name__ == '__main__':
1260+ try:
1261+ configure_volume(before_change=pre_mount_hook,
1262+ after_change=post_mount_hook)
1263+ except VolumeConfigurationError:
1264+ log('Storage could not be configured', ERROR)
1265+
1266+'''
1267+
1268+# XXX: Known limitations
1269+# - fstab is neither consulted nor updated
1270+
1271+import os
1272+from charmhelpers.core import hookenv
1273+from charmhelpers.core import host
1274+import yaml
1275+
1276+
1277+MOUNT_BASE = '/srv/juju/volumes'
1278+
1279+
1280+class VolumeConfigurationError(Exception):
1281+ '''Volume configuration data is missing or invalid'''
1282+ pass
1283+
1284+
1285+def get_config():
1286+ '''Gather and sanity-check volume configuration data'''
1287+ volume_config = {}
1288+ config = hookenv.config()
1289+
1290+ errors = False
1291+
1292+ if config.get('volume-ephemeral') in (True, 'True', 'true', 'Yes', 'yes'):
1293+ volume_config['ephemeral'] = True
1294+ else:
1295+ volume_config['ephemeral'] = False
1296+
1297+ try:
1298+ volume_map = yaml.safe_load(config.get('volume-map', '{}'))
1299+ except yaml.YAMLError as e:
1300+ hookenv.log("Error parsing YAML volume-map: {}".format(e),
1301+ hookenv.ERROR)
1302+ errors = True
1303+ if volume_map is None:
1304+ # probably an empty string
1305+ volume_map = {}
1306+ elif not isinstance(volume_map, dict):
1307+ hookenv.log("Volume-map should be a dictionary, not {}".format(
1308+ type(volume_map)))
1309+ errors = True
1310+
1311+ volume_config['device'] = volume_map.get(os.environ['JUJU_UNIT_NAME'])
1312+ if volume_config['device'] and volume_config['ephemeral']:
1313+ # asked for ephemeral storage but also defined a volume ID
1314+ hookenv.log('A volume is defined for this unit, but ephemeral '
1315+ 'storage was requested', hookenv.ERROR)
1316+ errors = True
1317+ elif not volume_config['device'] and not volume_config['ephemeral']:
1318+ # asked for permanent storage but did not define volume ID
1319+ hookenv.log('Ephemeral storage was requested, but there is no volume '
1320+ 'defined for this unit.', hookenv.ERROR)
1321+ errors = True
1322+
1323+ unit_mount_name = hookenv.local_unit().replace('/', '-')
1324+ volume_config['mountpoint'] = os.path.join(MOUNT_BASE, unit_mount_name)
1325+
1326+ if errors:
1327+ return None
1328+ return volume_config
1329+
1330+
1331+def mount_volume(config):
1332+ if os.path.exists(config['mountpoint']):
1333+ if not os.path.isdir(config['mountpoint']):
1334+ hookenv.log('Not a directory: {}'.format(config['mountpoint']))
1335+ raise VolumeConfigurationError()
1336+ else:
1337+ host.mkdir(config['mountpoint'])
1338+ if os.path.ismount(config['mountpoint']):
1339+ unmount_volume(config)
1340+ if not host.mount(config['device'], config['mountpoint'], persist=True):
1341+ raise VolumeConfigurationError()
1342+
1343+
1344+def unmount_volume(config):
1345+ if os.path.ismount(config['mountpoint']):
1346+ if not host.umount(config['mountpoint'], persist=True):
1347+ raise VolumeConfigurationError()
1348+
1349+
1350+def managed_mounts():
1351+ '''List of all mounted managed volumes'''
1352+ return filter(lambda mount: mount[0].startswith(MOUNT_BASE), host.mounts())
1353+
1354+
1355+def configure_volume(before_change=lambda: None, after_change=lambda: None):
1356+ '''Set up storage (or don't) according to the charm's volume configuration.
1357+ Returns the mount point or "ephemeral". before_change and after_change
1358+ are optional functions to be called if the volume configuration changes.
1359+ '''
1360+
1361+ config = get_config()
1362+ if not config:
1363+ hookenv.log('Failed to read volume configuration', hookenv.CRITICAL)
1364+ raise VolumeConfigurationError()
1365+
1366+ if config['ephemeral']:
1367+ if os.path.ismount(config['mountpoint']):
1368+ before_change()
1369+ unmount_volume(config)
1370+ after_change()
1371+ return 'ephemeral'
1372+ else:
1373+ # persistent storage
1374+ if os.path.ismount(config['mountpoint']):
1375+ mounts = dict(managed_mounts())
1376+ if mounts.get(config['mountpoint']) != config['device']:
1377+ before_change()
1378+ unmount_volume(config)
1379+ mount_volume(config)
1380+ after_change()
1381+ else:
1382+ before_change()
1383+ mount_volume(config)
1384+ after_change()
1385+ return config['mountpoint']
1386
1387=== added directory 'charmhelpers/contrib/database'
1388=== added file 'charmhelpers/contrib/database/__init__.py'
1389=== added file 'charmhelpers/contrib/database/mysql.py'
1390--- charmhelpers/contrib/database/mysql.py 1970-01-01 00:00:00 +0000
1391+++ charmhelpers/contrib/database/mysql.py 2016-03-10 22:55:31 +0000
1392@@ -0,0 +1,415 @@
1393+"""Helper for working with a MySQL database"""
1394+import json
1395+import re
1396+import sys
1397+import platform
1398+import os
1399+import glob
1400+
1401+# from string import upper
1402+
1403+from charmhelpers.core.host import (
1404+ mkdir,
1405+ pwgen,
1406+ write_file
1407+)
1408+from charmhelpers.core.hookenv import (
1409+ config as config_get,
1410+ relation_get,
1411+ related_units,
1412+ unit_get,
1413+ log,
1414+ DEBUG,
1415+ INFO,
1416+ WARNING,
1417+)
1418+from charmhelpers.fetch import (
1419+ apt_install,
1420+ apt_update,
1421+ filter_installed_packages,
1422+)
1423+from charmhelpers.contrib.peerstorage import (
1424+ peer_store,
1425+ peer_retrieve,
1426+)
1427+from charmhelpers.contrib.network.ip import get_host_ip
1428+
1429+try:
1430+ import MySQLdb
1431+except ImportError:
1432+ apt_update(fatal=True)
1433+ apt_install(filter_installed_packages(['python-mysqldb']), fatal=True)
1434+ import MySQLdb
1435+
1436+
1437+class MySQLHelper(object):
1438+
1439+ def __init__(self, rpasswdf_template, upasswdf_template, host='localhost',
1440+ migrate_passwd_to_peer_relation=True,
1441+ delete_ondisk_passwd_file=True):
1442+ self.host = host
1443+ # Password file path templates
1444+ self.root_passwd_file_template = rpasswdf_template
1445+ self.user_passwd_file_template = upasswdf_template
1446+
1447+ self.migrate_passwd_to_peer_relation = migrate_passwd_to_peer_relation
1448+ # If we migrate we have the option to delete local copy of root passwd
1449+ self.delete_ondisk_passwd_file = delete_ondisk_passwd_file
1450+
1451+ def connect(self, user='root', password=None):
1452+ log("Opening db connection for %s@%s" % (user, self.host), level=DEBUG)
1453+ if password==None:
1454+ self.connection = MySQLdb.connect(user=user, host=self.host)
1455+ else:
1456+ self.connection = MySQLdb.connect(user=user, host=self.host,
1457+ passwd=password)
1458+
1459+ def database_exists(self, db_name):
1460+ cursor = self.connection.cursor()
1461+ try:
1462+ cursor.execute("SHOW DATABASES")
1463+ databases = [i[0] for i in cursor.fetchall()]
1464+ finally:
1465+ cursor.close()
1466+
1467+ return db_name in databases
1468+
1469+ def create_database(self, db_name):
1470+ cursor = self.connection.cursor()
1471+ try:
1472+ cursor.execute("CREATE DATABASE {} CHARACTER SET UTF8"
1473+ .format(db_name))
1474+ finally:
1475+ cursor.close()
1476+
1477+ def grant_exists(self, db_name, db_user, remote_ip):
1478+ cursor = self.connection.cursor()
1479+ priv_string = "GRANT ALL PRIVILEGES ON `{}`.* " \
1480+ "TO '{}'@'{}'".format(db_name, db_user, remote_ip)
1481+ try:
1482+ cursor.execute("SHOW GRANTS for '{}'@'{}'".format(db_user,
1483+ remote_ip))
1484+ grants = [i[0] for i in cursor.fetchall()]
1485+ except MySQLdb.OperationalError:
1486+ return False
1487+ finally:
1488+ cursor.close()
1489+
1490+ # TODO: review for different grants
1491+ return priv_string in grants
1492+
1493+ def create_grant(self, db_name, db_user, remote_ip, password):
1494+ cursor = self.connection.cursor()
1495+ try:
1496+ # TODO: review for different grants
1497+ cursor.execute("GRANT ALL PRIVILEGES ON {}.* TO '{}'@'{}' "
1498+ "IDENTIFIED BY '{}'".format(db_name,
1499+ db_user,
1500+ remote_ip,
1501+ password))
1502+ finally:
1503+ cursor.close()
1504+
1505+ def create_admin_grant(self, db_user, remote_ip, password):
1506+ cursor = self.connection.cursor()
1507+ try:
1508+ cursor.execute("GRANT ALL PRIVILEGES ON *.* TO '{}'@'{}' "
1509+ "IDENTIFIED BY '{}'".format(db_user,
1510+ remote_ip,
1511+ password))
1512+ finally:
1513+ cursor.close()
1514+
1515+ def cleanup_grant(self, db_user, remote_ip):
1516+ cursor = self.connection.cursor()
1517+ try:
1518+ cursor.execute("DROP FROM mysql.user WHERE user='{}' "
1519+ "AND HOST='{}'".format(db_user,
1520+ remote_ip))
1521+ finally:
1522+ cursor.close()
1523+
1524+ def execute(self, sql):
1525+ """Execute arbitary SQL against the database."""
1526+ cursor = self.connection.cursor()
1527+ try:
1528+ cursor.execute(sql)
1529+ finally:
1530+ cursor.close()
1531+
1532+ def migrate_passwords_to_peer_relation(self, excludes=None):
1533+ """Migrate any passwords storage on disk to cluster peer relation."""
1534+ dirname = os.path.dirname(self.root_passwd_file_template)
1535+ path = os.path.join(dirname, '*.passwd')
1536+ for f in glob.glob(path):
1537+ if excludes and f in excludes:
1538+ log("Excluding %s from peer migration" % (f), level=DEBUG)
1539+ continue
1540+
1541+ key = os.path.basename(f)
1542+ with open(f, 'r') as passwd:
1543+ _value = passwd.read().strip()
1544+
1545+ try:
1546+ peer_store(key, _value)
1547+
1548+ if self.delete_ondisk_passwd_file:
1549+ os.unlink(f)
1550+ except ValueError:
1551+ # NOTE cluster relation not yet ready - skip for now
1552+ pass
1553+
1554+ def get_mysql_password_on_disk(self, username=None, password=None):
1555+ """Retrieve, generate or store a mysql password for the provided
1556+ username on disk."""
1557+ if username:
1558+ template = self.user_passwd_file_template
1559+ passwd_file = template.format(username)
1560+ else:
1561+ passwd_file = self.root_passwd_file_template
1562+
1563+ _password = None
1564+ if os.path.exists(passwd_file):
1565+ log("Using existing password file '%s'" % passwd_file, level=DEBUG)
1566+ with open(passwd_file, 'r') as passwd:
1567+ _password = passwd.read().strip()
1568+ else:
1569+ log("Generating new password file '%s'" % passwd_file, level=DEBUG)
1570+ if not os.path.isdir(os.path.dirname(passwd_file)):
1571+ # NOTE: need to ensure this is not mysql root dir (which needs
1572+ # to be mysql readable)
1573+ mkdir(os.path.dirname(passwd_file), owner='root', group='root',
1574+ perms=0o770)
1575+ # Force permissions - for some reason the chmod in makedirs
1576+ # fails
1577+ os.chmod(os.path.dirname(passwd_file), 0o770)
1578+
1579+ _password = password or pwgen(length=32)
1580+ write_file(passwd_file, _password, owner='root', group='root',
1581+ perms=0o660)
1582+
1583+ return _password
1584+
1585+ def passwd_keys(self, username):
1586+ """Generator to return keys used to store passwords in peer store.
1587+
1588+ NOTE: we support both legacy and new format to support mysql
1589+ charm prior to refactor. This is necessary to avoid LP 1451890.
1590+ """
1591+ keys = []
1592+ if username == 'mysql':
1593+ log("Bad username '%s'" % (username), level=WARNING)
1594+
1595+ if username:
1596+ # IMPORTANT: *newer* format must be returned first
1597+ keys.append('mysql-%s.passwd' % (username))
1598+ keys.append('%s.passwd' % (username))
1599+ else:
1600+ keys.append('mysql.passwd')
1601+
1602+ for key in keys:
1603+ yield key
1604+
1605+ def get_mysql_password(self, username=None, password=None):
1606+ """Retrieve, generate or store a mysql password for the provided
1607+ username using peer relation cluster."""
1608+ excludes = []
1609+
1610+ # First check peer relation.
1611+ try:
1612+ for key in self.passwd_keys(username):
1613+ _password = peer_retrieve(key)
1614+ if _password:
1615+ break
1616+
1617+ # If root password available don't update peer relation from local
1618+ if _password and not username:
1619+ excludes.append(self.root_passwd_file_template)
1620+
1621+ except ValueError:
1622+ # cluster relation is not yet started; use on-disk
1623+ _password = None
1624+
1625+ # If none available, generate new one
1626+ if not _password:
1627+ _password = self.get_mysql_password_on_disk(username, password)
1628+
1629+ # Put on wire if required
1630+ if self.migrate_passwd_to_peer_relation:
1631+ self.migrate_passwords_to_peer_relation(excludes=excludes)
1632+
1633+ return _password
1634+
1635+ def get_mysql_root_password(self, password=None):
1636+ """Retrieve or generate mysql root password for service units."""
1637+ return self.get_mysql_password(username=None, password=password)
1638+
1639+ def normalize_address(self, hostname):
1640+ """Ensure that address returned is an IP address (i.e. not fqdn)"""
1641+ if config_get('prefer-ipv6'):
1642+ # TODO: add support for ipv6 dns
1643+ return hostname
1644+
1645+ if hostname != unit_get('private-address'):
1646+ return get_host_ip(hostname, fallback=hostname)
1647+
1648+ # Otherwise assume localhost
1649+ return '127.0.0.1'
1650+
1651+ def get_allowed_units(self, database, username, relation_id=None):
1652+ """Get list of units with access grants for database with username.
1653+
1654+ This is typically used to provide shared-db relations with a list of
1655+ which units have been granted access to the given database.
1656+ """
1657+ self.connect(password=self.get_mysql_root_password())
1658+ allowed_units = set()
1659+ for unit in related_units(relation_id):
1660+ settings = relation_get(rid=relation_id, unit=unit)
1661+ # First check for setting with prefix, then without
1662+ for attr in ["%s_hostname" % (database), 'hostname']:
1663+ hosts = settings.get(attr, None)
1664+ if hosts:
1665+ break
1666+
1667+ if hosts:
1668+ # hostname can be json-encoded list of hostnames
1669+ try:
1670+ hosts = json.loads(hosts)
1671+ except ValueError:
1672+ hosts = [hosts]
1673+ else:
1674+ hosts = [settings['private-address']]
1675+
1676+ if hosts:
1677+ for host in hosts:
1678+ host = self.normalize_address(host)
1679+ if self.grant_exists(database, username, host):
1680+ log("Grant exists for host '%s' on db '%s'" %
1681+ (host, database), level=DEBUG)
1682+ if unit not in allowed_units:
1683+ allowed_units.add(unit)
1684+ else:
1685+ log("Grant does NOT exist for host '%s' on db '%s'" %
1686+ (host, database), level=DEBUG)
1687+ else:
1688+ log("No hosts found for grant check", level=INFO)
1689+
1690+ return allowed_units
1691+
1692+ def configure_db(self, hostname, database, username, admin=False):
1693+ """Configure access to database for username from hostname."""
1694+ self.connect(password=self.get_mysql_root_password())
1695+ if not self.database_exists(database):
1696+ self.create_database(database)
1697+
1698+ remote_ip = self.normalize_address(hostname)
1699+ password = self.get_mysql_password(username)
1700+ if not self.grant_exists(database, username, remote_ip):
1701+ if not admin:
1702+ self.create_grant(database, username, remote_ip, password)
1703+ else:
1704+ self.create_admin_grant(username, remote_ip, password)
1705+
1706+ return password
1707+
1708+
1709+class PerconaClusterHelper(object):
1710+
1711+ # Going for the biggest page size to avoid wasted bytes.
1712+ # InnoDB page size is 16MB
1713+
1714+ DEFAULT_PAGE_SIZE = 16 * 1024 * 1024
1715+ DEFAULT_INNODB_BUFFER_FACTOR = 0.50
1716+
1717+ def human_to_bytes(self, human):
1718+ """Convert human readable configuration options to bytes."""
1719+ num_re = re.compile('^[0-9]+$')
1720+ if num_re.match(human):
1721+ return human
1722+
1723+ factors = {
1724+ 'K': 1024,
1725+ 'M': 1048576,
1726+ 'G': 1073741824,
1727+ 'T': 1099511627776
1728+ }
1729+ modifier = human[-1]
1730+ if modifier in factors:
1731+ return int(human[:-1]) * factors[modifier]
1732+
1733+ if modifier == '%':
1734+ total_ram = self.human_to_bytes(self.get_mem_total())
1735+ if self.is_32bit_system() and total_ram > self.sys_mem_limit():
1736+ total_ram = self.sys_mem_limit()
1737+ factor = int(human[:-1]) * 0.01
1738+ pctram = total_ram * factor
1739+ return int(pctram - (pctram % self.DEFAULT_PAGE_SIZE))
1740+
1741+ raise ValueError("Can only convert K,M,G, or T")
1742+
1743+ def is_32bit_system(self):
1744+ """Determine whether system is 32 or 64 bit."""
1745+ try:
1746+ return sys.maxsize < 2 ** 32
1747+ except OverflowError:
1748+ return False
1749+
1750+ def sys_mem_limit(self):
1751+ """Determine the default memory limit for the current service unit."""
1752+ if platform.machine() in ['armv7l']:
1753+ _mem_limit = self.human_to_bytes('2700M') # experimentally determined
1754+ else:
1755+ # Limit for x86 based 32bit systems
1756+ _mem_limit = self.human_to_bytes('4G')
1757+
1758+ return _mem_limit
1759+
1760+ def get_mem_total(self):
1761+ """Calculate the total memory in the current service unit."""
1762+ with open('/proc/meminfo') as meminfo_file:
1763+ for line in meminfo_file:
1764+ key, mem = line.split(':', 2)
1765+ if key == 'MemTotal':
1766+ mtot, modifier = mem.strip().split(' ')
1767+ return '%s%s' % (mtot, modifier[0].upper())
1768+
1769+ def parse_config(self):
1770+ """Parse charm configuration and calculate values for config files."""
1771+ config = config_get()
1772+ mysql_config = {}
1773+ if 'max-connections' in config:
1774+ mysql_config['max_connections'] = config['max-connections']
1775+
1776+ if 'wait-timeout' in config:
1777+ mysql_config['wait_timeout'] = config['wait-timeout']
1778+
1779+ if 'innodb-flush-log-at-trx-commit' in config:
1780+ mysql_config['innodb_flush_log_at_trx_commit'] = config['innodb-flush-log-at-trx-commit']
1781+
1782+ # Set a sane default key_buffer size
1783+ mysql_config['key_buffer'] = self.human_to_bytes('32M')
1784+ total_memory = self.human_to_bytes(self.get_mem_total())
1785+
1786+ dataset_bytes = config.get('dataset-size', None)
1787+ innodb_buffer_pool_size = config.get('innodb-buffer-pool-size', None)
1788+
1789+ if innodb_buffer_pool_size:
1790+ innodb_buffer_pool_size = self.human_to_bytes(
1791+ innodb_buffer_pool_size)
1792+ elif dataset_bytes:
1793+ log("Option 'dataset-size' has been deprecated, please use"
1794+ "innodb_buffer_pool_size option instead", level="WARN")
1795+ innodb_buffer_pool_size = self.human_to_bytes(
1796+ dataset_bytes)
1797+ else:
1798+ innodb_buffer_pool_size = int(
1799+ total_memory * self.DEFAULT_INNODB_BUFFER_FACTOR)
1800+
1801+ if innodb_buffer_pool_size > total_memory:
1802+ log("innodb_buffer_pool_size; {} is greater than system available memory:{}".format(
1803+ innodb_buffer_pool_size,
1804+ total_memory), level='WARN')
1805+
1806+ mysql_config['innodb_buffer_pool_size'] = innodb_buffer_pool_size
1807+ return mysql_config
1808
1809=== added directory 'charmhelpers/contrib/hahelpers'
1810=== added file 'charmhelpers/contrib/hahelpers/__init__.py'
1811--- charmhelpers/contrib/hahelpers/__init__.py 1970-01-01 00:00:00 +0000
1812+++ charmhelpers/contrib/hahelpers/__init__.py 2016-03-10 22:55:31 +0000
1813@@ -0,0 +1,15 @@
1814+# Copyright 2014-2015 Canonical Limited.
1815+#
1816+# This file is part of charm-helpers.
1817+#
1818+# charm-helpers is free software: you can redistribute it and/or modify
1819+# it under the terms of the GNU Lesser General Public License version 3 as
1820+# published by the Free Software Foundation.
1821+#
1822+# charm-helpers is distributed in the hope that it will be useful,
1823+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1824+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1825+# GNU Lesser General Public License for more details.
1826+#
1827+# You should have received a copy of the GNU Lesser General Public License
1828+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1829
1830=== added file 'charmhelpers/contrib/hahelpers/cluster.py'
1831--- charmhelpers/contrib/hahelpers/cluster.py 1970-01-01 00:00:00 +0000
1832+++ charmhelpers/contrib/hahelpers/cluster.py 2016-03-10 22:55:31 +0000
1833@@ -0,0 +1,316 @@
1834+# Copyright 2014-2015 Canonical Limited.
1835+#
1836+# This file is part of charm-helpers.
1837+#
1838+# charm-helpers is free software: you can redistribute it and/or modify
1839+# it under the terms of the GNU Lesser General Public License version 3 as
1840+# published by the Free Software Foundation.
1841+#
1842+# charm-helpers is distributed in the hope that it will be useful,
1843+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1844+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1845+# GNU Lesser General Public License for more details.
1846+#
1847+# You should have received a copy of the GNU Lesser General Public License
1848+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1849+
1850+#
1851+# Copyright 2012 Canonical Ltd.
1852+#
1853+# Authors:
1854+# James Page <james.page@ubuntu.com>
1855+# Adam Gandelman <adamg@ubuntu.com>
1856+#
1857+
1858+"""
1859+Helpers for clustering and determining "cluster leadership" and other
1860+clustering-related helpers.
1861+"""
1862+
1863+import subprocess
1864+import os
1865+
1866+from socket import gethostname as get_unit_hostname
1867+
1868+import six
1869+
1870+from charmhelpers.core.hookenv import (
1871+ log,
1872+ relation_ids,
1873+ related_units as relation_list,
1874+ relation_get,
1875+ config as config_get,
1876+ INFO,
1877+ ERROR,
1878+ WARNING,
1879+ unit_get,
1880+ is_leader as juju_is_leader
1881+)
1882+from charmhelpers.core.decorators import (
1883+ retry_on_exception,
1884+)
1885+from charmhelpers.core.strutils import (
1886+ bool_from_string,
1887+)
1888+
1889+DC_RESOURCE_NAME = 'DC'
1890+
1891+
1892+class HAIncompleteConfig(Exception):
1893+ pass
1894+
1895+
1896+class CRMResourceNotFound(Exception):
1897+ pass
1898+
1899+
1900+class CRMDCNotFound(Exception):
1901+ pass
1902+
1903+
1904+def is_elected_leader(resource):
1905+ """
1906+ Returns True if the charm executing this is the elected cluster leader.
1907+
1908+ It relies on two mechanisms to determine leadership:
1909+ 1. If juju is sufficiently new and leadership election is supported,
1910+ the is_leader command will be used.
1911+ 2. If the charm is part of a corosync cluster, call corosync to
1912+ determine leadership.
1913+ 3. If the charm is not part of a corosync cluster, the leader is
1914+ determined as being "the alive unit with the lowest unit numer". In
1915+ other words, the oldest surviving unit.
1916+ """
1917+ try:
1918+ return juju_is_leader()
1919+ except NotImplementedError:
1920+ log('Juju leadership election feature not enabled'
1921+ ', using fallback support',
1922+ level=WARNING)
1923+
1924+ if is_clustered():
1925+ if not is_crm_leader(resource):
1926+ log('Deferring action to CRM leader.', level=INFO)
1927+ return False
1928+ else:
1929+ peers = peer_units()
1930+ if peers and not oldest_peer(peers):
1931+ log('Deferring action to oldest service unit.', level=INFO)
1932+ return False
1933+ return True
1934+
1935+
1936+def is_clustered():
1937+ for r_id in (relation_ids('ha') or []):
1938+ for unit in (relation_list(r_id) or []):
1939+ clustered = relation_get('clustered',
1940+ rid=r_id,
1941+ unit=unit)
1942+ if clustered:
1943+ return True
1944+ return False
1945+
1946+
1947+def is_crm_dc():
1948+ """
1949+ Determine leadership by querying the pacemaker Designated Controller
1950+ """
1951+ cmd = ['crm', 'status']
1952+ try:
1953+ status = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
1954+ if not isinstance(status, six.text_type):
1955+ status = six.text_type(status, "utf-8")
1956+ except subprocess.CalledProcessError as ex:
1957+ raise CRMDCNotFound(str(ex))
1958+
1959+ current_dc = ''
1960+ for line in status.split('\n'):
1961+ if line.startswith('Current DC'):
1962+ # Current DC: juju-lytrusty-machine-2 (168108163) - partition with quorum
1963+ current_dc = line.split(':')[1].split()[0]
1964+ if current_dc == get_unit_hostname():
1965+ return True
1966+ elif current_dc == 'NONE':
1967+ raise CRMDCNotFound('Current DC: NONE')
1968+
1969+ return False
1970+
1971+
1972+@retry_on_exception(5, base_delay=2,
1973+ exc_type=(CRMResourceNotFound, CRMDCNotFound))
1974+def is_crm_leader(resource, retry=False):
1975+ """
1976+ Returns True if the charm calling this is the elected corosync leader,
1977+ as returned by calling the external "crm" command.
1978+
1979+ We allow this operation to be retried to avoid the possibility of getting a
1980+ false negative. See LP #1396246 for more info.
1981+ """
1982+ if resource == DC_RESOURCE_NAME:
1983+ return is_crm_dc()
1984+ cmd = ['crm', 'resource', 'show', resource]
1985+ try:
1986+ status = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
1987+ if not isinstance(status, six.text_type):
1988+ status = six.text_type(status, "utf-8")
1989+ except subprocess.CalledProcessError:
1990+ status = None
1991+
1992+ if status and get_unit_hostname() in status:
1993+ return True
1994+
1995+ if status and "resource %s is NOT running" % (resource) in status:
1996+ raise CRMResourceNotFound("CRM resource %s not found" % (resource))
1997+
1998+ return False
1999+
2000+
2001+def is_leader(resource):
2002+ log("is_leader is deprecated. Please consider using is_crm_leader "
2003+ "instead.", level=WARNING)
2004+ return is_crm_leader(resource)
2005+
2006+
2007+def peer_units(peer_relation="cluster"):
2008+ peers = []
2009+ for r_id in (relation_ids(peer_relation) or []):
2010+ for unit in (relation_list(r_id) or []):
2011+ peers.append(unit)
2012+ return peers
2013+
2014+
2015+def peer_ips(peer_relation='cluster', addr_key='private-address'):
2016+ '''Return a dict of peers and their private-address'''
2017+ peers = {}
2018+ for r_id in relation_ids(peer_relation):
2019+ for unit in relation_list(r_id):
2020+ peers[unit] = relation_get(addr_key, rid=r_id, unit=unit)
2021+ return peers
2022+
2023+
2024+def oldest_peer(peers):
2025+ """Determines who the oldest peer is by comparing unit numbers."""
2026+ local_unit_no = int(os.getenv('JUJU_UNIT_NAME').split('/')[1])
2027+ for peer in peers:
2028+ remote_unit_no = int(peer.split('/')[1])
2029+ if remote_unit_no < local_unit_no:
2030+ return False
2031+ return True
2032+
2033+
2034+def eligible_leader(resource):
2035+ log("eligible_leader is deprecated. Please consider using "
2036+ "is_elected_leader instead.", level=WARNING)
2037+ return is_elected_leader(resource)
2038+
2039+
2040+def https():
2041+ '''
2042+ Determines whether enough data has been provided in configuration
2043+ or relation data to configure HTTPS
2044+ .
2045+ returns: boolean
2046+ '''
2047+ use_https = config_get('use-https')
2048+ if use_https and bool_from_string(use_https):
2049+ return True
2050+ if config_get('ssl_cert') and config_get('ssl_key'):
2051+ return True
2052+ for r_id in relation_ids('identity-service'):
2053+ for unit in relation_list(r_id):
2054+ # TODO - needs fixing for new helper as ssl_cert/key suffixes with CN
2055+ rel_state = [
2056+ relation_get('https_keystone', rid=r_id, unit=unit),
2057+ relation_get('ca_cert', rid=r_id, unit=unit),
2058+ ]
2059+ # NOTE: works around (LP: #1203241)
2060+ if (None not in rel_state) and ('' not in rel_state):
2061+ return True
2062+ return False
2063+
2064+
2065+def determine_api_port(public_port, singlenode_mode=False):
2066+ '''
2067+ Determine correct API server listening port based on
2068+ existence of HTTPS reverse proxy and/or haproxy.
2069+
2070+ public_port: int: standard public port for given service
2071+
2072+ singlenode_mode: boolean: Shuffle ports when only a single unit is present
2073+
2074+ returns: int: the correct listening port for the API service
2075+ '''
2076+ i = 0
2077+ if singlenode_mode:
2078+ i += 1
2079+ elif len(peer_units()) > 0 or is_clustered():
2080+ i += 1
2081+ if https():
2082+ i += 1
2083+ return public_port - (i * 10)
2084+
2085+
2086+def determine_apache_port(public_port, singlenode_mode=False):
2087+ '''
2088+ Description: Determine correct apache listening port based on public IP +
2089+ state of the cluster.
2090+
2091+ public_port: int: standard public port for given service
2092+
2093+ singlenode_mode: boolean: Shuffle ports when only a single unit is present
2094+
2095+ returns: int: the correct listening port for the HAProxy service
2096+ '''
2097+ i = 0
2098+ if singlenode_mode:
2099+ i += 1
2100+ elif len(peer_units()) > 0 or is_clustered():
2101+ i += 1
2102+ return public_port - (i * 10)
2103+
2104+
2105+def get_hacluster_config(exclude_keys=None):
2106+ '''
2107+ Obtains all relevant configuration from charm configuration required
2108+ for initiating a relation to hacluster:
2109+
2110+ ha-bindiface, ha-mcastport, vip
2111+
2112+ param: exclude_keys: list of setting key(s) to be excluded.
2113+ returns: dict: A dict containing settings keyed by setting name.
2114+ raises: HAIncompleteConfig if settings are missing.
2115+ '''
2116+ settings = ['ha-bindiface', 'ha-mcastport', 'vip']
2117+ conf = {}
2118+ for setting in settings:
2119+ if exclude_keys and setting in exclude_keys:
2120+ continue
2121+
2122+ conf[setting] = config_get(setting)
2123+ missing = []
2124+ [missing.append(s) for s, v in six.iteritems(conf) if v is None]
2125+ if missing:
2126+ log('Insufficient config data to configure hacluster.', level=ERROR)
2127+ raise HAIncompleteConfig
2128+ return conf
2129+
2130+
2131+def canonical_url(configs, vip_setting='vip'):
2132+ '''
2133+ Returns the correct HTTP URL to this host given the state of HTTPS
2134+ configuration and hacluster.
2135+
2136+ :configs : OSTemplateRenderer: A config tempating object to inspect for
2137+ a complete https context.
2138+
2139+ :vip_setting: str: Setting in charm config that specifies
2140+ VIP address.
2141+ '''
2142+ scheme = 'http'
2143+ if 'https' in configs.complete_contexts():
2144+ scheme = 'https'
2145+ if is_clustered():
2146+ addr = config_get(vip_setting)
2147+ else:
2148+ addr = unit_get('private-address')
2149+ return '%s://%s' % (scheme, addr)
2150
2151=== added directory 'charmhelpers/contrib/network'
2152=== added file 'charmhelpers/contrib/network/__init__.py'
2153--- charmhelpers/contrib/network/__init__.py 1970-01-01 00:00:00 +0000
2154+++ charmhelpers/contrib/network/__init__.py 2016-03-10 22:55:31 +0000
2155@@ -0,0 +1,15 @@
2156+# Copyright 2014-2015 Canonical Limited.
2157+#
2158+# This file is part of charm-helpers.
2159+#
2160+# charm-helpers is free software: you can redistribute it and/or modify
2161+# it under the terms of the GNU Lesser General Public License version 3 as
2162+# published by the Free Software Foundation.
2163+#
2164+# charm-helpers is distributed in the hope that it will be useful,
2165+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2166+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2167+# GNU Lesser General Public License for more details.
2168+#
2169+# You should have received a copy of the GNU Lesser General Public License
2170+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
2171
2172=== added file 'charmhelpers/contrib/network/ip.py'
2173--- charmhelpers/contrib/network/ip.py 1970-01-01 00:00:00 +0000
2174+++ charmhelpers/contrib/network/ip.py 2016-03-10 22:55:31 +0000
2175@@ -0,0 +1,458 @@
2176+# Copyright 2014-2015 Canonical Limited.
2177+#
2178+# This file is part of charm-helpers.
2179+#
2180+# charm-helpers is free software: you can redistribute it and/or modify
2181+# it under the terms of the GNU Lesser General Public License version 3 as
2182+# published by the Free Software Foundation.
2183+#
2184+# charm-helpers is distributed in the hope that it will be useful,
2185+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2186+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2187+# GNU Lesser General Public License for more details.
2188+#
2189+# You should have received a copy of the GNU Lesser General Public License
2190+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
2191+
2192+import glob
2193+import re
2194+import subprocess
2195+import six
2196+import socket
2197+
2198+from functools import partial
2199+
2200+from charmhelpers.core.hookenv import unit_get
2201+from charmhelpers.fetch import apt_install, apt_update
2202+from charmhelpers.core.hookenv import (
2203+ log,
2204+ WARNING,
2205+)
2206+
2207+try:
2208+ import netifaces
2209+except ImportError:
2210+ apt_update(fatal=True)
2211+ apt_install('python-netifaces', fatal=True)
2212+ import netifaces
2213+
2214+try:
2215+ import netaddr
2216+except ImportError:
2217+ apt_update(fatal=True)
2218+ apt_install('python-netaddr', fatal=True)
2219+ import netaddr
2220+
2221+
2222+def _validate_cidr(network):
2223+ try:
2224+ netaddr.IPNetwork(network)
2225+ except (netaddr.core.AddrFormatError, ValueError):
2226+ raise ValueError("Network (%s) is not in CIDR presentation format" %
2227+ network)
2228+
2229+
2230+def no_ip_found_error_out(network):
2231+ errmsg = ("No IP address found in network(s): %s" % network)
2232+ raise ValueError(errmsg)
2233+
2234+
2235+def get_address_in_network(network, fallback=None, fatal=False):
2236+ """Get an IPv4 or IPv6 address within the network from the host.
2237+
2238+ :param network (str): CIDR presentation format. For example,
2239+ '192.168.1.0/24'. Supports multiple networks as a space-delimited list.
2240+ :param fallback (str): If no address is found, return fallback.
2241+ :param fatal (boolean): If no address is found, fallback is not
2242+ set and fatal is True then exit(1).
2243+ """
2244+ if network is None:
2245+ if fallback is not None:
2246+ return fallback
2247+
2248+ if fatal:
2249+ no_ip_found_error_out(network)
2250+ else:
2251+ return None
2252+
2253+ networks = network.split() or [network]
2254+ for network in networks:
2255+ _validate_cidr(network)
2256+ network = netaddr.IPNetwork(network)
2257+ for iface in netifaces.interfaces():
2258+ addresses = netifaces.ifaddresses(iface)
2259+ if network.version == 4 and netifaces.AF_INET in addresses:
2260+ addr = addresses[netifaces.AF_INET][0]['addr']
2261+ netmask = addresses[netifaces.AF_INET][0]['netmask']
2262+ cidr = netaddr.IPNetwork("%s/%s" % (addr, netmask))
2263+ if cidr in network:
2264+ return str(cidr.ip)
2265+
2266+ if network.version == 6 and netifaces.AF_INET6 in addresses:
2267+ for addr in addresses[netifaces.AF_INET6]:
2268+ if not addr['addr'].startswith('fe80'):
2269+ cidr = netaddr.IPNetwork("%s/%s" % (addr['addr'],
2270+ addr['netmask']))
2271+ if cidr in network:
2272+ return str(cidr.ip)
2273+
2274+ if fallback is not None:
2275+ return fallback
2276+
2277+ if fatal:
2278+ no_ip_found_error_out(network)
2279+
2280+ return None
2281+
2282+
2283+def is_ipv6(address):
2284+ """Determine whether provided address is IPv6 or not."""
2285+ try:
2286+ address = netaddr.IPAddress(address)
2287+ except netaddr.AddrFormatError:
2288+ # probably a hostname - so not an address at all!
2289+ return False
2290+
2291+ return address.version == 6
2292+
2293+
2294+def is_address_in_network(network, address):
2295+ """
2296+ Determine whether the provided address is within a network range.
2297+
2298+ :param network (str): CIDR presentation format. For example,
2299+ '192.168.1.0/24'.
2300+ :param address: An individual IPv4 or IPv6 address without a net
2301+ mask or subnet prefix. For example, '192.168.1.1'.
2302+ :returns boolean: Flag indicating whether address is in network.
2303+ """
2304+ try:
2305+ network = netaddr.IPNetwork(network)
2306+ except (netaddr.core.AddrFormatError, ValueError):
2307+ raise ValueError("Network (%s) is not in CIDR presentation format" %
2308+ network)
2309+
2310+ try:
2311+ address = netaddr.IPAddress(address)
2312+ except (netaddr.core.AddrFormatError, ValueError):
2313+ raise ValueError("Address (%s) is not in correct presentation format" %
2314+ address)
2315+
2316+ if address in network:
2317+ return True
2318+ else:
2319+ return False
2320+
2321+
2322+def _get_for_address(address, key):
2323+ """Retrieve an attribute of or the physical interface that
2324+ the IP address provided could be bound to.
2325+
2326+ :param address (str): An individual IPv4 or IPv6 address without a net
2327+ mask or subnet prefix. For example, '192.168.1.1'.
2328+ :param key: 'iface' for the physical interface name or an attribute
2329+ of the configured interface, for example 'netmask'.
2330+ :returns str: Requested attribute or None if address is not bindable.
2331+ """
2332+ address = netaddr.IPAddress(address)
2333+ for iface in netifaces.interfaces():
2334+ addresses = netifaces.ifaddresses(iface)
2335+ if address.version == 4 and netifaces.AF_INET in addresses:
2336+ addr = addresses[netifaces.AF_INET][0]['addr']
2337+ netmask = addresses[netifaces.AF_INET][0]['netmask']
2338+ network = netaddr.IPNetwork("%s/%s" % (addr, netmask))
2339+ cidr = network.cidr
2340+ if address in cidr:
2341+ if key == 'iface':
2342+ return iface
2343+ else:
2344+ return addresses[netifaces.AF_INET][0][key]
2345+
2346+ if address.version == 6 and netifaces.AF_INET6 in addresses:
2347+ for addr in addresses[netifaces.AF_INET6]:
2348+ if not addr['addr'].startswith('fe80'):
2349+ network = netaddr.IPNetwork("%s/%s" % (addr['addr'],
2350+ addr['netmask']))
2351+ cidr = network.cidr
2352+ if address in cidr:
2353+ if key == 'iface':
2354+ return iface
2355+ elif key == 'netmask' and cidr:
2356+ return str(cidr).split('/')[1]
2357+ else:
2358+ return addr[key]
2359+
2360+ return None
2361+
2362+
2363+get_iface_for_address = partial(_get_for_address, key='iface')
2364+
2365+
2366+get_netmask_for_address = partial(_get_for_address, key='netmask')
2367+
2368+
2369+def format_ipv6_addr(address):
2370+ """If address is IPv6, wrap it in '[]' otherwise return None.
2371+
2372+ This is required by most configuration files when specifying IPv6
2373+ addresses.
2374+ """
2375+ if is_ipv6(address):
2376+ return "[%s]" % address
2377+
2378+ return None
2379+
2380+
2381+def get_iface_addr(iface='eth0', inet_type='AF_INET', inc_aliases=False,
2382+ fatal=True, exc_list=None):
2383+ """Return the assigned IP address for a given interface, if any."""
2384+ # Extract nic if passed /dev/ethX
2385+ if '/' in iface:
2386+ iface = iface.split('/')[-1]
2387+
2388+ if not exc_list:
2389+ exc_list = []
2390+
2391+ try:
2392+ inet_num = getattr(netifaces, inet_type)
2393+ except AttributeError:
2394+ raise Exception("Unknown inet type '%s'" % str(inet_type))
2395+
2396+ interfaces = netifaces.interfaces()
2397+ if inc_aliases:
2398+ ifaces = []
2399+ for _iface in interfaces:
2400+ if iface == _iface or _iface.split(':')[0] == iface:
2401+ ifaces.append(_iface)
2402+
2403+ if fatal and not ifaces:
2404+ raise Exception("Invalid interface '%s'" % iface)
2405+
2406+ ifaces.sort()
2407+ else:
2408+ if iface not in interfaces:
2409+ if fatal:
2410+ raise Exception("Interface '%s' not found " % (iface))
2411+ else:
2412+ return []
2413+
2414+ else:
2415+ ifaces = [iface]
2416+
2417+ addresses = []
2418+ for netiface in ifaces:
2419+ net_info = netifaces.ifaddresses(netiface)
2420+ if inet_num in net_info:
2421+ for entry in net_info[inet_num]:
2422+ if 'addr' in entry and entry['addr'] not in exc_list:
2423+ addresses.append(entry['addr'])
2424+
2425+ if fatal and not addresses:
2426+ raise Exception("Interface '%s' doesn't have any %s addresses." %
2427+ (iface, inet_type))
2428+
2429+ return sorted(addresses)
2430+
2431+
2432+get_ipv4_addr = partial(get_iface_addr, inet_type='AF_INET')
2433+
2434+
2435+def get_iface_from_addr(addr):
2436+ """Work out on which interface the provided address is configured."""
2437+ for iface in netifaces.interfaces():
2438+ addresses = netifaces.ifaddresses(iface)
2439+ for inet_type in addresses:
2440+ for _addr in addresses[inet_type]:
2441+ _addr = _addr['addr']
2442+ # link local
2443+ ll_key = re.compile("(.+)%.*")
2444+ raw = re.match(ll_key, _addr)
2445+ if raw:
2446+ _addr = raw.group(1)
2447+
2448+ if _addr == addr:
2449+ log("Address '%s' is configured on iface '%s'" %
2450+ (addr, iface))
2451+ return iface
2452+
2453+ msg = "Unable to infer net iface on which '%s' is configured" % (addr)
2454+ raise Exception(msg)
2455+
2456+
2457+def sniff_iface(f):
2458+ """Ensure decorated function is called with a value for iface.
2459+
2460+ If no iface provided, inject net iface inferred from unit private address.
2461+ """
2462+ def iface_sniffer(*args, **kwargs):
2463+ if not kwargs.get('iface', None):
2464+ kwargs['iface'] = get_iface_from_addr(unit_get('private-address'))
2465+
2466+ return f(*args, **kwargs)
2467+
2468+ return iface_sniffer
2469+
2470+
2471+@sniff_iface
2472+def get_ipv6_addr(iface=None, inc_aliases=False, fatal=True, exc_list=None,
2473+ dynamic_only=True):
2474+ """Get assigned IPv6 address for a given interface.
2475+
2476+ Returns list of addresses found. If no address found, returns empty list.
2477+
2478+ If iface is None, we infer the current primary interface by doing a reverse
2479+ lookup on the unit private-address.
2480+
2481+ We currently only support scope global IPv6 addresses i.e. non-temporary
2482+ addresses. If no global IPv6 address is found, return the first one found
2483+ in the ipv6 address list.
2484+ """
2485+ addresses = get_iface_addr(iface=iface, inet_type='AF_INET6',
2486+ inc_aliases=inc_aliases, fatal=fatal,
2487+ exc_list=exc_list)
2488+
2489+ if addresses:
2490+ global_addrs = []
2491+ for addr in addresses:
2492+ key_scope_link_local = re.compile("^fe80::..(.+)%(.+)")
2493+ m = re.match(key_scope_link_local, addr)
2494+ if m:
2495+ eui_64_mac = m.group(1)
2496+ iface = m.group(2)
2497+ else:
2498+ global_addrs.append(addr)
2499+
2500+ if global_addrs:
2501+ # Make sure any found global addresses are not temporary
2502+ cmd = ['ip', 'addr', 'show', iface]
2503+ out = subprocess.check_output(cmd).decode('UTF-8')
2504+ if dynamic_only:
2505+ key = re.compile("inet6 (.+)/[0-9]+ scope global dynamic.*")
2506+ else:
2507+ key = re.compile("inet6 (.+)/[0-9]+ scope global.*")
2508+
2509+ addrs = []
2510+ for line in out.split('\n'):
2511+ line = line.strip()
2512+ m = re.match(key, line)
2513+ if m and 'temporary' not in line:
2514+ # Return the first valid address we find
2515+ for addr in global_addrs:
2516+ if m.group(1) == addr:
2517+ if not dynamic_only or \
2518+ m.group(1).endswith(eui_64_mac):
2519+ addrs.append(addr)
2520+
2521+ if addrs:
2522+ return addrs
2523+
2524+ if fatal:
2525+ raise Exception("Interface '%s' does not have a scope global "
2526+ "non-temporary ipv6 address." % iface)
2527+
2528+ return []
2529+
2530+
2531+def get_bridges(vnic_dir='/sys/devices/virtual/net'):
2532+ """Return a list of bridges on the system."""
2533+ b_regex = "%s/*/bridge" % vnic_dir
2534+ return [x.replace(vnic_dir, '').split('/')[1] for x in glob.glob(b_regex)]
2535+
2536+
2537+def get_bridge_nics(bridge, vnic_dir='/sys/devices/virtual/net'):
2538+ """Return a list of nics comprising a given bridge on the system."""
2539+ brif_regex = "%s/%s/brif/*" % (vnic_dir, bridge)
2540+ return [x.split('/')[-1] for x in glob.glob(brif_regex)]
2541+
2542+
2543+def is_bridge_member(nic):
2544+ """Check if a given nic is a member of a bridge."""
2545+ for bridge in get_bridges():
2546+ if nic in get_bridge_nics(bridge):
2547+ return True
2548+
2549+ return False
2550+
2551+
2552+def is_ip(address):
2553+ """
2554+ Returns True if address is a valid IP address.
2555+ """
2556+ try:
2557+ # Test to see if already an IPv4 address
2558+ socket.inet_aton(address)
2559+ return True
2560+ except socket.error:
2561+ return False
2562+
2563+
2564+def ns_query(address):
2565+ try:
2566+ import dns.resolver
2567+ except ImportError:
2568+ apt_install('python-dnspython')
2569+ import dns.resolver
2570+
2571+ if isinstance(address, dns.name.Name):
2572+ rtype = 'PTR'
2573+ elif isinstance(address, six.string_types):
2574+ rtype = 'A'
2575+ else:
2576+ return None
2577+
2578+ answers = dns.resolver.query(address, rtype)
2579+ if answers:
2580+ return str(answers[0])
2581+ return None
2582+
2583+
2584+def get_host_ip(hostname, fallback=None):
2585+ """
2586+ Resolves the IP for a given hostname, or returns
2587+ the input if it is already an IP.
2588+ """
2589+ if is_ip(hostname):
2590+ return hostname
2591+
2592+ ip_addr = ns_query(hostname)
2593+ if not ip_addr:
2594+ try:
2595+ ip_addr = socket.gethostbyname(hostname)
2596+ except:
2597+ log("Failed to resolve hostname '%s'" % (hostname),
2598+ level=WARNING)
2599+ return fallback
2600+ return ip_addr
2601+
2602+
2603+def get_hostname(address, fqdn=True):
2604+ """
2605+ Resolves hostname for given IP, or returns the input
2606+ if it is already a hostname.
2607+ """
2608+ if is_ip(address):
2609+ try:
2610+ import dns.reversename
2611+ except ImportError:
2612+ apt_install("python-dnspython")
2613+ import dns.reversename
2614+
2615+ rev = dns.reversename.from_address(address)
2616+ result = ns_query(rev)
2617+
2618+ if not result:
2619+ try:
2620+ result = socket.gethostbyaddr(address)[0]
2621+ except:
2622+ return None
2623+ else:
2624+ result = address
2625+
2626+ if fqdn:
2627+ # strip trailing .
2628+ if result.endswith('.'):
2629+ return result[:-1]
2630+ else:
2631+ return result
2632+ else:
2633+ return result.split('.')[0]
2634
2635=== added directory 'charmhelpers/contrib/peerstorage'
2636=== added file 'charmhelpers/contrib/peerstorage/__init__.py'
2637--- charmhelpers/contrib/peerstorage/__init__.py 1970-01-01 00:00:00 +0000
2638+++ charmhelpers/contrib/peerstorage/__init__.py 2016-03-10 22:55:31 +0000
2639@@ -0,0 +1,269 @@
2640+# Copyright 2014-2015 Canonical Limited.
2641+#
2642+# This file is part of charm-helpers.
2643+#
2644+# charm-helpers is free software: you can redistribute it and/or modify
2645+# it under the terms of the GNU Lesser General Public License version 3 as
2646+# published by the Free Software Foundation.
2647+#
2648+# charm-helpers is distributed in the hope that it will be useful,
2649+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2650+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2651+# GNU Lesser General Public License for more details.
2652+#
2653+# You should have received a copy of the GNU Lesser General Public License
2654+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
2655+
2656+import json
2657+import six
2658+
2659+from charmhelpers.core.hookenv import relation_id as current_relation_id
2660+from charmhelpers.core.hookenv import (
2661+ is_relation_made,
2662+ relation_ids,
2663+ relation_get as _relation_get,
2664+ local_unit,
2665+ relation_set as _relation_set,
2666+ leader_get as _leader_get,
2667+ leader_set,
2668+ is_leader,
2669+)
2670+
2671+
2672+"""
2673+This helper provides functions to support use of a peer relation
2674+for basic key/value storage, with the added benefit that all storage
2675+can be replicated across peer units.
2676+
2677+Requirement to use:
2678+
2679+To use this, the "peer_echo()" method has to be called form the peer
2680+relation's relation-changed hook:
2681+
2682+@hooks.hook("cluster-relation-changed") # Adapt the to your peer relation name
2683+def cluster_relation_changed():
2684+ peer_echo()
2685+
2686+Once this is done, you can use peer storage from anywhere:
2687+
2688+@hooks.hook("some-hook")
2689+def some_hook():
2690+ # You can store and retrieve key/values this way:
2691+ if is_relation_made("cluster"): # from charmhelpers.core.hookenv
2692+ # There are peers available so we can work with peer storage
2693+ peer_store("mykey", "myvalue")
2694+ value = peer_retrieve("mykey")
2695+ print value
2696+ else:
2697+ print "No peers joind the relation, cannot share key/values :("
2698+"""
2699+
2700+
2701+def leader_get(attribute=None, rid=None):
2702+ """Wrapper to ensure that settings are migrated from the peer relation.
2703+
2704+ This is to support upgrading an environment that does not support
2705+ Juju leadership election to one that does.
2706+
2707+ If a setting is not extant in the leader-get but is on the relation-get
2708+ peer rel, it is migrated and marked as such so that it is not re-migrated.
2709+ """
2710+ migration_key = '__leader_get_migrated_settings__'
2711+ if not is_leader():
2712+ return _leader_get(attribute=attribute)
2713+
2714+ settings_migrated = False
2715+ leader_settings = _leader_get(attribute=attribute)
2716+ previously_migrated = _leader_get(attribute=migration_key)
2717+
2718+ if previously_migrated:
2719+ migrated = set(json.loads(previously_migrated))
2720+ else:
2721+ migrated = set([])
2722+
2723+ try:
2724+ if migration_key in leader_settings:
2725+ del leader_settings[migration_key]
2726+ except TypeError:
2727+ pass
2728+
2729+ if attribute:
2730+ if attribute in migrated:
2731+ return leader_settings
2732+
2733+ # If attribute not present in leader db, check if this unit has set
2734+ # the attribute in the peer relation
2735+ if not leader_settings:
2736+ peer_setting = _relation_get(attribute=attribute, unit=local_unit(),
2737+ rid=rid)
2738+ if peer_setting:
2739+ leader_set(settings={attribute: peer_setting})
2740+ leader_settings = peer_setting
2741+
2742+ if leader_settings:
2743+ settings_migrated = True
2744+ migrated.add(attribute)
2745+ else:
2746+ r_settings = _relation_get(unit=local_unit(), rid=rid)
2747+ if r_settings:
2748+ for key in set(r_settings.keys()).difference(migrated):
2749+ # Leader setting wins
2750+ if not leader_settings.get(key):
2751+ leader_settings[key] = r_settings[key]
2752+
2753+ settings_migrated = True
2754+ migrated.add(key)
2755+
2756+ if settings_migrated:
2757+ leader_set(**leader_settings)
2758+
2759+ if migrated and settings_migrated:
2760+ migrated = json.dumps(list(migrated))
2761+ leader_set(settings={migration_key: migrated})
2762+
2763+ return leader_settings
2764+
2765+
2766+def relation_set(relation_id=None, relation_settings=None, **kwargs):
2767+ """Attempt to use leader-set if supported in the current version of Juju,
2768+ otherwise falls back on relation-set.
2769+
2770+ Note that we only attempt to use leader-set if the provided relation_id is
2771+ a peer relation id or no relation id is provided (in which case we assume
2772+ we are within the peer relation context).
2773+ """
2774+ try:
2775+ if relation_id in relation_ids('cluster'):
2776+ return leader_set(settings=relation_settings, **kwargs)
2777+ else:
2778+ raise NotImplementedError
2779+ except NotImplementedError:
2780+ return _relation_set(relation_id=relation_id,
2781+ relation_settings=relation_settings, **kwargs)
2782+
2783+
2784+def relation_get(attribute=None, unit=None, rid=None):
2785+ """Attempt to use leader-get if supported in the current version of Juju,
2786+ otherwise falls back on relation-get.
2787+
2788+ Note that we only attempt to use leader-get if the provided rid is a peer
2789+ relation id or no relation id is provided (in which case we assume we are
2790+ within the peer relation context).
2791+ """
2792+ try:
2793+ if rid in relation_ids('cluster'):
2794+ return leader_get(attribute, rid)
2795+ else:
2796+ raise NotImplementedError
2797+ except NotImplementedError:
2798+ return _relation_get(attribute=attribute, rid=rid, unit=unit)
2799+
2800+
2801+def peer_retrieve(key, relation_name='cluster'):
2802+ """Retrieve a named key from peer relation `relation_name`."""
2803+ cluster_rels = relation_ids(relation_name)
2804+ if len(cluster_rels) > 0:
2805+ cluster_rid = cluster_rels[0]
2806+ return relation_get(attribute=key, rid=cluster_rid,
2807+ unit=local_unit())
2808+ else:
2809+ raise ValueError('Unable to detect'
2810+ 'peer relation {}'.format(relation_name))
2811+
2812+
2813+def peer_retrieve_by_prefix(prefix, relation_name='cluster', delimiter='_',
2814+ inc_list=None, exc_list=None):
2815+ """ Retrieve k/v pairs given a prefix and filter using {inc,exc}_list """
2816+ inc_list = inc_list if inc_list else []
2817+ exc_list = exc_list if exc_list else []
2818+ peerdb_settings = peer_retrieve('-', relation_name=relation_name)
2819+ matched = {}
2820+ if peerdb_settings is None:
2821+ return matched
2822+ for k, v in peerdb_settings.items():
2823+ full_prefix = prefix + delimiter
2824+ if k.startswith(full_prefix):
2825+ new_key = k.replace(full_prefix, '')
2826+ if new_key in exc_list:
2827+ continue
2828+ if new_key in inc_list or len(inc_list) == 0:
2829+ matched[new_key] = v
2830+ return matched
2831+
2832+
2833+def peer_store(key, value, relation_name='cluster'):
2834+ """Store the key/value pair on the named peer relation `relation_name`."""
2835+ cluster_rels = relation_ids(relation_name)
2836+ if len(cluster_rels) > 0:
2837+ cluster_rid = cluster_rels[0]
2838+ relation_set(relation_id=cluster_rid,
2839+ relation_settings={key: value})
2840+ else:
2841+ raise ValueError('Unable to detect '
2842+ 'peer relation {}'.format(relation_name))
2843+
2844+
2845+def peer_echo(includes=None, force=False):
2846+ """Echo filtered attributes back onto the same relation for storage.
2847+
2848+ This is a requirement to use the peerstorage module - it needs to be called
2849+ from the peer relation's changed hook.
2850+
2851+ If Juju leader support exists this will be a noop unless force is True.
2852+ """
2853+ try:
2854+ is_leader()
2855+ except NotImplementedError:
2856+ pass
2857+ else:
2858+ if not force:
2859+ return # NOOP if leader-election is supported
2860+
2861+ # Use original non-leader calls
2862+ relation_get = _relation_get
2863+ relation_set = _relation_set
2864+
2865+ rdata = relation_get()
2866+ echo_data = {}
2867+ if includes is None:
2868+ echo_data = rdata.copy()
2869+ for ex in ['private-address', 'public-address']:
2870+ if ex in echo_data:
2871+ echo_data.pop(ex)
2872+ else:
2873+ for attribute, value in six.iteritems(rdata):
2874+ for include in includes:
2875+ if include in attribute:
2876+ echo_data[attribute] = value
2877+ if len(echo_data) > 0:
2878+ relation_set(relation_settings=echo_data)
2879+
2880+
2881+def peer_store_and_set(relation_id=None, peer_relation_name='cluster',
2882+ peer_store_fatal=False, relation_settings=None,
2883+ delimiter='_', **kwargs):
2884+ """Store passed-in arguments both in argument relation and in peer storage.
2885+
2886+ It functions like doing relation_set() and peer_store() at the same time,
2887+ with the same data.
2888+
2889+ @param relation_id: the id of the relation to store the data on. Defaults
2890+ to the current relation.
2891+ @param peer_store_fatal: Set to True, the function will raise an exception
2892+ should the peer sotrage not be avialable."""
2893+
2894+ relation_settings = relation_settings if relation_settings else {}
2895+ relation_set(relation_id=relation_id,
2896+ relation_settings=relation_settings,
2897+ **kwargs)
2898+ if is_relation_made(peer_relation_name):
2899+ for key, value in six.iteritems(dict(list(kwargs.items()) +
2900+ list(relation_settings.items()))):
2901+ key_prefix = relation_id or current_relation_id()
2902+ peer_store(key_prefix + delimiter + key,
2903+ value,
2904+ relation_name=peer_relation_name)
2905+ else:
2906+ if peer_store_fatal:
2907+ raise ValueError('Unable to detect '
2908+ 'peer relation {}'.format(peer_relation_name))
2909
2910=== added directory 'charmhelpers/core'
2911=== added file 'charmhelpers/core/__init__.py'
2912--- charmhelpers/core/__init__.py 1970-01-01 00:00:00 +0000
2913+++ charmhelpers/core/__init__.py 2016-03-10 22:55:31 +0000
2914@@ -0,0 +1,15 @@
2915+# Copyright 2014-2015 Canonical Limited.
2916+#
2917+# This file is part of charm-helpers.
2918+#
2919+# charm-helpers is free software: you can redistribute it and/or modify
2920+# it under the terms of the GNU Lesser General Public License version 3 as
2921+# published by the Free Software Foundation.
2922+#
2923+# charm-helpers is distributed in the hope that it will be useful,
2924+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2925+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2926+# GNU Lesser General Public License for more details.
2927+#
2928+# You should have received a copy of the GNU Lesser General Public License
2929+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
2930
2931=== added file 'charmhelpers/core/decorators.py'
2932--- charmhelpers/core/decorators.py 1970-01-01 00:00:00 +0000
2933+++ charmhelpers/core/decorators.py 2016-03-10 22:55:31 +0000
2934@@ -0,0 +1,57 @@
2935+# Copyright 2014-2015 Canonical Limited.
2936+#
2937+# This file is part of charm-helpers.
2938+#
2939+# charm-helpers is free software: you can redistribute it and/or modify
2940+# it under the terms of the GNU Lesser General Public License version 3 as
2941+# published by the Free Software Foundation.
2942+#
2943+# charm-helpers is distributed in the hope that it will be useful,
2944+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2945+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2946+# GNU Lesser General Public License for more details.
2947+#
2948+# You should have received a copy of the GNU Lesser General Public License
2949+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
2950+
2951+#
2952+# Copyright 2014 Canonical Ltd.
2953+#
2954+# Authors:
2955+# Edward Hope-Morley <opentastic@gmail.com>
2956+#
2957+
2958+import time
2959+
2960+from charmhelpers.core.hookenv import (
2961+ log,
2962+ INFO,
2963+)
2964+
2965+
2966+def retry_on_exception(num_retries, base_delay=0, exc_type=Exception):
2967+ """If the decorated function raises exception exc_type, allow num_retries
2968+ retry attempts before raise the exception.
2969+ """
2970+ def _retry_on_exception_inner_1(f):
2971+ def _retry_on_exception_inner_2(*args, **kwargs):
2972+ retries = num_retries
2973+ multiplier = 1
2974+ while True:
2975+ try:
2976+ return f(*args, **kwargs)
2977+ except exc_type:
2978+ if not retries:
2979+ raise
2980+
2981+ delay = base_delay * multiplier
2982+ multiplier += 1
2983+ log("Retrying '%s' %d more times (delay=%s)" %
2984+ (f.__name__, retries, delay), level=INFO)
2985+ retries -= 1
2986+ if delay:
2987+ time.sleep(delay)
2988+
2989+ return _retry_on_exception_inner_2
2990+
2991+ return _retry_on_exception_inner_1
2992
2993=== added file 'charmhelpers/core/files.py'
2994--- charmhelpers/core/files.py 1970-01-01 00:00:00 +0000
2995+++ charmhelpers/core/files.py 2016-03-10 22:55:31 +0000
2996@@ -0,0 +1,45 @@
2997+#!/usr/bin/env python
2998+# -*- coding: utf-8 -*-
2999+
3000+# Copyright 2014-2015 Canonical Limited.
3001+#
3002+# This file is part of charm-helpers.
3003+#
3004+# charm-helpers is free software: you can redistribute it and/or modify
3005+# it under the terms of the GNU Lesser General Public License version 3 as
3006+# published by the Free Software Foundation.
3007+#
3008+# charm-helpers is distributed in the hope that it will be useful,
3009+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3010+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3011+# GNU Lesser General Public License for more details.
3012+#
3013+# You should have received a copy of the GNU Lesser General Public License
3014+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
3015+
3016+__author__ = 'Jorge Niedbalski <niedbalski@ubuntu.com>'
3017+
3018+import os
3019+import subprocess
3020+
3021+
3022+def sed(filename, before, after, flags='g'):
3023+ """
3024+ Search and replaces the given pattern on filename.
3025+
3026+ :param filename: relative or absolute file path.
3027+ :param before: expression to be replaced (see 'man sed')
3028+ :param after: expression to replace with (see 'man sed')
3029+ :param flags: sed-compatible regex flags in example, to make
3030+ the search and replace case insensitive, specify ``flags="i"``.
3031+ The ``g`` flag is always specified regardless, so you do not
3032+ need to remember to include it when overriding this parameter.
3033+ :returns: If the sed command exit code was zero then return,
3034+ otherwise raise CalledProcessError.
3035+ """
3036+ expression = r's/{0}/{1}/{2}'.format(before,
3037+ after, flags)
3038+
3039+ return subprocess.check_call(["sed", "-i", "-r", "-e",
3040+ expression,
3041+ os.path.expanduser(filename)])
3042
3043=== added file 'charmhelpers/core/fstab.py'
3044--- charmhelpers/core/fstab.py 1970-01-01 00:00:00 +0000
3045+++ charmhelpers/core/fstab.py 2016-03-10 22:55:31 +0000
3046@@ -0,0 +1,134 @@
3047+#!/usr/bin/env python
3048+# -*- coding: utf-8 -*-
3049+
3050+# Copyright 2014-2015 Canonical Limited.
3051+#
3052+# This file is part of charm-helpers.
3053+#
3054+# charm-helpers is free software: you can redistribute it and/or modify
3055+# it under the terms of the GNU Lesser General Public License version 3 as
3056+# published by the Free Software Foundation.
3057+#
3058+# charm-helpers is distributed in the hope that it will be useful,
3059+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3060+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3061+# GNU Lesser General Public License for more details.
3062+#
3063+# You should have received a copy of the GNU Lesser General Public License
3064+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
3065+
3066+import io
3067+import os
3068+
3069+__author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'
3070+
3071+
3072+class Fstab(io.FileIO):
3073+ """This class extends file in order to implement a file reader/writer
3074+ for file `/etc/fstab`
3075+ """
3076+
3077+ class Entry(object):
3078+ """Entry class represents a non-comment line on the `/etc/fstab` file
3079+ """
3080+ def __init__(self, device, mountpoint, filesystem,
3081+ options, d=0, p=0):
3082+ self.device = device
3083+ self.mountpoint = mountpoint
3084+ self.filesystem = filesystem
3085+
3086+ if not options:
3087+ options = "defaults"
3088+
3089+ self.options = options
3090+ self.d = int(d)
3091+ self.p = int(p)
3092+
3093+ def __eq__(self, o):
3094+ return str(self) == str(o)
3095+
3096+ def __str__(self):
3097+ return "{} {} {} {} {} {}".format(self.device,
3098+ self.mountpoint,
3099+ self.filesystem,
3100+ self.options,
3101+ self.d,
3102+ self.p)
3103+
3104+ DEFAULT_PATH = os.path.join(os.path.sep, 'etc', 'fstab')
3105+
3106+ def __init__(self, path=None):
3107+ if path:
3108+ self._path = path
3109+ else:
3110+ self._path = self.DEFAULT_PATH
3111+ super(Fstab, self).__init__(self._path, 'rb+')
3112+
3113+ def _hydrate_entry(self, line):
3114+ # NOTE: use split with no arguments to split on any
3115+ # whitespace including tabs
3116+ return Fstab.Entry(*filter(
3117+ lambda x: x not in ('', None),
3118+ line.strip("\n").split()))
3119+
3120+ @property
3121+ def entries(self):
3122+ self.seek(0)
3123+ for line in self.readlines():
3124+ line = line.decode('us-ascii')
3125+ try:
3126+ if line.strip() and not line.strip().startswith("#"):
3127+ yield self._hydrate_entry(line)
3128+ except ValueError:
3129+ pass
3130+
3131+ def get_entry_by_attr(self, attr, value):
3132+ for entry in self.entries:
3133+ e_attr = getattr(entry, attr)
3134+ if e_attr == value:
3135+ return entry
3136+ return None
3137+
3138+ def add_entry(self, entry):
3139+ if self.get_entry_by_attr('device', entry.device):
3140+ return False
3141+
3142+ self.write((str(entry) + '\n').encode('us-ascii'))
3143+ self.truncate()
3144+ return entry
3145+
3146+ def remove_entry(self, entry):
3147+ self.seek(0)
3148+
3149+ lines = [l.decode('us-ascii') for l in self.readlines()]
3150+
3151+ found = False
3152+ for index, line in enumerate(lines):
3153+ if line.strip() and not line.strip().startswith("#"):
3154+ if self._hydrate_entry(line) == entry:
3155+ found = True
3156+ break
3157+
3158+ if not found:
3159+ return False
3160+
3161+ lines.remove(line)
3162+
3163+ self.seek(0)
3164+ self.write(''.join(lines).encode('us-ascii'))
3165+ self.truncate()
3166+ return True
3167+
3168+ @classmethod
3169+ def remove_by_mountpoint(cls, mountpoint, path=None):
3170+ fstab = cls(path=path)
3171+ entry = fstab.get_entry_by_attr('mountpoint', mountpoint)
3172+ if entry:
3173+ return fstab.remove_entry(entry)
3174+ return False
3175+
3176+ @classmethod
3177+ def add(cls, device, mountpoint, filesystem, options=None, path=None):
3178+ return cls(path=path).add_entry(Fstab.Entry(device,
3179+ mountpoint, filesystem,
3180+ options=options))
3181
3182=== added file 'charmhelpers/core/hookenv.py'
3183--- charmhelpers/core/hookenv.py 1970-01-01 00:00:00 +0000
3184+++ charmhelpers/core/hookenv.py 2016-03-10 22:55:31 +0000
3185@@ -0,0 +1,978 @@
3186+# Copyright 2014-2015 Canonical Limited.
3187+#
3188+# This file is part of charm-helpers.
3189+#
3190+# charm-helpers is free software: you can redistribute it and/or modify
3191+# it under the terms of the GNU Lesser General Public License version 3 as
3192+# published by the Free Software Foundation.
3193+#
3194+# charm-helpers is distributed in the hope that it will be useful,
3195+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3196+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3197+# GNU Lesser General Public License for more details.
3198+#
3199+# You should have received a copy of the GNU Lesser General Public License
3200+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
3201+
3202+"Interactions with the Juju environment"
3203+# Copyright 2013 Canonical Ltd.
3204+#
3205+# Authors:
3206+# Charm Helpers Developers <juju@lists.ubuntu.com>
3207+
3208+from __future__ import print_function
3209+import copy
3210+from distutils.version import LooseVersion
3211+from functools import wraps
3212+import glob
3213+import os
3214+import json
3215+import yaml
3216+import subprocess
3217+import sys
3218+import errno
3219+import tempfile
3220+from subprocess import CalledProcessError
3221+
3222+import six
3223+if not six.PY3:
3224+ from UserDict import UserDict
3225+else:
3226+ from collections import UserDict
3227+
3228+CRITICAL = "CRITICAL"
3229+ERROR = "ERROR"
3230+WARNING = "WARNING"
3231+INFO = "INFO"
3232+DEBUG = "DEBUG"
3233+MARKER = object()
3234+
3235+cache = {}
3236+
3237+
3238+def cached(func):
3239+ """Cache return values for multiple executions of func + args
3240+
3241+ For example::
3242+
3243+ @cached
3244+ def unit_get(attribute):
3245+ pass
3246+
3247+ unit_get('test')
3248+
3249+ will cache the result of unit_get + 'test' for future calls.
3250+ """
3251+ @wraps(func)
3252+ def wrapper(*args, **kwargs):
3253+ global cache
3254+ key = str((func, args, kwargs))
3255+ try:
3256+ return cache[key]
3257+ except KeyError:
3258+ pass # Drop out of the exception handler scope.
3259+ res = func(*args, **kwargs)
3260+ cache[key] = res
3261+ return res
3262+ wrapper._wrapped = func
3263+ return wrapper
3264+
3265+
3266+def flush(key):
3267+ """Flushes any entries from function cache where the
3268+ key is found in the function+args """
3269+ flush_list = []
3270+ for item in cache:
3271+ if key in item:
3272+ flush_list.append(item)
3273+ for item in flush_list:
3274+ del cache[item]
3275+
3276+
3277+def log(message, level=None):
3278+ """Write a message to the juju log"""
3279+ command = ['juju-log']
3280+ if level:
3281+ command += ['-l', level]
3282+ if not isinstance(message, six.string_types):
3283+ message = repr(message)
3284+ command += [message]
3285+ # Missing juju-log should not cause failures in unit tests
3286+ # Send log output to stderr
3287+ try:
3288+ subprocess.call(command)
3289+ except OSError as e:
3290+ if e.errno == errno.ENOENT:
3291+ if level:
3292+ message = "{}: {}".format(level, message)
3293+ message = "juju-log: {}".format(message)
3294+ print(message, file=sys.stderr)
3295+ else:
3296+ raise
3297+
3298+
3299+class Serializable(UserDict):
3300+ """Wrapper, an object that can be serialized to yaml or json"""
3301+
3302+ def __init__(self, obj):
3303+ # wrap the object
3304+ UserDict.__init__(self)
3305+ self.data = obj
3306+
3307+ def __getattr__(self, attr):
3308+ # See if this object has attribute.
3309+ if attr in ("json", "yaml", "data"):
3310+ return self.__dict__[attr]
3311+ # Check for attribute in wrapped object.
3312+ got = getattr(self.data, attr, MARKER)
3313+ if got is not MARKER:
3314+ return got
3315+ # Proxy to the wrapped object via dict interface.
3316+ try:
3317+ return self.data[attr]
3318+ except KeyError:
3319+ raise AttributeError(attr)
3320+
3321+ def __getstate__(self):
3322+ # Pickle as a standard dictionary.
3323+ return self.data
3324+
3325+ def __setstate__(self, state):
3326+ # Unpickle into our wrapper.
3327+ self.data = state
3328+
3329+ def json(self):
3330+ """Serialize the object to json"""
3331+ return json.dumps(self.data)
3332+
3333+ def yaml(self):
3334+ """Serialize the object to yaml"""
3335+ return yaml.dump(self.data)
3336+
3337+
3338+def execution_environment():
3339+ """A convenient bundling of the current execution context"""
3340+ context = {}
3341+ context['conf'] = config()
3342+ if relation_id():
3343+ context['reltype'] = relation_type()
3344+ context['relid'] = relation_id()
3345+ context['rel'] = relation_get()
3346+ context['unit'] = local_unit()
3347+ context['rels'] = relations()
3348+ context['env'] = os.environ
3349+ return context
3350+
3351+
3352+def in_relation_hook():
3353+ """Determine whether we're running in a relation hook"""
3354+ return 'JUJU_RELATION' in os.environ
3355+
3356+
3357+def relation_type():
3358+ """The scope for the current relation hook"""
3359+ return os.environ.get('JUJU_RELATION', None)
3360+
3361+
3362+@cached
3363+def relation_id(relation_name=None, service_or_unit=None):
3364+ """The relation ID for the current or a specified relation"""
3365+ if not relation_name and not service_or_unit:
3366+ return os.environ.get('JUJU_RELATION_ID', None)
3367+ elif relation_name and service_or_unit:
3368+ service_name = service_or_unit.split('/')[0]
3369+ for relid in relation_ids(relation_name):
3370+ remote_service = remote_service_name(relid)
3371+ if remote_service == service_name:
3372+ return relid
3373+ else:
3374+ raise ValueError('Must specify neither or both of relation_name and service_or_unit')
3375+
3376+
3377+def local_unit():
3378+ """Local unit ID"""
3379+ return os.environ['JUJU_UNIT_NAME']
3380+
3381+
3382+def remote_unit():
3383+ """The remote unit for the current relation hook"""
3384+ return os.environ.get('JUJU_REMOTE_UNIT', None)
3385+
3386+
3387+def service_name():
3388+ """The name service group this unit belongs to"""
3389+ return local_unit().split('/')[0]
3390+
3391+
3392+@cached
3393+def remote_service_name(relid=None):
3394+ """The remote service name for a given relation-id (or the current relation)"""
3395+ if relid is None:
3396+ unit = remote_unit()
3397+ else:
3398+ units = related_units(relid)
3399+ unit = units[0] if units else None
3400+ return unit.split('/')[0] if unit else None
3401+
3402+
3403+def hook_name():
3404+ """The name of the currently executing hook"""
3405+ return os.environ.get('JUJU_HOOK_NAME', os.path.basename(sys.argv[0]))
3406+
3407+
3408+class Config(dict):
3409+ """A dictionary representation of the charm's config.yaml, with some
3410+ extra features:
3411+
3412+ - See which values in the dictionary have changed since the previous hook.
3413+ - For values that have changed, see what the previous value was.
3414+ - Store arbitrary data for use in a later hook.
3415+
3416+ NOTE: Do not instantiate this object directly - instead call
3417+ ``hookenv.config()``, which will return an instance of :class:`Config`.
3418+
3419+ Example usage::
3420+
3421+ >>> # inside a hook
3422+ >>> from charmhelpers.core import hookenv
3423+ >>> config = hookenv.config()
3424+ >>> config['foo']
3425+ 'bar'
3426+ >>> # store a new key/value for later use
3427+ >>> config['mykey'] = 'myval'
3428+
3429+
3430+ >>> # user runs `juju set mycharm foo=baz`
3431+ >>> # now we're inside subsequent config-changed hook
3432+ >>> config = hookenv.config()
3433+ >>> config['foo']
3434+ 'baz'
3435+ >>> # test to see if this val has changed since last hook
3436+ >>> config.changed('foo')
3437+ True
3438+ >>> # what was the previous value?
3439+ >>> config.previous('foo')
3440+ 'bar'
3441+ >>> # keys/values that we add are preserved across hooks
3442+ >>> config['mykey']
3443+ 'myval'
3444+
3445+ """
3446+ CONFIG_FILE_NAME = '.juju-persistent-config'
3447+
3448+ def __init__(self, *args, **kw):
3449+ super(Config, self).__init__(*args, **kw)
3450+ self.implicit_save = True
3451+ self._prev_dict = None
3452+ self.path = os.path.join(charm_dir(), Config.CONFIG_FILE_NAME)
3453+ if os.path.exists(self.path):
3454+ self.load_previous()
3455+ atexit(self._implicit_save)
3456+
3457+ def load_previous(self, path=None):
3458+ """Load previous copy of config from disk.
3459+
3460+ In normal usage you don't need to call this method directly - it
3461+ is called automatically at object initialization.
3462+
3463+ :param path:
3464+
3465+ File path from which to load the previous config. If `None`,
3466+ config is loaded from the default location. If `path` is
3467+ specified, subsequent `save()` calls will write to the same
3468+ path.
3469+
3470+ """
3471+ self.path = path or self.path
3472+ with open(self.path) as f:
3473+ self._prev_dict = json.load(f)
3474+ for k, v in copy.deepcopy(self._prev_dict).items():
3475+ if k not in self:
3476+ self[k] = v
3477+
3478+ def changed(self, key):
3479+ """Return True if the current value for this key is different from
3480+ the previous value.
3481+
3482+ """
3483+ if self._prev_dict is None:
3484+ return True
3485+ return self.previous(key) != self.get(key)
3486+
3487+ def previous(self, key):
3488+ """Return previous value for this key, or None if there
3489+ is no previous value.
3490+
3491+ """
3492+ if self._prev_dict:
3493+ return self._prev_dict.get(key)
3494+ return None
3495+
3496+ def save(self):
3497+ """Save this config to disk.
3498+
3499+ If the charm is using the :mod:`Services Framework <services.base>`
3500+ or :meth:'@hook <Hooks.hook>' decorator, this
3501+ is called automatically at the end of successful hook execution.
3502+ Otherwise, it should be called directly by user code.
3503+
3504+ To disable automatic saves, set ``implicit_save=False`` on this
3505+ instance.
3506+
3507+ """
3508+ with open(self.path, 'w') as f:
3509+ json.dump(self, f)
3510+
3511+ def _implicit_save(self):
3512+ if self.implicit_save:
3513+ self.save()
3514+
3515+
3516+@cached
3517+def config(scope=None):
3518+ """Juju charm configuration"""
3519+ config_cmd_line = ['config-get']
3520+ if scope is not None:
3521+ config_cmd_line.append(scope)
3522+ config_cmd_line.append('--format=json')
3523+ try:
3524+ config_data = json.loads(
3525+ subprocess.check_output(config_cmd_line).decode('UTF-8'))
3526+ if scope is not None:
3527+ return config_data
3528+ return Config(config_data)
3529+ except ValueError:
3530+ return None
3531+
3532+
3533+@cached
3534+def relation_get(attribute=None, unit=None, rid=None):
3535+ """Get relation information"""
3536+ _args = ['relation-get', '--format=json']
3537+ if rid:
3538+ _args.append('-r')
3539+ _args.append(rid)
3540+ _args.append(attribute or '-')
3541+ if unit:
3542+ _args.append(unit)
3543+ try:
3544+ return json.loads(subprocess.check_output(_args).decode('UTF-8'))
3545+ except ValueError:
3546+ return None
3547+ except CalledProcessError as e:
3548+ if e.returncode == 2:
3549+ return None
3550+ raise
3551+
3552+
3553+def relation_set(relation_id=None, relation_settings=None, **kwargs):
3554+ """Set relation information for the current unit"""
3555+ relation_settings = relation_settings if relation_settings else {}
3556+ relation_cmd_line = ['relation-set']
3557+ accepts_file = "--file" in subprocess.check_output(
3558+ relation_cmd_line + ["--help"], universal_newlines=True)
3559+ if relation_id is not None:
3560+ relation_cmd_line.extend(('-r', relation_id))
3561+ settings = relation_settings.copy()
3562+ settings.update(kwargs)
3563+ for key, value in settings.items():
3564+ # Force value to be a string: it always should, but some call
3565+ # sites pass in things like dicts or numbers.
3566+ if value is not None:
3567+ settings[key] = "{}".format(value)
3568+ if accepts_file:
3569+ # --file was introduced in Juju 1.23.2. Use it by default if
3570+ # available, since otherwise we'll break if the relation data is
3571+ # too big. Ideally we should tell relation-set to read the data from
3572+ # stdin, but that feature is broken in 1.23.2: Bug #1454678.
3573+ with tempfile.NamedTemporaryFile(delete=False) as settings_file:
3574+ settings_file.write(yaml.safe_dump(settings).encode("utf-8"))
3575+ subprocess.check_call(
3576+ relation_cmd_line + ["--file", settings_file.name])
3577+ os.remove(settings_file.name)
3578+ else:
3579+ for key, value in settings.items():
3580+ if value is None:
3581+ relation_cmd_line.append('{}='.format(key))
3582+ else:
3583+ relation_cmd_line.append('{}={}'.format(key, value))
3584+ subprocess.check_call(relation_cmd_line)
3585+ # Flush cache of any relation-gets for local unit
3586+ flush(local_unit())
3587+
3588+
3589+def relation_clear(r_id=None):
3590+ ''' Clears any relation data already set on relation r_id '''
3591+ settings = relation_get(rid=r_id,
3592+ unit=local_unit())
3593+ for setting in settings:
3594+ if setting not in ['public-address', 'private-address']:
3595+ settings[setting] = None
3596+ relation_set(relation_id=r_id,
3597+ **settings)
3598+
3599+
3600+@cached
3601+def relation_ids(reltype=None):
3602+ """A list of relation_ids"""
3603+ reltype = reltype or relation_type()
3604+ relid_cmd_line = ['relation-ids', '--format=json']
3605+ if reltype is not None:
3606+ relid_cmd_line.append(reltype)
3607+ return json.loads(
3608+ subprocess.check_output(relid_cmd_line).decode('UTF-8')) or []
3609+ return []
3610+
3611+
3612+@cached
3613+def related_units(relid=None):
3614+ """A list of related units"""
3615+ relid = relid or relation_id()
3616+ units_cmd_line = ['relation-list', '--format=json']
3617+ if relid is not None:
3618+ units_cmd_line.extend(('-r', relid))
3619+ return json.loads(
3620+ subprocess.check_output(units_cmd_line).decode('UTF-8')) or []
3621+
3622+
3623+@cached
3624+def relation_for_unit(unit=None, rid=None):
3625+ """Get the json represenation of a unit's relation"""
3626+ unit = unit or remote_unit()
3627+ relation = relation_get(unit=unit, rid=rid)
3628+ for key in relation:
3629+ if key.endswith('-list'):
3630+ relation[key] = relation[key].split()
3631+ relation['__unit__'] = unit
3632+ return relation
3633+
3634+
3635+@cached
3636+def relations_for_id(relid=None):
3637+ """Get relations of a specific relation ID"""
3638+ relation_data = []
3639+ relid = relid or relation_ids()
3640+ for unit in related_units(relid):
3641+ unit_data = relation_for_unit(unit, relid)
3642+ unit_data['__relid__'] = relid
3643+ relation_data.append(unit_data)
3644+ return relation_data
3645+
3646+
3647+@cached
3648+def relations_of_type(reltype=None):
3649+ """Get relations of a specific type"""
3650+ relation_data = []
3651+ reltype = reltype or relation_type()
3652+ for relid in relation_ids(reltype):
3653+ for relation in relations_for_id(relid):
3654+ relation['__relid__'] = relid
3655+ relation_data.append(relation)
3656+ return relation_data
3657+
3658+
3659+@cached
3660+def metadata():
3661+ """Get the current charm metadata.yaml contents as a python object"""
3662+ with open(os.path.join(charm_dir(), 'metadata.yaml')) as md:
3663+ return yaml.safe_load(md)
3664+
3665+
3666+@cached
3667+def relation_types():
3668+ """Get a list of relation types supported by this charm"""
3669+ rel_types = []
3670+ md = metadata()
3671+ for key in ('provides', 'requires', 'peers'):
3672+ section = md.get(key)
3673+ if section:
3674+ rel_types.extend(section.keys())
3675+ return rel_types
3676+
3677+
3678+@cached
3679+def peer_relation_id():
3680+ '''Get the peers relation id if a peers relation has been joined, else None.'''
3681+ md = metadata()
3682+ section = md.get('peers')
3683+ if section:
3684+ for key in section:
3685+ relids = relation_ids(key)
3686+ if relids:
3687+ return relids[0]
3688+ return None
3689+
3690+
3691+@cached
3692+def relation_to_interface(relation_name):
3693+ """
3694+ Given the name of a relation, return the interface that relation uses.
3695+
3696+ :returns: The interface name, or ``None``.
3697+ """
3698+ return relation_to_role_and_interface(relation_name)[1]
3699+
3700+
3701+@cached
3702+def relation_to_role_and_interface(relation_name):
3703+ """
3704+ Given the name of a relation, return the role and the name of the interface
3705+ that relation uses (where role is one of ``provides``, ``requires``, or ``peers``).
3706+
3707+ :returns: A tuple containing ``(role, interface)``, or ``(None, None)``.
3708+ """
3709+ _metadata = metadata()
3710+ for role in ('provides', 'requires', 'peers'):
3711+ interface = _metadata.get(role, {}).get(relation_name, {}).get('interface')
3712+ if interface:
3713+ return role, interface
3714+ return None, None
3715+
3716+
3717+@cached
3718+def role_and_interface_to_relations(role, interface_name):
3719+ """
3720+ Given a role and interface name, return a list of relation names for the
3721+ current charm that use that interface under that role (where role is one
3722+ of ``provides``, ``requires``, or ``peers``).
3723+
3724+ :returns: A list of relation names.
3725+ """
3726+ _metadata = metadata()
3727+ results = []
3728+ for relation_name, relation in _metadata.get(role, {}).items():
3729+ if relation['interface'] == interface_name:
3730+ results.append(relation_name)
3731+ return results
3732+
3733+
3734+@cached
3735+def interface_to_relations(interface_name):
3736+ """
3737+ Given an interface, return a list of relation names for the current
3738+ charm that use that interface.
3739+
3740+ :returns: A list of relation names.
3741+ """
3742+ results = []
3743+ for role in ('provides', 'requires', 'peers'):
3744+ results.extend(role_and_interface_to_relations(role, interface_name))
3745+ return results
3746+
3747+
3748+@cached
3749+def charm_name():
3750+ """Get the name of the current charm as is specified on metadata.yaml"""
3751+ return metadata().get('name')
3752+
3753+
3754+@cached
3755+def relations():
3756+ """Get a nested dictionary of relation data for all related units"""
3757+ rels = {}
3758+ for reltype in relation_types():
3759+ relids = {}
3760+ for relid in relation_ids(reltype):
3761+ units = {local_unit(): relation_get(unit=local_unit(), rid=relid)}
3762+ for unit in related_units(relid):
3763+ reldata = relation_get(unit=unit, rid=relid)
3764+ units[unit] = reldata
3765+ relids[relid] = units
3766+ rels[reltype] = relids
3767+ return rels
3768+
3769+
3770+@cached
3771+def is_relation_made(relation, keys='private-address'):
3772+ '''
3773+ Determine whether a relation is established by checking for
3774+ presence of key(s). If a list of keys is provided, they
3775+ must all be present for the relation to be identified as made
3776+ '''
3777+ if isinstance(keys, str):
3778+ keys = [keys]
3779+ for r_id in relation_ids(relation):
3780+ for unit in related_units(r_id):
3781+ context = {}
3782+ for k in keys:
3783+ context[k] = relation_get(k, rid=r_id,
3784+ unit=unit)
3785+ if None not in context.values():
3786+ return True
3787+ return False
3788+
3789+
3790+def open_port(port, protocol="TCP"):
3791+ """Open a service network port"""
3792+ _args = ['open-port']
3793+ _args.append('{}/{}'.format(port, protocol))
3794+ subprocess.check_call(_args)
3795+
3796+
3797+def close_port(port, protocol="TCP"):
3798+ """Close a service network port"""
3799+ _args = ['close-port']
3800+ _args.append('{}/{}'.format(port, protocol))
3801+ subprocess.check_call(_args)
3802+
3803+
3804+@cached
3805+def unit_get(attribute):
3806+ """Get the unit ID for the remote unit"""
3807+ _args = ['unit-get', '--format=json', attribute]
3808+ try:
3809+ return json.loads(subprocess.check_output(_args).decode('UTF-8'))
3810+ except ValueError:
3811+ return None
3812+
3813+
3814+def unit_public_ip():
3815+ """Get this unit's public IP address"""
3816+ return unit_get('public-address')
3817+
3818+
3819+def unit_private_ip():
3820+ """Get this unit's private IP address"""
3821+ return unit_get('private-address')
3822+
3823+
3824+@cached
3825+def storage_get(attribute=None, storage_id=None):
3826+ """Get storage attributes"""
3827+ _args = ['storage-get', '--format=json']
3828+ if storage_id:
3829+ _args.extend(('-s', storage_id))
3830+ if attribute:
3831+ _args.append(attribute)
3832+ try:
3833+ return json.loads(subprocess.check_output(_args).decode('UTF-8'))
3834+ except ValueError:
3835+ return None
3836+
3837+
3838+@cached
3839+def storage_list(storage_name=None):
3840+ """List the storage IDs for the unit"""
3841+ _args = ['storage-list', '--format=json']
3842+ if storage_name:
3843+ _args.append(storage_name)
3844+ try:
3845+ return json.loads(subprocess.check_output(_args).decode('UTF-8'))
3846+ except ValueError:
3847+ return None
3848+ except OSError as e:
3849+ import errno
3850+ if e.errno == errno.ENOENT:
3851+ # storage-list does not exist
3852+ return []
3853+ raise
3854+
3855+
3856+class UnregisteredHookError(Exception):
3857+ """Raised when an undefined hook is called"""
3858+ pass
3859+
3860+
3861+class Hooks(object):
3862+ """A convenient handler for hook functions.
3863+
3864+ Example::
3865+
3866+ hooks = Hooks()
3867+
3868+ # register a hook, taking its name from the function name
3869+ @hooks.hook()
3870+ def install():
3871+ pass # your code here
3872+
3873+ # register a hook, providing a custom hook name
3874+ @hooks.hook("config-changed")
3875+ def config_changed():
3876+ pass # your code here
3877+
3878+ if __name__ == "__main__":
3879+ # execute a hook based on the name the program is called by
3880+ hooks.execute(sys.argv)
3881+ """
3882+
3883+ def __init__(self, config_save=None):
3884+ super(Hooks, self).__init__()
3885+ self._hooks = {}
3886+
3887+ # For unknown reasons, we allow the Hooks constructor to override
3888+ # config().implicit_save.
3889+ if config_save is not None:
3890+ config().implicit_save = config_save
3891+
3892+ def register(self, name, function):
3893+ """Register a hook"""
3894+ self._hooks[name] = function
3895+
3896+ def execute(self, args):
3897+ """Execute a registered hook based on args[0]"""
3898+ _run_atstart()
3899+ hook_name = os.path.basename(args[0])
3900+ if hook_name in self._hooks:
3901+ try:
3902+ self._hooks[hook_name]()
3903+ except SystemExit as x:
3904+ if x.code is None or x.code == 0:
3905+ _run_atexit()
3906+ raise
3907+ _run_atexit()
3908+ else:
3909+ raise UnregisteredHookError(hook_name)
3910+
3911+ def hook(self, *hook_names):
3912+ """Decorator, registering them as hooks"""
3913+ def wrapper(decorated):
3914+ for hook_name in hook_names:
3915+ self.register(hook_name, decorated)
3916+ else:
3917+ self.register(decorated.__name__, decorated)
3918+ if '_' in decorated.__name__:
3919+ self.register(
3920+ decorated.__name__.replace('_', '-'), decorated)
3921+ return decorated
3922+ return wrapper
3923+
3924+
3925+def charm_dir():
3926+ """Return the root directory of the current charm"""
3927+ return os.environ.get('CHARM_DIR')
3928+
3929+
3930+@cached
3931+def action_get(key=None):
3932+ """Gets the value of an action parameter, or all key/value param pairs"""
3933+ cmd = ['action-get']
3934+ if key is not None:
3935+ cmd.append(key)
3936+ cmd.append('--format=json')
3937+ action_data = json.loads(subprocess.check_output(cmd).decode('UTF-8'))
3938+ return action_data
3939+
3940+
3941+def action_set(values):
3942+ """Sets the values to be returned after the action finishes"""
3943+ cmd = ['action-set']
3944+ for k, v in list(values.items()):
3945+ cmd.append('{}={}'.format(k, v))
3946+ subprocess.check_call(cmd)
3947+
3948+
3949+def action_fail(message):
3950+ """Sets the action status to failed and sets the error message.
3951+
3952+ The results set by action_set are preserved."""
3953+ subprocess.check_call(['action-fail', message])
3954+
3955+
3956+def action_name():
3957+ """Get the name of the currently executing action."""
3958+ return os.environ.get('JUJU_ACTION_NAME')
3959+
3960+
3961+def action_uuid():
3962+ """Get the UUID of the currently executing action."""
3963+ return os.environ.get('JUJU_ACTION_UUID')
3964+
3965+
3966+def action_tag():
3967+ """Get the tag for the currently executing action."""
3968+ return os.environ.get('JUJU_ACTION_TAG')
3969+
3970+
3971+def status_set(workload_state, message):
3972+ """Set the workload state with a message
3973+
3974+ Use status-set to set the workload state with a message which is visible
3975+ to the user via juju status. If the status-set command is not found then
3976+ assume this is juju < 1.23 and juju-log the message unstead.
3977+
3978+ workload_state -- valid juju workload state.
3979+ message -- status update message
3980+ """
3981+ valid_states = ['maintenance', 'blocked', 'waiting', 'active']
3982+ if workload_state not in valid_states:
3983+ raise ValueError(
3984+ '{!r} is not a valid workload state'.format(workload_state)
3985+ )
3986+ cmd = ['status-set', workload_state, message]
3987+ try:
3988+ ret = subprocess.call(cmd)
3989+ if ret == 0:
3990+ return
3991+ except OSError as e:
3992+ if e.errno != errno.ENOENT:
3993+ raise
3994+ log_message = 'status-set failed: {} {}'.format(workload_state,
3995+ message)
3996+ log(log_message, level='INFO')
3997+
3998+
3999+def status_get():
4000+ """Retrieve the previously set juju workload state and message
4001+
4002+ If the status-get command is not found then assume this is juju < 1.23 and
4003+ return 'unknown', ""
4004+
4005+ """
4006+ cmd = ['status-get', "--format=json", "--include-data"]
4007+ try:
4008+ raw_status = subprocess.check_output(cmd)
4009+ except OSError as e:
4010+ if e.errno == errno.ENOENT:
4011+ return ('unknown', "")
4012+ else:
4013+ raise
4014+ else:
4015+ status = json.loads(raw_status.decode("UTF-8"))
4016+ return (status["status"], status["message"])
4017+
4018+
4019+def translate_exc(from_exc, to_exc):
4020+ def inner_translate_exc1(f):
4021+ @wraps(f)
4022+ def inner_translate_exc2(*args, **kwargs):
4023+ try:
4024+ return f(*args, **kwargs)
4025+ except from_exc:
4026+ raise to_exc
4027+
4028+ return inner_translate_exc2
4029+
4030+ return inner_translate_exc1
4031+
4032+
4033+@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
4034+def is_leader():
4035+ """Does the current unit hold the juju leadership
4036+
4037+ Uses juju to determine whether the current unit is the leader of its peers
4038+ """
4039+ cmd = ['is-leader', '--format=json']
4040+ return json.loads(subprocess.check_output(cmd).decode('UTF-8'))
4041+
4042+
4043+@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
4044+def leader_get(attribute=None):
4045+ """Juju leader get value(s)"""
4046+ cmd = ['leader-get', '--format=json'] + [attribute or '-']
4047+ return json.loads(subprocess.check_output(cmd).decode('UTF-8'))
4048+
4049+
4050+@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
4051+def leader_set(settings=None, **kwargs):
4052+ """Juju leader set value(s)"""
4053+ # Don't log secrets.
4054+ # log("Juju leader-set '%s'" % (settings), level=DEBUG)
4055+ cmd = ['leader-set']
4056+ settings = settings or {}
4057+ settings.update(kwargs)
4058+ for k, v in settings.items():
4059+ if v is None:
4060+ cmd.append('{}='.format(k))
4061+ else:
4062+ cmd.append('{}={}'.format(k, v))
4063+ subprocess.check_call(cmd)
4064+
4065+
4066+@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
4067+def payload_register(ptype, klass, pid):
4068+ """ is used while a hook is running to let Juju know that a
4069+ payload has been started."""
4070+ cmd = ['payload-register']
4071+ for x in [ptype, klass, pid]:
4072+ cmd.append(x)
4073+ subprocess.check_call(cmd)
4074+
4075+
4076+@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
4077+def payload_unregister(klass, pid):
4078+ """ is used while a hook is running to let Juju know
4079+ that a payload has been manually stopped. The <class> and <id> provided
4080+ must match a payload that has been previously registered with juju using
4081+ payload-register."""
4082+ cmd = ['payload-unregister']
4083+ for x in [klass, pid]:
4084+ cmd.append(x)
4085+ subprocess.check_call(cmd)
4086+
4087+
4088+@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
4089+def payload_status_set(klass, pid, status):
4090+ """is used to update the current status of a registered payload.
4091+ The <class> and <id> provided must match a payload that has been previously
4092+ registered with juju using payload-register. The <status> must be one of the
4093+ follow: starting, started, stopping, stopped"""
4094+ cmd = ['payload-status-set']
4095+ for x in [klass, pid, status]:
4096+ cmd.append(x)
4097+ subprocess.check_call(cmd)
4098+
4099+
4100+@cached
4101+def juju_version():
4102+ """Full version string (eg. '1.23.3.1-trusty-amd64')"""
4103+ # Per https://bugs.launchpad.net/juju-core/+bug/1455368/comments/1
4104+ jujud = glob.glob('/var/lib/juju/tools/machine-*/jujud')[0]
4105+ return subprocess.check_output([jujud, 'version'],
4106+ universal_newlines=True).strip()
4107+
4108+
4109+@cached
4110+def has_juju_version(minimum_version):
4111+ """Return True if the Juju version is at least the provided version"""
4112+ return LooseVersion(juju_version()) >= LooseVersion(minimum_version)
4113+
4114+
4115+_atexit = []
4116+_atstart = []
4117+
4118+
4119+def atstart(callback, *args, **kwargs):
4120+ '''Schedule a callback to run before the main hook.
4121+
4122+ Callbacks are run in the order they were added.
4123+
4124+ This is useful for modules and classes to perform initialization
4125+ and inject behavior. In particular:
4126+
4127+ - Run common code before all of your hooks, such as logging
4128+ the hook name or interesting relation data.
4129+ - Defer object or module initialization that requires a hook
4130+ context until we know there actually is a hook context,
4131+ making testing easier.
4132+ - Rather than requiring charm authors to include boilerplate to
4133+ invoke your helper's behavior, have it run automatically if
4134+ your object is instantiated or module imported.
4135+
4136+ This is not at all useful after your hook framework as been launched.
4137+ '''
4138+ global _atstart
4139+ _atstart.append((callback, args, kwargs))
4140+
4141+
4142+def atexit(callback, *args, **kwargs):
4143+ '''Schedule a callback to run on successful hook completion.
4144+
4145+ Callbacks are run in the reverse order that they were added.'''
4146+ _atexit.append((callback, args, kwargs))
4147+
4148+
4149+def _run_atstart():
4150+ '''Hook frameworks must invoke this before running the main hook body.'''
4151+ global _atstart
4152+ for callback, args, kwargs in _atstart:
4153+ callback(*args, **kwargs)
4154+ del _atstart[:]
4155+
4156+
4157+def _run_atexit():
4158+ '''Hook frameworks must invoke this after the main hook body has
4159+ successfully completed. Do not invoke it if the hook fails.'''
4160+ global _atexit
4161+ for callback, args, kwargs in reversed(_atexit):
4162+ callback(*args, **kwargs)
4163+ del _atexit[:]
4164
4165=== added file 'charmhelpers/core/host.py'
4166--- charmhelpers/core/host.py 1970-01-01 00:00:00 +0000
4167+++ charmhelpers/core/host.py 2016-03-10 22:55:31 +0000
4168@@ -0,0 +1,659 @@
4169+# Copyright 2014-2015 Canonical Limited.
4170+#
4171+# This file is part of charm-helpers.
4172+#
4173+# charm-helpers is free software: you can redistribute it and/or modify
4174+# it under the terms of the GNU Lesser General Public License version 3 as
4175+# published by the Free Software Foundation.
4176+#
4177+# charm-helpers is distributed in the hope that it will be useful,
4178+# but WITHOUT ANY WARRANTY; without even the implied warranty of
4179+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
4180+# GNU Lesser General Public License for more details.
4181+#
4182+# You should have received a copy of the GNU Lesser General Public License
4183+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
4184+
4185+"""Tools for working with the host system"""
4186+# Copyright 2012 Canonical Ltd.
4187+#
4188+# Authors:
4189+# Nick Moffitt <nick.moffitt@canonical.com>
4190+# Matthew Wedgwood <matthew.wedgwood@canonical.com>
4191+
4192+import os
4193+import re
4194+import pwd
4195+import glob
4196+import grp
4197+import random
4198+import string
4199+import subprocess
4200+import hashlib
4201+from contextlib import contextmanager
4202+from collections import OrderedDict
4203+
4204+import six
4205+
4206+from .hookenv import log
4207+from .fstab import Fstab
4208+
4209+
4210+def service_start(service_name):
4211+ """Start a system service"""
4212+ return service('start', service_name)
4213+
4214+
4215+def service_stop(service_name):
4216+ """Stop a system service"""
4217+ return service('stop', service_name)
4218+
4219+
4220+def service_restart(service_name):
4221+ """Restart a system service"""
4222+ return service('restart', service_name)
4223+
4224+
4225+def service_reload(service_name, restart_on_failure=False):
4226+ """Reload a system service, optionally falling back to restart if
4227+ reload fails"""
4228+ service_result = service('reload', service_name)
4229+ if not service_result and restart_on_failure:
4230+ service_result = service('restart', service_name)
4231+ return service_result
4232+
4233+
4234+def service_pause(service_name, init_dir="/etc/init", initd_dir="/etc/init.d"):
4235+ """Pause a system service.
4236+
4237+ Stop it, and prevent it from starting again at boot."""
4238+ stopped = True
4239+ if service_running(service_name):
4240+ stopped = service_stop(service_name)
4241+ upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))
4242+ sysv_file = os.path.join(initd_dir, service_name)
4243+ if init_is_systemd():
4244+ service('disable', service_name)
4245+ elif os.path.exists(upstart_file):
4246+ override_path = os.path.join(
4247+ init_dir, '{}.override'.format(service_name))
4248+ with open(override_path, 'w') as fh:
4249+ fh.write("manual\n")
4250+ elif os.path.exists(sysv_file):
4251+ subprocess.check_call(["update-rc.d", service_name, "disable"])
4252+ else:
4253+ raise ValueError(
4254+ "Unable to detect {0} as SystemD, Upstart {1} or"
4255+ " SysV {2}".format(
4256+ service_name, upstart_file, sysv_file))
4257+ return stopped
4258+
4259+
4260+def service_resume(service_name, init_dir="/etc/init",
4261+ initd_dir="/etc/init.d"):
4262+ """Resume a system service.
4263+
4264+ Reenable starting again at boot. Start the service"""
4265+ upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))
4266+ sysv_file = os.path.join(initd_dir, service_name)
4267+ if init_is_systemd():
4268+ service('enable', service_name)
4269+ elif os.path.exists(upstart_file):
4270+ override_path = os.path.join(
4271+ init_dir, '{}.override'.format(service_name))
4272+ if os.path.exists(override_path):
4273+ os.unlink(override_path)
4274+ elif os.path.exists(sysv_file):
4275+ subprocess.check_call(["update-rc.d", service_name, "enable"])
4276+ else:
4277+ raise ValueError(
4278+ "Unable to detect {0} as SystemD, Upstart {1} or"
4279+ " SysV {2}".format(
4280+ service_name, upstart_file, sysv_file))
4281+
4282+ started = service_running(service_name)
4283+ if not started:
4284+ started = service_start(service_name)
4285+ return started
4286+
4287+
4288+def service(action, service_name):
4289+ """Control a system service"""
4290+ if init_is_systemd():
4291+ cmd = ['systemctl', action, service_name]
4292+ else:
4293+ cmd = ['service', service_name, action]
4294+ return subprocess.call(cmd) == 0
4295+
4296+
4297+def service_running(service_name):
4298+ """Determine whether a system service is running"""
4299+ if init_is_systemd():
4300+ return service('is-active', service_name)
4301+ else:
4302+ try:
4303+ output = subprocess.check_output(
4304+ ['service', service_name, 'status'],
4305+ stderr=subprocess.STDOUT).decode('UTF-8')
4306+ except subprocess.CalledProcessError:
4307+ return False
4308+ else:
4309+ if ("start/running" in output or "is running" in output or
4310+ "up and running" in output):
4311+ return True
4312+ else:
4313+ return False
4314+
4315+
4316+def service_available(service_name):
4317+ """Determine whether a system service is available"""
4318+ try:
4319+ subprocess.check_output(
4320+ ['service', service_name, 'status'],
4321+ stderr=subprocess.STDOUT).decode('UTF-8')
4322+ except subprocess.CalledProcessError as e:
4323+ return b'unrecognized service' not in e.output
4324+ else:
4325+ return True
4326+
4327+
4328+SYSTEMD_SYSTEM = '/run/systemd/system'
4329+
4330+
4331+def init_is_systemd():
4332+ return os.path.isdir(SYSTEMD_SYSTEM)
4333+
4334+
4335+def adduser(username, password=None, shell='/bin/bash', system_user=False,
4336+ primary_group=None, secondary_groups=None):
4337+ """
4338+ Add a user to the system.
4339+
4340+ Will log but otherwise succeed if the user already exists.
4341+
4342+ :param str username: Username to create
4343+ :param str password: Password for user; if ``None``, create a system user
4344+ :param str shell: The default shell for the user
4345+ :param bool system_user: Whether to create a login or system user
4346+ :param str primary_group: Primary group for user; defaults to their username
4347+ :param list secondary_groups: Optional list of additional groups
4348+
4349+ :returns: The password database entry struct, as returned by `pwd.getpwnam`
4350+ """
4351+ try:
4352+ user_info = pwd.getpwnam(username)
4353+ log('user {0} already exists!'.format(username))
4354+ except KeyError:
4355+ log('creating user {0}'.format(username))
4356+ cmd = ['useradd']
4357+ if system_user or password is None:
4358+ cmd.append('--system')
4359+ else:
4360+ cmd.extend([
4361+ '--create-home',
4362+ '--shell', shell,
4363+ '--password', password,
4364+ ])
4365+ if not primary_group:
4366+ try:
4367+ grp.getgrnam(username)
4368+ primary_group = username # avoid "group exists" error
4369+ except KeyError:
4370+ pass
4371+ if primary_group:
4372+ cmd.extend(['-g', primary_group])
4373+ if secondary_groups:
4374+ cmd.extend(['-G', ','.join(secondary_groups)])
4375+ cmd.append(username)
4376+ subprocess.check_call(cmd)
4377+ user_info = pwd.getpwnam(username)
4378+ return user_info
4379+
4380+
4381+def user_exists(username):
4382+ """Check if a user exists"""
4383+ try:
4384+ pwd.getpwnam(username)
4385+ user_exists = True
4386+ except KeyError:
4387+ user_exists = False
4388+ return user_exists
4389+
4390+
4391+def add_group(group_name, system_group=False):
4392+ """Add a group to the system"""
4393+ try:
4394+ group_info = grp.getgrnam(group_name)
4395+ log('group {0} already exists!'.format(group_name))
4396+ except KeyError:
4397+ log('creating group {0}'.format(group_name))
4398+ cmd = ['addgroup']
4399+ if system_group:
4400+ cmd.append('--system')
4401+ else:
4402+ cmd.extend([
4403+ '--group',
4404+ ])
4405+ cmd.append(group_name)
4406+ subprocess.check_call(cmd)
4407+ group_info = grp.getgrnam(group_name)
4408+ return group_info
4409+
4410+
4411+def add_user_to_group(username, group):
4412+ """Add a user to a group"""
4413+ cmd = ['gpasswd', '-a', username, group]
4414+ log("Adding user {} to group {}".format(username, group))
4415+ subprocess.check_call(cmd)
4416+
4417+
4418+def rsync(from_path, to_path, flags='-r', options=None):
4419+ """Replicate the contents of a path"""
4420+ options = options or ['--delete', '--executability']
4421+ cmd = ['/usr/bin/rsync', flags]
4422+ cmd.extend(options)
4423+ cmd.append(from_path)
4424+ cmd.append(to_path)
4425+ log(" ".join(cmd))
4426+ return subprocess.check_output(cmd).decode('UTF-8').strip()
4427+
4428+
4429+def symlink(source, destination):
4430+ """Create a symbolic link"""
4431+ log("Symlinking {} as {}".format(source, destination))
4432+ cmd = [
4433+ 'ln',
4434+ '-sf',
4435+ source,
4436+ destination,
4437+ ]
4438+ subprocess.check_call(cmd)
4439+
4440+
4441+def mkdir(path, owner='root', group='root', perms=0o555, force=False):
4442+ """Create a directory"""
4443+ log("Making dir {} {}:{} {:o}".format(path, owner, group,
4444+ perms))
4445+ uid = pwd.getpwnam(owner).pw_uid
4446+ gid = grp.getgrnam(group).gr_gid
4447+ realpath = os.path.abspath(path)
4448+ path_exists = os.path.exists(realpath)
4449+ if path_exists and force:
4450+ if not os.path.isdir(realpath):
4451+ log("Removing non-directory file {} prior to mkdir()".format(path))
4452+ os.unlink(realpath)
4453+ os.makedirs(realpath, perms)
4454+ elif not path_exists:
4455+ os.makedirs(realpath, perms)
4456+ os.chown(realpath, uid, gid)
4457+ os.chmod(realpath, perms)
4458+
4459+
4460+def write_file(path, content, owner='root', group='root', perms=0o444):
4461+ """Create or overwrite a file with the contents of a byte string."""
4462+ log("Writing file {} {}:{} {:o}".format(path, owner, group, perms))
4463+ uid = pwd.getpwnam(owner).pw_uid
4464+ gid = grp.getgrnam(group).gr_gid
4465+ with open(path, 'wb') as target:
4466+ os.fchown(target.fileno(), uid, gid)
4467+ os.fchmod(target.fileno(), perms)
4468+ target.write(content)
4469+
4470+
4471+def fstab_remove(mp):
4472+ """Remove the given mountpoint entry from /etc/fstab
4473+ """
4474+ return Fstab.remove_by_mountpoint(mp)
4475+
4476+
4477+def fstab_add(dev, mp, fs, options=None):
4478+ """Adds the given device entry to the /etc/fstab file
4479+ """
4480+ return Fstab.add(dev, mp, fs, options=options)
4481+
4482+
4483+def mount(device, mountpoint, options=None, persist=False, filesystem="ext3"):
4484+ """Mount a filesystem at a particular mountpoint"""
4485+ cmd_args = ['mount']
4486+ if options is not None:
4487+ cmd_args.extend(['-o', options])
4488+ cmd_args.extend([device, mountpoint])
4489+ try:
4490+ subprocess.check_output(cmd_args)
4491+ except subprocess.CalledProcessError as e:
4492+ log('Error mounting {} at {}\n{}'.format(device, mountpoint, e.output))
4493+ return False
4494+
4495+ if persist:
4496+ return fstab_add(device, mountpoint, filesystem, options=options)
4497+ return True
4498+
4499+
4500+def umount(mountpoint, persist=False):
4501+ """Unmount a filesystem"""
4502+ cmd_args = ['umount', mountpoint]
4503+ try:
4504+ subprocess.check_output(cmd_args)
4505+ except subprocess.CalledProcessError as e:
4506+ log('Error unmounting {}\n{}'.format(mountpoint, e.output))
4507+ return False
4508+
4509+ if persist:
4510+ return fstab_remove(mountpoint)
4511+ return True
4512+
4513+
4514+def mounts():
4515+ """Get a list of all mounted volumes as [[mountpoint,device],[...]]"""
4516+ with open('/proc/mounts') as f:
4517+ # [['/mount/point','/dev/path'],[...]]
4518+ system_mounts = [m[1::-1] for m in [l.strip().split()
4519+ for l in f.readlines()]]
4520+ return system_mounts
4521+
4522+
4523+def fstab_mount(mountpoint):
4524+ """Mount filesystem using fstab"""
4525+ cmd_args = ['mount', mountpoint]
4526+ try:
4527+ subprocess.check_output(cmd_args)
4528+ except subprocess.CalledProcessError as e:
4529+ log('Error unmounting {}\n{}'.format(mountpoint, e.output))
4530+ return False
4531+ return True
4532+
4533+
4534+def file_hash(path, hash_type='md5'):
4535+ """
4536+ Generate a hash checksum of the contents of 'path' or None if not found.
4537+
4538+ :param str hash_type: Any hash alrgorithm supported by :mod:`hashlib`,
4539+ such as md5, sha1, sha256, sha512, etc.
4540+ """
4541+ if os.path.exists(path):
4542+ h = getattr(hashlib, hash_type)()
4543+ with open(path, 'rb') as source:
4544+ h.update(source.read())
4545+ return h.hexdigest()
4546+ else:
4547+ return None
4548+
4549+
4550+def path_hash(path):
4551+ """
4552+ Generate a hash checksum of all files matching 'path'. Standard wildcards
4553+ like '*' and '?' are supported, see documentation for the 'glob' module for
4554+ more information.
4555+
4556+ :return: dict: A { filename: hash } dictionary for all matched files.
4557+ Empty if none found.
4558+ """
4559+ return {
4560+ filename: file_hash(filename)
4561+ for filename in glob.iglob(path)
4562+ }
4563+
4564+
4565+def check_hash(path, checksum, hash_type='md5'):
4566+ """
4567+ Validate a file using a cryptographic checksum.
4568+
4569+ :param str checksum: Value of the checksum used to validate the file.
4570+ :param str hash_type: Hash algorithm used to generate `checksum`.
4571+ Can be any hash alrgorithm supported by :mod:`hashlib`,
4572+ such as md5, sha1, sha256, sha512, etc.
4573+ :raises ChecksumError: If the file fails the checksum
4574+
4575+ """
4576+ actual_checksum = file_hash(path, hash_type)
4577+ if checksum != actual_checksum:
4578+ raise ChecksumError("'%s' != '%s'" % (checksum, actual_checksum))
4579+
4580+
4581+class ChecksumError(ValueError):
4582+ pass
4583+
4584+
4585+def restart_on_change(restart_map, stopstart=False):
4586+ """Restart services based on configuration files changing
4587+
4588+ This function is used a decorator, for example::
4589+
4590+ @restart_on_change({
4591+ '/etc/ceph/ceph.conf': [ 'cinder-api', 'cinder-volume' ]
4592+ '/etc/apache/sites-enabled/*': [ 'apache2' ]
4593+ })
4594+ def config_changed():
4595+ pass # your code here
4596+
4597+ In this example, the cinder-api and cinder-volume services
4598+ would be restarted if /etc/ceph/ceph.conf is changed by the
4599+ ceph_client_changed function. The apache2 service would be
4600+ restarted if any file matching the pattern got changed, created
4601+ or removed. Standard wildcards are supported, see documentation
4602+ for the 'glob' module for more information.
4603+ """
4604+ def wrap(f):
4605+ def wrapped_f(*args, **kwargs):
4606+ checksums = {path: path_hash(path) for path in restart_map}
4607+ f(*args, **kwargs)
4608+ restarts = []
4609+ for path in restart_map:
4610+ if path_hash(path) != checksums[path]:
4611+ restarts += restart_map[path]
4612+ services_list = list(OrderedDict.fromkeys(restarts))
4613+ if not stopstart:
4614+ for service_name in services_list:
4615+ service('restart', service_name)
4616+ else:
4617+ for action in ['stop', 'start']:
4618+ for service_name in services_list:
4619+ service(action, service_name)
4620+ return wrapped_f
4621+ return wrap
4622+
4623+
4624+def lsb_release():
4625+ """Return /etc/lsb-release in a dict"""
4626+ d = {}
4627+ with open('/etc/lsb-release', 'r') as lsb:
4628+ for l in lsb:
4629+ k, v = l.split('=')
4630+ d[k.strip()] = v.strip()
4631+ return d
4632+
4633+
4634+def pwgen(length=None):
4635+ """Generate a random pasword."""
4636+ if length is None:
4637+ # A random length is ok to use a weak PRNG
4638+ length = random.choice(range(35, 45))
4639+ alphanumeric_chars = [
4640+ l for l in (string.ascii_letters + string.digits)
4641+ if l not in 'l0QD1vAEIOUaeiou']
4642+ # Use a crypto-friendly PRNG (e.g. /dev/urandom) for making the
4643+ # actual password
4644+ random_generator = random.SystemRandom()
4645+ random_chars = [
4646+ random_generator.choice(alphanumeric_chars) for _ in range(length)]
4647+ return(''.join(random_chars))
4648+
4649+
4650+def is_phy_iface(interface):
4651+ """Returns True if interface is not virtual, otherwise False."""
4652+ if interface:
4653+ sys_net = '/sys/class/net'
4654+ if os.path.isdir(sys_net):
4655+ for iface in glob.glob(os.path.join(sys_net, '*')):
4656+ if '/virtual/' in os.path.realpath(iface):
4657+ continue
4658+
4659+ if interface == os.path.basename(iface):
4660+ return True
4661+
4662+ return False
4663+
4664+
4665+def get_bond_master(interface):
4666+ """Returns bond master if interface is bond slave otherwise None.
4667+
4668+ NOTE: the provided interface is expected to be physical
4669+ """
4670+ if interface:
4671+ iface_path = '/sys/class/net/%s' % (interface)
4672+ if os.path.exists(iface_path):
4673+ if '/virtual/' in os.path.realpath(iface_path):
4674+ return None
4675+
4676+ master = os.path.join(iface_path, 'master')
4677+ if os.path.exists(master):
4678+ master = os.path.realpath(master)
4679+ # make sure it is a bond master
4680+ if os.path.exists(os.path.join(master, 'bonding')):
4681+ return os.path.basename(master)
4682+
4683+ return None
4684+
4685+
4686+def list_nics(nic_type=None):
4687+ '''Return a list of nics of given type(s)'''
4688+ if isinstance(nic_type, six.string_types):
4689+ int_types = [nic_type]
4690+ else:
4691+ int_types = nic_type
4692+
4693+ interfaces = []
4694+ if nic_type:
4695+ for int_type in int_types:
4696+ cmd = ['ip', 'addr', 'show', 'label', int_type + '*']
4697+ ip_output = subprocess.check_output(cmd).decode('UTF-8')
4698+ ip_output = ip_output.split('\n')
4699+ ip_output = (line for line in ip_output if line)
4700+ for line in ip_output:
4701+ if line.split()[1].startswith(int_type):
4702+ matched = re.search('.*: (' + int_type +
4703+ r'[0-9]+\.[0-9]+)@.*', line)
4704+ if matched:
4705+ iface = matched.groups()[0]
4706+ else:
4707+ iface = line.split()[1].replace(":", "")
4708+
4709+ if iface not in interfaces:
4710+ interfaces.append(iface)
4711+ else:
4712+ cmd = ['ip', 'a']
4713+ ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n')
4714+ ip_output = (line.strip() for line in ip_output if line)
4715+
4716+ key = re.compile('^[0-9]+:\s+(.+):')
4717+ for line in ip_output:
4718+ matched = re.search(key, line)
4719+ if matched:
4720+ iface = matched.group(1)
4721+ iface = iface.partition("@")[0]
4722+ if iface not in interfaces:
4723+ interfaces.append(iface)
4724+
4725+ return interfaces
4726+
4727+
4728+def set_nic_mtu(nic, mtu):
4729+ '''Set MTU on a network interface'''
4730+ cmd = ['ip', 'link', 'set', nic, 'mtu', mtu]
4731+ subprocess.check_call(cmd)
4732+
4733+
4734+def get_nic_mtu(nic):
4735+ cmd = ['ip', 'addr', 'show', nic]
4736+ ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n')
4737+ mtu = ""
4738+ for line in ip_output:
4739+ words = line.split()
4740+ if 'mtu' in words:
4741+ mtu = words[words.index("mtu") + 1]
4742+ return mtu
4743+
4744+
4745+def get_nic_hwaddr(nic):
4746+ cmd = ['ip', '-o', '-0', 'addr', 'show', nic]
4747+ ip_output = subprocess.check_output(cmd).decode('UTF-8')
4748+ hwaddr = ""
4749+ words = ip_output.split()
4750+ if 'link/ether' in words:
4751+ hwaddr = words[words.index('link/ether') + 1]
4752+ return hwaddr
4753+
4754+
4755+def cmp_pkgrevno(package, revno, pkgcache=None):
4756+ '''Compare supplied revno with the revno of the installed package
4757+
4758+ * 1 => Installed revno is greater than supplied arg
4759+ * 0 => Installed revno is the same as supplied arg
4760+ * -1 => Installed revno is less than supplied arg
4761+
4762+ This function imports apt_cache function from charmhelpers.fetch if
4763+ the pkgcache argument is None. Be sure to add charmhelpers.fetch if
4764+ you call this function, or pass an apt_pkg.Cache() instance.
4765+ '''
4766+ import apt_pkg
4767+ if not pkgcache:
4768+ from charmhelpers.fetch import apt_cache
4769+ pkgcache = apt_cache()
4770+ pkg = pkgcache[package]
4771+ return apt_pkg.version_compare(pkg.current_ver.ver_str, revno)
4772+
4773+
4774+@contextmanager
4775+def chdir(d):
4776+ cur = os.getcwd()
4777+ try:
4778+ yield os.chdir(d)
4779+ finally:
4780+ os.chdir(cur)
4781+
4782+
4783+def chownr(path, owner, group, follow_links=True, chowntopdir=False):
4784+ """
4785+ Recursively change user and group ownership of files and directories
4786+ in given path. Doesn't chown path itself by default, only its children.
4787+
4788+ :param bool follow_links: Also Chown links if True
4789+ :param bool chowntopdir: Also chown path itself if True
4790+ """
4791+ uid = pwd.getpwnam(owner).pw_uid
4792+ gid = grp.getgrnam(group).gr_gid
4793+ if follow_links:
4794+ chown = os.chown
4795+ else:
4796+ chown = os.lchown
4797+
4798+ if chowntopdir:
4799+ broken_symlink = os.path.lexists(path) and not os.path.exists(path)
4800+ if not broken_symlink:
4801+ chown(path, uid, gid)
4802+ for root, dirs, files in os.walk(path):
4803+ for name in dirs + files:
4804+ full = os.path.join(root, name)
4805+ broken_symlink = os.path.lexists(full) and not os.path.exists(full)
4806+ if not broken_symlink:
4807+ chown(full, uid, gid)
4808+
4809+
4810+def lchownr(path, owner, group):
4811+ chownr(path, owner, group, follow_links=False)
4812+
4813+
4814+def get_total_ram():
4815+ '''The total amount of system RAM in bytes.
4816+
4817+ This is what is reported by the OS, and may be overcommitted when
4818+ there are multiple containers hosted on the same machine.
4819+ '''
4820+ with open('/proc/meminfo', 'r') as f:
4821+ for line in f.readlines():
4822+ if line:
4823+ key, value, unit = line.split()
4824+ if key == 'MemTotal:':
4825+ assert unit == 'kB', 'Unknown unit'
4826+ return int(value) * 1024 # Classic, not KiB.
4827+ raise NotImplementedError()
4828
4829=== added file 'charmhelpers/core/hugepage.py'
4830--- charmhelpers/core/hugepage.py 1970-01-01 00:00:00 +0000
4831+++ charmhelpers/core/hugepage.py 2016-03-10 22:55:31 +0000
4832@@ -0,0 +1,71 @@
4833+# -*- coding: utf-8 -*-
4834+
4835+# Copyright 2014-2015 Canonical Limited.
4836+#
4837+# This file is part of charm-helpers.
4838+#
4839+# charm-helpers is free software: you can redistribute it and/or modify
4840+# it under the terms of the GNU Lesser General Public License version 3 as
4841+# published by the Free Software Foundation.
4842+#
4843+# charm-helpers is distributed in the hope that it will be useful,
4844+# but WITHOUT ANY WARRANTY; without even the implied warranty of
4845+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
4846+# GNU Lesser General Public License for more details.
4847+#
4848+# You should have received a copy of the GNU Lesser General Public License
4849+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
4850+
4851+import yaml
4852+from charmhelpers.core import fstab
4853+from charmhelpers.core import sysctl
4854+from charmhelpers.core.host import (
4855+ add_group,
4856+ add_user_to_group,
4857+ fstab_mount,
4858+ mkdir,
4859+)
4860+from charmhelpers.core.strutils import bytes_from_string
4861+from subprocess import check_output
4862+
4863+
4864+def hugepage_support(user, group='hugetlb', nr_hugepages=256,
4865+ max_map_count=65536, mnt_point='/run/hugepages/kvm',
4866+ pagesize='2MB', mount=True, set_shmmax=False):
4867+ """Enable hugepages on system.
4868+
4869+ Args:
4870+ user (str) -- Username to allow access to hugepages to
4871+ group (str) -- Group name to own hugepages
4872+ nr_hugepages (int) -- Number of pages to reserve
4873+ max_map_count (int) -- Number of Virtual Memory Areas a process can own
4874+ mnt_point (str) -- Directory to mount hugepages on
4875+ pagesize (str) -- Size of hugepages
4876+ mount (bool) -- Whether to Mount hugepages
4877+ """
4878+ group_info = add_group(group)
4879+ gid = group_info.gr_gid
4880+ add_user_to_group(user, group)
4881+ if max_map_count < 2 * nr_hugepages:
4882+ max_map_count = 2 * nr_hugepages
4883+ sysctl_settings = {
4884+ 'vm.nr_hugepages': nr_hugepages,
4885+ 'vm.max_map_count': max_map_count,
4886+ 'vm.hugetlb_shm_group': gid,
4887+ }
4888+ if set_shmmax:
4889+ shmmax_current = int(check_output(['sysctl', '-n', 'kernel.shmmax']))
4890+ shmmax_minsize = bytes_from_string(pagesize) * nr_hugepages
4891+ if shmmax_minsize > shmmax_current:
4892+ sysctl_settings['kernel.shmmax'] = shmmax_minsize
4893+ sysctl.create(yaml.dump(sysctl_settings), '/etc/sysctl.d/10-hugepage.conf')
4894+ mkdir(mnt_point, owner='root', group='root', perms=0o755, force=False)
4895+ lfstab = fstab.Fstab()
4896+ fstab_entry = lfstab.get_entry_by_attr('mountpoint', mnt_point)
4897+ if fstab_entry:
4898+ lfstab.remove_entry(fstab_entry)
4899+ entry = lfstab.Entry('nodev', mnt_point, 'hugetlbfs',
4900+ 'mode=1770,gid={},pagesize={}'.format(gid, pagesize), 0, 0)
4901+ lfstab.add_entry(entry)
4902+ if mount:
4903+ fstab_mount(mnt_point)
4904
4905=== added file 'charmhelpers/core/kernel.py'
4906--- charmhelpers/core/kernel.py 1970-01-01 00:00:00 +0000
4907+++ charmhelpers/core/kernel.py 2016-03-10 22:55:31 +0000
4908@@ -0,0 +1,68 @@
4909+#!/usr/bin/env python
4910+# -*- coding: utf-8 -*-
4911+
4912+# Copyright 2014-2015 Canonical Limited.
4913+#
4914+# This file is part of charm-helpers.
4915+#
4916+# charm-helpers is free software: you can redistribute it and/or modify
4917+# it under the terms of the GNU Lesser General Public License version 3 as
4918+# published by the Free Software Foundation.
4919+#
4920+# charm-helpers is distributed in the hope that it will be useful,
4921+# but WITHOUT ANY WARRANTY; without even the implied warranty of
4922+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
4923+# GNU Lesser General Public License for more details.
4924+#
4925+# You should have received a copy of the GNU Lesser General Public License
4926+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
4927+
4928+__author__ = "Jorge Niedbalski <jorge.niedbalski@canonical.com>"
4929+
4930+from charmhelpers.core.hookenv import (
4931+ log,
4932+ INFO
4933+)
4934+
4935+from subprocess import check_call, check_output
4936+import re
4937+
4938+
4939+def modprobe(module, persist=True):
4940+ """Load a kernel module and configure for auto-load on reboot."""
4941+ cmd = ['modprobe', module]
4942+
4943+ log('Loading kernel module %s' % module, level=INFO)
4944+
4945+ check_call(cmd)
4946+ if persist:
4947+ with open('/etc/modules', 'r+') as modules:
4948+ if module not in modules.read():
4949+ modules.write(module)
4950+
4951+
4952+def rmmod(module, force=False):
4953+ """Remove a module from the linux kernel"""
4954+ cmd = ['rmmod']
4955+ if force:
4956+ cmd.append('-f')
4957+ cmd.append(module)
4958+ log('Removing kernel module %s' % module, level=INFO)
4959+ return check_call(cmd)
4960+
4961+
4962+def lsmod():
4963+ """Shows what kernel modules are currently loaded"""
4964+ return check_output(['lsmod'],
4965+ universal_newlines=True)
4966+
4967+
4968+def is_module_loaded(module):
4969+ """Checks if a kernel module is already loaded"""
4970+ matches = re.findall('^%s[ ]+' % module, lsmod(), re.M)
4971+ return len(matches) > 0
4972+
4973+
4974+def update_initramfs(version='all'):
4975+ """Updates an initramfs image"""
4976+ return check_call(["update-initramfs", "-k", version, "-u"])
4977
4978=== added directory 'charmhelpers/core/services'
4979=== added file 'charmhelpers/core/services/__init__.py'
4980--- charmhelpers/core/services/__init__.py 1970-01-01 00:00:00 +0000
4981+++ charmhelpers/core/services/__init__.py 2016-03-10 22:55:31 +0000
4982@@ -0,0 +1,18 @@
4983+# Copyright 2014-2015 Canonical Limited.
4984+#
4985+# This file is part of charm-helpers.
4986+#
4987+# charm-helpers is free software: you can redistribute it and/or modify
4988+# it under the terms of the GNU Lesser General Public License version 3 as
4989+# published by the Free Software Foundation.
4990+#
4991+# charm-helpers is distributed in the hope that it will be useful,
4992+# but WITHOUT ANY WARRANTY; without even the implied warranty of
4993+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
4994+# GNU Lesser General Public License for more details.
4995+#
4996+# You should have received a copy of the GNU Lesser General Public License
4997+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
4998+
4999+from .base import * # NOQA
5000+from .helpers import * # NOQA
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches

to all changes: