Merge ~canonical-bootstack/charm-policy-routing:change-to-reactive-add-static-routing into ~canonical-bootstack/charm-policy-routing:master

Proposed by Diko Parvanov
Status: Rejected
Rejected by: Alvaro Uria
Proposed branch: ~canonical-bootstack/charm-policy-routing:change-to-reactive-add-static-routing
Merge into: ~canonical-bootstack/charm-policy-routing:master
Diff against target: 30099 lines (+1007/-56)
25 files modified
Makefile (+49/-0)
README.md (+44/-2)
actions.yaml (+1/-0)
config.yaml (+21/-6)
dev/null (+0/-36)
example_config.json (+10/-0)
icon.svg (+279/-0)
interfaces/.empty (+1/-0)
layer.yaml (+2/-0)
layers/.empty (+1/-0)
lib/lib_policy_routing.py (+83/-0)
lib/lib_static_routing.py (+100/-0)
metadata.yaml (+14/-7)
reactive/handlers.py (+78/-0)
requirements.txt (+1/-0)
revision (+1/-0)
templates/service.j2 (+1/-0)
tests/functional/conftest.py (+71/-0)
tests/functional/juju_tools.py (+69/-0)
tests/functional/requirements.txt (+6/-0)
tests/functional/test_routing.py (+57/-0)
tests/unit/conftest.py (+70/-0)
tests/unit/requirements.txt (+1/-0)
tests/unit/test_routing.py (+9/-0)
tox.ini (+38/-5)
Reviewer Review Type Date Requested Status
Alvaro Uria (community) Disapprove
Drew Freiberger (community) Needs Fixing
Review via email: mp+371327@code.launchpad.net
To post a comment you must log in.
d5a2ee0... by Diko Parvanov

Fixed bug in functional testing

Revision history for this message
Drew Freiberger (afreiberger) wrote :

A few comments inline.

As the rewrite to reactive is basically a complete charm re-build, can we have this merge against an empty feature/version branch rather than against master for a review w/out all the charmhelpers removes/etc?

I think we need a pointer in this MR to either a bug or a spec you're writing against to discuss more in depth the requirements vs. the code.

I also don't see the reactive/handlers.py file because the MR cut it short...see suggestion regarding an MR against a blank feature branch.

review: Needs Fixing
Revision history for this message
David O Neill (dmzoneill) wrote :

Please see

https://code.launchpad.net/~canonical-bootstack/charm-policy-routing/+git/charm-policy-routing/+ref/feature/reactive-upgrade

git checkout --orphan feature/reactive-upgrade
git rm --cached -r .
# copied in latest code base
git add -A
git commit -S -a
git push git+ssh://<email address hidden>/~canonical-bootstack/charm-policy-routing feature/reactive-upgrade

Revision history for this message
David O Neill (dmzoneill) wrote :

Review:

From my review/understanding of this code, it seems that "lib_policy_routing.py" [1] is a forward port of the original code with a single "ExecStart" implementing routes.

Dikos lib_static_routing [2], supersedes this code providing support for multiple routes.

[1] also contains hard code magic numbers for table and priority, which serve very little purpose.

I would get rid of [1] altogether and provide support for table and priority in [2] dynamically.

Providing 2 different implementations in the same code base while is good for backwards capability, could provide unneeded confusion and increase our maintainability scope.

Further more [3] is solved by Dikos solution.

[1] https://git.launchpad.net/~canonical-bootstack/charm-policy-routing/tree/templates/service.j2?h=feature/reactive-upgrade
[2] https://git.launchpad.net/~canonical-bootstack/charm-policy-routing/tree/lib/lib_static_routing.py?h=feature/reactive-upgrade
[3] https://bugs.launchpad.net/charm-policy-routing/+bug/1792093

Revision history for this message
Alvaro Uria (aluria) wrote :

I'm going to reject this MP because the charm has been deprecated. Both committer and reviewers have worked on a new MP in charm-advanced-routing, though.

Thank you.

review: Disapprove

Unmerged commits

d5a2ee0... by Diko Parvanov

Fixed bug in functional testing

d7c1ed4... by Diko Parvanov

Upgraded to reactive and added static routing

 - Upgraded charm to reactive
 - Cleaned up old libraries
 - Code for polcy-routing re-written and separated in library
 - Added library for static routing
 - Modified config options and metadata (added a resource attachment)
 - Added multiple sanity checks and verifications + better juju logging
 - Added functionality testing with libjuju

Preview Diff

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

Subscribers

People subscribed via source and target branches

to all changes: