Merge lp:~paulgear/charms/trusty/ntpmaster/sync-charmhelpers into lp:charms/trusty/ntpmaster

Proposed by Paul Gear
Status: Merged
Merge reported by: Adam Israel
Merged at revision: not available
Proposed branch: lp:~paulgear/charms/trusty/ntpmaster/sync-charmhelpers
Merge into: lp:charms/trusty/ntpmaster
Diff against target: 22054 lines (+20254/-258)
145 files modified
hooks/charmhelpers/__init__.py (+38/-0)
hooks/charmhelpers/cli/README.rst (+57/-0)
hooks/charmhelpers/cli/__init__.py (+191/-0)
hooks/charmhelpers/cli/benchmark.py (+36/-0)
hooks/charmhelpers/cli/commands.py (+32/-0)
hooks/charmhelpers/cli/hookenv.py (+23/-0)
hooks/charmhelpers/cli/host.py (+31/-0)
hooks/charmhelpers/cli/unitdata.py (+39/-0)
hooks/charmhelpers/context.py (+206/-0)
hooks/charmhelpers/contrib/__init__.py (+15/-0)
hooks/charmhelpers/contrib/amulet/__init__.py (+15/-0)
hooks/charmhelpers/contrib/amulet/deployment.py (+95/-0)
hooks/charmhelpers/contrib/amulet/utils.py (+823/-0)
hooks/charmhelpers/contrib/ansible/__init__.py (+254/-0)
hooks/charmhelpers/contrib/benchmark/__init__.py (+126/-0)
hooks/charmhelpers/contrib/charmhelpers/IMPORT (+4/-0)
hooks/charmhelpers/contrib/charmhelpers/__init__.py (+208/-0)
hooks/charmhelpers/contrib/charmsupport/IMPORT (+14/-0)
hooks/charmhelpers/contrib/charmsupport/__init__.py (+15/-0)
hooks/charmhelpers/contrib/charmsupport/nrpe.py (+398/-0)
hooks/charmhelpers/contrib/charmsupport/volumes.py (+175/-0)
hooks/charmhelpers/contrib/database/mysql.py (+412/-0)
hooks/charmhelpers/contrib/hahelpers/__init__.py (+15/-0)
hooks/charmhelpers/contrib/hahelpers/apache.py (+82/-0)
hooks/charmhelpers/contrib/hahelpers/cluster.py (+316/-0)
hooks/charmhelpers/contrib/hardening/README.hardening.md (+38/-0)
hooks/charmhelpers/contrib/hardening/__init__.py (+15/-0)
hooks/charmhelpers/contrib/hardening/apache/__init__.py (+19/-0)
hooks/charmhelpers/contrib/hardening/apache/checks/__init__.py (+31/-0)
hooks/charmhelpers/contrib/hardening/apache/checks/config.py (+98/-0)
hooks/charmhelpers/contrib/hardening/apache/templates/alias.conf (+31/-0)
hooks/charmhelpers/contrib/hardening/apache/templates/hardening.conf (+18/-0)
hooks/charmhelpers/contrib/hardening/audits/__init__.py (+63/-0)
hooks/charmhelpers/contrib/hardening/audits/apache.py (+97/-0)
hooks/charmhelpers/contrib/hardening/audits/apt.py (+105/-0)
hooks/charmhelpers/contrib/hardening/audits/file.py (+551/-0)
hooks/charmhelpers/contrib/hardening/defaults/apache.yaml (+13/-0)
hooks/charmhelpers/contrib/hardening/defaults/apache.yaml.schema (+9/-0)
hooks/charmhelpers/contrib/hardening/defaults/mysql.yaml (+38/-0)
hooks/charmhelpers/contrib/hardening/defaults/mysql.yaml.schema (+15/-0)
hooks/charmhelpers/contrib/hardening/defaults/os.yaml (+67/-0)
hooks/charmhelpers/contrib/hardening/defaults/os.yaml.schema (+42/-0)
hooks/charmhelpers/contrib/hardening/defaults/ssh.yaml (+49/-0)
hooks/charmhelpers/contrib/hardening/defaults/ssh.yaml.schema (+42/-0)
hooks/charmhelpers/contrib/hardening/harden.py (+82/-0)
hooks/charmhelpers/contrib/hardening/host/__init__.py (+19/-0)
hooks/charmhelpers/contrib/hardening/host/checks/__init__.py (+50/-0)
hooks/charmhelpers/contrib/hardening/host/checks/apt.py (+39/-0)
hooks/charmhelpers/contrib/hardening/host/checks/limits.py (+55/-0)
hooks/charmhelpers/contrib/hardening/host/checks/login.py (+67/-0)
hooks/charmhelpers/contrib/hardening/host/checks/minimize_access.py (+52/-0)
hooks/charmhelpers/contrib/hardening/host/checks/pam.py (+134/-0)
hooks/charmhelpers/contrib/hardening/host/checks/profile.py (+45/-0)
hooks/charmhelpers/contrib/hardening/host/checks/securetty.py (+39/-0)
hooks/charmhelpers/contrib/hardening/host/checks/suid_sgid.py (+131/-0)
hooks/charmhelpers/contrib/hardening/host/checks/sysctl.py (+208/-0)
hooks/charmhelpers/contrib/hardening/host/templates/10.hardcore.conf (+8/-0)
hooks/charmhelpers/contrib/hardening/host/templates/99-juju-hardening.conf (+7/-0)
hooks/charmhelpers/contrib/hardening/host/templates/login.defs (+349/-0)
hooks/charmhelpers/contrib/hardening/host/templates/modules (+117/-0)
hooks/charmhelpers/contrib/hardening/host/templates/passwdqc.conf (+11/-0)
hooks/charmhelpers/contrib/hardening/host/templates/pinerolo_profile.sh (+8/-0)
hooks/charmhelpers/contrib/hardening/host/templates/securetty (+11/-0)
hooks/charmhelpers/contrib/hardening/host/templates/tally2 (+14/-0)
hooks/charmhelpers/contrib/hardening/mysql/__init__.py (+19/-0)
hooks/charmhelpers/contrib/hardening/mysql/checks/__init__.py (+31/-0)
hooks/charmhelpers/contrib/hardening/mysql/checks/config.py (+86/-0)
hooks/charmhelpers/contrib/hardening/mysql/templates/hardening.cnf (+12/-0)
hooks/charmhelpers/contrib/hardening/ssh/__init__.py (+19/-0)
hooks/charmhelpers/contrib/hardening/ssh/checks/__init__.py (+31/-0)
hooks/charmhelpers/contrib/hardening/ssh/checks/config.py (+394/-0)
hooks/charmhelpers/contrib/hardening/ssh/templates/ssh_config (+70/-0)
hooks/charmhelpers/contrib/hardening/ssh/templates/sshd_config (+159/-0)
hooks/charmhelpers/contrib/hardening/templating.py (+71/-0)
hooks/charmhelpers/contrib/hardening/utils.py (+156/-0)
hooks/charmhelpers/contrib/mellanox/infiniband.py (+151/-0)
hooks/charmhelpers/contrib/network/__init__.py (+15/-0)
hooks/charmhelpers/contrib/network/ip.py (+473/-0)
hooks/charmhelpers/contrib/network/ovs/__init__.py (+96/-0)
hooks/charmhelpers/contrib/network/ufw.py (+318/-0)
hooks/charmhelpers/contrib/openstack/__init__.py (+15/-0)
hooks/charmhelpers/contrib/openstack/alternatives.py (+33/-0)
hooks/charmhelpers/contrib/openstack/amulet/__init__.py (+15/-0)
hooks/charmhelpers/contrib/openstack/amulet/deployment.py (+302/-0)
hooks/charmhelpers/contrib/openstack/amulet/utils.py (+1012/-0)
hooks/charmhelpers/contrib/openstack/context.py (+1481/-0)
hooks/charmhelpers/contrib/openstack/files/__init__.py (+18/-0)
hooks/charmhelpers/contrib/openstack/files/check_haproxy.sh (+34/-0)
hooks/charmhelpers/contrib/openstack/files/check_haproxy_queue_depth.sh (+30/-0)
hooks/charmhelpers/contrib/openstack/ip.py (+151/-0)
hooks/charmhelpers/contrib/openstack/neutron.py (+384/-0)
hooks/charmhelpers/contrib/openstack/templates/__init__.py (+18/-0)
hooks/charmhelpers/contrib/openstack/templates/ceph.conf (+21/-0)
hooks/charmhelpers/contrib/openstack/templates/git.upstart (+17/-0)
hooks/charmhelpers/contrib/openstack/templates/haproxy.cfg (+66/-0)
hooks/charmhelpers/contrib/openstack/templates/openstack_https_frontend (+26/-0)
hooks/charmhelpers/contrib/openstack/templates/section-keystone-authtoken (+12/-0)
hooks/charmhelpers/contrib/openstack/templates/section-keystone-authtoken-legacy (+10/-0)
hooks/charmhelpers/contrib/openstack/templates/section-rabbitmq-oslo (+22/-0)
hooks/charmhelpers/contrib/openstack/templates/section-zeromq (+14/-0)
hooks/charmhelpers/contrib/openstack/templating.py (+323/-0)
hooks/charmhelpers/contrib/openstack/utils.py (+1572/-0)
hooks/charmhelpers/contrib/peerstorage/__init__.py (+269/-0)
hooks/charmhelpers/contrib/python/__init__.py (+15/-0)
hooks/charmhelpers/contrib/python/debug.py (+56/-0)
hooks/charmhelpers/contrib/python/packages.py (+145/-0)
hooks/charmhelpers/contrib/python/rpdb.py (+58/-0)
hooks/charmhelpers/contrib/python/version.py (+34/-0)
hooks/charmhelpers/contrib/saltstack/__init__.py (+118/-0)
hooks/charmhelpers/contrib/ssl/__init__.py (+94/-0)
hooks/charmhelpers/contrib/ssl/service.py (+279/-0)
hooks/charmhelpers/contrib/storage/__init__.py (+15/-0)
hooks/charmhelpers/contrib/storage/linux/__init__.py (+15/-0)
hooks/charmhelpers/contrib/storage/linux/ceph.py (+1195/-0)
hooks/charmhelpers/contrib/storage/linux/loopback.py (+88/-0)
hooks/charmhelpers/contrib/storage/linux/lvm.py (+105/-0)
hooks/charmhelpers/contrib/storage/linux/utils.py (+71/-0)
hooks/charmhelpers/contrib/templating/__init__.py (+15/-0)
hooks/charmhelpers/contrib/templating/contexts.py (+139/-0)
hooks/charmhelpers/contrib/templating/jinja.py (+40/-0)
hooks/charmhelpers/contrib/templating/pyformat.py (+29/-0)
hooks/charmhelpers/contrib/unison/__init__.py (+313/-0)
hooks/charmhelpers/coordinator.py (+607/-0)
hooks/charmhelpers/core/__init__.py (+15/-0)
hooks/charmhelpers/core/decorators.py (+57/-0)
hooks/charmhelpers/core/files.py (+45/-0)
hooks/charmhelpers/core/fstab.py (+30/-12)
hooks/charmhelpers/core/hookenv.py (+535/-52)
hooks/charmhelpers/core/host.py (+394/-84)
hooks/charmhelpers/core/hugepage.py (+71/-0)
hooks/charmhelpers/core/kernel.py (+68/-0)
hooks/charmhelpers/core/services/__init__.py (+18/-2)
hooks/charmhelpers/core/services/base.py (+59/-19)
hooks/charmhelpers/core/services/helpers.py (+66/-13)
hooks/charmhelpers/core/strutils.py (+72/-0)
hooks/charmhelpers/core/sysctl.py (+56/-0)
hooks/charmhelpers/core/templating.py (+38/-8)
hooks/charmhelpers/core/unitdata.py (+521/-0)
hooks/charmhelpers/fetch/__init__.py (+82/-28)
hooks/charmhelpers/fetch/archiveurl.py (+77/-18)
hooks/charmhelpers/fetch/bzrurl.py (+40/-22)
hooks/charmhelpers/fetch/giturl.py (+70/-0)
hooks/charmhelpers/payload/__init__.py (+17/-0)
hooks/charmhelpers/payload/archive.py (+73/-0)
hooks/charmhelpers/payload/execd.py (+66/-0)
To merge this branch: bzr merge lp:~paulgear/charms/trusty/ntpmaster/sync-charmhelpers
Reviewer Review Type Date Requested Status
Adam Israel (community) Approve
Review Queue (community) automated testing Needs Fixing
Review via email: mp+289605@code.launchpad.net

Description of the change

Sync the latest charmhelpers code, including charmhelpers.payload; this is to prepare for another MP to include exec.d support.

To post a comment you must log in.
Revision history for this message
Review Queue (review-queue) wrote :

This item has failed automated testing! Results available here http://juju-ci.vapour.ws:8080/job/charm-bundle-test-aws/3277/

review: Needs Fixing (automated testing)
Revision history for this message
Review Queue (review-queue) wrote :

This item has failed automated testing! Results available here http://juju-ci.vapour.ws:8080/job/charm-bundle-test-aws/3303/

review: Needs Fixing (automated testing)
Revision history for this message
Review Queue (review-queue) wrote :

This item has failed automated testing! Results available here http://juju-ci.vapour.ws:8080/job/charm-bundle-test-lxc/3259/

review: Needs Fixing (automated testing)
Revision history for this message
Adam Israel (aisrael) wrote :

Hi Paul,

Thanks for updating this. Looks good to me. +1

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'hooks/charmhelpers/__init__.py'
2--- hooks/charmhelpers/__init__.py 2013-07-08 18:28:02 +0000
3+++ hooks/charmhelpers/__init__.py 2016-03-21 06:13:21 +0000
4@@ -0,0 +1,38 @@
5+# Copyright 2014-2015 Canonical Limited.
6+#
7+# This file is part of charm-helpers.
8+#
9+# charm-helpers is free software: you can redistribute it and/or modify
10+# it under the terms of the GNU Lesser General Public License version 3 as
11+# published by the Free Software Foundation.
12+#
13+# charm-helpers is distributed in the hope that it will be useful,
14+# but WITHOUT ANY WARRANTY; without even the implied warranty of
15+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16+# GNU Lesser General Public License for more details.
17+#
18+# You should have received a copy of the GNU Lesser General Public License
19+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
20+
21+# Bootstrap charm-helpers, installing its dependencies if necessary using
22+# only standard libraries.
23+import subprocess
24+import sys
25+
26+try:
27+ import six # flake8: noqa
28+except ImportError:
29+ if sys.version_info.major == 2:
30+ subprocess.check_call(['apt-get', 'install', '-y', 'python-six'])
31+ else:
32+ subprocess.check_call(['apt-get', 'install', '-y', 'python3-six'])
33+ import six # flake8: noqa
34+
35+try:
36+ import yaml # flake8: noqa
37+except ImportError:
38+ if sys.version_info.major == 2:
39+ subprocess.check_call(['apt-get', 'install', '-y', 'python-yaml'])
40+ else:
41+ subprocess.check_call(['apt-get', 'install', '-y', 'python3-yaml'])
42+ import yaml # flake8: noqa
43
44=== added directory 'hooks/charmhelpers/cli'
45=== added file 'hooks/charmhelpers/cli/README.rst'
46--- hooks/charmhelpers/cli/README.rst 1970-01-01 00:00:00 +0000
47+++ hooks/charmhelpers/cli/README.rst 2016-03-21 06:13:21 +0000
48@@ -0,0 +1,57 @@
49+==========
50+Commandant
51+==========
52+
53+-----------------------------------------------------
54+Automatic command-line interfaces to Python functions
55+-----------------------------------------------------
56+
57+One of the benefits of ``libvirt`` is the uniformity of the interface: the C API (as well as the bindings in other languages) is a set of functions that accept parameters that are nearly identical to the command-line arguments. If you run ``virsh``, you get an interactive command prompt that supports all of the same commands that your shell scripts use as ``virsh`` subcommands.
58+
59+Command execution and stdio manipulation is the greatest common factor across all development systems in the POSIX environment. By exposing your functions as commands that manipulate streams of text, you can make life easier for all the Ruby and Erlang and Go programmers in your life.
60+
61+Goals
62+=====
63+
64+* Single decorator to expose a function as a command.
65+ * now two decorators - one "automatic" and one that allows authors to manipulate the arguments for fine-grained control.(MW)
66+* Automatic analysis of function signature through ``inspect.getargspec()``
67+* Command argument parser built automatically with ``argparse``
68+* Interactive interpreter loop object made with ``Cmd``
69+* Options to output structured return value data via ``pprint``, ``yaml`` or ``json`` dumps.
70+
71+Other Important Features that need writing
72+------------------------------------------
73+
74+* Help and Usage documentation can be automatically generated, but it will be important to let users override this behaviour
75+* The decorator should allow specifying further parameters to the parser's add_argument() calls, to specify types or to make arguments behave as boolean flags, etc.
76+ - Filename arguments are important, as good practice is for functions to accept file objects as parameters.
77+ - choices arguments help to limit bad input before the function is called
78+* Some automatic behaviour could make for better defaults, once the user can override them.
79+ - We could automatically detect arguments that default to False or True, and automatically support --no-foo for foo=True.
80+ - We could automatically support hyphens as alternates for underscores
81+ - Arguments defaulting to sequence types could support the ``append`` action.
82+
83+
84+-----------------------------------------------------
85+Implementing subcommands
86+-----------------------------------------------------
87+
88+(WIP)
89+
90+So as to avoid dependencies on the cli module, subcommands should be defined separately from their implementations. The recommmendation would be to place definitions into separate modules near the implementations which they expose.
91+
92+Some examples::
93+
94+ from charmhelpers.cli import CommandLine
95+ from charmhelpers.payload import execd
96+ from charmhelpers.foo import bar
97+
98+ cli = CommandLine()
99+
100+ cli.subcommand(execd.execd_run)
101+
102+ @cli.subcommand_builder("bar", help="Bar baz qux")
103+ def barcmd_builder(subparser):
104+ subparser.add_argument('argument1', help="yackety")
105+ return bar
106
107=== added file 'hooks/charmhelpers/cli/__init__.py'
108--- hooks/charmhelpers/cli/__init__.py 1970-01-01 00:00:00 +0000
109+++ hooks/charmhelpers/cli/__init__.py 2016-03-21 06:13:21 +0000
110@@ -0,0 +1,191 @@
111+# Copyright 2014-2015 Canonical Limited.
112+#
113+# This file is part of charm-helpers.
114+#
115+# charm-helpers is free software: you can redistribute it and/or modify
116+# it under the terms of the GNU Lesser General Public License version 3 as
117+# published by the Free Software Foundation.
118+#
119+# charm-helpers is distributed in the hope that it will be useful,
120+# but WITHOUT ANY WARRANTY; without even the implied warranty of
121+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
122+# GNU Lesser General Public License for more details.
123+#
124+# You should have received a copy of the GNU Lesser General Public License
125+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
126+
127+import inspect
128+import argparse
129+import sys
130+
131+from six.moves import zip
132+
133+import charmhelpers.core.unitdata
134+
135+
136+class OutputFormatter(object):
137+ def __init__(self, outfile=sys.stdout):
138+ self.formats = (
139+ "raw",
140+ "json",
141+ "py",
142+ "yaml",
143+ "csv",
144+ "tab",
145+ )
146+ self.outfile = outfile
147+
148+ def add_arguments(self, argument_parser):
149+ formatgroup = argument_parser.add_mutually_exclusive_group()
150+ choices = self.supported_formats
151+ formatgroup.add_argument("--format", metavar='FMT',
152+ help="Select output format for returned data, "
153+ "where FMT is one of: {}".format(choices),
154+ choices=choices, default='raw')
155+ for fmt in self.formats:
156+ fmtfunc = getattr(self, fmt)
157+ formatgroup.add_argument("-{}".format(fmt[0]),
158+ "--{}".format(fmt), action='store_const',
159+ const=fmt, dest='format',
160+ help=fmtfunc.__doc__)
161+
162+ @property
163+ def supported_formats(self):
164+ return self.formats
165+
166+ def raw(self, output):
167+ """Output data as raw string (default)"""
168+ if isinstance(output, (list, tuple)):
169+ output = '\n'.join(map(str, output))
170+ self.outfile.write(str(output))
171+
172+ def py(self, output):
173+ """Output data as a nicely-formatted python data structure"""
174+ import pprint
175+ pprint.pprint(output, stream=self.outfile)
176+
177+ def json(self, output):
178+ """Output data in JSON format"""
179+ import json
180+ json.dump(output, self.outfile)
181+
182+ def yaml(self, output):
183+ """Output data in YAML format"""
184+ import yaml
185+ yaml.safe_dump(output, self.outfile)
186+
187+ def csv(self, output):
188+ """Output data as excel-compatible CSV"""
189+ import csv
190+ csvwriter = csv.writer(self.outfile)
191+ csvwriter.writerows(output)
192+
193+ def tab(self, output):
194+ """Output data in excel-compatible tab-delimited format"""
195+ import csv
196+ csvwriter = csv.writer(self.outfile, dialect=csv.excel_tab)
197+ csvwriter.writerows(output)
198+
199+ def format_output(self, output, fmt='raw'):
200+ fmtfunc = getattr(self, fmt)
201+ fmtfunc(output)
202+
203+
204+class CommandLine(object):
205+ argument_parser = None
206+ subparsers = None
207+ formatter = None
208+ exit_code = 0
209+
210+ def __init__(self):
211+ if not self.argument_parser:
212+ self.argument_parser = argparse.ArgumentParser(description='Perform common charm tasks')
213+ if not self.formatter:
214+ self.formatter = OutputFormatter()
215+ self.formatter.add_arguments(self.argument_parser)
216+ if not self.subparsers:
217+ self.subparsers = self.argument_parser.add_subparsers(help='Commands')
218+
219+ def subcommand(self, command_name=None):
220+ """
221+ Decorate a function as a subcommand. Use its arguments as the
222+ command-line arguments"""
223+ def wrapper(decorated):
224+ cmd_name = command_name or decorated.__name__
225+ subparser = self.subparsers.add_parser(cmd_name,
226+ description=decorated.__doc__)
227+ for args, kwargs in describe_arguments(decorated):
228+ subparser.add_argument(*args, **kwargs)
229+ subparser.set_defaults(func=decorated)
230+ return decorated
231+ return wrapper
232+
233+ def test_command(self, decorated):
234+ """
235+ Subcommand is a boolean test function, so bool return values should be
236+ converted to a 0/1 exit code.
237+ """
238+ decorated._cli_test_command = True
239+ return decorated
240+
241+ def no_output(self, decorated):
242+ """
243+ Subcommand is not expected to return a value, so don't print a spurious None.
244+ """
245+ decorated._cli_no_output = True
246+ return decorated
247+
248+ def subcommand_builder(self, command_name, description=None):
249+ """
250+ Decorate a function that builds a subcommand. Builders should accept a
251+ single argument (the subparser instance) and return the function to be
252+ run as the command."""
253+ def wrapper(decorated):
254+ subparser = self.subparsers.add_parser(command_name)
255+ func = decorated(subparser)
256+ subparser.set_defaults(func=func)
257+ subparser.description = description or func.__doc__
258+ return wrapper
259+
260+ def run(self):
261+ "Run cli, processing arguments and executing subcommands."
262+ arguments = self.argument_parser.parse_args()
263+ argspec = inspect.getargspec(arguments.func)
264+ vargs = []
265+ for arg in argspec.args:
266+ vargs.append(getattr(arguments, arg))
267+ if argspec.varargs:
268+ vargs.extend(getattr(arguments, argspec.varargs))
269+ output = arguments.func(*vargs)
270+ if getattr(arguments.func, '_cli_test_command', False):
271+ self.exit_code = 0 if output else 1
272+ output = ''
273+ if getattr(arguments.func, '_cli_no_output', False):
274+ output = ''
275+ self.formatter.format_output(output, arguments.format)
276+ if charmhelpers.core.unitdata._KV:
277+ charmhelpers.core.unitdata._KV.flush()
278+
279+
280+cmdline = CommandLine()
281+
282+
283+def describe_arguments(func):
284+ """
285+ Analyze a function's signature and return a data structure suitable for
286+ passing in as arguments to an argparse parser's add_argument() method."""
287+
288+ argspec = inspect.getargspec(func)
289+ # we should probably raise an exception somewhere if func includes **kwargs
290+ if argspec.defaults:
291+ positional_args = argspec.args[:-len(argspec.defaults)]
292+ keyword_names = argspec.args[-len(argspec.defaults):]
293+ for arg, default in zip(keyword_names, argspec.defaults):
294+ yield ('--{}'.format(arg),), {'default': default}
295+ else:
296+ positional_args = argspec.args
297+
298+ for arg in positional_args:
299+ yield (arg,), {}
300+ if argspec.varargs:
301+ yield (argspec.varargs,), {'nargs': '*'}
302
303=== added file 'hooks/charmhelpers/cli/benchmark.py'
304--- hooks/charmhelpers/cli/benchmark.py 1970-01-01 00:00:00 +0000
305+++ hooks/charmhelpers/cli/benchmark.py 2016-03-21 06:13:21 +0000
306@@ -0,0 +1,36 @@
307+# Copyright 2014-2015 Canonical Limited.
308+#
309+# This file is part of charm-helpers.
310+#
311+# charm-helpers is free software: you can redistribute it and/or modify
312+# it under the terms of the GNU Lesser General Public License version 3 as
313+# published by the Free Software Foundation.
314+#
315+# charm-helpers is distributed in the hope that it will be useful,
316+# but WITHOUT ANY WARRANTY; without even the implied warranty of
317+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
318+# GNU Lesser General Public License for more details.
319+#
320+# You should have received a copy of the GNU Lesser General Public License
321+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
322+
323+from . import cmdline
324+from charmhelpers.contrib.benchmark import Benchmark
325+
326+
327+@cmdline.subcommand(command_name='benchmark-start')
328+def start():
329+ Benchmark.start()
330+
331+
332+@cmdline.subcommand(command_name='benchmark-finish')
333+def finish():
334+ Benchmark.finish()
335+
336+
337+@cmdline.subcommand_builder('benchmark-composite', description="Set the benchmark composite score")
338+def service(subparser):
339+ subparser.add_argument("value", help="The composite score.")
340+ subparser.add_argument("units", help="The units the composite score represents, i.e., 'reads/sec'.")
341+ subparser.add_argument("direction", help="'asc' if a lower score is better, 'desc' if a higher score is better.")
342+ return Benchmark.set_composite_score
343
344=== added file 'hooks/charmhelpers/cli/commands.py'
345--- hooks/charmhelpers/cli/commands.py 1970-01-01 00:00:00 +0000
346+++ hooks/charmhelpers/cli/commands.py 2016-03-21 06:13:21 +0000
347@@ -0,0 +1,32 @@
348+# Copyright 2014-2015 Canonical Limited.
349+#
350+# This file is part of charm-helpers.
351+#
352+# charm-helpers is free software: you can redistribute it and/or modify
353+# it under the terms of the GNU Lesser General Public License version 3 as
354+# published by the Free Software Foundation.
355+#
356+# charm-helpers is distributed in the hope that it will be useful,
357+# but WITHOUT ANY WARRANTY; without even the implied warranty of
358+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
359+# GNU Lesser General Public License for more details.
360+#
361+# You should have received a copy of the GNU Lesser General Public License
362+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
363+
364+"""
365+This module loads sub-modules into the python runtime so they can be
366+discovered via the inspect module. In order to prevent flake8 from (rightfully)
367+telling us these are unused modules, throw a ' # noqa' at the end of each import
368+so that the warning is suppressed.
369+"""
370+
371+from . import CommandLine # noqa
372+
373+"""
374+Import the sub-modules which have decorated subcommands to register with chlp.
375+"""
376+from . import host # noqa
377+from . import benchmark # noqa
378+from . import unitdata # noqa
379+from . import hookenv # noqa
380
381=== added file 'hooks/charmhelpers/cli/hookenv.py'
382--- hooks/charmhelpers/cli/hookenv.py 1970-01-01 00:00:00 +0000
383+++ hooks/charmhelpers/cli/hookenv.py 2016-03-21 06:13:21 +0000
384@@ -0,0 +1,23 @@
385+# Copyright 2014-2015 Canonical Limited.
386+#
387+# This file is part of charm-helpers.
388+#
389+# charm-helpers is free software: you can redistribute it and/or modify
390+# it under the terms of the GNU Lesser General Public License version 3 as
391+# published by the Free Software Foundation.
392+#
393+# charm-helpers is distributed in the hope that it will be useful,
394+# but WITHOUT ANY WARRANTY; without even the implied warranty of
395+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
396+# GNU Lesser General Public License for more details.
397+#
398+# You should have received a copy of the GNU Lesser General Public License
399+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
400+
401+from . import cmdline
402+from charmhelpers.core import hookenv
403+
404+
405+cmdline.subcommand('relation-id')(hookenv.relation_id._wrapped)
406+cmdline.subcommand('service-name')(hookenv.service_name)
407+cmdline.subcommand('remote-service-name')(hookenv.remote_service_name._wrapped)
408
409=== added file 'hooks/charmhelpers/cli/host.py'
410--- hooks/charmhelpers/cli/host.py 1970-01-01 00:00:00 +0000
411+++ hooks/charmhelpers/cli/host.py 2016-03-21 06:13:21 +0000
412@@ -0,0 +1,31 @@
413+# Copyright 2014-2015 Canonical Limited.
414+#
415+# This file is part of charm-helpers.
416+#
417+# charm-helpers is free software: you can redistribute it and/or modify
418+# it under the terms of the GNU Lesser General Public License version 3 as
419+# published by the Free Software Foundation.
420+#
421+# charm-helpers is distributed in the hope that it will be useful,
422+# but WITHOUT ANY WARRANTY; without even the implied warranty of
423+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
424+# GNU Lesser General Public License for more details.
425+#
426+# You should have received a copy of the GNU Lesser General Public License
427+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
428+
429+from . import cmdline
430+from charmhelpers.core import host
431+
432+
433+@cmdline.subcommand()
434+def mounts():
435+ "List mounts"
436+ return host.mounts()
437+
438+
439+@cmdline.subcommand_builder('service', description="Control system services")
440+def service(subparser):
441+ subparser.add_argument("action", help="The action to perform (start, stop, etc...)")
442+ subparser.add_argument("service_name", help="Name of the service to control")
443+ return host.service
444
445=== added file 'hooks/charmhelpers/cli/unitdata.py'
446--- hooks/charmhelpers/cli/unitdata.py 1970-01-01 00:00:00 +0000
447+++ hooks/charmhelpers/cli/unitdata.py 2016-03-21 06:13:21 +0000
448@@ -0,0 +1,39 @@
449+# Copyright 2014-2015 Canonical Limited.
450+#
451+# This file is part of charm-helpers.
452+#
453+# charm-helpers is free software: you can redistribute it and/or modify
454+# it under the terms of the GNU Lesser General Public License version 3 as
455+# published by the Free Software Foundation.
456+#
457+# charm-helpers is distributed in the hope that it will be useful,
458+# but WITHOUT ANY WARRANTY; without even the implied warranty of
459+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
460+# GNU Lesser General Public License for more details.
461+#
462+# You should have received a copy of the GNU Lesser General Public License
463+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
464+
465+from . import cmdline
466+from charmhelpers.core import unitdata
467+
468+
469+@cmdline.subcommand_builder('unitdata', description="Store and retrieve data")
470+def unitdata_cmd(subparser):
471+ nested = subparser.add_subparsers()
472+ get_cmd = nested.add_parser('get', help='Retrieve data')
473+ get_cmd.add_argument('key', help='Key to retrieve the value of')
474+ get_cmd.set_defaults(action='get', value=None)
475+ set_cmd = nested.add_parser('set', help='Store data')
476+ set_cmd.add_argument('key', help='Key to set')
477+ set_cmd.add_argument('value', help='Value to store')
478+ set_cmd.set_defaults(action='set')
479+
480+ def _unitdata_cmd(action, key, value):
481+ if action == 'get':
482+ return unitdata.kv().get(key)
483+ elif action == 'set':
484+ unitdata.kv().set(key, value)
485+ unitdata.kv().flush()
486+ return ''
487+ return _unitdata_cmd
488
489=== added file 'hooks/charmhelpers/context.py'
490--- hooks/charmhelpers/context.py 1970-01-01 00:00:00 +0000
491+++ hooks/charmhelpers/context.py 2016-03-21 06:13:21 +0000
492@@ -0,0 +1,206 @@
493+# Copyright 2015 Canonical Limited.
494+#
495+# This file is part of charm-helpers.
496+#
497+# charm-helpers is free software: you can redistribute it and/or modify
498+# it under the terms of the GNU Lesser General Public License version 3 as
499+# published by the Free Software Foundation.
500+#
501+# charm-helpers is distributed in the hope that it will be useful,
502+# but WITHOUT ANY WARRANTY; without even the implied warranty of
503+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
504+# GNU Lesser General Public License for more details.
505+#
506+# You should have received a copy of the GNU Lesser General Public License
507+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
508+'''
509+A Pythonic API to interact with the charm hook environment.
510+
511+:author: Stuart Bishop <stuart.bishop@canonical.com>
512+'''
513+
514+import six
515+
516+from charmhelpers.core import hookenv
517+
518+from collections import OrderedDict
519+if six.PY3:
520+ from collections import UserDict # pragma: nocover
521+else:
522+ from UserDict import IterableUserDict as UserDict # pragma: nocover
523+
524+
525+class Relations(OrderedDict):
526+ '''Mapping relation name -> relation id -> Relation.
527+
528+ >>> rels = Relations()
529+ >>> rels['sprog']['sprog:12']['client/6']['widget']
530+ 'remote widget'
531+ >>> rels['sprog']['sprog:12'].local['widget'] = 'local widget'
532+ >>> rels['sprog']['sprog:12'].local['widget']
533+ 'local widget'
534+ >>> rels.peer.local['widget']
535+ 'local widget on the peer relation'
536+ '''
537+ def __init__(self):
538+ super(Relations, self).__init__()
539+ for relname in sorted(hookenv.relation_types()):
540+ self[relname] = OrderedDict()
541+ relids = hookenv.relation_ids(relname)
542+ relids.sort(key=lambda x: int(x.split(':', 1)[-1]))
543+ for relid in relids:
544+ self[relname][relid] = Relation(relid)
545+
546+ @property
547+ def peer(self):
548+ peer_relid = hookenv.peer_relation_id()
549+ for rels in self.values():
550+ if peer_relid in rels:
551+ return rels[peer_relid]
552+
553+
554+class Relation(OrderedDict):
555+ '''Mapping of unit -> remote RelationInfo for a relation.
556+
557+ This is an OrderedDict mapping, ordered numerically by
558+ by unit number.
559+
560+ Also provides access to the local RelationInfo, and peer RelationInfo
561+ instances by the 'local' and 'peers' attributes.
562+
563+ >>> r = Relation('sprog:12')
564+ >>> r.keys()
565+ ['client/9', 'client/10'] # Ordered numerically
566+ >>> r['client/10']['widget'] # A remote RelationInfo setting
567+ 'remote widget'
568+ >>> r.local['widget'] # The local RelationInfo setting
569+ 'local widget'
570+ '''
571+ relid = None # The relation id.
572+ relname = None # The relation name (also known as relation type).
573+ service = None # The remote service name, if known.
574+ local = None # The local end's RelationInfo.
575+ peers = None # Map of peer -> RelationInfo. None if no peer relation.
576+
577+ def __init__(self, relid):
578+ remote_units = hookenv.related_units(relid)
579+ remote_units.sort(key=lambda u: int(u.split('/', 1)[-1]))
580+ super(Relation, self).__init__((unit, RelationInfo(relid, unit))
581+ for unit in remote_units)
582+
583+ self.relname = relid.split(':', 1)[0]
584+ self.relid = relid
585+ self.local = RelationInfo(relid, hookenv.local_unit())
586+
587+ for relinfo in self.values():
588+ self.service = relinfo.service
589+ break
590+
591+ # If we have peers, and they have joined both the provided peer
592+ # relation and this relation, we can peek at their data too.
593+ # This is useful for creating consensus without leadership.
594+ peer_relid = hookenv.peer_relation_id()
595+ if peer_relid and peer_relid != relid:
596+ peers = hookenv.related_units(peer_relid)
597+ if peers:
598+ peers.sort(key=lambda u: int(u.split('/', 1)[-1]))
599+ self.peers = OrderedDict((peer, RelationInfo(relid, peer))
600+ for peer in peers)
601+ else:
602+ self.peers = OrderedDict()
603+ else:
604+ self.peers = None
605+
606+ def __str__(self):
607+ return '{} ({})'.format(self.relid, self.service)
608+
609+
610+class RelationInfo(UserDict):
611+ '''The bag of data at an end of a relation.
612+
613+ Every unit participating in a relation has a single bag of
614+ data associated with that relation. This is that bag.
615+
616+ The bag of data for the local unit may be updated. Remote data
617+ is immutable and will remain static for the duration of the hook.
618+
619+ Changes made to the local units relation data only become visible
620+ to other units after the hook completes successfully. If the hook
621+ does not complete successfully, the changes are rolled back.
622+
623+ Unlike standard Python mappings, setting an item to None is the
624+ same as deleting it.
625+
626+ >>> relinfo = RelationInfo('db:12') # Default is the local unit.
627+ >>> relinfo['user'] = 'fred'
628+ >>> relinfo['user']
629+ 'fred'
630+ >>> relinfo['user'] = None
631+ >>> 'fred' in relinfo
632+ False
633+
634+ This class wraps hookenv.relation_get and hookenv.relation_set.
635+ All caching is left up to these two methods to avoid synchronization
636+ issues. Data is only loaded on demand.
637+ '''
638+ relid = None # The relation id.
639+ relname = None # The relation name (also know as the relation type).
640+ unit = None # The unit id.
641+ number = None # The unit number (integer).
642+ service = None # The service name.
643+
644+ def __init__(self, relid, unit):
645+ self.relname = relid.split(':', 1)[0]
646+ self.relid = relid
647+ self.unit = unit
648+ self.service, num = self.unit.split('/', 1)
649+ self.number = int(num)
650+
651+ def __str__(self):
652+ return '{} ({})'.format(self.relid, self.unit)
653+
654+ @property
655+ def data(self):
656+ return hookenv.relation_get(rid=self.relid, unit=self.unit)
657+
658+ def __setitem__(self, key, value):
659+ if self.unit != hookenv.local_unit():
660+ raise TypeError('Attempting to set {} on remote unit {}'
661+ ''.format(key, self.unit))
662+ if value is not None and not isinstance(value, six.string_types):
663+ # We don't do implicit casting. This would cause simple
664+ # types like integers to be read back as strings in subsequent
665+ # hooks, and mutable types would require a lot of wrapping
666+ # to ensure relation-set gets called when they are mutated.
667+ raise ValueError('Only string values allowed')
668+ hookenv.relation_set(self.relid, {key: value})
669+
670+ def __delitem__(self, key):
671+ # Deleting a key and setting it to null is the same thing in
672+ # Juju relations.
673+ self[key] = None
674+
675+
676+class Leader(UserDict):
677+ def __init__(self):
678+ pass # Don't call superclass initializer, as it will nuke self.data
679+
680+ @property
681+ def data(self):
682+ return hookenv.leader_get()
683+
684+ def __setitem__(self, key, value):
685+ if not hookenv.is_leader():
686+ raise TypeError('Not the leader. Cannot change leader settings.')
687+ if value is not None and not isinstance(value, six.string_types):
688+ # We don't do implicit casting. This would cause simple
689+ # types like integers to be read back as strings in subsequent
690+ # hooks, and mutable types would require a lot of wrapping
691+ # to ensure leader-set gets called when they are mutated.
692+ raise ValueError('Only string values allowed')
693+ hookenv.leader_set({key: value})
694+
695+ def __delitem__(self, key):
696+ # Deleting a key and setting it to null is the same thing in
697+ # Juju leadership settings.
698+ self[key] = None
699
700=== added directory 'hooks/charmhelpers/contrib'
701=== added file 'hooks/charmhelpers/contrib/__init__.py'
702--- hooks/charmhelpers/contrib/__init__.py 1970-01-01 00:00:00 +0000
703+++ hooks/charmhelpers/contrib/__init__.py 2016-03-21 06:13:21 +0000
704@@ -0,0 +1,15 @@
705+# Copyright 2014-2015 Canonical Limited.
706+#
707+# This file is part of charm-helpers.
708+#
709+# charm-helpers is free software: you can redistribute it and/or modify
710+# it under the terms of the GNU Lesser General Public License version 3 as
711+# published by the Free Software Foundation.
712+#
713+# charm-helpers is distributed in the hope that it will be useful,
714+# but WITHOUT ANY WARRANTY; without even the implied warranty of
715+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
716+# GNU Lesser General Public License for more details.
717+#
718+# You should have received a copy of the GNU Lesser General Public License
719+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
720
721=== added directory 'hooks/charmhelpers/contrib/amulet'
722=== added file 'hooks/charmhelpers/contrib/amulet/__init__.py'
723--- hooks/charmhelpers/contrib/amulet/__init__.py 1970-01-01 00:00:00 +0000
724+++ hooks/charmhelpers/contrib/amulet/__init__.py 2016-03-21 06:13:21 +0000
725@@ -0,0 +1,15 @@
726+# Copyright 2014-2015 Canonical Limited.
727+#
728+# This file is part of charm-helpers.
729+#
730+# charm-helpers is free software: you can redistribute it and/or modify
731+# it under the terms of the GNU Lesser General Public License version 3 as
732+# published by the Free Software Foundation.
733+#
734+# charm-helpers is distributed in the hope that it will be useful,
735+# but WITHOUT ANY WARRANTY; without even the implied warranty of
736+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
737+# GNU Lesser General Public License for more details.
738+#
739+# You should have received a copy of the GNU Lesser General Public License
740+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
741
742=== added file 'hooks/charmhelpers/contrib/amulet/deployment.py'
743--- hooks/charmhelpers/contrib/amulet/deployment.py 1970-01-01 00:00:00 +0000
744+++ hooks/charmhelpers/contrib/amulet/deployment.py 2016-03-21 06:13:21 +0000
745@@ -0,0 +1,95 @@
746+# Copyright 2014-2015 Canonical Limited.
747+#
748+# This file is part of charm-helpers.
749+#
750+# charm-helpers is free software: you can redistribute it and/or modify
751+# it under the terms of the GNU Lesser General Public License version 3 as
752+# published by the Free Software Foundation.
753+#
754+# charm-helpers is distributed in the hope that it will be useful,
755+# but WITHOUT ANY WARRANTY; without even the implied warranty of
756+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
757+# GNU Lesser General Public License for more details.
758+#
759+# You should have received a copy of the GNU Lesser General Public License
760+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
761+
762+import amulet
763+import os
764+import six
765+
766+
767+class AmuletDeployment(object):
768+ """Amulet deployment.
769+
770+ This class provides generic Amulet deployment and test runner
771+ methods.
772+ """
773+
774+ def __init__(self, series=None):
775+ """Initialize the deployment environment."""
776+ self.series = None
777+
778+ if series:
779+ self.series = series
780+ self.d = amulet.Deployment(series=self.series)
781+ else:
782+ self.d = amulet.Deployment()
783+
784+ def _add_services(self, this_service, other_services):
785+ """Add services.
786+
787+ Add services to the deployment where this_service is the local charm
788+ that we're testing and other_services are the other services that
789+ are being used in the local amulet tests.
790+ """
791+ if this_service['name'] != os.path.basename(os.getcwd()):
792+ s = this_service['name']
793+ msg = "The charm's root directory name needs to be {}".format(s)
794+ amulet.raise_status(amulet.FAIL, msg=msg)
795+
796+ if 'units' not in this_service:
797+ this_service['units'] = 1
798+
799+ self.d.add(this_service['name'], units=this_service['units'],
800+ constraints=this_service.get('constraints'))
801+
802+ for svc in other_services:
803+ if 'location' in svc:
804+ branch_location = svc['location']
805+ elif self.series:
806+ branch_location = 'cs:{}/{}'.format(self.series, svc['name']),
807+ else:
808+ branch_location = None
809+
810+ if 'units' not in svc:
811+ svc['units'] = 1
812+
813+ self.d.add(svc['name'], charm=branch_location, units=svc['units'],
814+ constraints=svc.get('constraints'))
815+
816+ def _add_relations(self, relations):
817+ """Add all of the relations for the services."""
818+ for k, v in six.iteritems(relations):
819+ self.d.relate(k, v)
820+
821+ def _configure_services(self, configs):
822+ """Configure all of the services."""
823+ for service, config in six.iteritems(configs):
824+ self.d.configure(service, config)
825+
826+ def _deploy(self):
827+ """Deploy environment and wait for all hooks to finish executing."""
828+ try:
829+ self.d.setup(timeout=900)
830+ self.d.sentry.wait(timeout=900)
831+ except amulet.helpers.TimeoutError:
832+ amulet.raise_status(amulet.FAIL, msg="Deployment timed out")
833+ except Exception:
834+ raise
835+
836+ def run_tests(self):
837+ """Run all of the methods that are prefixed with 'test_'."""
838+ for test in dir(self):
839+ if test.startswith('test_'):
840+ getattr(self, test)()
841
842=== added file 'hooks/charmhelpers/contrib/amulet/utils.py'
843--- hooks/charmhelpers/contrib/amulet/utils.py 1970-01-01 00:00:00 +0000
844+++ hooks/charmhelpers/contrib/amulet/utils.py 2016-03-21 06:13:21 +0000
845@@ -0,0 +1,823 @@
846+# Copyright 2014-2015 Canonical Limited.
847+#
848+# This file is part of charm-helpers.
849+#
850+# charm-helpers is free software: you can redistribute it and/or modify
851+# it under the terms of the GNU Lesser General Public License version 3 as
852+# published by the Free Software Foundation.
853+#
854+# charm-helpers is distributed in the hope that it will be useful,
855+# but WITHOUT ANY WARRANTY; without even the implied warranty of
856+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
857+# GNU Lesser General Public License for more details.
858+#
859+# You should have received a copy of the GNU Lesser General Public License
860+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
861+
862+import io
863+import json
864+import logging
865+import os
866+import re
867+import socket
868+import subprocess
869+import sys
870+import time
871+import uuid
872+
873+import amulet
874+import distro_info
875+import six
876+from six.moves import configparser
877+if six.PY3:
878+ from urllib import parse as urlparse
879+else:
880+ import urlparse
881+
882+
883+class AmuletUtils(object):
884+ """Amulet utilities.
885+
886+ This class provides common utility functions that are used by Amulet
887+ tests.
888+ """
889+
890+ def __init__(self, log_level=logging.ERROR):
891+ self.log = self.get_logger(level=log_level)
892+ self.ubuntu_releases = self.get_ubuntu_releases()
893+
894+ def get_logger(self, name="amulet-logger", level=logging.DEBUG):
895+ """Get a logger object that will log to stdout."""
896+ log = logging
897+ logger = log.getLogger(name)
898+ fmt = log.Formatter("%(asctime)s %(funcName)s "
899+ "%(levelname)s: %(message)s")
900+
901+ handler = log.StreamHandler(stream=sys.stdout)
902+ handler.setLevel(level)
903+ handler.setFormatter(fmt)
904+
905+ logger.addHandler(handler)
906+ logger.setLevel(level)
907+
908+ return logger
909+
910+ def valid_ip(self, ip):
911+ if re.match(r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$", ip):
912+ return True
913+ else:
914+ return False
915+
916+ def valid_url(self, url):
917+ p = re.compile(
918+ r'^(?:http|ftp)s?://'
919+ r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' # noqa
920+ r'localhost|'
921+ r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})'
922+ r'(?::\d+)?'
923+ r'(?:/?|[/?]\S+)$',
924+ re.IGNORECASE)
925+ if p.match(url):
926+ return True
927+ else:
928+ return False
929+
930+ def get_ubuntu_release_from_sentry(self, sentry_unit):
931+ """Get Ubuntu release codename from sentry unit.
932+
933+ :param sentry_unit: amulet sentry/service unit pointer
934+ :returns: list of strings - release codename, failure message
935+ """
936+ msg = None
937+ cmd = 'lsb_release -cs'
938+ release, code = sentry_unit.run(cmd)
939+ if code == 0:
940+ self.log.debug('{} lsb_release: {}'.format(
941+ sentry_unit.info['unit_name'], release))
942+ else:
943+ msg = ('{} `{}` returned {} '
944+ '{}'.format(sentry_unit.info['unit_name'],
945+ cmd, release, code))
946+ if release not in self.ubuntu_releases:
947+ msg = ("Release ({}) not found in Ubuntu releases "
948+ "({})".format(release, self.ubuntu_releases))
949+ return release, msg
950+
951+ def validate_services(self, commands):
952+ """Validate that lists of commands succeed on service units. Can be
953+ used to verify system services are running on the corresponding
954+ service units.
955+
956+ :param commands: dict with sentry keys and arbitrary command list vals
957+ :returns: None if successful, Failure string message otherwise
958+ """
959+ self.log.debug('Checking status of system services...')
960+
961+ # /!\ DEPRECATION WARNING (beisner):
962+ # New and existing tests should be rewritten to use
963+ # validate_services_by_name() as it is aware of init systems.
964+ self.log.warn('DEPRECATION WARNING: use '
965+ 'validate_services_by_name instead of validate_services '
966+ 'due to init system differences.')
967+
968+ for k, v in six.iteritems(commands):
969+ for cmd in v:
970+ output, code = k.run(cmd)
971+ self.log.debug('{} `{}` returned '
972+ '{}'.format(k.info['unit_name'],
973+ cmd, code))
974+ if code != 0:
975+ return "command `{}` returned {}".format(cmd, str(code))
976+ return None
977+
978+ def validate_services_by_name(self, sentry_services):
979+ """Validate system service status by service name, automatically
980+ detecting init system based on Ubuntu release codename.
981+
982+ :param sentry_services: dict with sentry keys and svc list values
983+ :returns: None if successful, Failure string message otherwise
984+ """
985+ self.log.debug('Checking status of system services...')
986+
987+ # Point at which systemd became a thing
988+ systemd_switch = self.ubuntu_releases.index('vivid')
989+
990+ for sentry_unit, services_list in six.iteritems(sentry_services):
991+ # Get lsb_release codename from unit
992+ release, ret = self.get_ubuntu_release_from_sentry(sentry_unit)
993+ if ret:
994+ return ret
995+
996+ for service_name in services_list:
997+ if (self.ubuntu_releases.index(release) >= systemd_switch or
998+ service_name in ['rabbitmq-server', 'apache2']):
999+ # init is systemd (or regular sysv)
1000+ cmd = 'sudo service {} status'.format(service_name)
1001+ output, code = sentry_unit.run(cmd)
1002+ service_running = code == 0
1003+ elif self.ubuntu_releases.index(release) < systemd_switch:
1004+ # init is upstart
1005+ cmd = 'sudo status {}'.format(service_name)
1006+ output, code = sentry_unit.run(cmd)
1007+ service_running = code == 0 and "start/running" in output
1008+
1009+ self.log.debug('{} `{}` returned '
1010+ '{}'.format(sentry_unit.info['unit_name'],
1011+ cmd, code))
1012+ if not service_running:
1013+ return u"command `{}` returned {} {}".format(
1014+ cmd, output, str(code))
1015+ return None
1016+
1017+ def _get_config(self, unit, filename):
1018+ """Get a ConfigParser object for parsing a unit's config file."""
1019+ file_contents = unit.file_contents(filename)
1020+
1021+ # NOTE(beisner): by default, ConfigParser does not handle options
1022+ # with no value, such as the flags used in the mysql my.cnf file.
1023+ # https://bugs.python.org/issue7005
1024+ config = configparser.ConfigParser(allow_no_value=True)
1025+ config.readfp(io.StringIO(file_contents))
1026+ return config
1027+
1028+ def validate_config_data(self, sentry_unit, config_file, section,
1029+ expected):
1030+ """Validate config file data.
1031+
1032+ Verify that the specified section of the config file contains
1033+ the expected option key:value pairs.
1034+
1035+ Compare expected dictionary data vs actual dictionary data.
1036+ The values in the 'expected' dictionary can be strings, bools, ints,
1037+ longs, or can be a function that evaluates a variable and returns a
1038+ bool.
1039+ """
1040+ self.log.debug('Validating config file data ({} in {} on {})'
1041+ '...'.format(section, config_file,
1042+ sentry_unit.info['unit_name']))
1043+ config = self._get_config(sentry_unit, config_file)
1044+
1045+ if section != 'DEFAULT' and not config.has_section(section):
1046+ return "section [{}] does not exist".format(section)
1047+
1048+ for k in expected.keys():
1049+ if not config.has_option(section, k):
1050+ return "section [{}] is missing option {}".format(section, k)
1051+
1052+ actual = config.get(section, k)
1053+ v = expected[k]
1054+ if (isinstance(v, six.string_types) or
1055+ isinstance(v, bool) or
1056+ isinstance(v, six.integer_types)):
1057+ # handle explicit values
1058+ if actual != v:
1059+ return "section [{}] {}:{} != expected {}:{}".format(
1060+ section, k, actual, k, expected[k])
1061+ # handle function pointers, such as not_null or valid_ip
1062+ elif not v(actual):
1063+ return "section [{}] {}:{} != expected {}:{}".format(
1064+ section, k, actual, k, expected[k])
1065+ return None
1066+
1067+ def _validate_dict_data(self, expected, actual):
1068+ """Validate dictionary data.
1069+
1070+ Compare expected dictionary data vs actual dictionary data.
1071+ The values in the 'expected' dictionary can be strings, bools, ints,
1072+ longs, or can be a function that evaluates a variable and returns a
1073+ bool.
1074+ """
1075+ self.log.debug('actual: {}'.format(repr(actual)))
1076+ self.log.debug('expected: {}'.format(repr(expected)))
1077+
1078+ for k, v in six.iteritems(expected):
1079+ if k in actual:
1080+ if (isinstance(v, six.string_types) or
1081+ isinstance(v, bool) or
1082+ isinstance(v, six.integer_types)):
1083+ # handle explicit values
1084+ if v != actual[k]:
1085+ return "{}:{}".format(k, actual[k])
1086+ # handle function pointers, such as not_null or valid_ip
1087+ elif not v(actual[k]):
1088+ return "{}:{}".format(k, actual[k])
1089+ else:
1090+ return "key '{}' does not exist".format(k)
1091+ return None
1092+
1093+ def validate_relation_data(self, sentry_unit, relation, expected):
1094+ """Validate actual relation data based on expected relation data."""
1095+ actual = sentry_unit.relation(relation[0], relation[1])
1096+ return self._validate_dict_data(expected, actual)
1097+
1098+ def _validate_list_data(self, expected, actual):
1099+ """Compare expected list vs actual list data."""
1100+ for e in expected:
1101+ if e not in actual:
1102+ return "expected item {} not found in actual list".format(e)
1103+ return None
1104+
1105+ def not_null(self, string):
1106+ if string is not None:
1107+ return True
1108+ else:
1109+ return False
1110+
1111+ def _get_file_mtime(self, sentry_unit, filename):
1112+ """Get last modification time of file."""
1113+ return sentry_unit.file_stat(filename)['mtime']
1114+
1115+ def _get_dir_mtime(self, sentry_unit, directory):
1116+ """Get last modification time of directory."""
1117+ return sentry_unit.directory_stat(directory)['mtime']
1118+
1119+ def _get_proc_start_time(self, sentry_unit, service, pgrep_full=None):
1120+ """Get start time of a process based on the last modification time
1121+ of the /proc/pid directory.
1122+
1123+ :sentry_unit: The sentry unit to check for the service on
1124+ :service: service name to look for in process table
1125+ :pgrep_full: [Deprecated] Use full command line search mode with pgrep
1126+ :returns: epoch time of service process start
1127+ :param commands: list of bash commands
1128+ :param sentry_units: list of sentry unit pointers
1129+ :returns: None if successful; Failure message otherwise
1130+ """
1131+ if pgrep_full is not None:
1132+ # /!\ DEPRECATION WARNING (beisner):
1133+ # No longer implemented, as pidof is now used instead of pgrep.
1134+ # https://bugs.launchpad.net/charm-helpers/+bug/1474030
1135+ self.log.warn('DEPRECATION WARNING: pgrep_full bool is no '
1136+ 'longer implemented re: lp 1474030.')
1137+
1138+ pid_list = self.get_process_id_list(sentry_unit, service)
1139+ pid = pid_list[0]
1140+ proc_dir = '/proc/{}'.format(pid)
1141+ self.log.debug('Pid for {} on {}: {}'.format(
1142+ service, sentry_unit.info['unit_name'], pid))
1143+
1144+ return self._get_dir_mtime(sentry_unit, proc_dir)
1145+
1146+ def service_restarted(self, sentry_unit, service, filename,
1147+ pgrep_full=None, sleep_time=20):
1148+ """Check if service was restarted.
1149+
1150+ Compare a service's start time vs a file's last modification time
1151+ (such as a config file for that service) to determine if the service
1152+ has been restarted.
1153+ """
1154+ # /!\ DEPRECATION WARNING (beisner):
1155+ # This method is prone to races in that no before-time is known.
1156+ # Use validate_service_config_changed instead.
1157+
1158+ # NOTE(beisner) pgrep_full is no longer implemented, as pidof is now
1159+ # used instead of pgrep. pgrep_full is still passed through to ensure
1160+ # deprecation WARNS. lp1474030
1161+ self.log.warn('DEPRECATION WARNING: use '
1162+ 'validate_service_config_changed instead of '
1163+ 'service_restarted due to known races.')
1164+
1165+ time.sleep(sleep_time)
1166+ if (self._get_proc_start_time(sentry_unit, service, pgrep_full) >=
1167+ self._get_file_mtime(sentry_unit, filename)):
1168+ return True
1169+ else:
1170+ return False
1171+
1172+ def service_restarted_since(self, sentry_unit, mtime, service,
1173+ pgrep_full=None, sleep_time=20,
1174+ retry_count=30, retry_sleep_time=10):
1175+ """Check if service was been started after a given time.
1176+
1177+ Args:
1178+ sentry_unit (sentry): The sentry unit to check for the service on
1179+ mtime (float): The epoch time to check against
1180+ service (string): service name to look for in process table
1181+ pgrep_full: [Deprecated] Use full command line search mode with pgrep
1182+ sleep_time (int): Initial sleep time (s) before looking for file
1183+ retry_sleep_time (int): Time (s) to sleep between retries
1184+ retry_count (int): If file is not found, how many times to retry
1185+
1186+ Returns:
1187+ bool: True if service found and its start time it newer than mtime,
1188+ False if service is older than mtime or if service was
1189+ not found.
1190+ """
1191+ # NOTE(beisner) pgrep_full is no longer implemented, as pidof is now
1192+ # used instead of pgrep. pgrep_full is still passed through to ensure
1193+ # deprecation WARNS. lp1474030
1194+
1195+ unit_name = sentry_unit.info['unit_name']
1196+ self.log.debug('Checking that %s service restarted since %s on '
1197+ '%s' % (service, mtime, unit_name))
1198+ time.sleep(sleep_time)
1199+ proc_start_time = None
1200+ tries = 0
1201+ while tries <= retry_count and not proc_start_time:
1202+ try:
1203+ proc_start_time = self._get_proc_start_time(sentry_unit,
1204+ service,
1205+ pgrep_full)
1206+ self.log.debug('Attempt {} to get {} proc start time on {} '
1207+ 'OK'.format(tries, service, unit_name))
1208+ except IOError as e:
1209+ # NOTE(beisner) - race avoidance, proc may not exist yet.
1210+ # https://bugs.launchpad.net/charm-helpers/+bug/1474030
1211+ self.log.debug('Attempt {} to get {} proc start time on {} '
1212+ 'failed\n{}'.format(tries, service,
1213+ unit_name, e))
1214+ time.sleep(retry_sleep_time)
1215+ tries += 1
1216+
1217+ if not proc_start_time:
1218+ self.log.warn('No proc start time found, assuming service did '
1219+ 'not start')
1220+ return False
1221+ if proc_start_time >= mtime:
1222+ self.log.debug('Proc start time is newer than provided mtime'
1223+ '(%s >= %s) on %s (OK)' % (proc_start_time,
1224+ mtime, unit_name))
1225+ return True
1226+ else:
1227+ self.log.warn('Proc start time (%s) is older than provided mtime '
1228+ '(%s) on %s, service did not '
1229+ 'restart' % (proc_start_time, mtime, unit_name))
1230+ return False
1231+
1232+ def config_updated_since(self, sentry_unit, filename, mtime,
1233+ sleep_time=20, retry_count=30,
1234+ retry_sleep_time=10):
1235+ """Check if file was modified after a given time.
1236+
1237+ Args:
1238+ sentry_unit (sentry): The sentry unit to check the file mtime on
1239+ filename (string): The file to check mtime of
1240+ mtime (float): The epoch time to check against
1241+ sleep_time (int): Initial sleep time (s) before looking for file
1242+ retry_sleep_time (int): Time (s) to sleep between retries
1243+ retry_count (int): If file is not found, how many times to retry
1244+
1245+ Returns:
1246+ bool: True if file was modified more recently than mtime, False if
1247+ file was modified before mtime, or if file not found.
1248+ """
1249+ unit_name = sentry_unit.info['unit_name']
1250+ self.log.debug('Checking that %s updated since %s on '
1251+ '%s' % (filename, mtime, unit_name))
1252+ time.sleep(sleep_time)
1253+ file_mtime = None
1254+ tries = 0
1255+ while tries <= retry_count and not file_mtime:
1256+ try:
1257+ file_mtime = self._get_file_mtime(sentry_unit, filename)
1258+ self.log.debug('Attempt {} to get {} file mtime on {} '
1259+ 'OK'.format(tries, filename, unit_name))
1260+ except IOError as e:
1261+ # NOTE(beisner) - race avoidance, file may not exist yet.
1262+ # https://bugs.launchpad.net/charm-helpers/+bug/1474030
1263+ self.log.debug('Attempt {} to get {} file mtime on {} '
1264+ 'failed\n{}'.format(tries, filename,
1265+ unit_name, e))
1266+ time.sleep(retry_sleep_time)
1267+ tries += 1
1268+
1269+ if not file_mtime:
1270+ self.log.warn('Could not determine file mtime, assuming '
1271+ 'file does not exist')
1272+ return False
1273+
1274+ if file_mtime >= mtime:
1275+ self.log.debug('File mtime is newer than provided mtime '
1276+ '(%s >= %s) on %s (OK)' % (file_mtime,
1277+ mtime, unit_name))
1278+ return True
1279+ else:
1280+ self.log.warn('File mtime is older than provided mtime'
1281+ '(%s < on %s) on %s' % (file_mtime,
1282+ mtime, unit_name))
1283+ return False
1284+
1285+ def validate_service_config_changed(self, sentry_unit, mtime, service,
1286+ filename, pgrep_full=None,
1287+ sleep_time=20, retry_count=30,
1288+ retry_sleep_time=10):
1289+ """Check service and file were updated after mtime
1290+
1291+ Args:
1292+ sentry_unit (sentry): The sentry unit to check for the service on
1293+ mtime (float): The epoch time to check against
1294+ service (string): service name to look for in process table
1295+ filename (string): The file to check mtime of
1296+ pgrep_full: [Deprecated] Use full command line search mode with pgrep
1297+ sleep_time (int): Initial sleep in seconds to pass to test helpers
1298+ retry_count (int): If service is not found, how many times to retry
1299+ retry_sleep_time (int): Time in seconds to wait between retries
1300+
1301+ Typical Usage:
1302+ u = OpenStackAmuletUtils(ERROR)
1303+ ...
1304+ mtime = u.get_sentry_time(self.cinder_sentry)
1305+ self.d.configure('cinder', {'verbose': 'True', 'debug': 'True'})
1306+ if not u.validate_service_config_changed(self.cinder_sentry,
1307+ mtime,
1308+ 'cinder-api',
1309+ '/etc/cinder/cinder.conf')
1310+ amulet.raise_status(amulet.FAIL, msg='update failed')
1311+ Returns:
1312+ bool: True if both service and file where updated/restarted after
1313+ mtime, False if service is older than mtime or if service was
1314+ not found or if filename was modified before mtime.
1315+ """
1316+
1317+ # NOTE(beisner) pgrep_full is no longer implemented, as pidof is now
1318+ # used instead of pgrep. pgrep_full is still passed through to ensure
1319+ # deprecation WARNS. lp1474030
1320+
1321+ service_restart = self.service_restarted_since(
1322+ sentry_unit, mtime,
1323+ service,
1324+ pgrep_full=pgrep_full,
1325+ sleep_time=sleep_time,
1326+ retry_count=retry_count,
1327+ retry_sleep_time=retry_sleep_time)
1328+
1329+ config_update = self.config_updated_since(
1330+ sentry_unit,
1331+ filename,
1332+ mtime,
1333+ sleep_time=sleep_time,
1334+ retry_count=retry_count,
1335+ retry_sleep_time=retry_sleep_time)
1336+
1337+ return service_restart and config_update
1338+
1339+ def get_sentry_time(self, sentry_unit):
1340+ """Return current epoch time on a sentry"""
1341+ cmd = "date +'%s'"
1342+ return float(sentry_unit.run(cmd)[0])
1343+
1344+ def relation_error(self, name, data):
1345+ return 'unexpected relation data in {} - {}'.format(name, data)
1346+
1347+ def endpoint_error(self, name, data):
1348+ return 'unexpected endpoint data in {} - {}'.format(name, data)
1349+
1350+ def get_ubuntu_releases(self):
1351+ """Return a list of all Ubuntu releases in order of release."""
1352+ _d = distro_info.UbuntuDistroInfo()
1353+ _release_list = _d.all
1354+ return _release_list
1355+
1356+ def file_to_url(self, file_rel_path):
1357+ """Convert a relative file path to a file URL."""
1358+ _abs_path = os.path.abspath(file_rel_path)
1359+ return urlparse.urlparse(_abs_path, scheme='file').geturl()
1360+
1361+ def check_commands_on_units(self, commands, sentry_units):
1362+ """Check that all commands in a list exit zero on all
1363+ sentry units in a list.
1364+
1365+ :param commands: list of bash commands
1366+ :param sentry_units: list of sentry unit pointers
1367+ :returns: None if successful; Failure message otherwise
1368+ """
1369+ self.log.debug('Checking exit codes for {} commands on {} '
1370+ 'sentry units...'.format(len(commands),
1371+ len(sentry_units)))
1372+ for sentry_unit in sentry_units:
1373+ for cmd in commands:
1374+ output, code = sentry_unit.run(cmd)
1375+ if code == 0:
1376+ self.log.debug('{} `{}` returned {} '
1377+ '(OK)'.format(sentry_unit.info['unit_name'],
1378+ cmd, code))
1379+ else:
1380+ return ('{} `{}` returned {} '
1381+ '{}'.format(sentry_unit.info['unit_name'],
1382+ cmd, code, output))
1383+ return None
1384+
1385+ def get_process_id_list(self, sentry_unit, process_name,
1386+ expect_success=True):
1387+ """Get a list of process ID(s) from a single sentry juju unit
1388+ for a single process name.
1389+
1390+ :param sentry_unit: Amulet sentry instance (juju unit)
1391+ :param process_name: Process name
1392+ :param expect_success: If False, expect the PID to be missing,
1393+ raise if it is present.
1394+ :returns: List of process IDs
1395+ """
1396+ cmd = 'pidof -x {}'.format(process_name)
1397+ if not expect_success:
1398+ cmd += " || exit 0 && exit 1"
1399+ output, code = sentry_unit.run(cmd)
1400+ if code != 0:
1401+ msg = ('{} `{}` returned {} '
1402+ '{}'.format(sentry_unit.info['unit_name'],
1403+ cmd, code, output))
1404+ amulet.raise_status(amulet.FAIL, msg=msg)
1405+ return str(output).split()
1406+
1407+ def get_unit_process_ids(self, unit_processes, expect_success=True):
1408+ """Construct a dict containing unit sentries, process names, and
1409+ process IDs.
1410+
1411+ :param unit_processes: A dictionary of Amulet sentry instance
1412+ to list of process names.
1413+ :param expect_success: if False expect the processes to not be
1414+ running, raise if they are.
1415+ :returns: Dictionary of Amulet sentry instance to dictionary
1416+ of process names to PIDs.
1417+ """
1418+ pid_dict = {}
1419+ for sentry_unit, process_list in six.iteritems(unit_processes):
1420+ pid_dict[sentry_unit] = {}
1421+ for process in process_list:
1422+ pids = self.get_process_id_list(
1423+ sentry_unit, process, expect_success=expect_success)
1424+ pid_dict[sentry_unit].update({process: pids})
1425+ return pid_dict
1426+
1427+ def validate_unit_process_ids(self, expected, actual):
1428+ """Validate process id quantities for services on units."""
1429+ self.log.debug('Checking units for running processes...')
1430+ self.log.debug('Expected PIDs: {}'.format(expected))
1431+ self.log.debug('Actual PIDs: {}'.format(actual))
1432+
1433+ if len(actual) != len(expected):
1434+ return ('Unit count mismatch. expected, actual: {}, '
1435+ '{} '.format(len(expected), len(actual)))
1436+
1437+ for (e_sentry, e_proc_names) in six.iteritems(expected):
1438+ e_sentry_name = e_sentry.info['unit_name']
1439+ if e_sentry in actual.keys():
1440+ a_proc_names = actual[e_sentry]
1441+ else:
1442+ return ('Expected sentry ({}) not found in actual dict data.'
1443+ '{}'.format(e_sentry_name, e_sentry))
1444+
1445+ if len(e_proc_names.keys()) != len(a_proc_names.keys()):
1446+ return ('Process name count mismatch. expected, actual: {}, '
1447+ '{}'.format(len(expected), len(actual)))
1448+
1449+ for (e_proc_name, e_pids_length), (a_proc_name, a_pids) in \
1450+ zip(e_proc_names.items(), a_proc_names.items()):
1451+ if e_proc_name != a_proc_name:
1452+ return ('Process name mismatch. expected, actual: {}, '
1453+ '{}'.format(e_proc_name, a_proc_name))
1454+
1455+ a_pids_length = len(a_pids)
1456+ fail_msg = ('PID count mismatch. {} ({}) expected, actual: '
1457+ '{}, {} ({})'.format(e_sentry_name, e_proc_name,
1458+ e_pids_length, a_pids_length,
1459+ a_pids))
1460+
1461+ # If expected is not bool, ensure PID quantities match
1462+ if not isinstance(e_pids_length, bool) and \
1463+ a_pids_length != e_pids_length:
1464+ return fail_msg
1465+ # If expected is bool True, ensure 1 or more PIDs exist
1466+ elif isinstance(e_pids_length, bool) and \
1467+ e_pids_length is True and a_pids_length < 1:
1468+ return fail_msg
1469+ # If expected is bool False, ensure 0 PIDs exist
1470+ elif isinstance(e_pids_length, bool) and \
1471+ e_pids_length is False and a_pids_length != 0:
1472+ return fail_msg
1473+ else:
1474+ self.log.debug('PID check OK: {} {} {}: '
1475+ '{}'.format(e_sentry_name, e_proc_name,
1476+ e_pids_length, a_pids))
1477+ return None
1478+
1479+ def validate_list_of_identical_dicts(self, list_of_dicts):
1480+ """Check that all dicts within a list are identical."""
1481+ hashes = []
1482+ for _dict in list_of_dicts:
1483+ hashes.append(hash(frozenset(_dict.items())))
1484+
1485+ self.log.debug('Hashes: {}'.format(hashes))
1486+ if len(set(hashes)) == 1:
1487+ self.log.debug('Dicts within list are identical')
1488+ else:
1489+ return 'Dicts within list are not identical'
1490+
1491+ return None
1492+
1493+ def validate_sectionless_conf(self, file_contents, expected):
1494+ """A crude conf parser. Useful to inspect configuration files which
1495+ do not have section headers (as would be necessary in order to use
1496+ the configparser). Such as openstack-dashboard or rabbitmq confs."""
1497+ for line in file_contents.split('\n'):
1498+ if '=' in line:
1499+ args = line.split('=')
1500+ if len(args) <= 1:
1501+ continue
1502+ key = args[0].strip()
1503+ value = args[1].strip()
1504+ if key in expected.keys():
1505+ if expected[key] != value:
1506+ msg = ('Config mismatch. Expected, actual: {}, '
1507+ '{}'.format(expected[key], value))
1508+ amulet.raise_status(amulet.FAIL, msg=msg)
1509+
1510+ def get_unit_hostnames(self, units):
1511+ """Return a dict of juju unit names to hostnames."""
1512+ host_names = {}
1513+ for unit in units:
1514+ host_names[unit.info['unit_name']] = \
1515+ str(unit.file_contents('/etc/hostname').strip())
1516+ self.log.debug('Unit host names: {}'.format(host_names))
1517+ return host_names
1518+
1519+ def run_cmd_unit(self, sentry_unit, cmd):
1520+ """Run a command on a unit, return the output and exit code."""
1521+ output, code = sentry_unit.run(cmd)
1522+ if code == 0:
1523+ self.log.debug('{} `{}` command returned {} '
1524+ '(OK)'.format(sentry_unit.info['unit_name'],
1525+ cmd, code))
1526+ else:
1527+ msg = ('{} `{}` command returned {} '
1528+ '{}'.format(sentry_unit.info['unit_name'],
1529+ cmd, code, output))
1530+ amulet.raise_status(amulet.FAIL, msg=msg)
1531+ return str(output), code
1532+
1533+ def file_exists_on_unit(self, sentry_unit, file_name):
1534+ """Check if a file exists on a unit."""
1535+ try:
1536+ sentry_unit.file_stat(file_name)
1537+ return True
1538+ except IOError:
1539+ return False
1540+ except Exception as e:
1541+ msg = 'Error checking file {}: {}'.format(file_name, e)
1542+ amulet.raise_status(amulet.FAIL, msg=msg)
1543+
1544+ def file_contents_safe(self, sentry_unit, file_name,
1545+ max_wait=60, fatal=False):
1546+ """Get file contents from a sentry unit. Wrap amulet file_contents
1547+ with retry logic to address races where a file checks as existing,
1548+ but no longer exists by the time file_contents is called.
1549+ Return None if file not found. Optionally raise if fatal is True."""
1550+ unit_name = sentry_unit.info['unit_name']
1551+ file_contents = False
1552+ tries = 0
1553+ while not file_contents and tries < (max_wait / 4):
1554+ try:
1555+ file_contents = sentry_unit.file_contents(file_name)
1556+ except IOError:
1557+ self.log.debug('Attempt {} to open file {} from {} '
1558+ 'failed'.format(tries, file_name,
1559+ unit_name))
1560+ time.sleep(4)
1561+ tries += 1
1562+
1563+ if file_contents:
1564+ return file_contents
1565+ elif not fatal:
1566+ return None
1567+ elif fatal:
1568+ msg = 'Failed to get file contents from unit.'
1569+ amulet.raise_status(amulet.FAIL, msg)
1570+
1571+ def port_knock_tcp(self, host="localhost", port=22, timeout=15):
1572+ """Open a TCP socket to check for a listening sevice on a host.
1573+
1574+ :param host: host name or IP address, default to localhost
1575+ :param port: TCP port number, default to 22
1576+ :param timeout: Connect timeout, default to 15 seconds
1577+ :returns: True if successful, False if connect failed
1578+ """
1579+
1580+ # Resolve host name if possible
1581+ try:
1582+ connect_host = socket.gethostbyname(host)
1583+ host_human = "{} ({})".format(connect_host, host)
1584+ except socket.error as e:
1585+ self.log.warn('Unable to resolve address: '
1586+ '{} ({}) Trying anyway!'.format(host, e))
1587+ connect_host = host
1588+ host_human = connect_host
1589+
1590+ # Attempt socket connection
1591+ try:
1592+ knock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
1593+ knock.settimeout(timeout)
1594+ knock.connect((connect_host, port))
1595+ knock.close()
1596+ self.log.debug('Socket connect OK for host '
1597+ '{} on port {}.'.format(host_human, port))
1598+ return True
1599+ except socket.error as e:
1600+ self.log.debug('Socket connect FAIL for'
1601+ ' {} port {} ({})'.format(host_human, port, e))
1602+ return False
1603+
1604+ def port_knock_units(self, sentry_units, port=22,
1605+ timeout=15, expect_success=True):
1606+ """Open a TCP socket to check for a listening sevice on each
1607+ listed juju unit.
1608+
1609+ :param sentry_units: list of sentry unit pointers
1610+ :param port: TCP port number, default to 22
1611+ :param timeout: Connect timeout, default to 15 seconds
1612+ :expect_success: True by default, set False to invert logic
1613+ :returns: None if successful, Failure message otherwise
1614+ """
1615+ for unit in sentry_units:
1616+ host = unit.info['public-address']
1617+ connected = self.port_knock_tcp(host, port, timeout)
1618+ if not connected and expect_success:
1619+ return 'Socket connect failed.'
1620+ elif connected and not expect_success:
1621+ return 'Socket connected unexpectedly.'
1622+
1623+ def get_uuid_epoch_stamp(self):
1624+ """Returns a stamp string based on uuid4 and epoch time. Useful in
1625+ generating test messages which need to be unique-ish."""
1626+ return '[{}-{}]'.format(uuid.uuid4(), time.time())
1627+
1628+# amulet juju action helpers:
1629+ def run_action(self, unit_sentry, action,
1630+ _check_output=subprocess.check_output,
1631+ params=None):
1632+ """Run the named action on a given unit sentry.
1633+
1634+ params a dict of parameters to use
1635+ _check_output parameter is used for dependency injection.
1636+
1637+ @return action_id.
1638+ """
1639+ unit_id = unit_sentry.info["unit_name"]
1640+ command = ["juju", "action", "do", "--format=json", unit_id, action]
1641+ if params is not None:
1642+ for key, value in params.iteritems():
1643+ command.append("{}={}".format(key, value))
1644+ self.log.info("Running command: %s\n" % " ".join(command))
1645+ output = _check_output(command, universal_newlines=True)
1646+ data = json.loads(output)
1647+ action_id = data[u'Action queued with id']
1648+ return action_id
1649+
1650+ def wait_on_action(self, action_id, _check_output=subprocess.check_output):
1651+ """Wait for a given action, returning if it completed or not.
1652+
1653+ _check_output parameter is used for dependency injection.
1654+ """
1655+ command = ["juju", "action", "fetch", "--format=json", "--wait=0",
1656+ action_id]
1657+ output = _check_output(command, universal_newlines=True)
1658+ data = json.loads(output)
1659+ return data.get(u"status") == "completed"
1660+
1661+ def status_get(self, unit):
1662+ """Return the current service status of this unit."""
1663+ raw_status, return_code = unit.run(
1664+ "status-get --format=json --include-data")
1665+ if return_code != 0:
1666+ return ("unknown", "")
1667+ status = json.loads(raw_status)
1668+ return (status["status"], status["message"])
1669
1670=== added directory 'hooks/charmhelpers/contrib/ansible'
1671=== added file 'hooks/charmhelpers/contrib/ansible/__init__.py'
1672--- hooks/charmhelpers/contrib/ansible/__init__.py 1970-01-01 00:00:00 +0000
1673+++ hooks/charmhelpers/contrib/ansible/__init__.py 2016-03-21 06:13:21 +0000
1674@@ -0,0 +1,254 @@
1675+# Copyright 2014-2015 Canonical Limited.
1676+#
1677+# This file is part of charm-helpers.
1678+#
1679+# charm-helpers is free software: you can redistribute it and/or modify
1680+# it under the terms of the GNU Lesser General Public License version 3 as
1681+# published by the Free Software Foundation.
1682+#
1683+# charm-helpers is distributed in the hope that it will be useful,
1684+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1685+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1686+# GNU Lesser General Public License for more details.
1687+#
1688+# You should have received a copy of the GNU Lesser General Public License
1689+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1690+
1691+# Copyright 2013 Canonical Ltd.
1692+#
1693+# Authors:
1694+# Charm Helpers Developers <juju@lists.ubuntu.com>
1695+"""Charm Helpers ansible - declare the state of your machines.
1696+
1697+This helper enables you to declare your machine state, rather than
1698+program it procedurally (and have to test each change to your procedures).
1699+Your install hook can be as simple as::
1700+
1701+ {{{
1702+ import charmhelpers.contrib.ansible
1703+
1704+
1705+ def install():
1706+ charmhelpers.contrib.ansible.install_ansible_support()
1707+ charmhelpers.contrib.ansible.apply_playbook('playbooks/install.yaml')
1708+ }}}
1709+
1710+and won't need to change (nor will its tests) when you change the machine
1711+state.
1712+
1713+All of your juju config and relation-data are available as template
1714+variables within your playbooks and templates. An install playbook looks
1715+something like::
1716+
1717+ {{{
1718+ ---
1719+ - hosts: localhost
1720+ user: root
1721+
1722+ tasks:
1723+ - name: Add private repositories.
1724+ template:
1725+ src: ../templates/private-repositories.list.jinja2
1726+ dest: /etc/apt/sources.list.d/private.list
1727+
1728+ - name: Update the cache.
1729+ apt: update_cache=yes
1730+
1731+ - name: Install dependencies.
1732+ apt: pkg={{ item }}
1733+ with_items:
1734+ - python-mimeparse
1735+ - python-webob
1736+ - sunburnt
1737+
1738+ - name: Setup groups.
1739+ group: name={{ item.name }} gid={{ item.gid }}
1740+ with_items:
1741+ - { name: 'deploy_user', gid: 1800 }
1742+ - { name: 'service_user', gid: 1500 }
1743+
1744+ ...
1745+ }}}
1746+
1747+Read more online about `playbooks`_ and standard ansible `modules`_.
1748+
1749+.. _playbooks: http://www.ansibleworks.com/docs/playbooks.html
1750+.. _modules: http://www.ansibleworks.com/docs/modules.html
1751+
1752+A further feature os the ansible hooks is to provide a light weight "action"
1753+scripting tool. This is a decorator that you apply to a function, and that
1754+function can now receive cli args, and can pass extra args to the playbook.
1755+
1756+e.g.
1757+
1758+
1759+@hooks.action()
1760+def some_action(amount, force="False"):
1761+ "Usage: some-action AMOUNT [force=True]" # <-- shown on error
1762+ # process the arguments
1763+ # do some calls
1764+ # return extra-vars to be passed to ansible-playbook
1765+ return {
1766+ 'amount': int(amount),
1767+ 'type': force,
1768+ }
1769+
1770+You can now create a symlink to hooks.py that can be invoked like a hook, but
1771+with cli params:
1772+
1773+# link actions/some-action to hooks/hooks.py
1774+
1775+actions/some-action amount=10 force=true
1776+
1777+"""
1778+import os
1779+import stat
1780+import subprocess
1781+import functools
1782+
1783+import charmhelpers.contrib.templating.contexts
1784+import charmhelpers.core.host
1785+import charmhelpers.core.hookenv
1786+import charmhelpers.fetch
1787+
1788+
1789+charm_dir = os.environ.get('CHARM_DIR', '')
1790+ansible_hosts_path = '/etc/ansible/hosts'
1791+# Ansible will automatically include any vars in the following
1792+# file in its inventory when run locally.
1793+ansible_vars_path = '/etc/ansible/host_vars/localhost'
1794+
1795+
1796+def install_ansible_support(from_ppa=True, ppa_location='ppa:rquillo/ansible'):
1797+ """Installs the ansible package.
1798+
1799+ By default it is installed from the `PPA`_ linked from
1800+ the ansible `website`_ or from a ppa specified by a charm config..
1801+
1802+ .. _PPA: https://launchpad.net/~rquillo/+archive/ansible
1803+ .. _website: http://docs.ansible.com/intro_installation.html#latest-releases-via-apt-ubuntu
1804+
1805+ If from_ppa is empty, you must ensure that the package is available
1806+ from a configured repository.
1807+ """
1808+ if from_ppa:
1809+ charmhelpers.fetch.add_source(ppa_location)
1810+ charmhelpers.fetch.apt_update(fatal=True)
1811+ charmhelpers.fetch.apt_install('ansible')
1812+ with open(ansible_hosts_path, 'w+') as hosts_file:
1813+ hosts_file.write('localhost ansible_connection=local')
1814+
1815+
1816+def apply_playbook(playbook, tags=None, extra_vars=None):
1817+ tags = tags or []
1818+ tags = ",".join(tags)
1819+ charmhelpers.contrib.templating.contexts.juju_state_to_yaml(
1820+ ansible_vars_path, namespace_separator='__',
1821+ allow_hyphens_in_keys=False, mode=(stat.S_IRUSR | stat.S_IWUSR))
1822+
1823+ # we want ansible's log output to be unbuffered
1824+ env = os.environ.copy()
1825+ env['PYTHONUNBUFFERED'] = "1"
1826+ call = [
1827+ 'ansible-playbook',
1828+ '-c',
1829+ 'local',
1830+ playbook,
1831+ ]
1832+ if tags:
1833+ call.extend(['--tags', '{}'.format(tags)])
1834+ if extra_vars:
1835+ extra = ["%s=%s" % (k, v) for k, v in extra_vars.items()]
1836+ call.extend(['--extra-vars', " ".join(extra)])
1837+ subprocess.check_call(call, env=env)
1838+
1839+
1840+class AnsibleHooks(charmhelpers.core.hookenv.Hooks):
1841+ """Run a playbook with the hook-name as the tag.
1842+
1843+ This helper builds on the standard hookenv.Hooks helper,
1844+ but additionally runs the playbook with the hook-name specified
1845+ using --tags (ie. running all the tasks tagged with the hook-name).
1846+
1847+ Example::
1848+
1849+ hooks = AnsibleHooks(playbook_path='playbooks/my_machine_state.yaml')
1850+
1851+ # All the tasks within my_machine_state.yaml tagged with 'install'
1852+ # will be run automatically after do_custom_work()
1853+ @hooks.hook()
1854+ def install():
1855+ do_custom_work()
1856+
1857+ # For most of your hooks, you won't need to do anything other
1858+ # than run the tagged tasks for the hook:
1859+ @hooks.hook('config-changed', 'start', 'stop')
1860+ def just_use_playbook():
1861+ pass
1862+
1863+ # As a convenience, you can avoid the above noop function by specifying
1864+ # the hooks which are handled by ansible-only and they'll be registered
1865+ # for you:
1866+ # hooks = AnsibleHooks(
1867+ # 'playbooks/my_machine_state.yaml',
1868+ # default_hooks=['config-changed', 'start', 'stop'])
1869+
1870+ if __name__ == "__main__":
1871+ # execute a hook based on the name the program is called by
1872+ hooks.execute(sys.argv)
1873+
1874+ """
1875+
1876+ def __init__(self, playbook_path, default_hooks=None):
1877+ """Register any hooks handled by ansible."""
1878+ super(AnsibleHooks, self).__init__()
1879+
1880+ self._actions = {}
1881+ self.playbook_path = playbook_path
1882+
1883+ default_hooks = default_hooks or []
1884+
1885+ def noop(*args, **kwargs):
1886+ pass
1887+
1888+ for hook in default_hooks:
1889+ self.register(hook, noop)
1890+
1891+ def register_action(self, name, function):
1892+ """Register a hook"""
1893+ self._actions[name] = function
1894+
1895+ def execute(self, args):
1896+ """Execute the hook followed by the playbook using the hook as tag."""
1897+ hook_name = os.path.basename(args[0])
1898+ extra_vars = None
1899+ if hook_name in self._actions:
1900+ extra_vars = self._actions[hook_name](args[1:])
1901+ else:
1902+ super(AnsibleHooks, self).execute(args)
1903+
1904+ charmhelpers.contrib.ansible.apply_playbook(
1905+ self.playbook_path, tags=[hook_name], extra_vars=extra_vars)
1906+
1907+ def action(self, *action_names):
1908+ """Decorator, registering them as actions"""
1909+ def action_wrapper(decorated):
1910+
1911+ @functools.wraps(decorated)
1912+ def wrapper(argv):
1913+ kwargs = dict(arg.split('=') for arg in argv)
1914+ try:
1915+ return decorated(**kwargs)
1916+ except TypeError as e:
1917+ if decorated.__doc__:
1918+ e.args += (decorated.__doc__,)
1919+ raise
1920+
1921+ self.register_action(decorated.__name__, wrapper)
1922+ if '_' in decorated.__name__:
1923+ self.register_action(
1924+ decorated.__name__.replace('_', '-'), wrapper)
1925+
1926+ return wrapper
1927+
1928+ return action_wrapper
1929
1930=== added directory 'hooks/charmhelpers/contrib/benchmark'
1931=== added file 'hooks/charmhelpers/contrib/benchmark/__init__.py'
1932--- hooks/charmhelpers/contrib/benchmark/__init__.py 1970-01-01 00:00:00 +0000
1933+++ hooks/charmhelpers/contrib/benchmark/__init__.py 2016-03-21 06:13:21 +0000
1934@@ -0,0 +1,126 @@
1935+# Copyright 2014-2015 Canonical Limited.
1936+#
1937+# This file is part of charm-helpers.
1938+#
1939+# charm-helpers is free software: you can redistribute it and/or modify
1940+# it under the terms of the GNU Lesser General Public License version 3 as
1941+# published by the Free Software Foundation.
1942+#
1943+# charm-helpers is distributed in the hope that it will be useful,
1944+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1945+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1946+# GNU Lesser General Public License for more details.
1947+#
1948+# You should have received a copy of the GNU Lesser General Public License
1949+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1950+
1951+import subprocess
1952+import time
1953+import os
1954+from distutils.spawn import find_executable
1955+
1956+from charmhelpers.core.hookenv import (
1957+ in_relation_hook,
1958+ relation_ids,
1959+ relation_set,
1960+ relation_get,
1961+)
1962+
1963+
1964+def action_set(key, val):
1965+ if find_executable('action-set'):
1966+ action_cmd = ['action-set']
1967+
1968+ if isinstance(val, dict):
1969+ for k, v in iter(val.items()):
1970+ action_set('%s.%s' % (key, k), v)
1971+ return True
1972+
1973+ action_cmd.append('%s=%s' % (key, val))
1974+ subprocess.check_call(action_cmd)
1975+ return True
1976+ return False
1977+
1978+
1979+class Benchmark():
1980+ """
1981+ Helper class for the `benchmark` interface.
1982+
1983+ :param list actions: Define the actions that are also benchmarks
1984+
1985+ From inside the benchmark-relation-changed hook, you would
1986+ Benchmark(['memory', 'cpu', 'disk', 'smoke', 'custom'])
1987+
1988+ Examples:
1989+
1990+ siege = Benchmark(['siege'])
1991+ siege.start()
1992+ [... run siege ...]
1993+ # The higher the score, the better the benchmark
1994+ siege.set_composite_score(16.70, 'trans/sec', 'desc')
1995+ siege.finish()
1996+
1997+
1998+ """
1999+
2000+ BENCHMARK_CONF = '/etc/benchmark.conf' # Replaced in testing
2001+
2002+ required_keys = [
2003+ 'hostname',
2004+ 'port',
2005+ 'graphite_port',
2006+ 'graphite_endpoint',
2007+ 'api_port'
2008+ ]
2009+
2010+ def __init__(self, benchmarks=None):
2011+ if in_relation_hook():
2012+ if benchmarks is not None:
2013+ for rid in sorted(relation_ids('benchmark')):
2014+ relation_set(relation_id=rid, relation_settings={
2015+ 'benchmarks': ",".join(benchmarks)
2016+ })
2017+
2018+ # Check the relation data
2019+ config = {}
2020+ for key in self.required_keys:
2021+ val = relation_get(key)
2022+ if val is not None:
2023+ config[key] = val
2024+ else:
2025+ # We don't have all of the required keys
2026+ config = {}
2027+ break
2028+
2029+ if len(config):
2030+ with open(self.BENCHMARK_CONF, 'w') as f:
2031+ for key, val in iter(config.items()):
2032+ f.write("%s=%s\n" % (key, val))
2033+
2034+ @staticmethod
2035+ def start():
2036+ action_set('meta.start', time.strftime('%Y-%m-%dT%H:%M:%SZ'))
2037+
2038+ """
2039+ If the collectd charm is also installed, tell it to send a snapshot
2040+ of the current profile data.
2041+ """
2042+ COLLECT_PROFILE_DATA = '/usr/local/bin/collect-profile-data'
2043+ if os.path.exists(COLLECT_PROFILE_DATA):
2044+ subprocess.check_output([COLLECT_PROFILE_DATA])
2045+
2046+ @staticmethod
2047+ def finish():
2048+ action_set('meta.stop', time.strftime('%Y-%m-%dT%H:%M:%SZ'))
2049+
2050+ @staticmethod
2051+ def set_composite_score(value, units, direction='asc'):
2052+ """
2053+ Set the composite score for a benchmark run. This is a single number
2054+ representative of the benchmark results. This could be the most
2055+ important metric, or an amalgamation of metric scores.
2056+ """
2057+ return action_set(
2058+ "meta.composite",
2059+ {'value': value, 'units': units, 'direction': direction}
2060+ )
2061
2062=== added directory 'hooks/charmhelpers/contrib/charmhelpers'
2063=== added file 'hooks/charmhelpers/contrib/charmhelpers/IMPORT'
2064--- hooks/charmhelpers/contrib/charmhelpers/IMPORT 1970-01-01 00:00:00 +0000
2065+++ hooks/charmhelpers/contrib/charmhelpers/IMPORT 2016-03-21 06:13:21 +0000
2066@@ -0,0 +1,4 @@
2067+Source lp:charm-tools/trunk
2068+
2069+charm-tools/helpers/python/charmhelpers/__init__.py -> charmhelpers/charmhelpers/contrib/charmhelpers/__init__.py
2070+charm-tools/helpers/python/charmhelpers/tests/test_charmhelpers.py -> charmhelpers/tests/contrib/charmhelpers/test_charmhelpers.py
2071
2072=== added file 'hooks/charmhelpers/contrib/charmhelpers/__init__.py'
2073--- hooks/charmhelpers/contrib/charmhelpers/__init__.py 1970-01-01 00:00:00 +0000
2074+++ hooks/charmhelpers/contrib/charmhelpers/__init__.py 2016-03-21 06:13:21 +0000
2075@@ -0,0 +1,208 @@
2076+# Copyright 2014-2015 Canonical Limited.
2077+#
2078+# This file is part of charm-helpers.
2079+#
2080+# charm-helpers is free software: you can redistribute it and/or modify
2081+# it under the terms of the GNU Lesser General Public License version 3 as
2082+# published by the Free Software Foundation.
2083+#
2084+# charm-helpers is distributed in the hope that it will be useful,
2085+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2086+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2087+# GNU Lesser General Public License for more details.
2088+#
2089+# You should have received a copy of the GNU Lesser General Public License
2090+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
2091+
2092+# Copyright 2012 Canonical Ltd. This software is licensed under the
2093+# GNU Affero General Public License version 3 (see the file LICENSE).
2094+
2095+import warnings
2096+warnings.warn("contrib.charmhelpers is deprecated", DeprecationWarning) # noqa
2097+
2098+import operator
2099+import tempfile
2100+import time
2101+import yaml
2102+import subprocess
2103+
2104+import six
2105+if six.PY3:
2106+ from urllib.request import urlopen
2107+ from urllib.error import (HTTPError, URLError)
2108+else:
2109+ from urllib2 import (urlopen, HTTPError, URLError)
2110+
2111+"""Helper functions for writing Juju charms in Python."""
2112+
2113+__metaclass__ = type
2114+__all__ = [
2115+ # 'get_config', # core.hookenv.config()
2116+ # 'log', # core.hookenv.log()
2117+ # 'log_entry', # core.hookenv.log()
2118+ # 'log_exit', # core.hookenv.log()
2119+ # 'relation_get', # core.hookenv.relation_get()
2120+ # 'relation_set', # core.hookenv.relation_set()
2121+ # 'relation_ids', # core.hookenv.relation_ids()
2122+ # 'relation_list', # core.hookenv.relation_units()
2123+ # 'config_get', # core.hookenv.config()
2124+ # 'unit_get', # core.hookenv.unit_get()
2125+ # 'open_port', # core.hookenv.open_port()
2126+ # 'close_port', # core.hookenv.close_port()
2127+ # 'service_control', # core.host.service()
2128+ 'unit_info', # client-side, NOT IMPLEMENTED
2129+ 'wait_for_machine', # client-side, NOT IMPLEMENTED
2130+ 'wait_for_page_contents', # client-side, NOT IMPLEMENTED
2131+ 'wait_for_relation', # client-side, NOT IMPLEMENTED
2132+ 'wait_for_unit', # client-side, NOT IMPLEMENTED
2133+]
2134+
2135+
2136+SLEEP_AMOUNT = 0.1
2137+
2138+
2139+# We create a juju_status Command here because it makes testing much,
2140+# much easier.
2141+def juju_status():
2142+ subprocess.check_call(['juju', 'status'])
2143+
2144+# re-implemented as charmhelpers.fetch.configure_sources()
2145+# def configure_source(update=False):
2146+# source = config_get('source')
2147+# if ((source.startswith('ppa:') or
2148+# source.startswith('cloud:') or
2149+# source.startswith('http:'))):
2150+# run('add-apt-repository', source)
2151+# if source.startswith("http:"):
2152+# run('apt-key', 'import', config_get('key'))
2153+# if update:
2154+# run('apt-get', 'update')
2155+
2156+
2157+# DEPRECATED: client-side only
2158+def make_charm_config_file(charm_config):
2159+ charm_config_file = tempfile.NamedTemporaryFile(mode='w+')
2160+ charm_config_file.write(yaml.dump(charm_config))
2161+ charm_config_file.flush()
2162+ # The NamedTemporaryFile instance is returned instead of just the name
2163+ # because we want to take advantage of garbage collection-triggered
2164+ # deletion of the temp file when it goes out of scope in the caller.
2165+ return charm_config_file
2166+
2167+
2168+# DEPRECATED: client-side only
2169+def unit_info(service_name, item_name, data=None, unit=None):
2170+ if data is None:
2171+ data = yaml.safe_load(juju_status())
2172+ service = data['services'].get(service_name)
2173+ if service is None:
2174+ # XXX 2012-02-08 gmb:
2175+ # This allows us to cope with the race condition that we
2176+ # have between deploying a service and having it come up in
2177+ # `juju status`. We could probably do with cleaning it up so
2178+ # that it fails a bit more noisily after a while.
2179+ return ''
2180+ units = service['units']
2181+ if unit is not None:
2182+ item = units[unit][item_name]
2183+ else:
2184+ # It might seem odd to sort the units here, but we do it to
2185+ # ensure that when no unit is specified, the first unit for the
2186+ # service (or at least the one with the lowest number) is the
2187+ # one whose data gets returned.
2188+ sorted_unit_names = sorted(units.keys())
2189+ item = units[sorted_unit_names[0]][item_name]
2190+ return item
2191+
2192+
2193+# DEPRECATED: client-side only
2194+def get_machine_data():
2195+ return yaml.safe_load(juju_status())['machines']
2196+
2197+
2198+# DEPRECATED: client-side only
2199+def wait_for_machine(num_machines=1, timeout=300):
2200+ """Wait `timeout` seconds for `num_machines` machines to come up.
2201+
2202+ This wait_for... function can be called by other wait_for functions
2203+ whose timeouts might be too short in situations where only a bare
2204+ Juju setup has been bootstrapped.
2205+
2206+ :return: A tuple of (num_machines, time_taken). This is used for
2207+ testing.
2208+ """
2209+ # You may think this is a hack, and you'd be right. The easiest way
2210+ # to tell what environment we're working in (LXC vs EC2) is to check
2211+ # the dns-name of the first machine. If it's localhost we're in LXC
2212+ # and we can just return here.
2213+ if get_machine_data()[0]['dns-name'] == 'localhost':
2214+ return 1, 0
2215+ start_time = time.time()
2216+ while True:
2217+ # Drop the first machine, since it's the Zookeeper and that's
2218+ # not a machine that we need to wait for. This will only work
2219+ # for EC2 environments, which is why we return early above if
2220+ # we're in LXC.
2221+ machine_data = get_machine_data()
2222+ non_zookeeper_machines = [
2223+ machine_data[key] for key in list(machine_data.keys())[1:]]
2224+ if len(non_zookeeper_machines) >= num_machines:
2225+ all_machines_running = True
2226+ for machine in non_zookeeper_machines:
2227+ if machine.get('instance-state') != 'running':
2228+ all_machines_running = False
2229+ break
2230+ if all_machines_running:
2231+ break
2232+ if time.time() - start_time >= timeout:
2233+ raise RuntimeError('timeout waiting for service to start')
2234+ time.sleep(SLEEP_AMOUNT)
2235+ return num_machines, time.time() - start_time
2236+
2237+
2238+# DEPRECATED: client-side only
2239+def wait_for_unit(service_name, timeout=480):
2240+ """Wait `timeout` seconds for a given service name to come up."""
2241+ wait_for_machine(num_machines=1)
2242+ start_time = time.time()
2243+ while True:
2244+ state = unit_info(service_name, 'agent-state')
2245+ if 'error' in state or state == 'started':
2246+ break
2247+ if time.time() - start_time >= timeout:
2248+ raise RuntimeError('timeout waiting for service to start')
2249+ time.sleep(SLEEP_AMOUNT)
2250+ if state != 'started':
2251+ raise RuntimeError('unit did not start, agent-state: ' + state)
2252+
2253+
2254+# DEPRECATED: client-side only
2255+def wait_for_relation(service_name, relation_name, timeout=120):
2256+ """Wait `timeout` seconds for a given relation to come up."""
2257+ start_time = time.time()
2258+ while True:
2259+ relation = unit_info(service_name, 'relations').get(relation_name)
2260+ if relation is not None and relation['state'] == 'up':
2261+ break
2262+ if time.time() - start_time >= timeout:
2263+ raise RuntimeError('timeout waiting for relation to be up')
2264+ time.sleep(SLEEP_AMOUNT)
2265+
2266+
2267+# DEPRECATED: client-side only
2268+def wait_for_page_contents(url, contents, timeout=120, validate=None):
2269+ if validate is None:
2270+ validate = operator.contains
2271+ start_time = time.time()
2272+ while True:
2273+ try:
2274+ stream = urlopen(url)
2275+ except (HTTPError, URLError):
2276+ pass
2277+ else:
2278+ page = stream.read()
2279+ if validate(page, contents):
2280+ return page
2281+ if time.time() - start_time >= timeout:
2282+ raise RuntimeError('timeout waiting for contents of ' + url)
2283+ time.sleep(SLEEP_AMOUNT)
2284
2285=== added directory 'hooks/charmhelpers/contrib/charmsupport'
2286=== added file 'hooks/charmhelpers/contrib/charmsupport/IMPORT'
2287--- hooks/charmhelpers/contrib/charmsupport/IMPORT 1970-01-01 00:00:00 +0000
2288+++ hooks/charmhelpers/contrib/charmsupport/IMPORT 2016-03-21 06:13:21 +0000
2289@@ -0,0 +1,14 @@
2290+Source: lp:charmsupport/trunk
2291+
2292+charmsupport/charmsupport/execd.py -> charm-helpers/charmhelpers/contrib/charmsupport/execd.py
2293+charmsupport/charmsupport/hookenv.py -> charm-helpers/charmhelpers/contrib/charmsupport/hookenv.py
2294+charmsupport/charmsupport/host.py -> charm-helpers/charmhelpers/contrib/charmsupport/host.py
2295+charmsupport/charmsupport/nrpe.py -> charm-helpers/charmhelpers/contrib/charmsupport/nrpe.py
2296+charmsupport/charmsupport/volumes.py -> charm-helpers/charmhelpers/contrib/charmsupport/volumes.py
2297+
2298+charmsupport/tests/test_execd.py -> charm-helpers/tests/contrib/charmsupport/test_execd.py
2299+charmsupport/tests/test_hookenv.py -> charm-helpers/tests/contrib/charmsupport/test_hookenv.py
2300+charmsupport/tests/test_host.py -> charm-helpers/tests/contrib/charmsupport/test_host.py
2301+charmsupport/tests/test_nrpe.py -> charm-helpers/tests/contrib/charmsupport/test_nrpe.py
2302+
2303+charmsupport/bin/charmsupport -> charm-helpers/bin/contrib/charmsupport/charmsupport
2304
2305=== added file 'hooks/charmhelpers/contrib/charmsupport/__init__.py'
2306--- hooks/charmhelpers/contrib/charmsupport/__init__.py 1970-01-01 00:00:00 +0000
2307+++ hooks/charmhelpers/contrib/charmsupport/__init__.py 2016-03-21 06:13:21 +0000
2308@@ -0,0 +1,15 @@
2309+# Copyright 2014-2015 Canonical Limited.
2310+#
2311+# This file is part of charm-helpers.
2312+#
2313+# charm-helpers is free software: you can redistribute it and/or modify
2314+# it under the terms of the GNU Lesser General Public License version 3 as
2315+# published by the Free Software Foundation.
2316+#
2317+# charm-helpers is distributed in the hope that it will be useful,
2318+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2319+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2320+# GNU Lesser General Public License for more details.
2321+#
2322+# You should have received a copy of the GNU Lesser General Public License
2323+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
2324
2325=== added file 'hooks/charmhelpers/contrib/charmsupport/nrpe.py'
2326--- hooks/charmhelpers/contrib/charmsupport/nrpe.py 1970-01-01 00:00:00 +0000
2327+++ hooks/charmhelpers/contrib/charmsupport/nrpe.py 2016-03-21 06:13:21 +0000
2328@@ -0,0 +1,398 @@
2329+# Copyright 2014-2015 Canonical Limited.
2330+#
2331+# This file is part of charm-helpers.
2332+#
2333+# charm-helpers is free software: you can redistribute it and/or modify
2334+# it under the terms of the GNU Lesser General Public License version 3 as
2335+# published by the Free Software Foundation.
2336+#
2337+# charm-helpers is distributed in the hope that it will be useful,
2338+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2339+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2340+# GNU Lesser General Public License for more details.
2341+#
2342+# You should have received a copy of the GNU Lesser General Public License
2343+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
2344+
2345+"""Compatibility with the nrpe-external-master charm"""
2346+# Copyright 2012 Canonical Ltd.
2347+#
2348+# Authors:
2349+# Matthew Wedgwood <matthew.wedgwood@canonical.com>
2350+
2351+import subprocess
2352+import pwd
2353+import grp
2354+import os
2355+import glob
2356+import shutil
2357+import re
2358+import shlex
2359+import yaml
2360+
2361+from charmhelpers.core.hookenv import (
2362+ config,
2363+ local_unit,
2364+ log,
2365+ relation_ids,
2366+ relation_set,
2367+ relations_of_type,
2368+)
2369+
2370+from charmhelpers.core.host import service
2371+
2372+# This module adds compatibility with the nrpe-external-master and plain nrpe
2373+# subordinate charms. To use it in your charm:
2374+#
2375+# 1. Update metadata.yaml
2376+#
2377+# provides:
2378+# (...)
2379+# nrpe-external-master:
2380+# interface: nrpe-external-master
2381+# scope: container
2382+#
2383+# and/or
2384+#
2385+# provides:
2386+# (...)
2387+# local-monitors:
2388+# interface: local-monitors
2389+# scope: container
2390+
2391+#
2392+# 2. Add the following to config.yaml
2393+#
2394+# nagios_context:
2395+# default: "juju"
2396+# type: string
2397+# description: |
2398+# Used by the nrpe subordinate charms.
2399+# A string that will be prepended to instance name to set the host name
2400+# in nagios. So for instance the hostname would be something like:
2401+# juju-myservice-0
2402+# If you're running multiple environments with the same services in them
2403+# this allows you to differentiate between them.
2404+# nagios_servicegroups:
2405+# default: ""
2406+# type: string
2407+# description: |
2408+# A comma-separated list of nagios servicegroups.
2409+# If left empty, the nagios_context will be used as the servicegroup
2410+#
2411+# 3. Add custom checks (Nagios plugins) to files/nrpe-external-master
2412+#
2413+# 4. Update your hooks.py with something like this:
2414+#
2415+# from charmsupport.nrpe import NRPE
2416+# (...)
2417+# def update_nrpe_config():
2418+# nrpe_compat = NRPE()
2419+# nrpe_compat.add_check(
2420+# shortname = "myservice",
2421+# description = "Check MyService",
2422+# check_cmd = "check_http -w 2 -c 10 http://localhost"
2423+# )
2424+# nrpe_compat.add_check(
2425+# "myservice_other",
2426+# "Check for widget failures",
2427+# check_cmd = "/srv/myapp/scripts/widget_check"
2428+# )
2429+# nrpe_compat.write()
2430+#
2431+# def config_changed():
2432+# (...)
2433+# update_nrpe_config()
2434+#
2435+# def nrpe_external_master_relation_changed():
2436+# update_nrpe_config()
2437+#
2438+# def local_monitors_relation_changed():
2439+# update_nrpe_config()
2440+#
2441+# 5. ln -s hooks.py nrpe-external-master-relation-changed
2442+# ln -s hooks.py local-monitors-relation-changed
2443+
2444+
2445+class CheckException(Exception):
2446+ pass
2447+
2448+
2449+class Check(object):
2450+ shortname_re = '[A-Za-z0-9-_]+$'
2451+ service_template = ("""
2452+#---------------------------------------------------
2453+# This file is Juju managed
2454+#---------------------------------------------------
2455+define service {{
2456+ use active-service
2457+ host_name {nagios_hostname}
2458+ service_description {nagios_hostname}[{shortname}] """
2459+ """{description}
2460+ check_command check_nrpe!{command}
2461+ servicegroups {nagios_servicegroup}
2462+}}
2463+""")
2464+
2465+ def __init__(self, shortname, description, check_cmd):
2466+ super(Check, self).__init__()
2467+ # XXX: could be better to calculate this from the service name
2468+ if not re.match(self.shortname_re, shortname):
2469+ raise CheckException("shortname must match {}".format(
2470+ Check.shortname_re))
2471+ self.shortname = shortname
2472+ self.command = "check_{}".format(shortname)
2473+ # Note: a set of invalid characters is defined by the
2474+ # Nagios server config
2475+ # The default is: illegal_object_name_chars=`~!$%^&*"|'<>?,()=
2476+ self.description = description
2477+ self.check_cmd = self._locate_cmd(check_cmd)
2478+
2479+ def _get_check_filename(self):
2480+ return os.path.join(NRPE.nrpe_confdir, '{}.cfg'.format(self.command))
2481+
2482+ def _get_service_filename(self, hostname):
2483+ return os.path.join(NRPE.nagios_exportdir,
2484+ 'service__{}_{}.cfg'.format(hostname, self.command))
2485+
2486+ def _locate_cmd(self, check_cmd):
2487+ search_path = (
2488+ '/usr/lib/nagios/plugins',
2489+ '/usr/local/lib/nagios/plugins',
2490+ )
2491+ parts = shlex.split(check_cmd)
2492+ for path in search_path:
2493+ if os.path.exists(os.path.join(path, parts[0])):
2494+ command = os.path.join(path, parts[0])
2495+ if len(parts) > 1:
2496+ command += " " + " ".join(parts[1:])
2497+ return command
2498+ log('Check command not found: {}'.format(parts[0]))
2499+ return ''
2500+
2501+ def _remove_service_files(self):
2502+ if not os.path.exists(NRPE.nagios_exportdir):
2503+ return
2504+ for f in os.listdir(NRPE.nagios_exportdir):
2505+ if f.endswith('_{}.cfg'.format(self.command)):
2506+ os.remove(os.path.join(NRPE.nagios_exportdir, f))
2507+
2508+ def remove(self, hostname):
2509+ nrpe_check_file = self._get_check_filename()
2510+ if os.path.exists(nrpe_check_file):
2511+ os.remove(nrpe_check_file)
2512+ self._remove_service_files()
2513+
2514+ def write(self, nagios_context, hostname, nagios_servicegroups):
2515+ nrpe_check_file = self._get_check_filename()
2516+ with open(nrpe_check_file, 'w') as nrpe_check_config:
2517+ nrpe_check_config.write("# check {}\n".format(self.shortname))
2518+ nrpe_check_config.write("command[{}]={}\n".format(
2519+ self.command, self.check_cmd))
2520+
2521+ if not os.path.exists(NRPE.nagios_exportdir):
2522+ log('Not writing service config as {} is not accessible'.format(
2523+ NRPE.nagios_exportdir))
2524+ else:
2525+ self.write_service_config(nagios_context, hostname,
2526+ nagios_servicegroups)
2527+
2528+ def write_service_config(self, nagios_context, hostname,
2529+ nagios_servicegroups):
2530+ self._remove_service_files()
2531+
2532+ templ_vars = {
2533+ 'nagios_hostname': hostname,
2534+ 'nagios_servicegroup': nagios_servicegroups,
2535+ 'description': self.description,
2536+ 'shortname': self.shortname,
2537+ 'command': self.command,
2538+ }
2539+ nrpe_service_text = Check.service_template.format(**templ_vars)
2540+ nrpe_service_file = self._get_service_filename(hostname)
2541+ with open(nrpe_service_file, 'w') as nrpe_service_config:
2542+ nrpe_service_config.write(str(nrpe_service_text))
2543+
2544+ def run(self):
2545+ subprocess.call(self.check_cmd)
2546+
2547+
2548+class NRPE(object):
2549+ nagios_logdir = '/var/log/nagios'
2550+ nagios_exportdir = '/var/lib/nagios/export'
2551+ nrpe_confdir = '/etc/nagios/nrpe.d'
2552+
2553+ def __init__(self, hostname=None):
2554+ super(NRPE, self).__init__()
2555+ self.config = config()
2556+ self.nagios_context = self.config['nagios_context']
2557+ if 'nagios_servicegroups' in self.config and self.config['nagios_servicegroups']:
2558+ self.nagios_servicegroups = self.config['nagios_servicegroups']
2559+ else:
2560+ self.nagios_servicegroups = self.nagios_context
2561+ self.unit_name = local_unit().replace('/', '-')
2562+ if hostname:
2563+ self.hostname = hostname
2564+ else:
2565+ nagios_hostname = get_nagios_hostname()
2566+ if nagios_hostname:
2567+ self.hostname = nagios_hostname
2568+ else:
2569+ self.hostname = "{}-{}".format(self.nagios_context, self.unit_name)
2570+ self.checks = []
2571+
2572+ def add_check(self, *args, **kwargs):
2573+ self.checks.append(Check(*args, **kwargs))
2574+
2575+ def remove_check(self, *args, **kwargs):
2576+ if kwargs.get('shortname') is None:
2577+ raise ValueError('shortname of check must be specified')
2578+
2579+ # Use sensible defaults if they're not specified - these are not
2580+ # actually used during removal, but they're required for constructing
2581+ # the Check object; check_disk is chosen because it's part of the
2582+ # nagios-plugins-basic package.
2583+ if kwargs.get('check_cmd') is None:
2584+ kwargs['check_cmd'] = 'check_disk'
2585+ if kwargs.get('description') is None:
2586+ kwargs['description'] = ''
2587+
2588+ check = Check(*args, **kwargs)
2589+ check.remove(self.hostname)
2590+
2591+ def write(self):
2592+ try:
2593+ nagios_uid = pwd.getpwnam('nagios').pw_uid
2594+ nagios_gid = grp.getgrnam('nagios').gr_gid
2595+ except:
2596+ log("Nagios user not set up, nrpe checks not updated")
2597+ return
2598+
2599+ if not os.path.exists(NRPE.nagios_logdir):
2600+ os.mkdir(NRPE.nagios_logdir)
2601+ os.chown(NRPE.nagios_logdir, nagios_uid, nagios_gid)
2602+
2603+ nrpe_monitors = {}
2604+ monitors = {"monitors": {"remote": {"nrpe": nrpe_monitors}}}
2605+ for nrpecheck in self.checks:
2606+ nrpecheck.write(self.nagios_context, self.hostname,
2607+ self.nagios_servicegroups)
2608+ nrpe_monitors[nrpecheck.shortname] = {
2609+ "command": nrpecheck.command,
2610+ }
2611+
2612+ service('restart', 'nagios-nrpe-server')
2613+
2614+ monitor_ids = relation_ids("local-monitors") + \
2615+ relation_ids("nrpe-external-master")
2616+ for rid in monitor_ids:
2617+ relation_set(relation_id=rid, monitors=yaml.dump(monitors))
2618+
2619+
2620+def get_nagios_hostcontext(relation_name='nrpe-external-master'):
2621+ """
2622+ Query relation with nrpe subordinate, return the nagios_host_context
2623+
2624+ :param str relation_name: Name of relation nrpe sub joined to
2625+ """
2626+ for rel in relations_of_type(relation_name):
2627+ if 'nagios_host_context' in rel:
2628+ return rel['nagios_host_context']
2629+
2630+
2631+def get_nagios_hostname(relation_name='nrpe-external-master'):
2632+ """
2633+ Query relation with nrpe subordinate, return the nagios_hostname
2634+
2635+ :param str relation_name: Name of relation nrpe sub joined to
2636+ """
2637+ for rel in relations_of_type(relation_name):
2638+ if 'nagios_hostname' in rel:
2639+ return rel['nagios_hostname']
2640+
2641+
2642+def get_nagios_unit_name(relation_name='nrpe-external-master'):
2643+ """
2644+ Return the nagios unit name prepended with host_context if needed
2645+
2646+ :param str relation_name: Name of relation nrpe sub joined to
2647+ """
2648+ host_context = get_nagios_hostcontext(relation_name)
2649+ if host_context:
2650+ unit = "%s:%s" % (host_context, local_unit())
2651+ else:
2652+ unit = local_unit()
2653+ return unit
2654+
2655+
2656+def add_init_service_checks(nrpe, services, unit_name):
2657+ """
2658+ Add checks for each service in list
2659+
2660+ :param NRPE nrpe: NRPE object to add check to
2661+ :param list services: List of services to check
2662+ :param str unit_name: Unit name to use in check description
2663+ """
2664+ for svc in services:
2665+ upstart_init = '/etc/init/%s.conf' % svc
2666+ sysv_init = '/etc/init.d/%s' % svc
2667+ if os.path.exists(upstart_init):
2668+ # Don't add a check for these services from neutron-gateway
2669+ if svc not in ['ext-port', 'os-charm-phy-nic-mtu']:
2670+ nrpe.add_check(
2671+ shortname=svc,
2672+ description='process check {%s}' % unit_name,
2673+ check_cmd='check_upstart_job %s' % svc
2674+ )
2675+ elif os.path.exists(sysv_init):
2676+ cronpath = '/etc/cron.d/nagios-service-check-%s' % svc
2677+ cron_file = ('*/5 * * * * root '
2678+ '/usr/local/lib/nagios/plugins/check_exit_status.pl '
2679+ '-s /etc/init.d/%s status > '
2680+ '/var/lib/nagios/service-check-%s.txt\n' % (svc,
2681+ svc)
2682+ )
2683+ f = open(cronpath, 'w')
2684+ f.write(cron_file)
2685+ f.close()
2686+ nrpe.add_check(
2687+ shortname=svc,
2688+ description='process check {%s}' % unit_name,
2689+ check_cmd='check_status_file.py -f '
2690+ '/var/lib/nagios/service-check-%s.txt' % svc,
2691+ )
2692+
2693+
2694+def copy_nrpe_checks():
2695+ """
2696+ Copy the nrpe checks into place
2697+
2698+ """
2699+ NAGIOS_PLUGINS = '/usr/local/lib/nagios/plugins'
2700+ nrpe_files_dir = os.path.join(os.getenv('CHARM_DIR'), 'hooks',
2701+ 'charmhelpers', 'contrib', 'openstack',
2702+ 'files')
2703+
2704+ if not os.path.exists(NAGIOS_PLUGINS):
2705+ os.makedirs(NAGIOS_PLUGINS)
2706+ for fname in glob.glob(os.path.join(nrpe_files_dir, "check_*")):
2707+ if os.path.isfile(fname):
2708+ shutil.copy2(fname,
2709+ os.path.join(NAGIOS_PLUGINS, os.path.basename(fname)))
2710+
2711+
2712+def add_haproxy_checks(nrpe, unit_name):
2713+ """
2714+ Add checks for each service in list
2715+
2716+ :param NRPE nrpe: NRPE object to add check to
2717+ :param str unit_name: Unit name to use in check description
2718+ """
2719+ nrpe.add_check(
2720+ shortname='haproxy_servers',
2721+ description='Check HAProxy {%s}' % unit_name,
2722+ check_cmd='check_haproxy.sh')
2723+ nrpe.add_check(
2724+ shortname='haproxy_queue',
2725+ description='Check HAProxy queue depth {%s}' % unit_name,
2726+ check_cmd='check_haproxy_queue_depth.sh')
2727
2728=== added file 'hooks/charmhelpers/contrib/charmsupport/volumes.py'
2729--- hooks/charmhelpers/contrib/charmsupport/volumes.py 1970-01-01 00:00:00 +0000
2730+++ hooks/charmhelpers/contrib/charmsupport/volumes.py 2016-03-21 06:13:21 +0000
2731@@ -0,0 +1,175 @@
2732+# Copyright 2014-2015 Canonical Limited.
2733+#
2734+# This file is part of charm-helpers.
2735+#
2736+# charm-helpers is free software: you can redistribute it and/or modify
2737+# it under the terms of the GNU Lesser General Public License version 3 as
2738+# published by the Free Software Foundation.
2739+#
2740+# charm-helpers is distributed in the hope that it will be useful,
2741+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2742+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2743+# GNU Lesser General Public License for more details.
2744+#
2745+# You should have received a copy of the GNU Lesser General Public License
2746+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
2747+
2748+'''
2749+Functions for managing volumes in juju units. One volume is supported per unit.
2750+Subordinates may have their own storage, provided it is on its own partition.
2751+
2752+Configuration stanzas::
2753+
2754+ volume-ephemeral:
2755+ type: boolean
2756+ default: true
2757+ description: >
2758+ If false, a volume is mounted as sepecified in "volume-map"
2759+ If true, ephemeral storage will be used, meaning that log data
2760+ will only exist as long as the machine. YOU HAVE BEEN WARNED.
2761+ volume-map:
2762+ type: string
2763+ default: {}
2764+ description: >
2765+ YAML map of units to device names, e.g:
2766+ "{ rsyslog/0: /dev/vdb, rsyslog/1: /dev/vdb }"
2767+ Service units will raise a configure-error if volume-ephemeral
2768+ is 'true' and no volume-map value is set. Use 'juju set' to set a
2769+ value and 'juju resolved' to complete configuration.
2770+
2771+Usage::
2772+
2773+ from charmsupport.volumes import configure_volume, VolumeConfigurationError
2774+ from charmsupport.hookenv import log, ERROR
2775+ def post_mount_hook():
2776+ stop_service('myservice')
2777+ def post_mount_hook():
2778+ start_service('myservice')
2779+
2780+ if __name__ == '__main__':
2781+ try:
2782+ configure_volume(before_change=pre_mount_hook,
2783+ after_change=post_mount_hook)
2784+ except VolumeConfigurationError:
2785+ log('Storage could not be configured', ERROR)
2786+
2787+'''
2788+
2789+# XXX: Known limitations
2790+# - fstab is neither consulted nor updated
2791+
2792+import os
2793+from charmhelpers.core import hookenv
2794+from charmhelpers.core import host
2795+import yaml
2796+
2797+
2798+MOUNT_BASE = '/srv/juju/volumes'
2799+
2800+
2801+class VolumeConfigurationError(Exception):
2802+ '''Volume configuration data is missing or invalid'''
2803+ pass
2804+
2805+
2806+def get_config():
2807+ '''Gather and sanity-check volume configuration data'''
2808+ volume_config = {}
2809+ config = hookenv.config()
2810+
2811+ errors = False
2812+
2813+ if config.get('volume-ephemeral') in (True, 'True', 'true', 'Yes', 'yes'):
2814+ volume_config['ephemeral'] = True
2815+ else:
2816+ volume_config['ephemeral'] = False
2817+
2818+ try:
2819+ volume_map = yaml.safe_load(config.get('volume-map', '{}'))
2820+ except yaml.YAMLError as e:
2821+ hookenv.log("Error parsing YAML volume-map: {}".format(e),
2822+ hookenv.ERROR)
2823+ errors = True
2824+ if volume_map is None:
2825+ # probably an empty string
2826+ volume_map = {}
2827+ elif not isinstance(volume_map, dict):
2828+ hookenv.log("Volume-map should be a dictionary, not {}".format(
2829+ type(volume_map)))
2830+ errors = True
2831+
2832+ volume_config['device'] = volume_map.get(os.environ['JUJU_UNIT_NAME'])
2833+ if volume_config['device'] and volume_config['ephemeral']:
2834+ # asked for ephemeral storage but also defined a volume ID
2835+ hookenv.log('A volume is defined for this unit, but ephemeral '
2836+ 'storage was requested', hookenv.ERROR)
2837+ errors = True
2838+ elif not volume_config['device'] and not volume_config['ephemeral']:
2839+ # asked for permanent storage but did not define volume ID
2840+ hookenv.log('Ephemeral storage was requested, but there is no volume '
2841+ 'defined for this unit.', hookenv.ERROR)
2842+ errors = True
2843+
2844+ unit_mount_name = hookenv.local_unit().replace('/', '-')
2845+ volume_config['mountpoint'] = os.path.join(MOUNT_BASE, unit_mount_name)
2846+
2847+ if errors:
2848+ return None
2849+ return volume_config
2850+
2851+
2852+def mount_volume(config):
2853+ if os.path.exists(config['mountpoint']):
2854+ if not os.path.isdir(config['mountpoint']):
2855+ hookenv.log('Not a directory: {}'.format(config['mountpoint']))
2856+ raise VolumeConfigurationError()
2857+ else:
2858+ host.mkdir(config['mountpoint'])
2859+ if os.path.ismount(config['mountpoint']):
2860+ unmount_volume(config)
2861+ if not host.mount(config['device'], config['mountpoint'], persist=True):
2862+ raise VolumeConfigurationError()
2863+
2864+
2865+def unmount_volume(config):
2866+ if os.path.ismount(config['mountpoint']):
2867+ if not host.umount(config['mountpoint'], persist=True):
2868+ raise VolumeConfigurationError()
2869+
2870+
2871+def managed_mounts():
2872+ '''List of all mounted managed volumes'''
2873+ return filter(lambda mount: mount[0].startswith(MOUNT_BASE), host.mounts())
2874+
2875+
2876+def configure_volume(before_change=lambda: None, after_change=lambda: None):
2877+ '''Set up storage (or don't) according to the charm's volume configuration.
2878+ Returns the mount point or "ephemeral". before_change and after_change
2879+ are optional functions to be called if the volume configuration changes.
2880+ '''
2881+
2882+ config = get_config()
2883+ if not config:
2884+ hookenv.log('Failed to read volume configuration', hookenv.CRITICAL)
2885+ raise VolumeConfigurationError()
2886+
2887+ if config['ephemeral']:
2888+ if os.path.ismount(config['mountpoint']):
2889+ before_change()
2890+ unmount_volume(config)
2891+ after_change()
2892+ return 'ephemeral'
2893+ else:
2894+ # persistent storage
2895+ if os.path.ismount(config['mountpoint']):
2896+ mounts = dict(managed_mounts())
2897+ if mounts.get(config['mountpoint']) != config['device']:
2898+ before_change()
2899+ unmount_volume(config)
2900+ mount_volume(config)
2901+ after_change()
2902+ else:
2903+ before_change()
2904+ mount_volume(config)
2905+ after_change()
2906+ return config['mountpoint']
2907
2908=== added directory 'hooks/charmhelpers/contrib/database'
2909=== added file 'hooks/charmhelpers/contrib/database/__init__.py'
2910=== added file 'hooks/charmhelpers/contrib/database/mysql.py'
2911--- hooks/charmhelpers/contrib/database/mysql.py 1970-01-01 00:00:00 +0000
2912+++ hooks/charmhelpers/contrib/database/mysql.py 2016-03-21 06:13:21 +0000
2913@@ -0,0 +1,412 @@
2914+"""Helper for working with a MySQL database"""
2915+import json
2916+import re
2917+import sys
2918+import platform
2919+import os
2920+import glob
2921+
2922+# from string import upper
2923+
2924+from charmhelpers.core.host import (
2925+ mkdir,
2926+ pwgen,
2927+ write_file
2928+)
2929+from charmhelpers.core.hookenv import (
2930+ config as config_get,
2931+ relation_get,
2932+ related_units,
2933+ unit_get,
2934+ log,
2935+ DEBUG,
2936+ INFO,
2937+ WARNING,
2938+)
2939+from charmhelpers.fetch import (
2940+ apt_install,
2941+ apt_update,
2942+ filter_installed_packages,
2943+)
2944+from charmhelpers.contrib.peerstorage import (
2945+ peer_store,
2946+ peer_retrieve,
2947+)
2948+from charmhelpers.contrib.network.ip import get_host_ip
2949+
2950+try:
2951+ import MySQLdb
2952+except ImportError:
2953+ apt_update(fatal=True)
2954+ apt_install(filter_installed_packages(['python-mysqldb']), fatal=True)
2955+ import MySQLdb
2956+
2957+
2958+class MySQLHelper(object):
2959+
2960+ def __init__(self, rpasswdf_template, upasswdf_template, host='localhost',
2961+ migrate_passwd_to_peer_relation=True,
2962+ delete_ondisk_passwd_file=True):
2963+ self.host = host
2964+ # Password file path templates
2965+ self.root_passwd_file_template = rpasswdf_template
2966+ self.user_passwd_file_template = upasswdf_template
2967+
2968+ self.migrate_passwd_to_peer_relation = migrate_passwd_to_peer_relation
2969+ # If we migrate we have the option to delete local copy of root passwd
2970+ self.delete_ondisk_passwd_file = delete_ondisk_passwd_file
2971+
2972+ def connect(self, user='root', password=None):
2973+ log("Opening db connection for %s@%s" % (user, self.host), level=DEBUG)
2974+ self.connection = MySQLdb.connect(user=user, host=self.host,
2975+ passwd=password)
2976+
2977+ def database_exists(self, db_name):
2978+ cursor = self.connection.cursor()
2979+ try:
2980+ cursor.execute("SHOW DATABASES")
2981+ databases = [i[0] for i in cursor.fetchall()]
2982+ finally:
2983+ cursor.close()
2984+
2985+ return db_name in databases
2986+
2987+ def create_database(self, db_name):
2988+ cursor = self.connection.cursor()
2989+ try:
2990+ cursor.execute("CREATE DATABASE {} CHARACTER SET UTF8"
2991+ .format(db_name))
2992+ finally:
2993+ cursor.close()
2994+
2995+ def grant_exists(self, db_name, db_user, remote_ip):
2996+ cursor = self.connection.cursor()
2997+ priv_string = "GRANT ALL PRIVILEGES ON `{}`.* " \
2998+ "TO '{}'@'{}'".format(db_name, db_user, remote_ip)
2999+ try:
3000+ cursor.execute("SHOW GRANTS for '{}'@'{}'".format(db_user,
3001+ remote_ip))
3002+ grants = [i[0] for i in cursor.fetchall()]
3003+ except MySQLdb.OperationalError:
3004+ return False
3005+ finally:
3006+ cursor.close()
3007+
3008+ # TODO: review for different grants
3009+ return priv_string in grants
3010+
3011+ def create_grant(self, db_name, db_user, remote_ip, password):
3012+ cursor = self.connection.cursor()
3013+ try:
3014+ # TODO: review for different grants
3015+ cursor.execute("GRANT ALL PRIVILEGES ON {}.* TO '{}'@'{}' "
3016+ "IDENTIFIED BY '{}'".format(db_name,
3017+ db_user,
3018+ remote_ip,
3019+ password))
3020+ finally:
3021+ cursor.close()
3022+
3023+ def create_admin_grant(self, db_user, remote_ip, password):
3024+ cursor = self.connection.cursor()
3025+ try:
3026+ cursor.execute("GRANT ALL PRIVILEGES ON *.* TO '{}'@'{}' "
3027+ "IDENTIFIED BY '{}'".format(db_user,
3028+ remote_ip,
3029+ password))
3030+ finally:
3031+ cursor.close()
3032+
3033+ def cleanup_grant(self, db_user, remote_ip):
3034+ cursor = self.connection.cursor()
3035+ try:
3036+ cursor.execute("DROP FROM mysql.user WHERE user='{}' "
3037+ "AND HOST='{}'".format(db_user,
3038+ remote_ip))
3039+ finally:
3040+ cursor.close()
3041+
3042+ def execute(self, sql):
3043+ """Execute arbitary SQL against the database."""
3044+ cursor = self.connection.cursor()
3045+ try:
3046+ cursor.execute(sql)
3047+ finally:
3048+ cursor.close()
3049+
3050+ def migrate_passwords_to_peer_relation(self, excludes=None):
3051+ """Migrate any passwords storage on disk to cluster peer relation."""
3052+ dirname = os.path.dirname(self.root_passwd_file_template)
3053+ path = os.path.join(dirname, '*.passwd')
3054+ for f in glob.glob(path):
3055+ if excludes and f in excludes:
3056+ log("Excluding %s from peer migration" % (f), level=DEBUG)
3057+ continue
3058+
3059+ key = os.path.basename(f)
3060+ with open(f, 'r') as passwd:
3061+ _value = passwd.read().strip()
3062+
3063+ try:
3064+ peer_store(key, _value)
3065+
3066+ if self.delete_ondisk_passwd_file:
3067+ os.unlink(f)
3068+ except ValueError:
3069+ # NOTE cluster relation not yet ready - skip for now
3070+ pass
3071+
3072+ def get_mysql_password_on_disk(self, username=None, password=None):
3073+ """Retrieve, generate or store a mysql password for the provided
3074+ username on disk."""
3075+ if username:
3076+ template = self.user_passwd_file_template
3077+ passwd_file = template.format(username)
3078+ else:
3079+ passwd_file = self.root_passwd_file_template
3080+
3081+ _password = None
3082+ if os.path.exists(passwd_file):
3083+ log("Using existing password file '%s'" % passwd_file, level=DEBUG)
3084+ with open(passwd_file, 'r') as passwd:
3085+ _password = passwd.read().strip()
3086+ else:
3087+ log("Generating new password file '%s'" % passwd_file, level=DEBUG)
3088+ if not os.path.isdir(os.path.dirname(passwd_file)):
3089+ # NOTE: need to ensure this is not mysql root dir (which needs
3090+ # to be mysql readable)
3091+ mkdir(os.path.dirname(passwd_file), owner='root', group='root',
3092+ perms=0o770)
3093+ # Force permissions - for some reason the chmod in makedirs
3094+ # fails
3095+ os.chmod(os.path.dirname(passwd_file), 0o770)
3096+
3097+ _password = password or pwgen(length=32)
3098+ write_file(passwd_file, _password, owner='root', group='root',
3099+ perms=0o660)
3100+
3101+ return _password
3102+
3103+ def passwd_keys(self, username):
3104+ """Generator to return keys used to store passwords in peer store.
3105+
3106+ NOTE: we support both legacy and new format to support mysql
3107+ charm prior to refactor. This is necessary to avoid LP 1451890.
3108+ """
3109+ keys = []
3110+ if username == 'mysql':
3111+ log("Bad username '%s'" % (username), level=WARNING)
3112+
3113+ if username:
3114+ # IMPORTANT: *newer* format must be returned first
3115+ keys.append('mysql-%s.passwd' % (username))
3116+ keys.append('%s.passwd' % (username))
3117+ else:
3118+ keys.append('mysql.passwd')
3119+
3120+ for key in keys:
3121+ yield key
3122+
3123+ def get_mysql_password(self, username=None, password=None):
3124+ """Retrieve, generate or store a mysql password for the provided
3125+ username using peer relation cluster."""
3126+ excludes = []
3127+
3128+ # First check peer relation.
3129+ try:
3130+ for key in self.passwd_keys(username):
3131+ _password = peer_retrieve(key)
3132+ if _password:
3133+ break
3134+
3135+ # If root password available don't update peer relation from local
3136+ if _password and not username:
3137+ excludes.append(self.root_passwd_file_template)
3138+
3139+ except ValueError:
3140+ # cluster relation is not yet started; use on-disk
3141+ _password = None
3142+
3143+ # If none available, generate new one
3144+ if not _password:
3145+ _password = self.get_mysql_password_on_disk(username, password)
3146+
3147+ # Put on wire if required
3148+ if self.migrate_passwd_to_peer_relation:
3149+ self.migrate_passwords_to_peer_relation(excludes=excludes)
3150+
3151+ return _password
3152+
3153+ def get_mysql_root_password(self, password=None):
3154+ """Retrieve or generate mysql root password for service units."""
3155+ return self.get_mysql_password(username=None, password=password)
3156+
3157+ def normalize_address(self, hostname):
3158+ """Ensure that address returned is an IP address (i.e. not fqdn)"""
3159+ if config_get('prefer-ipv6'):
3160+ # TODO: add support for ipv6 dns
3161+ return hostname
3162+
3163+ if hostname != unit_get('private-address'):
3164+ return get_host_ip(hostname, fallback=hostname)
3165+
3166+ # Otherwise assume localhost
3167+ return '127.0.0.1'
3168+
3169+ def get_allowed_units(self, database, username, relation_id=None):
3170+ """Get list of units with access grants for database with username.
3171+
3172+ This is typically used to provide shared-db relations with a list of
3173+ which units have been granted access to the given database.
3174+ """
3175+ self.connect(password=self.get_mysql_root_password())
3176+ allowed_units = set()
3177+ for unit in related_units(relation_id):
3178+ settings = relation_get(rid=relation_id, unit=unit)
3179+ # First check for setting with prefix, then without
3180+ for attr in ["%s_hostname" % (database), 'hostname']:
3181+ hosts = settings.get(attr, None)
3182+ if hosts:
3183+ break
3184+
3185+ if hosts:
3186+ # hostname can be json-encoded list of hostnames
3187+ try:
3188+ hosts = json.loads(hosts)
3189+ except ValueError:
3190+ hosts = [hosts]
3191+ else:
3192+ hosts = [settings['private-address']]
3193+
3194+ if hosts:
3195+ for host in hosts:
3196+ host = self.normalize_address(host)
3197+ if self.grant_exists(database, username, host):
3198+ log("Grant exists for host '%s' on db '%s'" %
3199+ (host, database), level=DEBUG)
3200+ if unit not in allowed_units:
3201+ allowed_units.add(unit)
3202+ else:
3203+ log("Grant does NOT exist for host '%s' on db '%s'" %
3204+ (host, database), level=DEBUG)
3205+ else:
3206+ log("No hosts found for grant check", level=INFO)
3207+
3208+ return allowed_units
3209+
3210+ def configure_db(self, hostname, database, username, admin=False):
3211+ """Configure access to database for username from hostname."""
3212+ self.connect(password=self.get_mysql_root_password())
3213+ if not self.database_exists(database):
3214+ self.create_database(database)
3215+
3216+ remote_ip = self.normalize_address(hostname)
3217+ password = self.get_mysql_password(username)
3218+ if not self.grant_exists(database, username, remote_ip):
3219+ if not admin:
3220+ self.create_grant(database, username, remote_ip, password)
3221+ else:
3222+ self.create_admin_grant(username, remote_ip, password)
3223+
3224+ return password
3225+
3226+
3227+class PerconaClusterHelper(object):
3228+
3229+ # Going for the biggest page size to avoid wasted bytes.
3230+ # InnoDB page size is 16MB
3231+
3232+ DEFAULT_PAGE_SIZE = 16 * 1024 * 1024
3233+ DEFAULT_INNODB_BUFFER_FACTOR = 0.50
3234+
3235+ def human_to_bytes(self, human):
3236+ """Convert human readable configuration options to bytes."""
3237+ num_re = re.compile('^[0-9]+$')
3238+ if num_re.match(human):
3239+ return human
3240+
3241+ factors = {
3242+ 'K': 1024,
3243+ 'M': 1048576,
3244+ 'G': 1073741824,
3245+ 'T': 1099511627776
3246+ }
3247+ modifier = human[-1]
3248+ if modifier in factors:
3249+ return int(human[:-1]) * factors[modifier]
3250+
3251+ if modifier == '%':
3252+ total_ram = self.human_to_bytes(self.get_mem_total())
3253+ if self.is_32bit_system() and total_ram > self.sys_mem_limit():
3254+ total_ram = self.sys_mem_limit()
3255+ factor = int(human[:-1]) * 0.01
3256+ pctram = total_ram * factor
3257+ return int(pctram - (pctram % self.DEFAULT_PAGE_SIZE))
3258+
3259+ raise ValueError("Can only convert K,M,G, or T")
3260+
3261+ def is_32bit_system(self):
3262+ """Determine whether system is 32 or 64 bit."""
3263+ try:
3264+ return sys.maxsize < 2 ** 32
3265+ except OverflowError:
3266+ return False
3267+
3268+ def sys_mem_limit(self):
3269+ """Determine the default memory limit for the current service unit."""
3270+ if platform.machine() in ['armv7l']:
3271+ _mem_limit = self.human_to_bytes('2700M') # experimentally determined
3272+ else:
3273+ # Limit for x86 based 32bit systems
3274+ _mem_limit = self.human_to_bytes('4G')
3275+
3276+ return _mem_limit
3277+
3278+ def get_mem_total(self):
3279+ """Calculate the total memory in the current service unit."""
3280+ with open('/proc/meminfo') as meminfo_file:
3281+ for line in meminfo_file:
3282+ key, mem = line.split(':', 2)
3283+ if key == 'MemTotal':
3284+ mtot, modifier = mem.strip().split(' ')
3285+ return '%s%s' % (mtot, modifier[0].upper())
3286+
3287+ def parse_config(self):
3288+ """Parse charm configuration and calculate values for config files."""
3289+ config = config_get()
3290+ mysql_config = {}
3291+ if 'max-connections' in config:
3292+ mysql_config['max_connections'] = config['max-connections']
3293+
3294+ if 'wait-timeout' in config:
3295+ mysql_config['wait_timeout'] = config['wait-timeout']
3296+
3297+ if 'innodb-flush-log-at-trx-commit' in config:
3298+ mysql_config['innodb_flush_log_at_trx_commit'] = config['innodb-flush-log-at-trx-commit']
3299+
3300+ # Set a sane default key_buffer size
3301+ mysql_config['key_buffer'] = self.human_to_bytes('32M')
3302+ total_memory = self.human_to_bytes(self.get_mem_total())
3303+
3304+ dataset_bytes = config.get('dataset-size', None)
3305+ innodb_buffer_pool_size = config.get('innodb-buffer-pool-size', None)
3306+
3307+ if innodb_buffer_pool_size:
3308+ innodb_buffer_pool_size = self.human_to_bytes(
3309+ innodb_buffer_pool_size)
3310+ elif dataset_bytes:
3311+ log("Option 'dataset-size' has been deprecated, please use"
3312+ "innodb_buffer_pool_size option instead", level="WARN")
3313+ innodb_buffer_pool_size = self.human_to_bytes(
3314+ dataset_bytes)
3315+ else:
3316+ innodb_buffer_pool_size = int(
3317+ total_memory * self.DEFAULT_INNODB_BUFFER_FACTOR)
3318+
3319+ if innodb_buffer_pool_size > total_memory:
3320+ log("innodb_buffer_pool_size; {} is greater than system available memory:{}".format(
3321+ innodb_buffer_pool_size,
3322+ total_memory), level='WARN')
3323+
3324+ mysql_config['innodb_buffer_pool_size'] = innodb_buffer_pool_size
3325+ return mysql_config
3326
3327=== added directory 'hooks/charmhelpers/contrib/hahelpers'
3328=== added file 'hooks/charmhelpers/contrib/hahelpers/__init__.py'
3329--- hooks/charmhelpers/contrib/hahelpers/__init__.py 1970-01-01 00:00:00 +0000
3330+++ hooks/charmhelpers/contrib/hahelpers/__init__.py 2016-03-21 06:13:21 +0000
3331@@ -0,0 +1,15 @@
3332+# Copyright 2014-2015 Canonical Limited.
3333+#
3334+# This file is part of charm-helpers.
3335+#
3336+# charm-helpers is free software: you can redistribute it and/or modify
3337+# it under the terms of the GNU Lesser General Public License version 3 as
3338+# published by the Free Software Foundation.
3339+#
3340+# charm-helpers is distributed in the hope that it will be useful,
3341+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3342+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3343+# GNU Lesser General Public License for more details.
3344+#
3345+# You should have received a copy of the GNU Lesser General Public License
3346+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
3347
3348=== added file 'hooks/charmhelpers/contrib/hahelpers/apache.py'
3349--- hooks/charmhelpers/contrib/hahelpers/apache.py 1970-01-01 00:00:00 +0000
3350+++ hooks/charmhelpers/contrib/hahelpers/apache.py 2016-03-21 06:13:21 +0000
3351@@ -0,0 +1,82 @@
3352+# Copyright 2014-2015 Canonical Limited.
3353+#
3354+# This file is part of charm-helpers.
3355+#
3356+# charm-helpers is free software: you can redistribute it and/or modify
3357+# it under the terms of the GNU Lesser General Public License version 3 as
3358+# published by the Free Software Foundation.
3359+#
3360+# charm-helpers is distributed in the hope that it will be useful,
3361+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3362+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3363+# GNU Lesser General Public License for more details.
3364+#
3365+# You should have received a copy of the GNU Lesser General Public License
3366+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
3367+
3368+#
3369+# Copyright 2012 Canonical Ltd.
3370+#
3371+# This file is sourced from lp:openstack-charm-helpers
3372+#
3373+# Authors:
3374+# James Page <james.page@ubuntu.com>
3375+# Adam Gandelman <adamg@ubuntu.com>
3376+#
3377+
3378+import subprocess
3379+
3380+from charmhelpers.core.hookenv import (
3381+ config as config_get,
3382+ relation_get,
3383+ relation_ids,
3384+ related_units as relation_list,
3385+ log,
3386+ INFO,
3387+)
3388+
3389+
3390+def get_cert(cn=None):
3391+ # TODO: deal with multiple https endpoints via charm config
3392+ cert = config_get('ssl_cert')
3393+ key = config_get('ssl_key')
3394+ if not (cert and key):
3395+ log("Inspecting identity-service relations for SSL certificate.",
3396+ level=INFO)
3397+ cert = key = None
3398+ if cn:
3399+ ssl_cert_attr = 'ssl_cert_{}'.format(cn)
3400+ ssl_key_attr = 'ssl_key_{}'.format(cn)
3401+ else:
3402+ ssl_cert_attr = 'ssl_cert'
3403+ ssl_key_attr = 'ssl_key'
3404+ for r_id in relation_ids('identity-service'):
3405+ for unit in relation_list(r_id):
3406+ if not cert:
3407+ cert = relation_get(ssl_cert_attr,
3408+ rid=r_id, unit=unit)
3409+ if not key:
3410+ key = relation_get(ssl_key_attr,
3411+ rid=r_id, unit=unit)
3412+ return (cert, key)
3413+
3414+
3415+def get_ca_cert():
3416+ ca_cert = config_get('ssl_ca')
3417+ if ca_cert is None:
3418+ log("Inspecting identity-service relations for CA SSL certificate.",
3419+ level=INFO)
3420+ for r_id in relation_ids('identity-service'):
3421+ for unit in relation_list(r_id):
3422+ if ca_cert is None:
3423+ ca_cert = relation_get('ca_cert',
3424+ rid=r_id, unit=unit)
3425+ return ca_cert
3426+
3427+
3428+def install_ca_cert(ca_cert):
3429+ if ca_cert:
3430+ with open('/usr/local/share/ca-certificates/keystone_juju_ca_cert.crt',
3431+ 'w') as crt:
3432+ crt.write(ca_cert)
3433+ subprocess.check_call(['update-ca-certificates', '--fresh'])
3434
3435=== added file 'hooks/charmhelpers/contrib/hahelpers/cluster.py'
3436--- hooks/charmhelpers/contrib/hahelpers/cluster.py 1970-01-01 00:00:00 +0000
3437+++ hooks/charmhelpers/contrib/hahelpers/cluster.py 2016-03-21 06:13:21 +0000
3438@@ -0,0 +1,316 @@
3439+# Copyright 2014-2015 Canonical Limited.
3440+#
3441+# This file is part of charm-helpers.
3442+#
3443+# charm-helpers is free software: you can redistribute it and/or modify
3444+# it under the terms of the GNU Lesser General Public License version 3 as
3445+# published by the Free Software Foundation.
3446+#
3447+# charm-helpers is distributed in the hope that it will be useful,
3448+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3449+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3450+# GNU Lesser General Public License for more details.
3451+#
3452+# You should have received a copy of the GNU Lesser General Public License
3453+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
3454+
3455+#
3456+# Copyright 2012 Canonical Ltd.
3457+#
3458+# Authors:
3459+# James Page <james.page@ubuntu.com>
3460+# Adam Gandelman <adamg@ubuntu.com>
3461+#
3462+
3463+"""
3464+Helpers for clustering and determining "cluster leadership" and other
3465+clustering-related helpers.
3466+"""
3467+
3468+import subprocess
3469+import os
3470+
3471+from socket import gethostname as get_unit_hostname
3472+
3473+import six
3474+
3475+from charmhelpers.core.hookenv import (
3476+ log,
3477+ relation_ids,
3478+ related_units as relation_list,
3479+ relation_get,
3480+ config as config_get,
3481+ INFO,
3482+ ERROR,
3483+ WARNING,
3484+ unit_get,
3485+ is_leader as juju_is_leader
3486+)
3487+from charmhelpers.core.decorators import (
3488+ retry_on_exception,
3489+)
3490+from charmhelpers.core.strutils import (
3491+ bool_from_string,
3492+)
3493+
3494+DC_RESOURCE_NAME = 'DC'
3495+
3496+
3497+class HAIncompleteConfig(Exception):
3498+ pass
3499+
3500+
3501+class CRMResourceNotFound(Exception):
3502+ pass
3503+
3504+
3505+class CRMDCNotFound(Exception):
3506+ pass
3507+
3508+
3509+def is_elected_leader(resource):
3510+ """
3511+ Returns True if the charm executing this is the elected cluster leader.
3512+
3513+ It relies on two mechanisms to determine leadership:
3514+ 1. If juju is sufficiently new and leadership election is supported,
3515+ the is_leader command will be used.
3516+ 2. If the charm is part of a corosync cluster, call corosync to
3517+ determine leadership.
3518+ 3. If the charm is not part of a corosync cluster, the leader is
3519+ determined as being "the alive unit with the lowest unit numer". In
3520+ other words, the oldest surviving unit.
3521+ """
3522+ try:
3523+ return juju_is_leader()
3524+ except NotImplementedError:
3525+ log('Juju leadership election feature not enabled'
3526+ ', using fallback support',
3527+ level=WARNING)
3528+
3529+ if is_clustered():
3530+ if not is_crm_leader(resource):
3531+ log('Deferring action to CRM leader.', level=INFO)
3532+ return False
3533+ else:
3534+ peers = peer_units()
3535+ if peers and not oldest_peer(peers):
3536+ log('Deferring action to oldest service unit.', level=INFO)
3537+ return False
3538+ return True
3539+
3540+
3541+def is_clustered():
3542+ for r_id in (relation_ids('ha') or []):
3543+ for unit in (relation_list(r_id) or []):
3544+ clustered = relation_get('clustered',
3545+ rid=r_id,
3546+ unit=unit)
3547+ if clustered:
3548+ return True
3549+ return False
3550+
3551+
3552+def is_crm_dc():
3553+ """
3554+ Determine leadership by querying the pacemaker Designated Controller
3555+ """
3556+ cmd = ['crm', 'status']
3557+ try:
3558+ status = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
3559+ if not isinstance(status, six.text_type):
3560+ status = six.text_type(status, "utf-8")
3561+ except subprocess.CalledProcessError as ex:
3562+ raise CRMDCNotFound(str(ex))
3563+
3564+ current_dc = ''
3565+ for line in status.split('\n'):
3566+ if line.startswith('Current DC'):
3567+ # Current DC: juju-lytrusty-machine-2 (168108163) - partition with quorum
3568+ current_dc = line.split(':')[1].split()[0]
3569+ if current_dc == get_unit_hostname():
3570+ return True
3571+ elif current_dc == 'NONE':
3572+ raise CRMDCNotFound('Current DC: NONE')
3573+
3574+ return False
3575+
3576+
3577+@retry_on_exception(5, base_delay=2,
3578+ exc_type=(CRMResourceNotFound, CRMDCNotFound))
3579+def is_crm_leader(resource, retry=False):
3580+ """
3581+ Returns True if the charm calling this is the elected corosync leader,
3582+ as returned by calling the external "crm" command.
3583+
3584+ We allow this operation to be retried to avoid the possibility of getting a
3585+ false negative. See LP #1396246 for more info.
3586+ """
3587+ if resource == DC_RESOURCE_NAME:
3588+ return is_crm_dc()
3589+ cmd = ['crm', 'resource', 'show', resource]
3590+ try:
3591+ status = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
3592+ if not isinstance(status, six.text_type):
3593+ status = six.text_type(status, "utf-8")
3594+ except subprocess.CalledProcessError:
3595+ status = None
3596+
3597+ if status and get_unit_hostname() in status:
3598+ return True
3599+
3600+ if status and "resource %s is NOT running" % (resource) in status:
3601+ raise CRMResourceNotFound("CRM resource %s not found" % (resource))
3602+
3603+ return False
3604+
3605+
3606+def is_leader(resource):
3607+ log("is_leader is deprecated. Please consider using is_crm_leader "
3608+ "instead.", level=WARNING)
3609+ return is_crm_leader(resource)
3610+
3611+
3612+def peer_units(peer_relation="cluster"):
3613+ peers = []
3614+ for r_id in (relation_ids(peer_relation) or []):
3615+ for unit in (relation_list(r_id) or []):
3616+ peers.append(unit)
3617+ return peers
3618+
3619+
3620+def peer_ips(peer_relation='cluster', addr_key='private-address'):
3621+ '''Return a dict of peers and their private-address'''
3622+ peers = {}
3623+ for r_id in relation_ids(peer_relation):
3624+ for unit in relation_list(r_id):
3625+ peers[unit] = relation_get(addr_key, rid=r_id, unit=unit)
3626+ return peers
3627+
3628+
3629+def oldest_peer(peers):
3630+ """Determines who the oldest peer is by comparing unit numbers."""
3631+ local_unit_no = int(os.getenv('JUJU_UNIT_NAME').split('/')[1])
3632+ for peer in peers:
3633+ remote_unit_no = int(peer.split('/')[1])
3634+ if remote_unit_no < local_unit_no:
3635+ return False
3636+ return True
3637+
3638+
3639+def eligible_leader(resource):
3640+ log("eligible_leader is deprecated. Please consider using "
3641+ "is_elected_leader instead.", level=WARNING)
3642+ return is_elected_leader(resource)
3643+
3644+
3645+def https():
3646+ '''
3647+ Determines whether enough data has been provided in configuration
3648+ or relation data to configure HTTPS
3649+ .
3650+ returns: boolean
3651+ '''
3652+ use_https = config_get('use-https')
3653+ if use_https and bool_from_string(use_https):
3654+ return True
3655+ if config_get('ssl_cert') and config_get('ssl_key'):
3656+ return True
3657+ for r_id in relation_ids('identity-service'):
3658+ for unit in relation_list(r_id):
3659+ # TODO - needs fixing for new helper as ssl_cert/key suffixes with CN
3660+ rel_state = [
3661+ relation_get('https_keystone', rid=r_id, unit=unit),
3662+ relation_get('ca_cert', rid=r_id, unit=unit),
3663+ ]
3664+ # NOTE: works around (LP: #1203241)
3665+ if (None not in rel_state) and ('' not in rel_state):
3666+ return True
3667+ return False
3668+
3669+
3670+def determine_api_port(public_port, singlenode_mode=False):
3671+ '''
3672+ Determine correct API server listening port based on
3673+ existence of HTTPS reverse proxy and/or haproxy.
3674+
3675+ public_port: int: standard public port for given service
3676+
3677+ singlenode_mode: boolean: Shuffle ports when only a single unit is present
3678+
3679+ returns: int: the correct listening port for the API service
3680+ '''
3681+ i = 0
3682+ if singlenode_mode:
3683+ i += 1
3684+ elif len(peer_units()) > 0 or is_clustered():
3685+ i += 1
3686+ if https():
3687+ i += 1
3688+ return public_port - (i * 10)
3689+
3690+
3691+def determine_apache_port(public_port, singlenode_mode=False):
3692+ '''
3693+ Description: Determine correct apache listening port based on public IP +
3694+ state of the cluster.
3695+
3696+ public_port: int: standard public port for given service
3697+
3698+ singlenode_mode: boolean: Shuffle ports when only a single unit is present
3699+
3700+ returns: int: the correct listening port for the HAProxy service
3701+ '''
3702+ i = 0
3703+ if singlenode_mode:
3704+ i += 1
3705+ elif len(peer_units()) > 0 or is_clustered():
3706+ i += 1
3707+ return public_port - (i * 10)
3708+
3709+
3710+def get_hacluster_config(exclude_keys=None):
3711+ '''
3712+ Obtains all relevant configuration from charm configuration required
3713+ for initiating a relation to hacluster:
3714+
3715+ ha-bindiface, ha-mcastport, vip
3716+
3717+ param: exclude_keys: list of setting key(s) to be excluded.
3718+ returns: dict: A dict containing settings keyed by setting name.
3719+ raises: HAIncompleteConfig if settings are missing.
3720+ '''
3721+ settings = ['ha-bindiface', 'ha-mcastport', 'vip']
3722+ conf = {}
3723+ for setting in settings:
3724+ if exclude_keys and setting in exclude_keys:
3725+ continue
3726+
3727+ conf[setting] = config_get(setting)
3728+ missing = []
3729+ [missing.append(s) for s, v in six.iteritems(conf) if v is None]
3730+ if missing:
3731+ log('Insufficient config data to configure hacluster.', level=ERROR)
3732+ raise HAIncompleteConfig
3733+ return conf
3734+
3735+
3736+def canonical_url(configs, vip_setting='vip'):
3737+ '''
3738+ Returns the correct HTTP URL to this host given the state of HTTPS
3739+ configuration and hacluster.
3740+
3741+ :configs : OSTemplateRenderer: A config tempating object to inspect for
3742+ a complete https context.
3743+
3744+ :vip_setting: str: Setting in charm config that specifies
3745+ VIP address.
3746+ '''
3747+ scheme = 'http'
3748+ if 'https' in configs.complete_contexts():
3749+ scheme = 'https'
3750+ if is_clustered():
3751+ addr = config_get(vip_setting)
3752+ else:
3753+ addr = unit_get('private-address')
3754+ return '%s://%s' % (scheme, addr)
3755
3756=== added directory 'hooks/charmhelpers/contrib/hardening'
3757=== added file 'hooks/charmhelpers/contrib/hardening/README.hardening.md'
3758--- hooks/charmhelpers/contrib/hardening/README.hardening.md 1970-01-01 00:00:00 +0000
3759+++ hooks/charmhelpers/contrib/hardening/README.hardening.md 2016-03-21 06:13:21 +0000
3760@@ -0,0 +1,38 @@
3761+# Juju charm-helpers hardening library
3762+
3763+## Description
3764+
3765+This library provides multiple implementations of system and application
3766+hardening that conform to the standards of http://hardening.io/.
3767+
3768+Current implementations include:
3769+
3770+ * OS
3771+ * SSH
3772+ * MySQL
3773+ * Apache
3774+
3775+## Requirements
3776+
3777+* Juju Charms
3778+
3779+## Usage
3780+
3781+1. Synchronise this library into your charm and add the harden() decorator
3782+ (from contrib.hardening.harden) to any functions or methods you want to use
3783+ to trigger hardening of your application/system.
3784+
3785+2. Add a config option called 'harden' to your charm config.yaml and set it to
3786+ a space-delimited list of hardening modules you want to run e.g. "os ssh"
3787+
3788+3. Override any config defaults (contrib.hardening.defaults) by adding a file
3789+ called hardening.yaml to your charm root containing the name(s) of the
3790+ modules whose settings you want override at root level and then any settings
3791+ with overrides e.g.
3792+
3793+ os:
3794+ general:
3795+ desktop_enable: True
3796+
3797+4. Now just run your charm as usual and hardening will be applied each time the
3798+ hook runs.
3799
3800=== added file 'hooks/charmhelpers/contrib/hardening/__init__.py'
3801--- hooks/charmhelpers/contrib/hardening/__init__.py 1970-01-01 00:00:00 +0000
3802+++ hooks/charmhelpers/contrib/hardening/__init__.py 2016-03-21 06:13:21 +0000
3803@@ -0,0 +1,15 @@
3804+# Copyright 2016 Canonical Limited.
3805+#
3806+# This file is part of charm-helpers.
3807+#
3808+# charm-helpers is free software: you can redistribute it and/or modify
3809+# it under the terms of the GNU Lesser General Public License version 3 as
3810+# published by the Free Software Foundation.
3811+#
3812+# charm-helpers is distributed in the hope that it will be useful,
3813+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3814+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3815+# GNU Lesser General Public License for more details.
3816+#
3817+# You should have received a copy of the GNU Lesser General Public License
3818+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
3819
3820=== added directory 'hooks/charmhelpers/contrib/hardening/apache'
3821=== added file 'hooks/charmhelpers/contrib/hardening/apache/__init__.py'
3822--- hooks/charmhelpers/contrib/hardening/apache/__init__.py 1970-01-01 00:00:00 +0000
3823+++ hooks/charmhelpers/contrib/hardening/apache/__init__.py 2016-03-21 06:13:21 +0000
3824@@ -0,0 +1,19 @@
3825+# Copyright 2016 Canonical Limited.
3826+#
3827+# This file is part of charm-helpers.
3828+#
3829+# charm-helpers is free software: you can redistribute it and/or modify
3830+# it under the terms of the GNU Lesser General Public License version 3 as
3831+# published by the Free Software Foundation.
3832+#
3833+# charm-helpers is distributed in the hope that it will be useful,
3834+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3835+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3836+# GNU Lesser General Public License for more details.
3837+#
3838+# You should have received a copy of the GNU Lesser General Public License
3839+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
3840+
3841+from os import path
3842+
3843+TEMPLATES_DIR = path.join(path.dirname(__file__), 'templates')
3844
3845=== added directory 'hooks/charmhelpers/contrib/hardening/apache/checks'
3846=== added file 'hooks/charmhelpers/contrib/hardening/apache/checks/__init__.py'
3847--- hooks/charmhelpers/contrib/hardening/apache/checks/__init__.py 1970-01-01 00:00:00 +0000
3848+++ hooks/charmhelpers/contrib/hardening/apache/checks/__init__.py 2016-03-21 06:13:21 +0000
3849@@ -0,0 +1,31 @@
3850+# Copyright 2016 Canonical Limited.
3851+#
3852+# This file is part of charm-helpers.
3853+#
3854+# charm-helpers is free software: you can redistribute it and/or modify
3855+# it under the terms of the GNU Lesser General Public License version 3 as
3856+# published by the Free Software Foundation.
3857+#
3858+# charm-helpers is distributed in the hope that it will be useful,
3859+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3860+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3861+# GNU Lesser General Public License for more details.
3862+#
3863+# You should have received a copy of the GNU Lesser General Public License
3864+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
3865+
3866+from charmhelpers.core.hookenv import (
3867+ log,
3868+ DEBUG,
3869+)
3870+from charmhelpers.contrib.hardening.apache.checks import config
3871+
3872+
3873+def run_apache_checks():
3874+ log("Starting Apache hardening checks.", level=DEBUG)
3875+ checks = config.get_audits()
3876+ for check in checks:
3877+ log("Running '%s' check" % (check.__class__.__name__), level=DEBUG)
3878+ check.ensure_compliance()
3879+
3880+ log("Apache hardening checks complete.", level=DEBUG)
3881
3882=== added file 'hooks/charmhelpers/contrib/hardening/apache/checks/config.py'
3883--- hooks/charmhelpers/contrib/hardening/apache/checks/config.py 1970-01-01 00:00:00 +0000
3884+++ hooks/charmhelpers/contrib/hardening/apache/checks/config.py 2016-03-21 06:13:21 +0000
3885@@ -0,0 +1,98 @@
3886+# Copyright 2016 Canonical Limited.
3887+#
3888+# This file is part of charm-helpers.
3889+#
3890+# charm-helpers is free software: you can redistribute it and/or modify
3891+# it under the terms of the GNU Lesser General Public License version 3 as
3892+# published by the Free Software Foundation.
3893+#
3894+# charm-helpers is distributed in the hope that it will be useful,
3895+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3896+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3897+# GNU Lesser General Public License for more details.
3898+#
3899+# You should have received a copy of the GNU Lesser General Public License
3900+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
3901+
3902+import os
3903+import re
3904+import subprocess
3905+
3906+
3907+from charmhelpers.core.hookenv import log
3908+from charmhelpers.core.hookenv import INFO
3909+
3910+from charmhelpers.contrib.hardening.audits.file import FilePermissionAudit
3911+from charmhelpers.contrib.hardening.audits.file import DirectoryPermissionAudit
3912+from charmhelpers.contrib.hardening.audits.file import NoReadWriteForOther
3913+from charmhelpers.contrib.hardening.audits.file import TemplatedFile
3914+from charmhelpers.contrib.hardening.audits.apache import DisabledModuleAudit
3915+
3916+from charmhelpers.contrib.hardening.apache import TEMPLATES_DIR
3917+from charmhelpers.contrib.hardening import utils
3918+
3919+
3920+def get_audits():
3921+ """Get Apache hardening config audits.
3922+
3923+ :returns: dictionary of audits
3924+ """
3925+ if subprocess.call(['which', 'apache2'], stdout=subprocess.PIPE) != 0:
3926+ log("Apache server does not appear to be installed on this node - "
3927+ "skipping apache hardening", level=INFO)
3928+ return []
3929+
3930+ context = ApacheConfContext()
3931+ settings = utils.get_settings('apache')
3932+ audits = [
3933+ FilePermissionAudit(paths='/etc/apache2/apache2.conf', user='root',
3934+ group='root', mode=0o0640),
3935+
3936+ TemplatedFile(os.path.join(settings['common']['apache_dir'],
3937+ 'mods-available/alias.conf'),
3938+ context,
3939+ TEMPLATES_DIR,
3940+ mode=0o0755,
3941+ user='root',
3942+ service_actions=[{'service': 'apache2',
3943+ 'actions': ['restart']}]),
3944+
3945+ TemplatedFile(os.path.join(settings['common']['apache_dir'],
3946+ 'conf-enabled/hardening.conf'),
3947+ context,
3948+ TEMPLATES_DIR,
3949+ mode=0o0640,
3950+ user='root',
3951+ service_actions=[{'service': 'apache2',
3952+ 'actions': ['restart']}]),
3953+
3954+ DirectoryPermissionAudit(settings['common']['apache_dir'],
3955+ user='root',
3956+ group='root',
3957+ mode=0o640),
3958+
3959+ DisabledModuleAudit(settings['hardening']['modules_to_disable']),
3960+
3961+ NoReadWriteForOther(settings['common']['apache_dir']),
3962+ ]
3963+
3964+ return audits
3965+
3966+
3967+class ApacheConfContext(object):
3968+ """Defines the set of key/value pairs to set in a apache config file.
3969+
3970+ This context, when called, will return a dictionary containing the
3971+ key/value pairs of setting to specify in the
3972+ /etc/apache/conf-enabled/hardening.conf file.
3973+ """
3974+ def __call__(self):
3975+ settings = utils.get_settings('apache')
3976+ ctxt = settings['hardening']
3977+
3978+ out = subprocess.check_output(['apache2', '-v'])
3979+ ctxt['apache_version'] = re.search(r'.+version: Apache/(.+?)\s.+',
3980+ out).group(1)
3981+ ctxt['apache_icondir'] = '/usr/share/apache2/icons/'
3982+ ctxt['traceenable'] = settings['hardening']['traceenable']
3983+ return ctxt
3984
3985=== added directory 'hooks/charmhelpers/contrib/hardening/apache/templates'
3986=== added file 'hooks/charmhelpers/contrib/hardening/apache/templates/alias.conf'
3987--- hooks/charmhelpers/contrib/hardening/apache/templates/alias.conf 1970-01-01 00:00:00 +0000
3988+++ hooks/charmhelpers/contrib/hardening/apache/templates/alias.conf 2016-03-21 06:13:21 +0000
3989@@ -0,0 +1,31 @@
3990+###############################################################################
3991+# WARNING: This configuration file is maintained by Juju. Local changes may
3992+# be overwritten.
3993+###############################################################################
3994+<IfModule alias_module>
3995+ #
3996+ # Aliases: Add here as many aliases as you need (with no limit). The format is
3997+ # Alias fakename realname
3998+ #
3999+ # Note that if you include a trailing / on fakename then the server will
4000+ # require it to be present in the URL. So "/icons" isn't aliased in this
4001+ # example, only "/icons/". If the fakename is slash-terminated, then the
4002+ # realname must also be slash terminated, and if the fakename omits the
4003+ # trailing slash, the realname must also omit it.
4004+ #
4005+ # We include the /icons/ alias for FancyIndexed directory listings. If
4006+ # you do not use FancyIndexing, you may comment this out.
4007+ #
4008+ Alias /icons/ "{{ apache_icondir }}/"
4009+
4010+ <Directory "{{ apache_icondir }}">
4011+ Options -Indexes -MultiViews -FollowSymLinks
4012+ AllowOverride None
4013+{% if apache_version == '2.4' -%}
4014+ Require all granted
4015+{% else -%}
4016+ Order allow,deny
4017+ Allow from all
4018+{% endif %}
4019+ </Directory>
4020+</IfModule>
4021
4022=== added file 'hooks/charmhelpers/contrib/hardening/apache/templates/hardening.conf'
4023--- hooks/charmhelpers/contrib/hardening/apache/templates/hardening.conf 1970-01-01 00:00:00 +0000
4024+++ hooks/charmhelpers/contrib/hardening/apache/templates/hardening.conf 2016-03-21 06:13:21 +0000
4025@@ -0,0 +1,18 @@
4026+###############################################################################
4027+# WARNING: This configuration file is maintained by Juju. Local changes may
4028+# be overwritten.
4029+###############################################################################
4030+
4031+<Location / >
4032+ <LimitExcept {{ allowed_http_methods }} >
4033+ # http://httpd.apache.org/docs/2.4/upgrading.html
4034+ {% if apache_version > '2.2' -%}
4035+ Require all granted
4036+ {% else -%}
4037+ Order Allow,Deny
4038+ Deny from all
4039+ {% endif %}
4040+ </LimitExcept>
4041+</Location>
4042+
4043+TraceEnable {{ traceenable }}
4044
4045=== added directory 'hooks/charmhelpers/contrib/hardening/audits'
4046=== added file 'hooks/charmhelpers/contrib/hardening/audits/__init__.py'
4047--- hooks/charmhelpers/contrib/hardening/audits/__init__.py 1970-01-01 00:00:00 +0000
4048+++ hooks/charmhelpers/contrib/hardening/audits/__init__.py 2016-03-21 06:13:21 +0000
4049@@ -0,0 +1,63 @@
4050+# Copyright 2016 Canonical Limited.
4051+#
4052+# This file is part of charm-helpers.
4053+#
4054+# charm-helpers is free software: you can redistribute it and/or modify
4055+# it under the terms of the GNU Lesser General Public License version 3 as
4056+# published by the Free Software Foundation.
4057+#
4058+# charm-helpers is distributed in the hope that it will be useful,
4059+# but WITHOUT ANY WARRANTY; without even the implied warranty of
4060+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
4061+# GNU Lesser General Public License for more details.
4062+#
4063+# You should have received a copy of the GNU Lesser General Public License
4064+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
4065+
4066+
4067+class BaseAudit(object): # NO-QA
4068+ """Base class for hardening checks.
4069+
4070+ The lifecycle of a hardening check is to first check to see if the system
4071+ is in compliance for the specified check. If it is not in compliance, the
4072+ check method will return a value which will be supplied to the.
4073+ """
4074+ def __init__(self, *args, **kwargs):
4075+ self.unless = kwargs.get('unless', None)
4076+ super(BaseAudit, self).__init__()
4077+
4078+ def ensure_compliance(self):
4079+ """Checks to see if the current hardening check is in compliance or
4080+ not.
4081+
4082+ If the check that is performed is not in compliance, then an exception
4083+ should be raised.
4084+ """
4085+ pass
4086+
4087+ def _take_action(self):
4088+ """Determines whether to perform the action or not.
4089+
4090+ Checks whether or not an action should be taken. This is determined by
4091+ the truthy value for the unless parameter. If unless is a callback
4092+ method, it will be invoked with no parameters in order to determine
4093+ whether or not the action should be taken. Otherwise, the truthy value
4094+ of the unless attribute will determine if the action should be
4095+ performed.
4096+ """
4097+ # Do the action if there isn't an unless override.
4098+ if self.unless is None:
4099+ return True
4100+
4101+ # Invoke the callback if there is one.
4102+ if hasattr(self.unless, '__call__'):
4103+ results = self.unless()
4104+ if results:
4105+ return False
4106+ else:
4107+ return True
4108+
4109+ if self.unless:
4110+ return False
4111+ else:
4112+ return True
4113
4114=== added file 'hooks/charmhelpers/contrib/hardening/audits/apache.py'
4115--- hooks/charmhelpers/contrib/hardening/audits/apache.py 1970-01-01 00:00:00 +0000
4116+++ hooks/charmhelpers/contrib/hardening/audits/apache.py 2016-03-21 06:13:21 +0000
4117@@ -0,0 +1,97 @@
4118+# Copyright 2016 Canonical Limited.
4119+#
4120+# This file is part of charm-helpers.
4121+#
4122+# charm-helpers is free software: you can redistribute it and/or modify
4123+# it under the terms of the GNU Lesser General Public License version 3 as
4124+# published by the Free Software Foundation.
4125+#
4126+# charm-helpers is distributed in the hope that it will be useful,
4127+# but WITHOUT ANY WARRANTY; without even the implied warranty of
4128+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
4129+# GNU Lesser General Public License for more details.
4130+#
4131+# You should have received a copy of the GNU Lesser General Public License
4132+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
4133+
4134+import re
4135+import subprocess
4136+
4137+from six import string_types
4138+
4139+from charmhelpers.core.hookenv import log
4140+from charmhelpers.core.hookenv import INFO
4141+from charmhelpers.core.hookenv import ERROR
4142+
4143+from charmhelpers.contrib.hardening.audits import BaseAudit
4144+
4145+
4146+class DisabledModuleAudit(BaseAudit):
4147+ """Audits Apache2 modules.
4148+
4149+ Determines if the apache2 modules are enabled. If the modules are enabled
4150+ then they are removed in the ensure_compliance.
4151+ """
4152+ def __init__(self, modules):
4153+ if modules is None:
4154+ self.modules = []
4155+ elif isinstance(modules, string_types):
4156+ self.modules = [modules]
4157+ else:
4158+ self.modules = modules
4159+
4160+ def ensure_compliance(self):
4161+ """Ensures that the modules are not loaded."""
4162+ if not self.modules:
4163+ return
4164+
4165+ try:
4166+ loaded_modules = self._get_loaded_modules()
4167+ non_compliant_modules = []
4168+ for module in self.modules:
4169+ if module in loaded_modules:
4170+ log('Module %s is enabled and should not be.', level=INFO)
4171+ non_compliant_modules.append(module)
4172+
4173+ if len(non_compliant_modules) == 0:
4174+ return
4175+
4176+ for module in non_compliant_modules:
4177+ self._disable_module(module)
4178+ self._restart_apache()
4179+ except subprocess.CalledProcessError as e:
4180+ log('Error occurred auditing apache module compliance. '
4181+ 'This may have been already reported. '
4182+ 'Output is: %s' % e.output, level=ERROR)
4183+
4184+ @staticmethod
4185+ def _get_loaded_modules():
4186+ """Returns the modules which are enabled in Apache."""
4187+ output = subprocess.check_output(['apache2ctl', '-M'])
4188+ modules = []
4189+ for line in output.strip().split():
4190+ # Each line of the enabled module output looks like:
4191+ # module_name (static|shared)
4192+ # Plus a header line at the top of the output which is stripped
4193+ # out by the regex.
4194+ matcher = re.search(r'^ (\S*)', line)
4195+ if matcher:
4196+ modules.append(matcher.group(1))
4197+ return modules
4198+
4199+ @staticmethod
4200+ def _disable_module(module):
4201+ """Disables the specified module in Apache."""
4202+ try:
4203+ subprocess.check_call(['a2dismod', module])
4204+ except subprocess.CalledProcessError as e:
4205+ # Note: catch error here to allow the attempt of disabling
4206+ # multiple modules in one go rather than failing after the
4207+ # first module fails.
4208+ log('Error occurred disabling module %s. '
4209+ 'Output is: %s' % (module, e.output), level=ERROR)
4210+
4211+ @staticmethod
4212+ def _restart_apache():
4213+ """Restarts the apache process"""
4214+ subprocess.check_output(['service', 'apache2', 'restart'])
4215
4216=== added file 'hooks/charmhelpers/contrib/hardening/audits/apt.py'
4217--- hooks/charmhelpers/contrib/hardening/audits/apt.py 1970-01-01 00:00:00 +0000
4218+++ hooks/charmhelpers/contrib/hardening/audits/apt.py 2016-03-21 06:13:21 +0000
4219@@ -0,0 +1,105 @@
4220+# Copyright 2016 Canonical Limited.
4221+#
4222+# This file is part of charm-helpers.
4223+#
4224+# charm-helpers is free software: you can redistribute it and/or modify
4225+# it under the terms of the GNU Lesser General Public License version 3 as
4226+# published by the Free Software Foundation.
4227+#
4228+# charm-helpers is distributed in the hope that it will be useful,
4229+# but WITHOUT ANY WARRANTY; without even the implied warranty of
4230+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
4231+# GNU Lesser General Public License for more details.
4232+#
4233+# You should have received a copy of the GNU Lesser General Public License
4234+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
4235+
4236+from __future__ import absolute_import # required for external apt import
4237+from apt import apt_pkg
4238+from six import string_types
4239+
4240+from charmhelpers.fetch import (
4241+ apt_cache,
4242+ apt_purge
4243+)
4244+from charmhelpers.core.hookenv import (
4245+ log,
4246+ DEBUG,
4247+ WARNING,
4248+)
4249+from charmhelpers.contrib.hardening.audits import BaseAudit
4250+
4251+
4252+class AptConfig(BaseAudit):
4253+
4254+ def __init__(self, config, **kwargs):
4255+ self.config = config
4256+
4257+ def verify_config(self):
4258+ apt_pkg.init()
4259+ for cfg in self.config:
4260+ value = apt_pkg.config.get(cfg['key'], cfg.get('default', ''))
4261+ if value and value != cfg['expected']:
4262+ log("APT config '%s' has unexpected value '%s' "
4263+ "(expected='%s')" %
4264+ (cfg['key'], value, cfg['expected']), level=WARNING)
4265+
4266+ def ensure_compliance(self):
4267+ self.verify_config()
4268+
4269+
4270+class RestrictedPackages(BaseAudit):
4271+ """Class used to audit restricted packages on the system."""
4272+
4273+ def __init__(self, pkgs, **kwargs):
4274+ super(RestrictedPackages, self).__init__(**kwargs)
4275+ if isinstance(pkgs, string_types) or not hasattr(pkgs, '__iter__'):
4276+ self.pkgs = [pkgs]
4277+ else:
4278+ self.pkgs = pkgs
4279+
4280+ def ensure_compliance(self):
4281+ cache = apt_cache()
4282+
4283+ for p in self.pkgs:
4284+ if p not in cache:
4285+ continue
4286+
4287+ pkg = cache[p]
4288+ if not self.is_virtual_package(pkg):
4289+ if not pkg.current_ver:
4290+ log("Package '%s' is not installed." % pkg.name,
4291+ level=DEBUG)
4292+ continue
4293+ else:
4294+ log("Restricted package '%s' is installed" % pkg.name,
4295+ level=WARNING)
4296+ self.delete_package(cache, pkg)
4297+ else:
4298+ log("Checking restricted virtual package '%s' provides" %
4299+ pkg.name, level=DEBUG)
4300+ self.delete_package(cache, pkg)
4301+
4302+ def delete_package(self, cache, pkg):
4303+ """Deletes the package from the system.
4304+
4305+ Deletes the package form the system, properly handling virtual
4306+ packages.
4307+
4308+ :param cache: the apt cache
4309+ :param pkg: the package to remove
4310+ """
4311+ if self.is_virtual_package(pkg):
4312+ log("Package '%s' appears to be virtual - purging provides" %
4313+ pkg.name, level=DEBUG)
4314+ for _p in pkg.provides_list:
4315+ self.delete_package(cache, _p[2].parent_pkg)
4316+ elif not pkg.current_ver:
4317+ log("Package '%s' not installed" % pkg.name, level=DEBUG)
4318+ return
4319+ else:
4320+ log("Purging package '%s'" % pkg.name, level=DEBUG)
4321+ apt_purge(pkg.name)
4322+
4323+ def is_virtual_package(self, pkg):
4324+ return pkg.has_provides and not pkg.has_versions
4325
4326=== added file 'hooks/charmhelpers/contrib/hardening/audits/file.py'
4327--- hooks/charmhelpers/contrib/hardening/audits/file.py 1970-01-01 00:00:00 +0000
4328+++ hooks/charmhelpers/contrib/hardening/audits/file.py 2016-03-21 06:13:21 +0000
4329@@ -0,0 +1,551 @@
4330+# Copyright 2016 Canonical Limited.
4331+#
4332+# This file is part of charm-helpers.
4333+#
4334+# charm-helpers is free software: you can redistribute it and/or modify
4335+# it under the terms of the GNU Lesser General Public License version 3 as
4336+# published by the Free Software Foundation.
4337+#
4338+# charm-helpers is distributed in the hope that it will be useful,
4339+# but WITHOUT ANY WARRANTY; without even the implied warranty of
4340+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
4341+# GNU Lesser General Public License for more details.
4342+#
4343+# You should have received a copy of the GNU Lesser General Public License
4344+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
4345+
4346+import grp
4347+import os
4348+import pwd
4349+import re
4350+
4351+from subprocess import (
4352+ CalledProcessError,
4353+ check_output,
4354+ check_call,
4355+)
4356+from traceback import format_exc
4357+from six import string_types
4358+from stat import (
4359+ S_ISGID,
4360+ S_ISUID
4361+)
4362+
4363+from charmhelpers.core.hookenv import (
4364+ log,
4365+ DEBUG,
4366+ INFO,
4367+ WARNING,
4368+ ERROR,
4369+)
4370+from charmhelpers.core import unitdata
4371+from charmhelpers.core.host import file_hash
4372+from charmhelpers.contrib.hardening.audits import BaseAudit
4373+from charmhelpers.contrib.hardening.templating import (
4374+ get_template_path,
4375+ render_and_write,
4376+)
4377+from charmhelpers.contrib.hardening import utils
4378+
4379+
4380+class BaseFileAudit(BaseAudit):
4381+ """Base class for file audits.
4382+
4383+ Provides api stubs for compliance check flow that must be used by any class
4384+ that implemented this one.
4385+ """
4386+
4387+ def __init__(self, paths, always_comply=False, *args, **kwargs):
4388+ """
4389+ :param paths: string path of list of paths of files we want to apply
4390+ compliance checks are criteria to.
4391+ :param always_comply: if true compliance criteria is always applied
4392+ else compliance is skipped for non-existent
4393+ paths.
4394+ """
4395+ super(BaseFileAudit, self).__init__(*args, **kwargs)
4396+ self.always_comply = always_comply
4397+ if isinstance(paths, string_types) or not hasattr(paths, '__iter__'):
4398+ self.paths = [paths]
4399+ else:
4400+ self.paths = paths
4401+
4402+ def ensure_compliance(self):
4403+ """Ensure that the all registered files comply to registered criteria.
4404+ """
4405+ for p in self.paths:
4406+ if os.path.exists(p):
4407+ if self.is_compliant(p):
4408+ continue
4409+
4410+ log('File %s is not in compliance.' % p, level=INFO)
4411+ else:
4412+ if not self.always_comply:
4413+ log("Non-existent path '%s' - skipping compliance check"
4414+ % (p), level=INFO)
4415+ continue
4416+
4417+ if self._take_action():
4418+ log("Applying compliance criteria to '%s'" % (p), level=INFO)
4419+ self.comply(p)
4420+
4421+ def is_compliant(self, path):
4422+ """Audits the path to see if it is compliance.
4423+
4424+ :param path: the path to the file that should be checked.
4425+ """
4426+ raise NotImplementedError
4427+
4428+ def comply(self, path):
4429+ """Enforces the compliance of a path.
4430+
4431+ :param path: the path to the file that should be enforced.
4432+ """
4433+ raise NotImplementedError
4434+
4435+ @classmethod
4436+ def _get_stat(cls, path):
4437+ """Returns the Posix st_stat information for the specified file path.
4438+
4439+ :param path: the path to get the st_stat information for.
4440+ :returns: an st_stat object for the path or None if the path doesn't
4441+ exist.
4442+ """
4443+ return os.stat(path)
4444+
4445+
4446+class FilePermissionAudit(BaseFileAudit):
4447+ """Implements an audit for file permissions and ownership for a user.
4448+
4449+ This class implements functionality that ensures that a specific user/group
4450+ will own the file(s) specified and that the permissions specified are
4451+ applied properly to the file.
4452+ """
4453+ def __init__(self, paths, user, group=None, mode=0o600, **kwargs):
4454+ self.user = user
4455+ self.group = group
4456+ self.mode = mode
4457+ super(FilePermissionAudit, self).__init__(paths, user, group, mode,
4458+ **kwargs)
4459+
4460+ @property
4461+ def user(self):
4462+ return self._user
4463+
4464+ @user.setter
4465+ def user(self, name):
4466+ try:
4467+ user = pwd.getpwnam(name)
4468+ except KeyError:
4469+ log('Unknown user %s' % name, level=ERROR)
4470+ user = None
4471+ self._user = user
4472+
4473+ @property
4474+ def group(self):
4475+ return self._group
4476+
4477+ @group.setter
4478+ def group(self, name):
4479+ try:
4480+ group = None
4481+ if name:
4482+ group = grp.getgrnam(name)
4483+ else:
4484+ group = grp.getgrgid(self.user.pw_gid)
4485+ except KeyError:
4486+ log('Unknown group %s' % name, level=ERROR)
4487+ self._group = group
4488+
4489+ def is_compliant(self, path):
4490+ """Checks if the path is in compliance.
4491+
4492+ Used to determine if the path specified meets the necessary
4493+ requirements to be in compliance with the check itself.
4494+
4495+ :param path: the file path to check
4496+ :returns: True if the path is compliant, False otherwise.
4497+ """
4498+ stat = self._get_stat(path)
4499+ user = self.user
4500+ group = self.group
4501+
4502+ compliant = True
4503+ if stat.st_uid != user.pw_uid or stat.st_gid != group.gr_gid:
4504+ log('File %s is not owned by %s:%s.' % (path, user.pw_name,
4505+ group.gr_name),
4506+ level=INFO)
4507+ compliant = False
4508+
4509+ # POSIX refers to the st_mode bits as corresponding to both the
4510+ # file type and file permission bits, where the least significant 12
4511+ # bits (o7777) are the suid (11), sgid (10), sticky bits (9), and the
4512+ # file permission bits (8-0)
4513+ perms = stat.st_mode & 0o7777
4514+ if perms != self.mode:
4515+ log('File %s has incorrect permissions, currently set to %s' %
4516+ (path, oct(stat.st_mode & 0o7777)), level=INFO)
4517+ compliant = False
4518+
4519+ return compliant
4520+
4521+ def comply(self, path):
4522+ """Issues a chown and chmod to the file paths specified."""
4523+ utils.ensure_permissions(path, self.user.pw_name, self.group.gr_name,
4524+ self.mode)
4525+
4526+
4527+class DirectoryPermissionAudit(FilePermissionAudit):
4528+ """Performs a permission check for the specified directory path."""
4529+
4530+ def __init__(self, paths, user, group=None, mode=0o600,
4531+ recursive=True, **kwargs):
4532+ super(DirectoryPermissionAudit, self).__init__(paths, user, group,
4533+ mode, **kwargs)
4534+ self.recursive = recursive
4535+
4536+ def is_compliant(self, path):
4537+ """Checks if the directory is compliant.
4538+
4539+ Used to determine if the path specified and all of its children
4540+ directories are in compliance with the check itself.
4541+
4542+ :param path: the directory path to check
4543+ :returns: True if the directory tree is compliant, otherwise False.
4544+ """
4545+ if not os.path.isdir(path):
4546+ log('Path specified %s is not a directory.' % path, level=ERROR)
4547+ raise ValueError("%s is not a directory." % path)
4548+
4549+ if not self.recursive:
4550+ return super(DirectoryPermissionAudit, self).is_compliant(path)
4551+
4552+ compliant = True
4553+ for root, dirs, _ in os.walk(path):
4554+ if len(dirs) > 0:
4555+ continue
4556+
4557+ if not super(DirectoryPermissionAudit, self).is_compliant(root):
4558+ compliant = False
4559+ continue
4560+
4561+ return compliant
4562+
4563+ def comply(self, path):
4564+ for root, dirs, _ in os.walk(path):
4565+ if len(dirs) > 0:
4566+ super(DirectoryPermissionAudit, self).comply(root)
4567+
4568+
4569+class ReadOnly(BaseFileAudit):
4570+ """Audits that files and folders are read only."""
4571+ def __init__(self, paths, *args, **kwargs):
4572+ super(ReadOnly, self).__init__(paths=paths, *args, **kwargs)
4573+
4574+ def is_compliant(self, path):
4575+ try:
4576+ output = check_output(['find', path, '-perm', '-go+w',
4577+ '-type', 'f']).strip()
4578+
4579+ # The find above will find any files which have permission sets
4580+ # which allow too broad of write access. As such, the path is
4581+ # compliant if there is no output.
4582+ if output:
4583+ return False
4584+
4585+ return True
4586+ except CalledProcessError as e:
4587+ log('Error occurred checking finding writable files for %s. '
4588+ 'Error information is: command %s failed with returncode '
4589+ '%d and output %s.\n%s' % (path, e.cmd, e.returncode, e.output,
4590+ format_exc(e)), level=ERROR)
4591+ return False
4592+
4593+ def comply(self, path):
4594+ try:
4595+ check_output(['chmod', 'go-w', '-R', path])
4596+ except CalledProcessError as e:
4597+ log('Error occurred removing writeable permissions for %s. '
4598+ 'Error information is: command %s failed with returncode '
4599+ '%d and output %s.\n%s' % (path, e.cmd, e.returncode, e.output,
4600+ format_exc(e)), level=ERROR)
4601+
4602+
4603+class NoReadWriteForOther(BaseFileAudit):
4604+ """Ensures that the files found under the base path are readable or
4605+ writable by anyone other than the owner or the group.
4606+ """
4607+ def __init__(self, paths):
4608+ super(NoReadWriteForOther, self).__init__(paths)
4609+
4610+ def is_compliant(self, path):
4611+ try:
4612+ cmd = ['find', path, '-perm', '-o+r', '-type', 'f', '-o',
4613+ '-perm', '-o+w', '-type', 'f']
4614+ output = check_output(cmd).strip()
4615+
4616+ # The find above here will find any files which have read or
4617+ # write permissions for other, meaning there is too broad of access
4618+ # to read/write the file. As such, the path is compliant if there's
4619+ # no output.
4620+ if output:
4621+ return False
4622+
4623+ return True
4624+ except CalledProcessError as e:
4625+ log('Error occurred while finding files which are readable or '
4626+ 'writable to the world in %s. '
4627+ 'Command output is: %s.' % (path, e.output), level=ERROR)
4628+
4629+ def comply(self, path):
4630+ try:
4631+ check_output(['chmod', '-R', 'o-rw', path])
4632+ except CalledProcessError as e:
4633+ log('Error occurred attempting to change modes of files under '
4634+ 'path %s. Output of command is: %s' % (path, e.output))
4635+
4636+
4637+class NoSUIDSGIDAudit(BaseFileAudit):
4638+ """Audits that specified files do not have SUID/SGID bits set."""
4639+ def __init__(self, paths, *args, **kwargs):
4640+ super(NoSUIDSGIDAudit, self).__init__(paths=paths, *args, **kwargs)
4641+
4642+ def is_compliant(self, path):
4643+ stat = self._get_stat(path)
4644+ if (stat.st_mode & (S_ISGID | S_ISUID)) != 0:
4645+ return False
4646+
4647+ return True
4648+
4649+ def comply(self, path):
4650+ try:
4651+ log('Removing suid/sgid from %s.' % path, level=DEBUG)
4652+ check_output(['chmod', '-s', path])
4653+ except CalledProcessError as e:
4654+ log('Error occurred removing suid/sgid from %s.'
4655+ 'Error information is: command %s failed with returncode '
4656+ '%d and output %s.\n%s' % (path, e.cmd, e.returncode, e.output,
4657+ format_exc(e)), level=ERROR)
4658+
4659+
4660+class TemplatedFile(BaseFileAudit):
4661+ """The TemplatedFileAudit audits the contents of a templated file.
4662+
4663+ This audit renders a file from a template, sets the appropriate file
4664+ permissions, then generates a hashsum with which to check the content
4665+ changed.
4666+ """
4667+ def __init__(self, path, context, template_dir, mode, user='root',
4668+ group='root', service_actions=None, **kwargs):
4669+ self.context = context
4670+ self.user = user
4671+ self.group = group
4672+ self.mode = mode
4673+ self.template_dir = template_dir
4674+ self.service_actions = service_actions
4675+ super(TemplatedFile, self).__init__(paths=path, always_comply=True,
4676+ **kwargs)
4677+
4678+ def is_compliant(self, path):
4679+ """Determines if the templated file is compliant.
4680+
4681+ A templated file is only compliant if it has not changed (as
4682+ determined by its sha256 hashsum) AND its file permissions are set
4683+ appropriately.
4684+
4685+ :param path: the path to check compliance.
4686+ """
4687+ same_templates = self.templates_match(path)
4688+ same_content = self.contents_match(path)
4689+ same_permissions = self.permissions_match(path)
4690+
4691+ if same_content and same_permissions and same_templates:
4692+ return True
4693+
4694+ return False
4695+
4696+ def run_service_actions(self):
4697+ """Run any actions on services requested."""
4698+ if not self.service_actions:
4699+ return
4700+
4701+ for svc_action in self.service_actions:
4702+ name = svc_action['service']
4703+ actions = svc_action['actions']
4704+ log("Running service '%s' actions '%s'" % (name, actions),
4705+ level=DEBUG)
4706+ for action in actions:
4707+ cmd = ['service', name, action]
4708+ try:
4709+ check_call(cmd)
4710+ except CalledProcessError as exc:
4711+ log("Service name='%s' action='%s' failed - %s" %
4712+ (name, action, exc), level=WARNING)
4713+
4714+ def comply(self, path):
4715+ """Ensures the contents and the permissions of the file.
4716+
4717+ :param path: the path to correct
4718+ """
4719+ dirname = os.path.dirname(path)
4720+ if not os.path.exists(dirname):
4721+ os.makedirs(dirname)
4722+
4723+ self.pre_write()
4724+ render_and_write(self.template_dir, path, self.context())
4725+ utils.ensure_permissions(path, self.user, self.group, self.mode)
4726+ self.run_service_actions()
4727+ self.save_checksum(path)
4728+ self.post_write()
4729+
4730+ def pre_write(self):
4731+ """Invoked prior to writing the template."""
4732+ pass
4733+
4734+ def post_write(self):
4735+ """Invoked after writing the template."""
4736+ pass
4737+
4738+ def templates_match(self, path):
4739+ """Determines if the template files are the same.
4740+
4741+ The template file equality is determined by the hashsum of the
4742+ template files themselves. If there is no hashsum, then the content
4743+ cannot be sure to be the same so treat it as if they changed.
4744+ Otherwise, return whether or not the hashsums are the same.
4745+
4746+ :param path: the path to check
4747+ :returns: boolean
4748+ """
4749+ template_path = get_template_path(self.template_dir, path)
4750+ key = 'hardening:template:%s' % template_path
4751+ template_checksum = file_hash(template_path)
4752+ kv = unitdata.kv()
4753+ stored_tmplt_checksum = kv.get(key)
4754+ if not stored_tmplt_checksum:
4755+ kv.set(key, template_checksum)
4756+ kv.flush()
4757+ log('Saved template checksum for %s.' % template_path,
4758+ level=DEBUG)
4759+ # Since we don't have a template checksum, then assume it doesn't
4760+ # match and return that the template is different.
4761+ return False
4762+ elif stored_tmplt_checksum != template_checksum:
4763+ kv.set(key, template_checksum)
4764+ kv.flush()
4765+ log('Updated template checksum for %s.' % template_path,
4766+ level=DEBUG)
4767+ return False
4768+
4769+ # Here the template hasn't changed based upon the calculated
4770+ # checksum of the template and what was previously stored.
4771+ return True
4772+
4773+ def contents_match(self, path):
4774+ """Determines if the file content is the same.
4775+
4776+ This is determined by comparing hashsum of the file contents and
4777+ the saved hashsum. If there is no hashsum, then the content cannot
4778+ be sure to be the same so treat them as if they are not the same.
4779+ Otherwise, return True if the hashsums are the same, False if they
4780+ are not the same.
4781+
4782+ :param path: the file to check.
4783+ """
4784+ checksum = file_hash(path)
4785+
4786+ kv = unitdata.kv()
4787+ stored_checksum = kv.get('hardening:%s' % path)
4788+ if not stored_checksum:
4789+ # If the checksum hasn't been generated, return False to ensure
4790+ # the file is written and the checksum stored.
4791+ log('Checksum for %s has not been calculated.' % path, level=DEBUG)
4792+ return False
4793+ elif stored_checksum != checksum:
4794+ log('Checksum mismatch for %s.' % path, level=DEBUG)
4795+ return False
4796+
4797+ return True
4798+
4799+ def permissions_match(self, path):
4800+ """Determines if the file owner and permissions match.
4801+
4802+ :param path: the path to check.
4803+ """
4804+ audit = FilePermissionAudit(path, self.user, self.group, self.mode)
4805+ return audit.is_compliant(path)
4806+
4807+ def save_checksum(self, path):
4808+ """Calculates and saves the checksum for the path specified.
4809+
4810+ :param path: the path of the file to save the checksum.
4811+ """
4812+ checksum = file_hash(path)
4813+ kv = unitdata.kv()
4814+ kv.set('hardening:%s' % path, checksum)
4815+ kv.flush()
4816+
4817+
4818+class DeletedFile(BaseFileAudit):
4819+ """Audit to ensure that a file is deleted."""
4820+ def __init__(self, paths):
4821+ super(DeletedFile, self).__init__(paths)
4822+
4823+ def is_compliant(self, path):
4824+ return not os.path.exists(path)
4825+
4826+ def comply(self, path):
4827+ os.remove(path)
4828+
4829+
4830+class FileContentAudit(BaseFileAudit):
4831+ """Audit the contents of a file."""
4832+ def __init__(self, paths, cases, **kwargs):
4833+ # Cases we expect to pass
4834+ self.pass_cases = cases.get('pass', [])
4835+ # Cases we expect to fail
4836+ self.fail_cases = cases.get('fail', [])
4837+ super(FileContentAudit, self).__init__(paths, **kwargs)
4838+
4839+ def is_compliant(self, path):
4840+ """
4841+ Given a set of content matching cases, check that all cases match
4842+ as expected with the contents of the file. Cases can be expected to
4843+ pass of fail.
4844+
4845+ :param path: Path of file to check.
4846+ :returns: Boolean value representing whether or not all cases are
4847+ found to be compliant.
4848+ """
4849+ log("Auditing contents of file '%s'" % (path), level=DEBUG)
4850+ with open(path, 'r') as fd:
4851+ contents = fd.read()
4852+
4853+ matches = 0
4854+ for pattern in self.pass_cases:
4855+ key = re.compile(pattern, flags=re.MULTILINE)
4856+ results = re.search(key, contents)
4857+ if results:
4858+ matches += 1
4859+ else:
4860+ log("Pattern '%s' was expected to pass but instead it failed"
4861+ % (pattern), level=WARNING)
4862+
4863+ for pattern in self.fail_cases:
4864+ key = re.compile(pattern, flags=re.MULTILINE)
4865+ results = re.search(key, contents)
4866+ if not results:
4867+ matches += 1
4868+ else:
4869+ log("Pattern '%s' was expected to fail but instead it passed"
4870+ % (pattern), level=WARNING)
4871+
4872+ total = len(self.pass_cases) + len(self.fail_cases)
4873+ log("Checked %s cases and %s passed" % (total, matches), level=DEBUG)
4874+ return matches == total
4875+
4876+ def comply(self, *args, **kwargs):
4877+ """NOOP since we just issue warnings. This is to avoid the
4878+ NotImplememtedError.
4879+ """
4880+ log("Not applying any compliance criteria, only checks.", level=INFO)
4881
4882=== added directory 'hooks/charmhelpers/contrib/hardening/defaults'
4883=== added file 'hooks/charmhelpers/contrib/hardening/defaults/apache.yaml'
4884--- hooks/charmhelpers/contrib/hardening/defaults/apache.yaml 1970-01-01 00:00:00 +0000
4885+++ hooks/charmhelpers/contrib/hardening/defaults/apache.yaml 2016-03-21 06:13:21 +0000
4886@@ -0,0 +1,13 @@
4887+# NOTE: this file contains the default configuration for the 'apache' hardening
4888+# code. If you want to override any settings you must add them to a file
4889+# called hardening.yaml in the root directory of your charm using the
4890+# name 'apache' as the root key followed by any of the following with new
4891+# values.
4892+
4893+common:
4894+ apache_dir: '/etc/apache2'
4895+
4896+hardening:
4897+ traceenable: 'off'
4898+ allowed_http_methods: "GET POST"
4899+ modules_to_disable: [ cgi, cgid ]
4900\ No newline at end of file
4901
4902=== added file 'hooks/charmhelpers/contrib/hardening/defaults/apache.yaml.schema'
4903--- hooks/charmhelpers/contrib/hardening/defaults/apache.yaml.schema 1970-01-01 00:00:00 +0000
4904+++ hooks/charmhelpers/contrib/hardening/defaults/apache.yaml.schema 2016-03-21 06:13:21 +0000
4905@@ -0,0 +1,9 @@
4906+# NOTE: this schema must contain all valid keys from it's associated defaults
4907+# file. It is used to validate user-provided overrides.
4908+common:
4909+ apache_dir:
4910+ traceenable:
4911+
4912+hardening:
4913+ allowed_http_methods:
4914+ modules_to_disable:
4915
4916=== added file 'hooks/charmhelpers/contrib/hardening/defaults/mysql.yaml'
4917--- hooks/charmhelpers/contrib/hardening/defaults/mysql.yaml 1970-01-01 00:00:00 +0000
4918+++ hooks/charmhelpers/contrib/hardening/defaults/mysql.yaml 2016-03-21 06:13:21 +0000
4919@@ -0,0 +1,38 @@
4920+# NOTE: this file contains the default configuration for the 'mysql' hardening
4921+# code. If you want to override any settings you must add them to a file
4922+# called hardening.yaml in the root directory of your charm using the
4923+# name 'mysql' as the root key followed by any of the following with new
4924+# values.
4925+
4926+security:
4927+ # @see http://www.symantec.com/connect/articles/securing-mysql-step-step
4928+ # @see http://dev.mysql.com/doc/refman/5.7/en/server-options.html#option_mysqld_chroot
4929+ chroot: None
4930+
4931+ # @see http://dev.mysql.com/doc/refman/5.7/en/server-options.html#option_mysqld_safe-user-create
4932+ safe-user-create: 1
4933+
4934+ # @see http://dev.mysql.com/doc/refman/5.7/en/server-options.html#option_mysqld_secure-auth
4935+ secure-auth: 1
4936+
4937+ # @see http://dev.mysql.com/doc/refman/5.7/en/server-options.html#option_mysqld_symbolic-links
4938+ skip-symbolic-links: 1
4939+
4940+ # @see http://dev.mysql.com/doc/refman/5.7/en/server-options.html#option_mysqld_skip-show-database
4941+ skip-show-database: True
4942+
4943+ # @see http://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html#sysvar_local_infile
4944+ local-infile: 0
4945+
4946+ # @see https://dev.mysql.com/doc/refman/5.7/en/server-options.html#option_mysqld_allow-suspicious-udfs
4947+ allow-suspicious-udfs: 0
4948+
4949+ # @see https://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html#sysvar_automatic_sp_privileges
4950+ automatic-sp-privileges: 0
4951+
4952+ # @see https://dev.mysql.com/doc/refman/5.7/en/server-options.html#option_mysqld_secure-file-priv
4953+ secure-file-priv: /tmp
4954+
4955+hardening:
4956+ mysql-conf: /etc/mysql/my.cnf
4957+ hardening-conf: /etc/mysql/conf.d/hardening.cnf
4958
4959=== added file 'hooks/charmhelpers/contrib/hardening/defaults/mysql.yaml.schema'
4960--- hooks/charmhelpers/contrib/hardening/defaults/mysql.yaml.schema 1970-01-01 00:00:00 +0000
4961+++ hooks/charmhelpers/contrib/hardening/defaults/mysql.yaml.schema 2016-03-21 06:13:21 +0000
4962@@ -0,0 +1,15 @@
4963+# NOTE: this schema must contain all valid keys from it's associated defaults
4964+# file. It is used to validate user-provided overrides.
4965+security:
4966+ chroot:
4967+ safe-user-create:
4968+ secure-auth:
4969+ skip-symbolic-links:
4970+ skip-show-database:
4971+ local-infile:
4972+ allow-suspicious-udfs:
4973+ automatic-sp-privileges:
4974+ secure-file-priv:
4975+hardening:
4976+ mysql-conf:
4977+ hardening-conf:
4978\ No newline at end of file
4979
4980=== added file 'hooks/charmhelpers/contrib/hardening/defaults/os.yaml'
4981--- hooks/charmhelpers/contrib/hardening/defaults/os.yaml 1970-01-01 00:00:00 +0000
4982+++ hooks/charmhelpers/contrib/hardening/defaults/os.yaml 2016-03-21 06:13:21 +0000
4983@@ -0,0 +1,67 @@
4984+# NOTE: this file contains the default configuration for the 'os' hardening
4985+# code. If you want to override any settings you must add them to a file
4986+# called hardening.yaml in the root directory of your charm using the
4987+# name 'os' as the root key followed by any of the following with new
4988+# values.
4989+
4990+general:
4991+ desktop_enable: False # (type:boolean)
4992+
4993+environment:
4994+ extra_user_paths: []
4995+ umask: 027
4996+ root_path: /
4997+
4998+auth:
4999+ pw_max_age: 60
5000+ # discourage password cycling
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches

to all changes: