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
diff --git a/Makefile b/Makefile
0new file mode 1006440new file mode 100644
index 0000000..a8c1fd7
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,49 @@
1help:
2 @echo "This project supports the following targets"
3 @echo ""
4 @echo " make help - show this text"
5 @echo " make submodules - make sure that the submodules are up-to-date"
6 @echo " make lint - run flake8"
7 @echo " make test - run the unittests and lint"
8 @echo " make unittest - run the tests defined in the unittest subdirectory"
9 @echo " make functional - run the tests defined in the functional subdirectory"
10 @echo " make release - build the charm"
11 @echo " make clean - remove unneeded files"
12 @echo ""
13
14submodules:
15 @echo "Cloning submodules"
16 @git submodule update --init --recursive
17
18lint:
19 @echo "Running flake8"
20 @tox -e lint
21
22test: unittest functional lint
23
24unittest:
25 @tox -e unit
26
27functional: build
28 @PYTEST_KEEP_MODEL=$(PYTEST_KEEP_MODEL) \
29 PYTEST_CLOUD_NAME=$(PYTEST_CLOUD_NAME) \
30 PYTEST_CLOUD_REGION=$(PYTEST_CLOUD_REGION) \
31 tox -e functional
32
33build:
34 @echo "Building charm to base directory $(JUJU_REPOSITORY)"
35 @-git describe --tags > ./repo-info
36 @LAYER_PATH=./layers INTERFACE_PATH=./interfaces TERM=linux \
37 JUJU_REPOSITORY=$(JUJU_REPOSITORY) charm build . --force
38
39release: clean build
40 @echo "Charm is built at $(JUJU_REPOSITORY)/builds"
41
42clean:
43 @echo "Cleaning files"
44 @if [ -d .tox ] ; then rm -r .tox ; fi
45 @if [ -d .pytest_cache ] ; then rm -r .pytest_cache ; fi
46 @find . -iname __pycache__ -exec rm -r {} +
47
48# The targets below don't depend on a file
49.PHONY: lint test unittest functional build release clean help submodules
diff --git a/README.md b/README.md
index 37b5cb0..bf9ccc5 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,50 @@
1# Overview1# Overview
22
3This charm allows for the configuration of simple policy routing rules on the deployed host3This subordinate charm allows for the configuration of simple policy routing rules on the deployed host
4and adding static routing to configured services via a JSON file.
45
5# Usage6# Usage
67
7 juju deploy cs:~canonical-bootstack/policy-routing8
9# Build
10```
11cd charm-policy-routing
12charm build
13```
14
15# Usage
16Add to an existing application using juju-info relation.
17
18Example:
19```
20juju deploy cs:~canonical-bootstack/policy-routing
21juju add-relation ubuntu policy-routing
22```
23
24# Configuration
25The user can configure the following parameters:
26* enable-policy-routing: Enable policy routing, this requires ```policy-routing-cidr``` and ```policy-routing-gateway``` to be set.
27* 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```
28
29A example_config.json file is provided with the codebase.
30
31# Testing
32To run lint tests:
33```bash
34tox -e lint
35
36```
37To run unit tests:
38```bash
39tox -e unit
40```
41Functional tests have been developed using python-libjuju, deploying a simple ubuntu charm and adding the charm as a subordinate.
42
43To run tests using python-libjuju:
44```bash
45tox -e functional
46```
47
48# Contact Information
49Diko Parvanov <diko.parvanov@canonical.com>
850
diff --git a/actions.yaml b/actions.yaml
9new file mode 10064451new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/actions.yaml
@@ -0,0 +1 @@
1
diff --git a/hooks/charmhelpers/core/host_factory/__init__.py b/actions/.empty
0similarity index 100%2similarity index 100%
1rename from hooks/charmhelpers/core/host_factory/__init__.py3rename from hooks/charmhelpers/core/host_factory/__init__.py
2rename to actions/.empty4rename to actions/.empty
diff --git a/config.yaml b/config.yaml
index 2fffd8e..ef67514 100644
--- a/config.yaml
+++ b/config.yaml
@@ -1,13 +1,28 @@
1options:1options:
2 cidr:2 enable-policy-routing:
3 type: boolean
4 default: False
5 description: |
6 Wheter to use and setup the policy routing.
7 policy-routing-cidr:
3 type: string8 type: string
4 default:9 default: ""
5 description: |10 description: |
6 CIDR of the network interface to setup a policy routing.11 CIDR of the network interface to setup a policy routing.
7 e.g. 192.168.0.0/2412 e.g. 192.168.0.0/24
8 gateway:13 policy-routing-gateway:
9 type: string14 type: string
10 default:15 default: ""
16 description: |
17 The gateway to be used from the network interface for policy routing
18 specified with the CIDR. e.g. 192.168.0.254
19 enable-static-routing:
20 type: boolean
21 default: False
11 description: |22 description: |
12 The gateway to be used from the network interface specified with23 Wheter to use the custom routing configuration provided by the charm.
13 the CIDR. e.g. 192.168.0.25424 A JSON file must be provided to the charm as a resoruce before this
25 option set to True with
26 `juju attach-resource policy-routing routing_information=routing.json`
27 The file mst have the following format and have a valid JSON syntax.
28 Example [{'net': '1.2.3.4/20', 'gateway': '1.2.3.4'}, {...}]
diff --git a/dev-requirements.txt b/dev-requirements.txt
14deleted file mode 10064429deleted file mode 100644
index ee490ce..0000000
--- a/dev-requirements.txt
+++ /dev/null
@@ -1,4 +0,0 @@
1charmhelpers
2# zaza
3git+https://github.com/CanonicalBootStack/zaza.git#egg=zaza
4
diff --git a/example_config.json b/example_config.json
5new file mode 1006440new file mode 100644
index 0000000..7d70ea0
--- /dev/null
+++ b/example_config.json
@@ -0,0 +1,10 @@
1[
2 {
3 "net": "192.168.0.0/24",
4 "gateway": "10.5.0.6"
5 },
6 {
7 "net": "10.205.18.28/32",
8 "gateway": "213.173.194.1"
9 }
10]
diff --git a/hooks/charmhelpers/__init__.py b/hooks/charmhelpers/__init__.py
0deleted file mode 10064411deleted file mode 100644
index e7aa471..0000000
--- a/hooks/charmhelpers/__init__.py
+++ /dev/null
@@ -1,97 +0,0 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15# Bootstrap charm-helpers, installing its dependencies if necessary using
16# only standard libraries.
17from __future__ import print_function
18from __future__ import absolute_import
19
20import functools
21import inspect
22import subprocess
23import sys
24
25try:
26 import six # flake8: noqa
27except ImportError:
28 if sys.version_info.major == 2:
29 subprocess.check_call(['apt-get', 'install', '-y', 'python-six'])
30 else:
31 subprocess.check_call(['apt-get', 'install', '-y', 'python3-six'])
32 import six # flake8: noqa
33
34try:
35 import yaml # flake8: noqa
36except ImportError:
37 if sys.version_info.major == 2:
38 subprocess.check_call(['apt-get', 'install', '-y', 'python-yaml'])
39 else:
40 subprocess.check_call(['apt-get', 'install', '-y', 'python3-yaml'])
41 import yaml # flake8: noqa
42
43
44# Holds a list of mapping of mangled function names that have been deprecated
45# using the @deprecate decorator below. This is so that the warning is only
46# printed once for each usage of the function.
47__deprecated_functions = {}
48
49
50def deprecate(warning, date=None, log=None):
51 """Add a deprecation warning the first time the function is used.
52 The date, which is a string in semi-ISO8660 format indicate the year-month
53 that the function is officially going to be removed.
54
55 usage:
56
57 @deprecate('use core/fetch/add_source() instead', '2017-04')
58 def contributed_add_source_thing(...):
59 ...
60
61 And it then prints to the log ONCE that the function is deprecated.
62 The reason for passing the logging function (log) is so that hookenv.log
63 can be used for a charm if needed.
64
65 :param warning: String to indicat where it has moved ot.
66 :param date: optional sting, in YYYY-MM format to indicate when the
67 function will definitely (probably) be removed.
68 :param log: The log function to call to log. If not, logs to stdout
69 """
70 def wrap(f):
71
72 @functools.wraps(f)
73 def wrapped_f(*args, **kwargs):
74 try:
75 module = inspect.getmodule(f)
76 file = inspect.getsourcefile(f)
77 lines = inspect.getsourcelines(f)
78 f_name = "{}-{}-{}..{}-{}".format(
79 module.__name__, file, lines[0], lines[-1], f.__name__)
80 except (IOError, TypeError):
81 # assume it was local, so just use the name of the function
82 f_name = f.__name__
83 if f_name not in __deprecated_functions:
84 __deprecated_functions[f_name] = True
85 s = "DEPRECATION WARNING: Function {} is being removed".format(
86 f.__name__)
87 if date:
88 s = "{} on/around {}".format(s, date)
89 if warning:
90 s = "{} : {}".format(s, warning)
91 if log:
92 log(s)
93 else:
94 print(s)
95 return f(*args, **kwargs)
96 return wrapped_f
97 return wrap
diff --git a/hooks/charmhelpers/__pycache__/__init__.cpython-36.pyc b/hooks/charmhelpers/__pycache__/__init__.cpython-36.pyc
98deleted file mode 1006440deleted file mode 100644
index 3f89be3..0000000
99Binary files a/hooks/charmhelpers/__pycache__/__init__.cpython-36.pyc and /dev/null differ1Binary files a/hooks/charmhelpers/__pycache__/__init__.cpython-36.pyc and /dev/null differ
diff --git a/hooks/charmhelpers/__pycache__/osplatform.cpython-36.pyc b/hooks/charmhelpers/__pycache__/osplatform.cpython-36.pyc
100deleted file mode 1006442deleted file mode 100644
index 3dedd73..0000000
101Binary files a/hooks/charmhelpers/__pycache__/osplatform.cpython-36.pyc and /dev/null differ3Binary files a/hooks/charmhelpers/__pycache__/osplatform.cpython-36.pyc and /dev/null differ
diff --git a/hooks/charmhelpers/cli/README.rst b/hooks/charmhelpers/cli/README.rst
102deleted file mode 1006444deleted file mode 100644
index f7901c0..0000000
--- a/hooks/charmhelpers/cli/README.rst
+++ /dev/null
@@ -1,57 +0,0 @@
1==========
2Commandant
3==========
4
5-----------------------------------------------------
6Automatic command-line interfaces to Python functions
7-----------------------------------------------------
8
9One 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.
10
11Command 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.
12
13Goals
14=====
15
16* Single decorator to expose a function as a command.
17 * now two decorators - one "automatic" and one that allows authors to manipulate the arguments for fine-grained control.(MW)
18* Automatic analysis of function signature through ``inspect.getargspec()``
19* Command argument parser built automatically with ``argparse``
20* Interactive interpreter loop object made with ``Cmd``
21* Options to output structured return value data via ``pprint``, ``yaml`` or ``json`` dumps.
22
23Other Important Features that need writing
24------------------------------------------
25
26* Help and Usage documentation can be automatically generated, but it will be important to let users override this behaviour
27* 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.
28 - Filename arguments are important, as good practice is for functions to accept file objects as parameters.
29 - choices arguments help to limit bad input before the function is called
30* Some automatic behaviour could make for better defaults, once the user can override them.
31 - We could automatically detect arguments that default to False or True, and automatically support --no-foo for foo=True.
32 - We could automatically support hyphens as alternates for underscores
33 - Arguments defaulting to sequence types could support the ``append`` action.
34
35
36-----------------------------------------------------
37Implementing subcommands
38-----------------------------------------------------
39
40(WIP)
41
42So 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.
43
44Some examples::
45
46 from charmhelpers.cli import CommandLine
47 from charmhelpers.payload import execd
48 from charmhelpers.foo import bar
49
50 cli = CommandLine()
51
52 cli.subcommand(execd.execd_run)
53
54 @cli.subcommand_builder("bar", help="Bar baz qux")
55 def barcmd_builder(subparser):
56 subparser.add_argument('argument1', help="yackety")
57 return bar
diff --git a/hooks/charmhelpers/cli/__init__.py b/hooks/charmhelpers/cli/__init__.py
58deleted file mode 1006440deleted file mode 100644
index 389b490..0000000
--- a/hooks/charmhelpers/cli/__init__.py
+++ /dev/null
@@ -1,189 +0,0 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15import inspect
16import argparse
17import sys
18
19from six.moves import zip
20
21import charmhelpers.core.unitdata
22
23
24class OutputFormatter(object):
25 def __init__(self, outfile=sys.stdout):
26 self.formats = (
27 "raw",
28 "json",
29 "py",
30 "yaml",
31 "csv",
32 "tab",
33 )
34 self.outfile = outfile
35
36 def add_arguments(self, argument_parser):
37 formatgroup = argument_parser.add_mutually_exclusive_group()
38 choices = self.supported_formats
39 formatgroup.add_argument("--format", metavar='FMT',
40 help="Select output format for returned data, "
41 "where FMT is one of: {}".format(choices),
42 choices=choices, default='raw')
43 for fmt in self.formats:
44 fmtfunc = getattr(self, fmt)
45 formatgroup.add_argument("-{}".format(fmt[0]),
46 "--{}".format(fmt), action='store_const',
47 const=fmt, dest='format',
48 help=fmtfunc.__doc__)
49
50 @property
51 def supported_formats(self):
52 return self.formats
53
54 def raw(self, output):
55 """Output data as raw string (default)"""
56 if isinstance(output, (list, tuple)):
57 output = '\n'.join(map(str, output))
58 self.outfile.write(str(output))
59
60 def py(self, output):
61 """Output data as a nicely-formatted python data structure"""
62 import pprint
63 pprint.pprint(output, stream=self.outfile)
64
65 def json(self, output):
66 """Output data in JSON format"""
67 import json
68 json.dump(output, self.outfile)
69
70 def yaml(self, output):
71 """Output data in YAML format"""
72 import yaml
73 yaml.safe_dump(output, self.outfile)
74
75 def csv(self, output):
76 """Output data as excel-compatible CSV"""
77 import csv
78 csvwriter = csv.writer(self.outfile)
79 csvwriter.writerows(output)
80
81 def tab(self, output):
82 """Output data in excel-compatible tab-delimited format"""
83 import csv
84 csvwriter = csv.writer(self.outfile, dialect=csv.excel_tab)
85 csvwriter.writerows(output)
86
87 def format_output(self, output, fmt='raw'):
88 fmtfunc = getattr(self, fmt)
89 fmtfunc(output)
90
91
92class CommandLine(object):
93 argument_parser = None
94 subparsers = None
95 formatter = None
96 exit_code = 0
97
98 def __init__(self):
99 if not self.argument_parser:
100 self.argument_parser = argparse.ArgumentParser(description='Perform common charm tasks')
101 if not self.formatter:
102 self.formatter = OutputFormatter()
103 self.formatter.add_arguments(self.argument_parser)
104 if not self.subparsers:
105 self.subparsers = self.argument_parser.add_subparsers(help='Commands')
106
107 def subcommand(self, command_name=None):
108 """
109 Decorate a function as a subcommand. Use its arguments as the
110 command-line arguments"""
111 def wrapper(decorated):
112 cmd_name = command_name or decorated.__name__
113 subparser = self.subparsers.add_parser(cmd_name,
114 description=decorated.__doc__)
115 for args, kwargs in describe_arguments(decorated):
116 subparser.add_argument(*args, **kwargs)
117 subparser.set_defaults(func=decorated)
118 return decorated
119 return wrapper
120
121 def test_command(self, decorated):
122 """
123 Subcommand is a boolean test function, so bool return values should be
124 converted to a 0/1 exit code.
125 """
126 decorated._cli_test_command = True
127 return decorated
128
129 def no_output(self, decorated):
130 """
131 Subcommand is not expected to return a value, so don't print a spurious None.
132 """
133 decorated._cli_no_output = True
134 return decorated
135
136 def subcommand_builder(self, command_name, description=None):
137 """
138 Decorate a function that builds a subcommand. Builders should accept a
139 single argument (the subparser instance) and return the function to be
140 run as the command."""
141 def wrapper(decorated):
142 subparser = self.subparsers.add_parser(command_name)
143 func = decorated(subparser)
144 subparser.set_defaults(func=func)
145 subparser.description = description or func.__doc__
146 return wrapper
147
148 def run(self):
149 "Run cli, processing arguments and executing subcommands."
150 arguments = self.argument_parser.parse_args()
151 argspec = inspect.getargspec(arguments.func)
152 vargs = []
153 for arg in argspec.args:
154 vargs.append(getattr(arguments, arg))
155 if argspec.varargs:
156 vargs.extend(getattr(arguments, argspec.varargs))
157 output = arguments.func(*vargs)
158 if getattr(arguments.func, '_cli_test_command', False):
159 self.exit_code = 0 if output else 1
160 output = ''
161 if getattr(arguments.func, '_cli_no_output', False):
162 output = ''
163 self.formatter.format_output(output, arguments.format)
164 if charmhelpers.core.unitdata._KV:
165 charmhelpers.core.unitdata._KV.flush()
166
167
168cmdline = CommandLine()
169
170
171def describe_arguments(func):
172 """
173 Analyze a function's signature and return a data structure suitable for
174 passing in as arguments to an argparse parser's add_argument() method."""
175
176 argspec = inspect.getargspec(func)
177 # we should probably raise an exception somewhere if func includes **kwargs
178 if argspec.defaults:
179 positional_args = argspec.args[:-len(argspec.defaults)]
180 keyword_names = argspec.args[-len(argspec.defaults):]
181 for arg, default in zip(keyword_names, argspec.defaults):
182 yield ('--{}'.format(arg),), {'default': default}
183 else:
184 positional_args = argspec.args
185
186 for arg in positional_args:
187 yield (arg,), {}
188 if argspec.varargs:
189 yield (argspec.varargs,), {'nargs': '*'}
diff --git a/hooks/charmhelpers/cli/benchmark.py b/hooks/charmhelpers/cli/benchmark.py
190deleted file mode 1006440deleted file mode 100644
index 303af14..0000000
--- a/hooks/charmhelpers/cli/benchmark.py
+++ /dev/null
@@ -1,34 +0,0 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15from . import cmdline
16from charmhelpers.contrib.benchmark import Benchmark
17
18
19@cmdline.subcommand(command_name='benchmark-start')
20def start():
21 Benchmark.start()
22
23
24@cmdline.subcommand(command_name='benchmark-finish')
25def finish():
26 Benchmark.finish()
27
28
29@cmdline.subcommand_builder('benchmark-composite', description="Set the benchmark composite score")
30def service(subparser):
31 subparser.add_argument("value", help="The composite score.")
32 subparser.add_argument("units", help="The units the composite score represents, i.e., 'reads/sec'.")
33 subparser.add_argument("direction", help="'asc' if a lower score is better, 'desc' if a higher score is better.")
34 return Benchmark.set_composite_score
diff --git a/hooks/charmhelpers/cli/commands.py b/hooks/charmhelpers/cli/commands.py
35deleted file mode 1006440deleted file mode 100644
index b931056..0000000
--- a/hooks/charmhelpers/cli/commands.py
+++ /dev/null
@@ -1,30 +0,0 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""
16This module loads sub-modules into the python runtime so they can be
17discovered via the inspect module. In order to prevent flake8 from (rightfully)
18telling us these are unused modules, throw a ' # noqa' at the end of each import
19so that the warning is suppressed.
20"""
21
22from . import CommandLine # noqa
23
24"""
25Import the sub-modules which have decorated subcommands to register with chlp.
26"""
27from . import host # noqa
28from . import benchmark # noqa
29from . import unitdata # noqa
30from . import hookenv # noqa
diff --git a/hooks/charmhelpers/cli/hookenv.py b/hooks/charmhelpers/cli/hookenv.py
31deleted file mode 1006440deleted file mode 100644
index bd72f44..0000000
--- a/hooks/charmhelpers/cli/hookenv.py
+++ /dev/null
@@ -1,21 +0,0 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15from . import cmdline
16from charmhelpers.core import hookenv
17
18
19cmdline.subcommand('relation-id')(hookenv.relation_id._wrapped)
20cmdline.subcommand('service-name')(hookenv.service_name)
21cmdline.subcommand('remote-service-name')(hookenv.remote_service_name._wrapped)
diff --git a/hooks/charmhelpers/cli/host.py b/hooks/charmhelpers/cli/host.py
22deleted file mode 1006440deleted file mode 100644
index 4039684..0000000
--- a/hooks/charmhelpers/cli/host.py
+++ /dev/null
@@ -1,29 +0,0 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15from . import cmdline
16from charmhelpers.core import host
17
18
19@cmdline.subcommand()
20def mounts():
21 "List mounts"
22 return host.mounts()
23
24
25@cmdline.subcommand_builder('service', description="Control system services")
26def service(subparser):
27 subparser.add_argument("action", help="The action to perform (start, stop, etc...)")
28 subparser.add_argument("service_name", help="Name of the service to control")
29 return host.service
diff --git a/hooks/charmhelpers/cli/unitdata.py b/hooks/charmhelpers/cli/unitdata.py
30deleted file mode 1006440deleted file mode 100644
index c572858..0000000
--- a/hooks/charmhelpers/cli/unitdata.py
+++ /dev/null
@@ -1,37 +0,0 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15from . import cmdline
16from charmhelpers.core import unitdata
17
18
19@cmdline.subcommand_builder('unitdata', description="Store and retrieve data")
20def unitdata_cmd(subparser):
21 nested = subparser.add_subparsers()
22 get_cmd = nested.add_parser('get', help='Retrieve data')
23 get_cmd.add_argument('key', help='Key to retrieve the value of')
24 get_cmd.set_defaults(action='get', value=None)
25 set_cmd = nested.add_parser('set', help='Store data')
26 set_cmd.add_argument('key', help='Key to set')
27 set_cmd.add_argument('value', help='Value to store')
28 set_cmd.set_defaults(action='set')
29
30 def _unitdata_cmd(action, key, value):
31 if action == 'get':
32 return unitdata.kv().get(key)
33 elif action == 'set':
34 unitdata.kv().set(key, value)
35 unitdata.kv().flush()
36 return ''
37 return _unitdata_cmd
diff --git a/hooks/charmhelpers/context.py b/hooks/charmhelpers/context.py
38deleted file mode 1006440deleted file mode 100644
index 0186474..0000000
--- a/hooks/charmhelpers/context.py
+++ /dev/null
@@ -1,205 +0,0 @@
1# Copyright 2015 Canonical Limited.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15'''
16A Pythonic API to interact with the charm hook environment.
17
18:author: Stuart Bishop <stuart.bishop@canonical.com>
19'''
20
21import six
22
23from charmhelpers.core import hookenv
24
25from collections import OrderedDict
26if six.PY3:
27 from collections import UserDict # pragma: nocover
28else:
29 from UserDict import IterableUserDict as UserDict # pragma: nocover
30
31
32class Relations(OrderedDict):
33 '''Mapping relation name -> relation id -> Relation.
34
35 >>> rels = Relations()
36 >>> rels['sprog']['sprog:12']['client/6']['widget']
37 'remote widget'
38 >>> rels['sprog']['sprog:12'].local['widget'] = 'local widget'
39 >>> rels['sprog']['sprog:12'].local['widget']
40 'local widget'
41 >>> rels.peer.local['widget']
42 'local widget on the peer relation'
43 '''
44 def __init__(self):
45 super(Relations, self).__init__()
46 for relname in sorted(hookenv.relation_types()):
47 self[relname] = OrderedDict()
48 relids = hookenv.relation_ids(relname)
49 relids.sort(key=lambda x: int(x.split(':', 1)[-1]))
50 for relid in relids:
51 self[relname][relid] = Relation(relid)
52
53 @property
54 def peer(self):
55 peer_relid = hookenv.peer_relation_id()
56 for rels in self.values():
57 if peer_relid in rels:
58 return rels[peer_relid]
59
60
61class Relation(OrderedDict):
62 '''Mapping of unit -> remote RelationInfo for a relation.
63
64 This is an OrderedDict mapping, ordered numerically by
65 by unit number.
66
67 Also provides access to the local RelationInfo, and peer RelationInfo
68 instances by the 'local' and 'peers' attributes.
69
70 >>> r = Relation('sprog:12')
71 >>> r.keys()
72 ['client/9', 'client/10'] # Ordered numerically
73 >>> r['client/10']['widget'] # A remote RelationInfo setting
74 'remote widget'
75 >>> r.local['widget'] # The local RelationInfo setting
76 'local widget'
77 '''
78 relid = None # The relation id.
79 relname = None # The relation name (also known as relation type).
80 service = None # The remote service name, if known.
81 local = None # The local end's RelationInfo.
82 peers = None # Map of peer -> RelationInfo. None if no peer relation.
83
84 def __init__(self, relid):
85 remote_units = hookenv.related_units(relid)
86 remote_units.sort(key=lambda u: int(u.split('/', 1)[-1]))
87 super(Relation, self).__init__((unit, RelationInfo(relid, unit))
88 for unit in remote_units)
89
90 self.relname = relid.split(':', 1)[0]
91 self.relid = relid
92 self.local = RelationInfo(relid, hookenv.local_unit())
93
94 for relinfo in self.values():
95 self.service = relinfo.service
96 break
97
98 # If we have peers, and they have joined both the provided peer
99 # relation and this relation, we can peek at their data too.
100 # This is useful for creating consensus without leadership.
101 peer_relid = hookenv.peer_relation_id()
102 if peer_relid and peer_relid != relid:
103 peers = hookenv.related_units(peer_relid)
104 if peers:
105 peers.sort(key=lambda u: int(u.split('/', 1)[-1]))
106 self.peers = OrderedDict((peer, RelationInfo(relid, peer))
107 for peer in peers)
108 else:
109 self.peers = OrderedDict()
110 else:
111 self.peers = None
112
113 def __str__(self):
114 return '{} ({})'.format(self.relid, self.service)
115
116
117class RelationInfo(UserDict):
118 '''The bag of data at an end of a relation.
119
120 Every unit participating in a relation has a single bag of
121 data associated with that relation. This is that bag.
122
123 The bag of data for the local unit may be updated. Remote data
124 is immutable and will remain static for the duration of the hook.
125
126 Changes made to the local units relation data only become visible
127 to other units after the hook completes successfully. If the hook
128 does not complete successfully, the changes are rolled back.
129
130 Unlike standard Python mappings, setting an item to None is the
131 same as deleting it.
132
133 >>> relinfo = RelationInfo('db:12') # Default is the local unit.
134 >>> relinfo['user'] = 'fred'
135 >>> relinfo['user']
136 'fred'
137 >>> relinfo['user'] = None
138 >>> 'fred' in relinfo
139 False
140
141 This class wraps hookenv.relation_get and hookenv.relation_set.
142 All caching is left up to these two methods to avoid synchronization
143 issues. Data is only loaded on demand.
144 '''
145 relid = None # The relation id.
146 relname = None # The relation name (also know as the relation type).
147 unit = None # The unit id.
148 number = None # The unit number (integer).
149 service = None # The service name.
150
151 def __init__(self, relid, unit):
152 self.relname = relid.split(':', 1)[0]
153 self.relid = relid
154 self.unit = unit
155 self.service, num = self.unit.split('/', 1)
156 self.number = int(num)
157
158 def __str__(self):
159 return '{} ({})'.format(self.relid, self.unit)
160
161 @property
162 def data(self):
163 return hookenv.relation_get(rid=self.relid, unit=self.unit)
164
165 def __setitem__(self, key, value):
166 if self.unit != hookenv.local_unit():
167 raise TypeError('Attempting to set {} on remote unit {}'
168 ''.format(key, self.unit))
169 if value is not None and not isinstance(value, six.string_types):
170 # We don't do implicit casting. This would cause simple
171 # types like integers to be read back as strings in subsequent
172 # hooks, and mutable types would require a lot of wrapping
173 # to ensure relation-set gets called when they are mutated.
174 raise ValueError('Only string values allowed')
175 hookenv.relation_set(self.relid, {key: value})
176
177 def __delitem__(self, key):
178 # Deleting a key and setting it to null is the same thing in
179 # Juju relations.
180 self[key] = None
181
182
183class Leader(UserDict):
184 def __init__(self):
185 pass # Don't call superclass initializer, as it will nuke self.data
186
187 @property
188 def data(self):
189 return hookenv.leader_get()
190
191 def __setitem__(self, key, value):
192 if not hookenv.is_leader():
193 raise TypeError('Not the leader. Cannot change leader settings.')
194 if value is not None and not isinstance(value, six.string_types):
195 # We don't do implicit casting. This would cause simple
196 # types like integers to be read back as strings in subsequent
197 # hooks, and mutable types would require a lot of wrapping
198 # to ensure leader-set gets called when they are mutated.
199 raise ValueError('Only string values allowed')
200 hookenv.leader_set({key: value})
201
202 def __delitem__(self, key):
203 # Deleting a key and setting it to null is the same thing in
204 # Juju leadership settings.
205 self[key] = None
diff --git a/hooks/charmhelpers/contrib/__init__.py b/hooks/charmhelpers/contrib/__init__.py
206deleted file mode 1006440deleted file mode 100644
index d7567b8..0000000
--- a/hooks/charmhelpers/contrib/__init__.py
+++ /dev/null
@@ -1,13 +0,0 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
diff --git a/hooks/charmhelpers/contrib/amulet/__init__.py b/hooks/charmhelpers/contrib/amulet/__init__.py
14deleted file mode 1006440deleted file mode 100644
index d7567b8..0000000
--- a/hooks/charmhelpers/contrib/amulet/__init__.py
+++ /dev/null
@@ -1,13 +0,0 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
diff --git a/hooks/charmhelpers/contrib/amulet/deployment.py b/hooks/charmhelpers/contrib/amulet/deployment.py
14deleted file mode 1006440deleted file mode 100644
index d21d01d..0000000
--- a/hooks/charmhelpers/contrib/amulet/deployment.py
+++ /dev/null
@@ -1,99 +0,0 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15import amulet
16import os
17import six
18
19
20class AmuletDeployment(object):
21 """Amulet deployment.
22
23 This class provides generic Amulet deployment and test runner
24 methods.
25 """
26
27 def __init__(self, series=None):
28 """Initialize the deployment environment."""
29 self.series = None
30
31 if series:
32 self.series = series
33 self.d = amulet.Deployment(series=self.series)
34 else:
35 self.d = amulet.Deployment()
36
37 def _add_services(self, this_service, other_services):
38 """Add services.
39
40 Add services to the deployment where this_service is the local charm
41 that we're testing and other_services are the other services that
42 are being used in the local amulet tests.
43 """
44 if this_service['name'] != os.path.basename(os.getcwd()):
45 s = this_service['name']
46 msg = "The charm's root directory name needs to be {}".format(s)
47 amulet.raise_status(amulet.FAIL, msg=msg)
48
49 if 'units' not in this_service:
50 this_service['units'] = 1
51
52 self.d.add(this_service['name'], units=this_service['units'],
53 constraints=this_service.get('constraints'),
54 storage=this_service.get('storage'))
55
56 for svc in other_services:
57 if 'location' in svc:
58 branch_location = svc['location']
59 elif self.series:
60 branch_location = 'cs:{}/{}'.format(self.series, svc['name']),
61 else:
62 branch_location = None
63
64 if 'units' not in svc:
65 svc['units'] = 1
66
67 self.d.add(svc['name'], charm=branch_location, units=svc['units'],
68 constraints=svc.get('constraints'),
69 storage=svc.get('storage'))
70
71 def _add_relations(self, relations):
72 """Add all of the relations for the services."""
73 for k, v in six.iteritems(relations):
74 self.d.relate(k, v)
75
76 def _configure_services(self, configs):
77 """Configure all of the services."""
78 for service, config in six.iteritems(configs):
79 self.d.configure(service, config)
80
81 def _deploy(self):
82 """Deploy environment and wait for all hooks to finish executing."""
83 timeout = int(os.environ.get('AMULET_SETUP_TIMEOUT', 900))
84 try:
85 self.d.setup(timeout=timeout)
86 self.d.sentry.wait(timeout=timeout)
87 except amulet.helpers.TimeoutError:
88 amulet.raise_status(
89 amulet.FAIL,
90 msg="Deployment timed out ({}s)".format(timeout)
91 )
92 except Exception:
93 raise
94
95 def run_tests(self):
96 """Run all of the methods that are prefixed with 'test_'."""
97 for test in dir(self):
98 if test.startswith('test_'):
99 getattr(self, test)()
diff --git a/hooks/charmhelpers/contrib/amulet/utils.py b/hooks/charmhelpers/contrib/amulet/utils.py
100deleted file mode 1006440deleted file mode 100644
index 8a6b764..0000000
--- a/hooks/charmhelpers/contrib/amulet/utils.py
+++ /dev/null
@@ -1,821 +0,0 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15import io
16import json
17import logging
18import os
19import re
20import socket
21import subprocess
22import sys
23import time
24import uuid
25
26import amulet
27import distro_info
28import six
29from six.moves import configparser
30if six.PY3:
31 from urllib import parse as urlparse
32else:
33 import urlparse
34
35
36class AmuletUtils(object):
37 """Amulet utilities.
38
39 This class provides common utility functions that are used by Amulet
40 tests.
41 """
42
43 def __init__(self, log_level=logging.ERROR):
44 self.log = self.get_logger(level=log_level)
45 self.ubuntu_releases = self.get_ubuntu_releases()
46
47 def get_logger(self, name="amulet-logger", level=logging.DEBUG):
48 """Get a logger object that will log to stdout."""
49 log = logging
50 logger = log.getLogger(name)
51 fmt = log.Formatter("%(asctime)s %(funcName)s "
52 "%(levelname)s: %(message)s")
53
54 handler = log.StreamHandler(stream=sys.stdout)
55 handler.setLevel(level)
56 handler.setFormatter(fmt)
57
58 logger.addHandler(handler)
59 logger.setLevel(level)
60
61 return logger
62
63 def valid_ip(self, ip):
64 if re.match(r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$", ip):
65 return True
66 else:
67 return False
68
69 def valid_url(self, url):
70 p = re.compile(
71 r'^(?:http|ftp)s?://'
72 r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' # noqa
73 r'localhost|'
74 r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})'
75 r'(?::\d+)?'
76 r'(?:/?|[/?]\S+)$',
77 re.IGNORECASE)
78 if p.match(url):
79 return True
80 else:
81 return False
82
83 def get_ubuntu_release_from_sentry(self, sentry_unit):
84 """Get Ubuntu release codename from sentry unit.
85
86 :param sentry_unit: amulet sentry/service unit pointer
87 :returns: list of strings - release codename, failure message
88 """
89 msg = None
90 cmd = 'lsb_release -cs'
91 release, code = sentry_unit.run(cmd)
92 if code == 0:
93 self.log.debug('{} lsb_release: {}'.format(
94 sentry_unit.info['unit_name'], release))
95 else:
96 msg = ('{} `{}` returned {} '
97 '{}'.format(sentry_unit.info['unit_name'],
98 cmd, release, code))
99 if release not in self.ubuntu_releases:
100 msg = ("Release ({}) not found in Ubuntu releases "
101 "({})".format(release, self.ubuntu_releases))
102 return release, msg
103
104 def validate_services(self, commands):
105 """Validate that lists of commands succeed on service units. Can be
106 used to verify system services are running on the corresponding
107 service units.
108
109 :param commands: dict with sentry keys and arbitrary command list vals
110 :returns: None if successful, Failure string message otherwise
111 """
112 self.log.debug('Checking status of system services...')
113
114 # /!\ DEPRECATION WARNING (beisner):
115 # New and existing tests should be rewritten to use
116 # validate_services_by_name() as it is aware of init systems.
117 self.log.warn('DEPRECATION WARNING: use '
118 'validate_services_by_name instead of validate_services '
119 'due to init system differences.')
120
121 for k, v in six.iteritems(commands):
122 for cmd in v:
123 output, code = k.run(cmd)
124 self.log.debug('{} `{}` returned '
125 '{}'.format(k.info['unit_name'],
126 cmd, code))
127 if code != 0:
128 return "command `{}` returned {}".format(cmd, str(code))
129 return None
130
131 def validate_services_by_name(self, sentry_services):
132 """Validate system service status by service name, automatically
133 detecting init system based on Ubuntu release codename.
134
135 :param sentry_services: dict with sentry keys and svc list values
136 :returns: None if successful, Failure string message otherwise
137 """
138 self.log.debug('Checking status of system services...')
139
140 # Point at which systemd became a thing
141 systemd_switch = self.ubuntu_releases.index('vivid')
142
143 for sentry_unit, services_list in six.iteritems(sentry_services):
144 # Get lsb_release codename from unit
145 release, ret = self.get_ubuntu_release_from_sentry(sentry_unit)
146 if ret:
147 return ret
148
149 for service_name in services_list:
150 if (self.ubuntu_releases.index(release) >= systemd_switch or
151 service_name in ['rabbitmq-server', 'apache2',
152 'memcached']):
153 # init is systemd (or regular sysv)
154 cmd = 'sudo service {} status'.format(service_name)
155 output, code = sentry_unit.run(cmd)
156 service_running = code == 0
157 elif self.ubuntu_releases.index(release) < systemd_switch:
158 # init is upstart
159 cmd = 'sudo status {}'.format(service_name)
160 output, code = sentry_unit.run(cmd)
161 service_running = code == 0 and "start/running" in output
162
163 self.log.debug('{} `{}` returned '
164 '{}'.format(sentry_unit.info['unit_name'],
165 cmd, code))
166 if not service_running:
167 return u"command `{}` returned {} {}".format(
168 cmd, output, str(code))
169 return None
170
171 def _get_config(self, unit, filename):
172 """Get a ConfigParser object for parsing a unit's config file."""
173 file_contents = unit.file_contents(filename)
174
175 # NOTE(beisner): by default, ConfigParser does not handle options
176 # with no value, such as the flags used in the mysql my.cnf file.
177 # https://bugs.python.org/issue7005
178 config = configparser.ConfigParser(allow_no_value=True)
179 config.readfp(io.StringIO(file_contents))
180 return config
181
182 def validate_config_data(self, sentry_unit, config_file, section,
183 expected):
184 """Validate config file data.
185
186 Verify that the specified section of the config file contains
187 the expected option key:value pairs.
188
189 Compare expected dictionary data vs actual dictionary data.
190 The values in the 'expected' dictionary can be strings, bools, ints,
191 longs, or can be a function that evaluates a variable and returns a
192 bool.
193 """
194 self.log.debug('Validating config file data ({} in {} on {})'
195 '...'.format(section, config_file,
196 sentry_unit.info['unit_name']))
197 config = self._get_config(sentry_unit, config_file)
198
199 if section != 'DEFAULT' and not config.has_section(section):
200 return "section [{}] does not exist".format(section)
201
202 for k in expected.keys():
203 if not config.has_option(section, k):
204 return "section [{}] is missing option {}".format(section, k)
205
206 actual = config.get(section, k)
207 v = expected[k]
208 if (isinstance(v, six.string_types) or
209 isinstance(v, bool) or
210 isinstance(v, six.integer_types)):
211 # handle explicit values
212 if actual != v:
213 return "section [{}] {}:{} != expected {}:{}".format(
214 section, k, actual, k, expected[k])
215 # handle function pointers, such as not_null or valid_ip
216 elif not v(actual):
217 return "section [{}] {}:{} != expected {}:{}".format(
218 section, k, actual, k, expected[k])
219 return None
220
221 def _validate_dict_data(self, expected, actual):
222 """Validate dictionary data.
223
224 Compare expected dictionary data vs actual dictionary data.
225 The values in the 'expected' dictionary can be strings, bools, ints,
226 longs, or can be a function that evaluates a variable and returns a
227 bool.
228 """
229 self.log.debug('actual: {}'.format(repr(actual)))
230 self.log.debug('expected: {}'.format(repr(expected)))
231
232 for k, v in six.iteritems(expected):
233 if k in actual:
234 if (isinstance(v, six.string_types) or
235 isinstance(v, bool) or
236 isinstance(v, six.integer_types)):
237 # handle explicit values
238 if v != actual[k]:
239 return "{}:{}".format(k, actual[k])
240 # handle function pointers, such as not_null or valid_ip
241 elif not v(actual[k]):
242 return "{}:{}".format(k, actual[k])
243 else:
244 return "key '{}' does not exist".format(k)
245 return None
246
247 def validate_relation_data(self, sentry_unit, relation, expected):
248 """Validate actual relation data based on expected relation data."""
249 actual = sentry_unit.relation(relation[0], relation[1])
250 return self._validate_dict_data(expected, actual)
251
252 def _validate_list_data(self, expected, actual):
253 """Compare expected list vs actual list data."""
254 for e in expected:
255 if e not in actual:
256 return "expected item {} not found in actual list".format(e)
257 return None
258
259 def not_null(self, string):
260 if string is not None:
261 return True
262 else:
263 return False
264
265 def _get_file_mtime(self, sentry_unit, filename):
266 """Get last modification time of file."""
267 return sentry_unit.file_stat(filename)['mtime']
268
269 def _get_dir_mtime(self, sentry_unit, directory):
270 """Get last modification time of directory."""
271 return sentry_unit.directory_stat(directory)['mtime']
272
273 def _get_proc_start_time(self, sentry_unit, service, pgrep_full=None):
274 """Get start time of a process based on the last modification time
275 of the /proc/pid directory.
276
277 :sentry_unit: The sentry unit to check for the service on
278 :service: service name to look for in process table
279 :pgrep_full: [Deprecated] Use full command line search mode with pgrep
280 :returns: epoch time of service process start
281 :param commands: list of bash commands
282 :param sentry_units: list of sentry unit pointers
283 :returns: None if successful; Failure message otherwise
284 """
285 if pgrep_full is not None:
286 # /!\ DEPRECATION WARNING (beisner):
287 # No longer implemented, as pidof is now used instead of pgrep.
288 # https://bugs.launchpad.net/charm-helpers/+bug/1474030
289 self.log.warn('DEPRECATION WARNING: pgrep_full bool is no '
290 'longer implemented re: lp 1474030.')
291
292 pid_list = self.get_process_id_list(sentry_unit, service)
293 pid = pid_list[0]
294 proc_dir = '/proc/{}'.format(pid)
295 self.log.debug('Pid for {} on {}: {}'.format(
296 service, sentry_unit.info['unit_name'], pid))
297
298 return self._get_dir_mtime(sentry_unit, proc_dir)
299
300 def service_restarted(self, sentry_unit, service, filename,
301 pgrep_full=None, sleep_time=20):
302 """Check if service was restarted.
303
304 Compare a service's start time vs a file's last modification time
305 (such as a config file for that service) to determine if the service
306 has been restarted.
307 """
308 # /!\ DEPRECATION WARNING (beisner):
309 # This method is prone to races in that no before-time is known.
310 # Use validate_service_config_changed instead.
311
312 # NOTE(beisner) pgrep_full is no longer implemented, as pidof is now
313 # used instead of pgrep. pgrep_full is still passed through to ensure
314 # deprecation WARNS. lp1474030
315 self.log.warn('DEPRECATION WARNING: use '
316 'validate_service_config_changed instead of '
317 'service_restarted due to known races.')
318
319 time.sleep(sleep_time)
320 if (self._get_proc_start_time(sentry_unit, service, pgrep_full) >=
321 self._get_file_mtime(sentry_unit, filename)):
322 return True
323 else:
324 return False
325
326 def service_restarted_since(self, sentry_unit, mtime, service,
327 pgrep_full=None, sleep_time=20,
328 retry_count=30, retry_sleep_time=10):
329 """Check if service was been started after a given time.
330
331 Args:
332 sentry_unit (sentry): The sentry unit to check for the service on
333 mtime (float): The epoch time to check against
334 service (string): service name to look for in process table
335 pgrep_full: [Deprecated] Use full command line search mode with pgrep
336 sleep_time (int): Initial sleep time (s) before looking for file
337 retry_sleep_time (int): Time (s) to sleep between retries
338 retry_count (int): If file is not found, how many times to retry
339
340 Returns:
341 bool: True if service found and its start time it newer than mtime,
342 False if service is older than mtime or if service was
343 not found.
344 """
345 # NOTE(beisner) pgrep_full is no longer implemented, as pidof is now
346 # used instead of pgrep. pgrep_full is still passed through to ensure
347 # deprecation WARNS. lp1474030
348
349 unit_name = sentry_unit.info['unit_name']
350 self.log.debug('Checking that %s service restarted since %s on '
351 '%s' % (service, mtime, unit_name))
352 time.sleep(sleep_time)
353 proc_start_time = None
354 tries = 0
355 while tries <= retry_count and not proc_start_time:
356 try:
357 proc_start_time = self._get_proc_start_time(sentry_unit,
358 service,
359 pgrep_full)
360 self.log.debug('Attempt {} to get {} proc start time on {} '
361 'OK'.format(tries, service, unit_name))
362 except IOError as e:
363 # NOTE(beisner) - race avoidance, proc may not exist yet.
364 # https://bugs.launchpad.net/charm-helpers/+bug/1474030
365 self.log.debug('Attempt {} to get {} proc start time on {} '
366 'failed\n{}'.format(tries, service,
367 unit_name, e))
368 time.sleep(retry_sleep_time)
369 tries += 1
370
371 if not proc_start_time:
372 self.log.warn('No proc start time found, assuming service did '
373 'not start')
374 return False
375 if proc_start_time >= mtime:
376 self.log.debug('Proc start time is newer than provided mtime'
377 '(%s >= %s) on %s (OK)' % (proc_start_time,
378 mtime, unit_name))
379 return True
380 else:
381 self.log.warn('Proc start time (%s) is older than provided mtime '
382 '(%s) on %s, service did not '
383 'restart' % (proc_start_time, mtime, unit_name))
384 return False
385
386 def config_updated_since(self, sentry_unit, filename, mtime,
387 sleep_time=20, retry_count=30,
388 retry_sleep_time=10):
389 """Check if file was modified after a given time.
390
391 Args:
392 sentry_unit (sentry): The sentry unit to check the file mtime on
393 filename (string): The file to check mtime of
394 mtime (float): The epoch time to check against
395 sleep_time (int): Initial sleep time (s) before looking for file
396 retry_sleep_time (int): Time (s) to sleep between retries
397 retry_count (int): If file is not found, how many times to retry
398
399 Returns:
400 bool: True if file was modified more recently than mtime, False if
401 file was modified before mtime, or if file not found.
402 """
403 unit_name = sentry_unit.info['unit_name']
404 self.log.debug('Checking that %s updated since %s on '
405 '%s' % (filename, mtime, unit_name))
406 time.sleep(sleep_time)
407 file_mtime = None
408 tries = 0
409 while tries <= retry_count and not file_mtime:
410 try:
411 file_mtime = self._get_file_mtime(sentry_unit, filename)
412 self.log.debug('Attempt {} to get {} file mtime on {} '
413 'OK'.format(tries, filename, unit_name))
414 except IOError as e:
415 # NOTE(beisner) - race avoidance, file may not exist yet.
416 # https://bugs.launchpad.net/charm-helpers/+bug/1474030
417 self.log.debug('Attempt {} to get {} file mtime on {} '
418 'failed\n{}'.format(tries, filename,
419 unit_name, e))
420 time.sleep(retry_sleep_time)
421 tries += 1
422
423 if not file_mtime:
424 self.log.warn('Could not determine file mtime, assuming '
425 'file does not exist')
426 return False
427
428 if file_mtime >= mtime:
429 self.log.debug('File mtime is newer than provided mtime '
430 '(%s >= %s) on %s (OK)' % (file_mtime,
431 mtime, unit_name))
432 return True
433 else:
434 self.log.warn('File mtime is older than provided mtime'
435 '(%s < on %s) on %s' % (file_mtime,
436 mtime, unit_name))
437 return False
438
439 def validate_service_config_changed(self, sentry_unit, mtime, service,
440 filename, pgrep_full=None,
441 sleep_time=20, retry_count=30,
442 retry_sleep_time=10):
443 """Check service and file were updated after mtime
444
445 Args:
446 sentry_unit (sentry): The sentry unit to check for the service on
447 mtime (float): The epoch time to check against
448 service (string): service name to look for in process table
449 filename (string): The file to check mtime of
450 pgrep_full: [Deprecated] Use full command line search mode with pgrep
451 sleep_time (int): Initial sleep in seconds to pass to test helpers
452 retry_count (int): If service is not found, how many times to retry
453 retry_sleep_time (int): Time in seconds to wait between retries
454
455 Typical Usage:
456 u = OpenStackAmuletUtils(ERROR)
457 ...
458 mtime = u.get_sentry_time(self.cinder_sentry)
459 self.d.configure('cinder', {'verbose': 'True', 'debug': 'True'})
460 if not u.validate_service_config_changed(self.cinder_sentry,
461 mtime,
462 'cinder-api',
463 '/etc/cinder/cinder.conf')
464 amulet.raise_status(amulet.FAIL, msg='update failed')
465 Returns:
466 bool: True if both service and file where updated/restarted after
467 mtime, False if service is older than mtime or if service was
468 not found or if filename was modified before mtime.
469 """
470
471 # NOTE(beisner) pgrep_full is no longer implemented, as pidof is now
472 # used instead of pgrep. pgrep_full is still passed through to ensure
473 # deprecation WARNS. lp1474030
474
475 service_restart = self.service_restarted_since(
476 sentry_unit, mtime,
477 service,
478 pgrep_full=pgrep_full,
479 sleep_time=sleep_time,
480 retry_count=retry_count,
481 retry_sleep_time=retry_sleep_time)
482
483 config_update = self.config_updated_since(
484 sentry_unit,
485 filename,
486 mtime,
487 sleep_time=sleep_time,
488 retry_count=retry_count,
489 retry_sleep_time=retry_sleep_time)
490
491 return service_restart and config_update
492
493 def get_sentry_time(self, sentry_unit):
494 """Return current epoch time on a sentry"""
495 cmd = "date +'%s'"
496 return float(sentry_unit.run(cmd)[0])
497
498 def relation_error(self, name, data):
499 return 'unexpected relation data in {} - {}'.format(name, data)
500
501 def endpoint_error(self, name, data):
502 return 'unexpected endpoint data in {} - {}'.format(name, data)
503
504 def get_ubuntu_releases(self):
505 """Return a list of all Ubuntu releases in order of release."""
506 _d = distro_info.UbuntuDistroInfo()
507 _release_list = _d.all
508 return _release_list
509
510 def file_to_url(self, file_rel_path):
511 """Convert a relative file path to a file URL."""
512 _abs_path = os.path.abspath(file_rel_path)
513 return urlparse.urlparse(_abs_path, scheme='file').geturl()
514
515 def check_commands_on_units(self, commands, sentry_units):
516 """Check that all commands in a list exit zero on all
517 sentry units in a list.
518
519 :param commands: list of bash commands
520 :param sentry_units: list of sentry unit pointers
521 :returns: None if successful; Failure message otherwise
522 """
523 self.log.debug('Checking exit codes for {} commands on {} '
524 'sentry units...'.format(len(commands),
525 len(sentry_units)))
526 for sentry_unit in sentry_units:
527 for cmd in commands:
528 output, code = sentry_unit.run(cmd)
529 if code == 0:
530 self.log.debug('{} `{}` returned {} '
531 '(OK)'.format(sentry_unit.info['unit_name'],
532 cmd, code))
533 else:
534 return ('{} `{}` returned {} '
535 '{}'.format(sentry_unit.info['unit_name'],
536 cmd, code, output))
537 return None
538
539 def get_process_id_list(self, sentry_unit, process_name,
540 expect_success=True):
541 """Get a list of process ID(s) from a single sentry juju unit
542 for a single process name.
543
544 :param sentry_unit: Amulet sentry instance (juju unit)
545 :param process_name: Process name
546 :param expect_success: If False, expect the PID to be missing,
547 raise if it is present.
548 :returns: List of process IDs
549 """
550 cmd = 'pidof -x "{}"'.format(process_name)
551 if not expect_success:
552 cmd += " || exit 0 && exit 1"
553 output, code = sentry_unit.run(cmd)
554 if code != 0:
555 msg = ('{} `{}` returned {} '
556 '{}'.format(sentry_unit.info['unit_name'],
557 cmd, code, output))
558 amulet.raise_status(amulet.FAIL, msg=msg)
559 return str(output).split()
560
561 def get_unit_process_ids(self, unit_processes, expect_success=True):
562 """Construct a dict containing unit sentries, process names, and
563 process IDs.
564
565 :param unit_processes: A dictionary of Amulet sentry instance
566 to list of process names.
567 :param expect_success: if False expect the processes to not be
568 running, raise if they are.
569 :returns: Dictionary of Amulet sentry instance to dictionary
570 of process names to PIDs.
571 """
572 pid_dict = {}
573 for sentry_unit, process_list in six.iteritems(unit_processes):
574 pid_dict[sentry_unit] = {}
575 for process in process_list:
576 pids = self.get_process_id_list(
577 sentry_unit, process, expect_success=expect_success)
578 pid_dict[sentry_unit].update({process: pids})
579 return pid_dict
580
581 def validate_unit_process_ids(self, expected, actual):
582 """Validate process id quantities for services on units."""
583 self.log.debug('Checking units for running processes...')
584 self.log.debug('Expected PIDs: {}'.format(expected))
585 self.log.debug('Actual PIDs: {}'.format(actual))
586
587 if len(actual) != len(expected):
588 return ('Unit count mismatch. expected, actual: {}, '
589 '{} '.format(len(expected), len(actual)))
590
591 for (e_sentry, e_proc_names) in six.iteritems(expected):
592 e_sentry_name = e_sentry.info['unit_name']
593 if e_sentry in actual.keys():
594 a_proc_names = actual[e_sentry]
595 else:
596 return ('Expected sentry ({}) not found in actual dict data.'
597 '{}'.format(e_sentry_name, e_sentry))
598
599 if len(e_proc_names.keys()) != len(a_proc_names.keys()):
600 return ('Process name count mismatch. expected, actual: {}, '
601 '{}'.format(len(expected), len(actual)))
602
603 for (e_proc_name, e_pids), (a_proc_name, a_pids) in \
604 zip(e_proc_names.items(), a_proc_names.items()):
605 if e_proc_name != a_proc_name:
606 return ('Process name mismatch. expected, actual: {}, '
607 '{}'.format(e_proc_name, a_proc_name))
608
609 a_pids_length = len(a_pids)
610 fail_msg = ('PID count mismatch. {} ({}) expected, actual: '
611 '{}, {} ({})'.format(e_sentry_name, e_proc_name,
612 e_pids, a_pids_length,
613 a_pids))
614
615 # If expected is a list, ensure at least one PID quantity match
616 if isinstance(e_pids, list) and \
617 a_pids_length not in e_pids:
618 return fail_msg
619 # If expected is not bool and not list,
620 # ensure PID quantities match
621 elif not isinstance(e_pids, bool) and \
622 not isinstance(e_pids, list) and \
623 a_pids_length != e_pids:
624 return fail_msg
625 # If expected is bool True, ensure 1 or more PIDs exist
626 elif isinstance(e_pids, bool) and \
627 e_pids is True and a_pids_length < 1:
628 return fail_msg
629 # If expected is bool False, ensure 0 PIDs exist
630 elif isinstance(e_pids, bool) and \
631 e_pids is False and a_pids_length != 0:
632 return fail_msg
633 else:
634 self.log.debug('PID check OK: {} {} {}: '
635 '{}'.format(e_sentry_name, e_proc_name,
636 e_pids, a_pids))
637 return None
638
639 def validate_list_of_identical_dicts(self, list_of_dicts):
640 """Check that all dicts within a list are identical."""
641 hashes = []
642 for _dict in list_of_dicts:
643 hashes.append(hash(frozenset(_dict.items())))
644
645 self.log.debug('Hashes: {}'.format(hashes))
646 if len(set(hashes)) == 1:
647 self.log.debug('Dicts within list are identical')
648 else:
649 return 'Dicts within list are not identical'
650
651 return None
652
653 def validate_sectionless_conf(self, file_contents, expected):
654 """A crude conf parser. Useful to inspect configuration files which
655 do not have section headers (as would be necessary in order to use
656 the configparser). Such as openstack-dashboard or rabbitmq confs."""
657 for line in file_contents.split('\n'):
658 if '=' in line:
659 args = line.split('=')
660 if len(args) <= 1:
661 continue
662 key = args[0].strip()
663 value = args[1].strip()
664 if key in expected.keys():
665 if expected[key] != value:
666 msg = ('Config mismatch. Expected, actual: {}, '
667 '{}'.format(expected[key], value))
668 amulet.raise_status(amulet.FAIL, msg=msg)
669
670 def get_unit_hostnames(self, units):
671 """Return a dict of juju unit names to hostnames."""
672 host_names = {}
673 for unit in units:
674 host_names[unit.info['unit_name']] = \
675 str(unit.file_contents('/etc/hostname').strip())
676 self.log.debug('Unit host names: {}'.format(host_names))
677 return host_names
678
679 def run_cmd_unit(self, sentry_unit, cmd):
680 """Run a command on a unit, return the output and exit code."""
681 output, code = sentry_unit.run(cmd)
682 if code == 0:
683 self.log.debug('{} `{}` command returned {} '
684 '(OK)'.format(sentry_unit.info['unit_name'],
685 cmd, code))
686 else:
687 msg = ('{} `{}` command returned {} '
688 '{}'.format(sentry_unit.info['unit_name'],
689 cmd, code, output))
690 amulet.raise_status(amulet.FAIL, msg=msg)
691 return str(output), code
692
693 def file_exists_on_unit(self, sentry_unit, file_name):
694 """Check if a file exists on a unit."""
695 try:
696 sentry_unit.file_stat(file_name)
697 return True
698 except IOError:
699 return False
700 except Exception as e:
701 msg = 'Error checking file {}: {}'.format(file_name, e)
702 amulet.raise_status(amulet.FAIL, msg=msg)
703
704 def file_contents_safe(self, sentry_unit, file_name,
705 max_wait=60, fatal=False):
706 """Get file contents from a sentry unit. Wrap amulet file_contents
707 with retry logic to address races where a file checks as existing,
708 but no longer exists by the time file_contents is called.
709 Return None if file not found. Optionally raise if fatal is True."""
710 unit_name = sentry_unit.info['unit_name']
711 file_contents = False
712 tries = 0
713 while not file_contents and tries < (max_wait / 4):
714 try:
715 file_contents = sentry_unit.file_contents(file_name)
716 except IOError:
717 self.log.debug('Attempt {} to open file {} from {} '
718 'failed'.format(tries, file_name,
719 unit_name))
720 time.sleep(4)
721 tries += 1
722
723 if file_contents:
724 return file_contents
725 elif not fatal:
726 return None
727 elif fatal:
728 msg = 'Failed to get file contents from unit.'
729 amulet.raise_status(amulet.FAIL, msg)
730
731 def port_knock_tcp(self, host="localhost", port=22, timeout=15):
732 """Open a TCP socket to check for a listening sevice on a host.
733
734 :param host: host name or IP address, default to localhost
735 :param port: TCP port number, default to 22
736 :param timeout: Connect timeout, default to 15 seconds
737 :returns: True if successful, False if connect failed
738 """
739
740 # Resolve host name if possible
741 try:
742 connect_host = socket.gethostbyname(host)
743 host_human = "{} ({})".format(connect_host, host)
744 except socket.error as e:
745 self.log.warn('Unable to resolve address: '
746 '{} ({}) Trying anyway!'.format(host, e))
747 connect_host = host
748 host_human = connect_host
749
750 # Attempt socket connection
751 try:
752 knock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
753 knock.settimeout(timeout)
754 knock.connect((connect_host, port))
755 knock.close()
756 self.log.debug('Socket connect OK for host '
757 '{} on port {}.'.format(host_human, port))
758 return True
759 except socket.error as e:
760 self.log.debug('Socket connect FAIL for'
761 ' {} port {} ({})'.format(host_human, port, e))
762 return False
763
764 def port_knock_units(self, sentry_units, port=22,
765 timeout=15, expect_success=True):
766 """Open a TCP socket to check for a listening sevice on each
767 listed juju unit.
768
769 :param sentry_units: list of sentry unit pointers
770 :param port: TCP port number, default to 22
771 :param timeout: Connect timeout, default to 15 seconds
772 :expect_success: True by default, set False to invert logic
773 :returns: None if successful, Failure message otherwise
774 """
775 for unit in sentry_units:
776 host = unit.info['public-address']
777 connected = self.port_knock_tcp(host, port, timeout)
778 if not connected and expect_success:
779 return 'Socket connect failed.'
780 elif connected and not expect_success:
781 return 'Socket connected unexpectedly.'
782
783 def get_uuid_epoch_stamp(self):
784 """Returns a stamp string based on uuid4 and epoch time. Useful in
785 generating test messages which need to be unique-ish."""
786 return '[{}-{}]'.format(uuid.uuid4(), time.time())
787
788 # amulet juju action helpers:
789 def run_action(self, unit_sentry, action,
790 _check_output=subprocess.check_output,
791 params=None):
792 """Translate to amulet's built in run_action(). Deprecated.
793
794 Run the named action on a given unit sentry.
795
796 params a dict of parameters to use
797 _check_output parameter is no longer used
798
799 @return action_id.
800 """
801 self.log.warn('charmhelpers.contrib.amulet.utils.run_action has been '
802 'deprecated for amulet.run_action')
803 return unit_sentry.run_action(action, action_args=params)
804
805 def wait_on_action(self, action_id, _check_output=subprocess.check_output):
806 """Wait for a given action, returning if it completed or not.
807
808 action_id a string action uuid
809 _check_output parameter is no longer used
810 """
811 data = amulet.actions.get_action_output(action_id, full_output=True)
812 return data.get(u"status") == "completed"
813
814 def status_get(self, unit):
815 """Return the current service status of this unit."""
816 raw_status, return_code = unit.run(
817 "status-get --format=json --include-data")
818 if return_code != 0:
819 return ("unknown", "")
820 status = json.loads(raw_status)
821 return (status["status"], status["message"])
diff --git a/hooks/charmhelpers/contrib/ansible/__init__.py b/hooks/charmhelpers/contrib/ansible/__init__.py
822deleted file mode 1006440deleted file mode 100644
index 5281946..0000000
--- a/hooks/charmhelpers/contrib/ansible/__init__.py
+++ /dev/null
@@ -1,252 +0,0 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15# Copyright 2013 Canonical Ltd.
16#
17# Authors:
18# Charm Helpers Developers <juju@lists.ubuntu.com>
19"""Charm Helpers ansible - declare the state of your machines.
20
21This helper enables you to declare your machine state, rather than
22program it procedurally (and have to test each change to your procedures).
23Your install hook can be as simple as::
24
25 {{{
26 import charmhelpers.contrib.ansible
27
28
29 def install():
30 charmhelpers.contrib.ansible.install_ansible_support()
31 charmhelpers.contrib.ansible.apply_playbook('playbooks/install.yaml')
32 }}}
33
34and won't need to change (nor will its tests) when you change the machine
35state.
36
37All of your juju config and relation-data are available as template
38variables within your playbooks and templates. An install playbook looks
39something like::
40
41 {{{
42 ---
43 - hosts: localhost
44 user: root
45
46 tasks:
47 - name: Add private repositories.
48 template:
49 src: ../templates/private-repositories.list.jinja2
50 dest: /etc/apt/sources.list.d/private.list
51
52 - name: Update the cache.
53 apt: update_cache=yes
54
55 - name: Install dependencies.
56 apt: pkg={{ item }}
57 with_items:
58 - python-mimeparse
59 - python-webob
60 - sunburnt
61
62 - name: Setup groups.
63 group: name={{ item.name }} gid={{ item.gid }}
64 with_items:
65 - { name: 'deploy_user', gid: 1800 }
66 - { name: 'service_user', gid: 1500 }
67
68 ...
69 }}}
70
71Read more online about `playbooks`_ and standard ansible `modules`_.
72
73.. _playbooks: http://www.ansibleworks.com/docs/playbooks.html
74.. _modules: http://www.ansibleworks.com/docs/modules.html
75
76A further feature os the ansible hooks is to provide a light weight "action"
77scripting tool. This is a decorator that you apply to a function, and that
78function can now receive cli args, and can pass extra args to the playbook.
79
80e.g.
81
82
83@hooks.action()
84def some_action(amount, force="False"):
85 "Usage: some-action AMOUNT [force=True]" # <-- shown on error
86 # process the arguments
87 # do some calls
88 # return extra-vars to be passed to ansible-playbook
89 return {
90 'amount': int(amount),
91 'type': force,
92 }
93
94You can now create a symlink to hooks.py that can be invoked like a hook, but
95with cli params:
96
97# link actions/some-action to hooks/hooks.py
98
99actions/some-action amount=10 force=true
100
101"""
102import os
103import stat
104import subprocess
105import functools
106
107import charmhelpers.contrib.templating.contexts
108import charmhelpers.core.host
109import charmhelpers.core.hookenv
110import charmhelpers.fetch
111
112
113charm_dir = os.environ.get('CHARM_DIR', '')
114ansible_hosts_path = '/etc/ansible/hosts'
115# Ansible will automatically include any vars in the following
116# file in its inventory when run locally.
117ansible_vars_path = '/etc/ansible/host_vars/localhost'
118
119
120def install_ansible_support(from_ppa=True, ppa_location='ppa:rquillo/ansible'):
121 """Installs the ansible package.
122
123 By default it is installed from the `PPA`_ linked from
124 the ansible `website`_ or from a ppa specified by a charm config..
125
126 .. _PPA: https://launchpad.net/~rquillo/+archive/ansible
127 .. _website: http://docs.ansible.com/intro_installation.html#latest-releases-via-apt-ubuntu
128
129 If from_ppa is empty, you must ensure that the package is available
130 from a configured repository.
131 """
132 if from_ppa:
133 charmhelpers.fetch.add_source(ppa_location)
134 charmhelpers.fetch.apt_update(fatal=True)
135 charmhelpers.fetch.apt_install('ansible')
136 with open(ansible_hosts_path, 'w+') as hosts_file:
137 hosts_file.write('localhost ansible_connection=local ansible_remote_tmp=/root/.ansible/tmp')
138
139
140def apply_playbook(playbook, tags=None, extra_vars=None):
141 tags = tags or []
142 tags = ",".join(tags)
143 charmhelpers.contrib.templating.contexts.juju_state_to_yaml(
144 ansible_vars_path, namespace_separator='__',
145 allow_hyphens_in_keys=False, mode=(stat.S_IRUSR | stat.S_IWUSR))
146
147 # we want ansible's log output to be unbuffered
148 env = os.environ.copy()
149 env['PYTHONUNBUFFERED'] = "1"
150 call = [
151 'ansible-playbook',
152 '-c',
153 'local',
154 playbook,
155 ]
156 if tags:
157 call.extend(['--tags', '{}'.format(tags)])
158 if extra_vars:
159 extra = ["%s=%s" % (k, v) for k, v in extra_vars.items()]
160 call.extend(['--extra-vars', " ".join(extra)])
161 subprocess.check_call(call, env=env)
162
163
164class AnsibleHooks(charmhelpers.core.hookenv.Hooks):
165 """Run a playbook with the hook-name as the tag.
166
167 This helper builds on the standard hookenv.Hooks helper,
168 but additionally runs the playbook with the hook-name specified
169 using --tags (ie. running all the tasks tagged with the hook-name).
170
171 Example::
172
173 hooks = AnsibleHooks(playbook_path='playbooks/my_machine_state.yaml')
174
175 # All the tasks within my_machine_state.yaml tagged with 'install'
176 # will be run automatically after do_custom_work()
177 @hooks.hook()
178 def install():
179 do_custom_work()
180
181 # For most of your hooks, you won't need to do anything other
182 # than run the tagged tasks for the hook:
183 @hooks.hook('config-changed', 'start', 'stop')
184 def just_use_playbook():
185 pass
186
187 # As a convenience, you can avoid the above noop function by specifying
188 # the hooks which are handled by ansible-only and they'll be registered
189 # for you:
190 # hooks = AnsibleHooks(
191 # 'playbooks/my_machine_state.yaml',
192 # default_hooks=['config-changed', 'start', 'stop'])
193
194 if __name__ == "__main__":
195 # execute a hook based on the name the program is called by
196 hooks.execute(sys.argv)
197
198 """
199
200 def __init__(self, playbook_path, default_hooks=None):
201 """Register any hooks handled by ansible."""
202 super(AnsibleHooks, self).__init__()
203
204 self._actions = {}
205 self.playbook_path = playbook_path
206
207 default_hooks = default_hooks or []
208
209 def noop(*args, **kwargs):
210 pass
211
212 for hook in default_hooks:
213 self.register(hook, noop)
214
215 def register_action(self, name, function):
216 """Register a hook"""
217 self._actions[name] = function
218
219 def execute(self, args):
220 """Execute the hook followed by the playbook using the hook as tag."""
221 hook_name = os.path.basename(args[0])
222 extra_vars = None
223 if hook_name in self._actions:
224 extra_vars = self._actions[hook_name](args[1:])
225 else:
226 super(AnsibleHooks, self).execute(args)
227
228 charmhelpers.contrib.ansible.apply_playbook(
229 self.playbook_path, tags=[hook_name], extra_vars=extra_vars)
230
231 def action(self, *action_names):
232 """Decorator, registering them as actions"""
233 def action_wrapper(decorated):
234
235 @functools.wraps(decorated)
236 def wrapper(argv):
237 kwargs = dict(arg.split('=') for arg in argv)
238 try:
239 return decorated(**kwargs)
240 except TypeError as e:
241 if decorated.__doc__:
242 e.args += (decorated.__doc__,)
243 raise
244
245 self.register_action(decorated.__name__, wrapper)
246 if '_' in decorated.__name__:
247 self.register_action(
248 decorated.__name__.replace('_', '-'), wrapper)
249
250 return wrapper
251
252 return action_wrapper
diff --git a/hooks/charmhelpers/contrib/benchmark/__init__.py b/hooks/charmhelpers/contrib/benchmark/__init__.py
253deleted file mode 1006440deleted file mode 100644
index c35f7fe..0000000
--- a/hooks/charmhelpers/contrib/benchmark/__init__.py
+++ /dev/null
@@ -1,124 +0,0 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15import subprocess
16import time
17import os
18from distutils.spawn import find_executable
19
20from charmhelpers.core.hookenv import (
21 in_relation_hook,
22 relation_ids,
23 relation_set,
24 relation_get,
25)
26
27
28def action_set(key, val):
29 if find_executable('action-set'):
30 action_cmd = ['action-set']
31
32 if isinstance(val, dict):
33 for k, v in iter(val.items()):
34 action_set('%s.%s' % (key, k), v)
35 return True
36
37 action_cmd.append('%s=%s' % (key, val))
38 subprocess.check_call(action_cmd)
39 return True
40 return False
41
42
43class Benchmark():
44 """
45 Helper class for the `benchmark` interface.
46
47 :param list actions: Define the actions that are also benchmarks
48
49 From inside the benchmark-relation-changed hook, you would
50 Benchmark(['memory', 'cpu', 'disk', 'smoke', 'custom'])
51
52 Examples:
53
54 siege = Benchmark(['siege'])
55 siege.start()
56 [... run siege ...]
57 # The higher the score, the better the benchmark
58 siege.set_composite_score(16.70, 'trans/sec', 'desc')
59 siege.finish()
60
61
62 """
63
64 BENCHMARK_CONF = '/etc/benchmark.conf' # Replaced in testing
65
66 required_keys = [
67 'hostname',
68 'port',
69 'graphite_port',
70 'graphite_endpoint',
71 'api_port'
72 ]
73
74 def __init__(self, benchmarks=None):
75 if in_relation_hook():
76 if benchmarks is not None:
77 for rid in sorted(relation_ids('benchmark')):
78 relation_set(relation_id=rid, relation_settings={
79 'benchmarks': ",".join(benchmarks)
80 })
81
82 # Check the relation data
83 config = {}
84 for key in self.required_keys:
85 val = relation_get(key)
86 if val is not None:
87 config[key] = val
88 else:
89 # We don't have all of the required keys
90 config = {}
91 break
92
93 if len(config):
94 with open(self.BENCHMARK_CONF, 'w') as f:
95 for key, val in iter(config.items()):
96 f.write("%s=%s\n" % (key, val))
97
98 @staticmethod
99 def start():
100 action_set('meta.start', time.strftime('%Y-%m-%dT%H:%M:%SZ'))
101
102 """
103 If the collectd charm is also installed, tell it to send a snapshot
104 of the current profile data.
105 """
106 COLLECT_PROFILE_DATA = '/usr/local/bin/collect-profile-data'
107 if os.path.exists(COLLECT_PROFILE_DATA):
108 subprocess.check_output([COLLECT_PROFILE_DATA])
109
110 @staticmethod
111 def finish():
112 action_set('meta.stop', time.strftime('%Y-%m-%dT%H:%M:%SZ'))
113
114 @staticmethod
115 def set_composite_score(value, units, direction='asc'):
116 """
117 Set the composite score for a benchmark run. This is a single number
118 representative of the benchmark results. This could be the most
119 important metric, or an amalgamation of metric scores.
120 """
121 return action_set(
122 "meta.composite",
123 {'value': value, 'units': units, 'direction': direction}
124 )
diff --git a/hooks/charmhelpers/contrib/charmhelpers/IMPORT b/hooks/charmhelpers/contrib/charmhelpers/IMPORT
125deleted file mode 1006440deleted file mode 100644
index d41cb04..0000000
--- a/hooks/charmhelpers/contrib/charmhelpers/IMPORT
+++ /dev/null
@@ -1,4 +0,0 @@
1Source lp:charm-tools/trunk
2
3charm-tools/helpers/python/charmhelpers/__init__.py -> charmhelpers/charmhelpers/contrib/charmhelpers/__init__.py
4charm-tools/helpers/python/charmhelpers/tests/test_charmhelpers.py -> charmhelpers/tests/contrib/charmhelpers/test_charmhelpers.py
diff --git a/hooks/charmhelpers/contrib/charmhelpers/__init__.py b/hooks/charmhelpers/contrib/charmhelpers/__init__.py
5deleted file mode 1006440deleted file mode 100644
index ed63e81..0000000
--- a/hooks/charmhelpers/contrib/charmhelpers/__init__.py
+++ /dev/null
@@ -1,203 +0,0 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15import warnings
16warnings.warn("contrib.charmhelpers is deprecated", DeprecationWarning) # noqa
17
18import operator
19import tempfile
20import time
21import yaml
22import subprocess
23
24import six
25if six.PY3:
26 from urllib.request import urlopen
27 from urllib.error import (HTTPError, URLError)
28else:
29 from urllib2 import (urlopen, HTTPError, URLError)
30
31"""Helper functions for writing Juju charms in Python."""
32
33__metaclass__ = type
34__all__ = [
35 # 'get_config', # core.hookenv.config()
36 # 'log', # core.hookenv.log()
37 # 'log_entry', # core.hookenv.log()
38 # 'log_exit', # core.hookenv.log()
39 # 'relation_get', # core.hookenv.relation_get()
40 # 'relation_set', # core.hookenv.relation_set()
41 # 'relation_ids', # core.hookenv.relation_ids()
42 # 'relation_list', # core.hookenv.relation_units()
43 # 'config_get', # core.hookenv.config()
44 # 'unit_get', # core.hookenv.unit_get()
45 # 'open_port', # core.hookenv.open_port()
46 # 'close_port', # core.hookenv.close_port()
47 # 'service_control', # core.host.service()
48 'unit_info', # client-side, NOT IMPLEMENTED
49 'wait_for_machine', # client-side, NOT IMPLEMENTED
50 'wait_for_page_contents', # client-side, NOT IMPLEMENTED
51 'wait_for_relation', # client-side, NOT IMPLEMENTED
52 'wait_for_unit', # client-side, NOT IMPLEMENTED
53]
54
55
56SLEEP_AMOUNT = 0.1
57
58
59# We create a juju_status Command here because it makes testing much,
60# much easier.
61def juju_status():
62 subprocess.check_call(['juju', 'status'])
63
64# re-implemented as charmhelpers.fetch.configure_sources()
65# def configure_source(update=False):
66# source = config_get('source')
67# if ((source.startswith('ppa:') or
68# source.startswith('cloud:') or
69# source.startswith('http:'))):
70# run('add-apt-repository', source)
71# if source.startswith("http:"):
72# run('apt-key', 'import', config_get('key'))
73# if update:
74# run('apt-get', 'update')
75
76
77# DEPRECATED: client-side only
78def make_charm_config_file(charm_config):
79 charm_config_file = tempfile.NamedTemporaryFile(mode='w+')
80 charm_config_file.write(yaml.dump(charm_config))
81 charm_config_file.flush()
82 # The NamedTemporaryFile instance is returned instead of just the name
83 # because we want to take advantage of garbage collection-triggered
84 # deletion of the temp file when it goes out of scope in the caller.
85 return charm_config_file
86
87
88# DEPRECATED: client-side only
89def unit_info(service_name, item_name, data=None, unit=None):
90 if data is None:
91 data = yaml.safe_load(juju_status())
92 service = data['services'].get(service_name)
93 if service is None:
94 # XXX 2012-02-08 gmb:
95 # This allows us to cope with the race condition that we
96 # have between deploying a service and having it come up in
97 # `juju status`. We could probably do with cleaning it up so
98 # that it fails a bit more noisily after a while.
99 return ''
100 units = service['units']
101 if unit is not None:
102 item = units[unit][item_name]
103 else:
104 # It might seem odd to sort the units here, but we do it to
105 # ensure that when no unit is specified, the first unit for the
106 # service (or at least the one with the lowest number) is the
107 # one whose data gets returned.
108 sorted_unit_names = sorted(units.keys())
109 item = units[sorted_unit_names[0]][item_name]
110 return item
111
112
113# DEPRECATED: client-side only
114def get_machine_data():
115 return yaml.safe_load(juju_status())['machines']
116
117
118# DEPRECATED: client-side only
119def wait_for_machine(num_machines=1, timeout=300):
120 """Wait `timeout` seconds for `num_machines` machines to come up.
121
122 This wait_for... function can be called by other wait_for functions
123 whose timeouts might be too short in situations where only a bare
124 Juju setup has been bootstrapped.
125
126 :return: A tuple of (num_machines, time_taken). This is used for
127 testing.
128 """
129 # You may think this is a hack, and you'd be right. The easiest way
130 # to tell what environment we're working in (LXC vs EC2) is to check
131 # the dns-name of the first machine. If it's localhost we're in LXC
132 # and we can just return here.
133 if get_machine_data()[0]['dns-name'] == 'localhost':
134 return 1, 0
135 start_time = time.time()
136 while True:
137 # Drop the first machine, since it's the Zookeeper and that's
138 # not a machine that we need to wait for. This will only work
139 # for EC2 environments, which is why we return early above if
140 # we're in LXC.
141 machine_data = get_machine_data()
142 non_zookeeper_machines = [
143 machine_data[key] for key in list(machine_data.keys())[1:]]
144 if len(non_zookeeper_machines) >= num_machines:
145 all_machines_running = True
146 for machine in non_zookeeper_machines:
147 if machine.get('instance-state') != 'running':
148 all_machines_running = False
149 break
150 if all_machines_running:
151 break
152 if time.time() - start_time >= timeout:
153 raise RuntimeError('timeout waiting for service to start')
154 time.sleep(SLEEP_AMOUNT)
155 return num_machines, time.time() - start_time
156
157
158# DEPRECATED: client-side only
159def wait_for_unit(service_name, timeout=480):
160 """Wait `timeout` seconds for a given service name to come up."""
161 wait_for_machine(num_machines=1)
162 start_time = time.time()
163 while True:
164 state = unit_info(service_name, 'agent-state')
165 if 'error' in state or state == 'started':
166 break
167 if time.time() - start_time >= timeout:
168 raise RuntimeError('timeout waiting for service to start')
169 time.sleep(SLEEP_AMOUNT)
170 if state != 'started':
171 raise RuntimeError('unit did not start, agent-state: ' + state)
172
173
174# DEPRECATED: client-side only
175def wait_for_relation(service_name, relation_name, timeout=120):
176 """Wait `timeout` seconds for a given relation to come up."""
177 start_time = time.time()
178 while True:
179 relation = unit_info(service_name, 'relations').get(relation_name)
180 if relation is not None and relation['state'] == 'up':
181 break
182 if time.time() - start_time >= timeout:
183 raise RuntimeError('timeout waiting for relation to be up')
184 time.sleep(SLEEP_AMOUNT)
185
186
187# DEPRECATED: client-side only
188def wait_for_page_contents(url, contents, timeout=120, validate=None):
189 if validate is None:
190 validate = operator.contains
191 start_time = time.time()
192 while True:
193 try:
194 stream = urlopen(url)
195 except (HTTPError, URLError):
196 pass
197 else:
198 page = stream.read()
199 if validate(page, contents):
200 return page
201 if time.time() - start_time >= timeout:
202 raise RuntimeError('timeout waiting for contents of ' + url)
203 time.sleep(SLEEP_AMOUNT)
diff --git a/hooks/charmhelpers/contrib/charmsupport/IMPORT b/hooks/charmhelpers/contrib/charmsupport/IMPORT
204deleted file mode 1006440deleted file mode 100644
index 554fddd..0000000
--- a/hooks/charmhelpers/contrib/charmsupport/IMPORT
+++ /dev/null
@@ -1,14 +0,0 @@
1Source: lp:charmsupport/trunk
2
3charmsupport/charmsupport/execd.py -> charm-helpers/charmhelpers/contrib/charmsupport/execd.py
4charmsupport/charmsupport/hookenv.py -> charm-helpers/charmhelpers/contrib/charmsupport/hookenv.py
5charmsupport/charmsupport/host.py -> charm-helpers/charmhelpers/contrib/charmsupport/host.py
6charmsupport/charmsupport/nrpe.py -> charm-helpers/charmhelpers/contrib/charmsupport/nrpe.py
7charmsupport/charmsupport/volumes.py -> charm-helpers/charmhelpers/contrib/charmsupport/volumes.py
8
9charmsupport/tests/test_execd.py -> charm-helpers/tests/contrib/charmsupport/test_execd.py
10charmsupport/tests/test_hookenv.py -> charm-helpers/tests/contrib/charmsupport/test_hookenv.py
11charmsupport/tests/test_host.py -> charm-helpers/tests/contrib/charmsupport/test_host.py
12charmsupport/tests/test_nrpe.py -> charm-helpers/tests/contrib/charmsupport/test_nrpe.py
13
14charmsupport/bin/charmsupport -> charm-helpers/bin/contrib/charmsupport/charmsupport
diff --git a/hooks/charmhelpers/contrib/charmsupport/__init__.py b/hooks/charmhelpers/contrib/charmsupport/__init__.py
15deleted file mode 1006440deleted file mode 100644
index d7567b8..0000000
--- a/hooks/charmhelpers/contrib/charmsupport/__init__.py
+++ /dev/null
@@ -1,13 +0,0 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
diff --git a/hooks/charmhelpers/contrib/charmsupport/nrpe.py b/hooks/charmhelpers/contrib/charmsupport/nrpe.py
14deleted file mode 1006440deleted file mode 100644
index e3d10c1..0000000
--- a/hooks/charmhelpers/contrib/charmsupport/nrpe.py
+++ /dev/null
@@ -1,450 +0,0 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""Compatibility with the nrpe-external-master charm"""
16# Copyright 2012 Canonical Ltd.
17#
18# Authors:
19# Matthew Wedgwood <matthew.wedgwood@canonical.com>
20
21import subprocess
22import pwd
23import grp
24import os
25import glob
26import shutil
27import re
28import shlex
29import yaml
30
31from charmhelpers.core.hookenv import (
32 config,
33 hook_name,
34 local_unit,
35 log,
36 relation_ids,
37 relation_set,
38 relations_of_type,
39)
40
41from charmhelpers.core.host import service
42from charmhelpers.core import host
43
44# This module adds compatibility with the nrpe-external-master and plain nrpe
45# subordinate charms. To use it in your charm:
46#
47# 1. Update metadata.yaml
48#
49# provides:
50# (...)
51# nrpe-external-master:
52# interface: nrpe-external-master
53# scope: container
54#
55# and/or
56#
57# provides:
58# (...)
59# local-monitors:
60# interface: local-monitors
61# scope: container
62
63#
64# 2. Add the following to config.yaml
65#
66# nagios_context:
67# default: "juju"
68# type: string
69# description: |
70# Used by the nrpe subordinate charms.
71# A string that will be prepended to instance name to set the host name
72# in nagios. So for instance the hostname would be something like:
73# juju-myservice-0
74# If you're running multiple environments with the same services in them
75# this allows you to differentiate between them.
76# nagios_servicegroups:
77# default: ""
78# type: string
79# description: |
80# A comma-separated list of nagios servicegroups.
81# If left empty, the nagios_context will be used as the servicegroup
82#
83# 3. Add custom checks (Nagios plugins) to files/nrpe-external-master
84#
85# 4. Update your hooks.py with something like this:
86#
87# from charmsupport.nrpe import NRPE
88# (...)
89# def update_nrpe_config():
90# nrpe_compat = NRPE()
91# nrpe_compat.add_check(
92# shortname = "myservice",
93# description = "Check MyService",
94# check_cmd = "check_http -w 2 -c 10 http://localhost"
95# )
96# nrpe_compat.add_check(
97# "myservice_other",
98# "Check for widget failures",
99# check_cmd = "/srv/myapp/scripts/widget_check"
100# )
101# nrpe_compat.write()
102#
103# def config_changed():
104# (...)
105# update_nrpe_config()
106#
107# def nrpe_external_master_relation_changed():
108# update_nrpe_config()
109#
110# def local_monitors_relation_changed():
111# update_nrpe_config()
112#
113# 4.a If your charm is a subordinate charm set primary=False
114#
115# from charmsupport.nrpe import NRPE
116# (...)
117# def update_nrpe_config():
118# nrpe_compat = NRPE(primary=False)
119#
120# 5. ln -s hooks.py nrpe-external-master-relation-changed
121# ln -s hooks.py local-monitors-relation-changed
122
123
124class CheckException(Exception):
125 pass
126
127
128class Check(object):
129 shortname_re = '[A-Za-z0-9-_.]+$'
130 service_template = ("""
131#---------------------------------------------------
132# This file is Juju managed
133#---------------------------------------------------
134define service {{
135 use active-service
136 host_name {nagios_hostname}
137 service_description {nagios_hostname}[{shortname}] """
138 """{description}
139 check_command check_nrpe!{command}
140 servicegroups {nagios_servicegroup}
141}}
142""")
143
144 def __init__(self, shortname, description, check_cmd):
145 super(Check, self).__init__()
146 # XXX: could be better to calculate this from the service name
147 if not re.match(self.shortname_re, shortname):
148 raise CheckException("shortname must match {}".format(
149 Check.shortname_re))
150 self.shortname = shortname
151 self.command = "check_{}".format(shortname)
152 # Note: a set of invalid characters is defined by the
153 # Nagios server config
154 # The default is: illegal_object_name_chars=`~!$%^&*"|'<>?,()=
155 self.description = description
156 self.check_cmd = self._locate_cmd(check_cmd)
157
158 def _get_check_filename(self):
159 return os.path.join(NRPE.nrpe_confdir, '{}.cfg'.format(self.command))
160
161 def _get_service_filename(self, hostname):
162 return os.path.join(NRPE.nagios_exportdir,
163 'service__{}_{}.cfg'.format(hostname, self.command))
164
165 def _locate_cmd(self, check_cmd):
166 search_path = (
167 '/usr/lib/nagios/plugins',
168 '/usr/local/lib/nagios/plugins',
169 )
170 parts = shlex.split(check_cmd)
171 for path in search_path:
172 if os.path.exists(os.path.join(path, parts[0])):
173 command = os.path.join(path, parts[0])
174 if len(parts) > 1:
175 command += " " + " ".join(parts[1:])
176 return command
177 log('Check command not found: {}'.format(parts[0]))
178 return ''
179
180 def _remove_service_files(self):
181 if not os.path.exists(NRPE.nagios_exportdir):
182 return
183 for f in os.listdir(NRPE.nagios_exportdir):
184 if f.endswith('_{}.cfg'.format(self.command)):
185 os.remove(os.path.join(NRPE.nagios_exportdir, f))
186
187 def remove(self, hostname):
188 nrpe_check_file = self._get_check_filename()
189 if os.path.exists(nrpe_check_file):
190 os.remove(nrpe_check_file)
191 self._remove_service_files()
192
193 def write(self, nagios_context, hostname, nagios_servicegroups):
194 nrpe_check_file = self._get_check_filename()
195 with open(nrpe_check_file, 'w') as nrpe_check_config:
196 nrpe_check_config.write("# check {}\n".format(self.shortname))
197 if nagios_servicegroups:
198 nrpe_check_config.write(
199 "# The following header was added automatically by juju\n")
200 nrpe_check_config.write(
201 "# Modifying it will affect nagios monitoring and alerting\n")
202 nrpe_check_config.write(
203 "# servicegroups: {}\n".format(nagios_servicegroups))
204 nrpe_check_config.write("command[{}]={}\n".format(
205 self.command, self.check_cmd))
206
207 if not os.path.exists(NRPE.nagios_exportdir):
208 log('Not writing service config as {} is not accessible'.format(
209 NRPE.nagios_exportdir))
210 else:
211 self.write_service_config(nagios_context, hostname,
212 nagios_servicegroups)
213
214 def write_service_config(self, nagios_context, hostname,
215 nagios_servicegroups):
216 self._remove_service_files()
217
218 templ_vars = {
219 'nagios_hostname': hostname,
220 'nagios_servicegroup': nagios_servicegroups,
221 'description': self.description,
222 'shortname': self.shortname,
223 'command': self.command,
224 }
225 nrpe_service_text = Check.service_template.format(**templ_vars)
226 nrpe_service_file = self._get_service_filename(hostname)
227 with open(nrpe_service_file, 'w') as nrpe_service_config:
228 nrpe_service_config.write(str(nrpe_service_text))
229
230 def run(self):
231 subprocess.call(self.check_cmd)
232
233
234class NRPE(object):
235 nagios_logdir = '/var/log/nagios'
236 nagios_exportdir = '/var/lib/nagios/export'
237 nrpe_confdir = '/etc/nagios/nrpe.d'
238 homedir = '/var/lib/nagios' # home dir provided by nagios-nrpe-server
239
240 def __init__(self, hostname=None, primary=True):
241 super(NRPE, self).__init__()
242 self.config = config()
243 self.primary = primary
244 self.nagios_context = self.config['nagios_context']
245 if 'nagios_servicegroups' in self.config and self.config['nagios_servicegroups']:
246 self.nagios_servicegroups = self.config['nagios_servicegroups']
247 else:
248 self.nagios_servicegroups = self.nagios_context
249 self.unit_name = local_unit().replace('/', '-')
250 if hostname:
251 self.hostname = hostname
252 else:
253 nagios_hostname = get_nagios_hostname()
254 if nagios_hostname:
255 self.hostname = nagios_hostname
256 else:
257 self.hostname = "{}-{}".format(self.nagios_context, self.unit_name)
258 self.checks = []
259 # Iff in an nrpe-external-master relation hook, set primary status
260 relation = relation_ids('nrpe-external-master')
261 if relation:
262 log("Setting charm primary status {}".format(primary))
263 for rid in relation_ids('nrpe-external-master'):
264 relation_set(relation_id=rid, relation_settings={'primary': self.primary})
265
266 def add_check(self, *args, **kwargs):
267 self.checks.append(Check(*args, **kwargs))
268
269 def remove_check(self, *args, **kwargs):
270 if kwargs.get('shortname') is None:
271 raise ValueError('shortname of check must be specified')
272
273 # Use sensible defaults if they're not specified - these are not
274 # actually used during removal, but they're required for constructing
275 # the Check object; check_disk is chosen because it's part of the
276 # nagios-plugins-basic package.
277 if kwargs.get('check_cmd') is None:
278 kwargs['check_cmd'] = 'check_disk'
279 if kwargs.get('description') is None:
280 kwargs['description'] = ''
281
282 check = Check(*args, **kwargs)
283 check.remove(self.hostname)
284
285 def write(self):
286 try:
287 nagios_uid = pwd.getpwnam('nagios').pw_uid
288 nagios_gid = grp.getgrnam('nagios').gr_gid
289 except Exception:
290 log("Nagios user not set up, nrpe checks not updated")
291 return
292
293 if not os.path.exists(NRPE.nagios_logdir):
294 os.mkdir(NRPE.nagios_logdir)
295 os.chown(NRPE.nagios_logdir, nagios_uid, nagios_gid)
296
297 nrpe_monitors = {}
298 monitors = {"monitors": {"remote": {"nrpe": nrpe_monitors}}}
299 for nrpecheck in self.checks:
300 nrpecheck.write(self.nagios_context, self.hostname,
301 self.nagios_servicegroups)
302 nrpe_monitors[nrpecheck.shortname] = {
303 "command": nrpecheck.command,
304 }
305
306 # update-status hooks are configured to firing every 5 minutes by
307 # default. When nagios-nrpe-server is restarted, the nagios server
308 # reports checks failing causing unneccessary alerts. Let's not restart
309 # on update-status hooks.
310 if not hook_name() == 'update-status':
311 service('restart', 'nagios-nrpe-server')
312
313 monitor_ids = relation_ids("local-monitors") + \
314 relation_ids("nrpe-external-master")
315 for rid in monitor_ids:
316 relation_set(relation_id=rid, monitors=yaml.dump(monitors))
317
318
319def get_nagios_hostcontext(relation_name='nrpe-external-master'):
320 """
321 Query relation with nrpe subordinate, return the nagios_host_context
322
323 :param str relation_name: Name of relation nrpe sub joined to
324 """
325 for rel in relations_of_type(relation_name):
326 if 'nagios_host_context' in rel:
327 return rel['nagios_host_context']
328
329
330def get_nagios_hostname(relation_name='nrpe-external-master'):
331 """
332 Query relation with nrpe subordinate, return the nagios_hostname
333
334 :param str relation_name: Name of relation nrpe sub joined to
335 """
336 for rel in relations_of_type(relation_name):
337 if 'nagios_hostname' in rel:
338 return rel['nagios_hostname']
339
340
341def get_nagios_unit_name(relation_name='nrpe-external-master'):
342 """
343 Return the nagios unit name prepended with host_context if needed
344
345 :param str relation_name: Name of relation nrpe sub joined to
346 """
347 host_context = get_nagios_hostcontext(relation_name)
348 if host_context:
349 unit = "%s:%s" % (host_context, local_unit())
350 else:
351 unit = local_unit()
352 return unit
353
354
355def add_init_service_checks(nrpe, services, unit_name, immediate_check=True):
356 """
357 Add checks for each service in list
358
359 :param NRPE nrpe: NRPE object to add check to
360 :param list services: List of services to check
361 :param str unit_name: Unit name to use in check description
362 :param bool immediate_check: For sysv init, run the service check immediately
363 """
364 for svc in services:
365 # Don't add a check for these services from neutron-gateway
366 if svc in ['ext-port', 'os-charm-phy-nic-mtu']:
367 next
368
369 upstart_init = '/etc/init/%s.conf' % svc
370 sysv_init = '/etc/init.d/%s' % svc
371
372 if host.init_is_systemd():
373 nrpe.add_check(
374 shortname=svc,
375 description='process check {%s}' % unit_name,
376 check_cmd='check_systemd.py %s' % svc
377 )
378 elif os.path.exists(upstart_init):
379 nrpe.add_check(
380 shortname=svc,
381 description='process check {%s}' % unit_name,
382 check_cmd='check_upstart_job %s' % svc
383 )
384 elif os.path.exists(sysv_init):
385 cronpath = '/etc/cron.d/nagios-service-check-%s' % svc
386 checkpath = '%s/service-check-%s.txt' % (nrpe.homedir, svc)
387 croncmd = (
388 '/usr/local/lib/nagios/plugins/check_exit_status.pl '
389 '-e -s /etc/init.d/%s status' % svc
390 )
391 cron_file = '*/5 * * * * root %s > %s\n' % (croncmd, checkpath)
392 f = open(cronpath, 'w')
393 f.write(cron_file)
394 f.close()
395 nrpe.add_check(
396 shortname=svc,
397 description='service check {%s}' % unit_name,
398 check_cmd='check_status_file.py -f %s' % checkpath,
399 )
400 # if /var/lib/nagios doesn't exist open(checkpath, 'w') will fail
401 # (LP: #1670223).
402 if immediate_check and os.path.isdir(nrpe.homedir):
403 f = open(checkpath, 'w')
404 subprocess.call(
405 croncmd.split(),
406 stdout=f,
407 stderr=subprocess.STDOUT
408 )
409 f.close()
410 os.chmod(checkpath, 0o644)
411
412
413def copy_nrpe_checks(nrpe_files_dir=None):
414 """
415 Copy the nrpe checks into place
416
417 """
418 NAGIOS_PLUGINS = '/usr/local/lib/nagios/plugins'
419 default_nrpe_files_dir = os.path.join(
420 os.getenv('CHARM_DIR'),
421 'hooks',
422 'charmhelpers',
423 'contrib',
424 'openstack',
425 'files')
426 if not nrpe_files_dir:
427 nrpe_files_dir = default_nrpe_files_dir
428 if not os.path.exists(NAGIOS_PLUGINS):
429 os.makedirs(NAGIOS_PLUGINS)
430 for fname in glob.glob(os.path.join(nrpe_files_dir, "check_*")):
431 if os.path.isfile(fname):
432 shutil.copy2(fname,
433 os.path.join(NAGIOS_PLUGINS, os.path.basename(fname)))
434
435
436def add_haproxy_checks(nrpe, unit_name):
437 """
438 Add checks for each service in list
439
440 :param NRPE nrpe: NRPE object to add check to
441 :param str unit_name: Unit name to use in check description
442 """
443 nrpe.add_check(
444 shortname='haproxy_servers',
445 description='Check HAProxy {%s}' % unit_name,
446 check_cmd='check_haproxy.sh')
447 nrpe.add_check(
448 shortname='haproxy_queue',
449 description='Check HAProxy queue depth {%s}' % unit_name,
450 check_cmd='check_haproxy_queue_depth.sh')
diff --git a/hooks/charmhelpers/contrib/charmsupport/volumes.py b/hooks/charmhelpers/contrib/charmsupport/volumes.py
451deleted file mode 1006440deleted file mode 100644
index 7ea43f0..0000000
--- a/hooks/charmhelpers/contrib/charmsupport/volumes.py
+++ /dev/null
@@ -1,173 +0,0 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15'''
16Functions for managing volumes in juju units. One volume is supported per unit.
17Subordinates may have their own storage, provided it is on its own partition.
18
19Configuration stanzas::
20
21 volume-ephemeral:
22 type: boolean
23 default: true
24 description: >
25 If false, a volume is mounted as sepecified in "volume-map"
26 If true, ephemeral storage will be used, meaning that log data
27 will only exist as long as the machine. YOU HAVE BEEN WARNED.
28 volume-map:
29 type: string
30 default: {}
31 description: >
32 YAML map of units to device names, e.g:
33 "{ rsyslog/0: /dev/vdb, rsyslog/1: /dev/vdb }"
34 Service units will raise a configure-error if volume-ephemeral
35 is 'true' and no volume-map value is set. Use 'juju set' to set a
36 value and 'juju resolved' to complete configuration.
37
38Usage::
39
40 from charmsupport.volumes import configure_volume, VolumeConfigurationError
41 from charmsupport.hookenv import log, ERROR
42 def post_mount_hook():
43 stop_service('myservice')
44 def post_mount_hook():
45 start_service('myservice')
46
47 if __name__ == '__main__':
48 try:
49 configure_volume(before_change=pre_mount_hook,
50 after_change=post_mount_hook)
51 except VolumeConfigurationError:
52 log('Storage could not be configured', ERROR)
53
54'''
55
56# XXX: Known limitations
57# - fstab is neither consulted nor updated
58
59import os
60from charmhelpers.core import hookenv
61from charmhelpers.core import host
62import yaml
63
64
65MOUNT_BASE = '/srv/juju/volumes'
66
67
68class VolumeConfigurationError(Exception):
69 '''Volume configuration data is missing or invalid'''
70 pass
71
72
73def get_config():
74 '''Gather and sanity-check volume configuration data'''
75 volume_config = {}
76 config = hookenv.config()
77
78 errors = False
79
80 if config.get('volume-ephemeral') in (True, 'True', 'true', 'Yes', 'yes'):
81 volume_config['ephemeral'] = True
82 else:
83 volume_config['ephemeral'] = False
84
85 try:
86 volume_map = yaml.safe_load(config.get('volume-map', '{}'))
87 except yaml.YAMLError as e:
88 hookenv.log("Error parsing YAML volume-map: {}".format(e),
89 hookenv.ERROR)
90 errors = True
91 if volume_map is None:
92 # probably an empty string
93 volume_map = {}
94 elif not isinstance(volume_map, dict):
95 hookenv.log("Volume-map should be a dictionary, not {}".format(
96 type(volume_map)))
97 errors = True
98
99 volume_config['device'] = volume_map.get(os.environ['JUJU_UNIT_NAME'])
100 if volume_config['device'] and volume_config['ephemeral']:
101 # asked for ephemeral storage but also defined a volume ID
102 hookenv.log('A volume is defined for this unit, but ephemeral '
103 'storage was requested', hookenv.ERROR)
104 errors = True
105 elif not volume_config['device'] and not volume_config['ephemeral']:
106 # asked for permanent storage but did not define volume ID
107 hookenv.log('Ephemeral storage was requested, but there is no volume '
108 'defined for this unit.', hookenv.ERROR)
109 errors = True
110
111 unit_mount_name = hookenv.local_unit().replace('/', '-')
112 volume_config['mountpoint'] = os.path.join(MOUNT_BASE, unit_mount_name)
113
114 if errors:
115 return None
116 return volume_config
117
118
119def mount_volume(config):
120 if os.path.exists(config['mountpoint']):
121 if not os.path.isdir(config['mountpoint']):
122 hookenv.log('Not a directory: {}'.format(config['mountpoint']))
123 raise VolumeConfigurationError()
124 else:
125 host.mkdir(config['mountpoint'])
126 if os.path.ismount(config['mountpoint']):
127 unmount_volume(config)
128 if not host.mount(config['device'], config['mountpoint'], persist=True):
129 raise VolumeConfigurationError()
130
131
132def unmount_volume(config):
133 if os.path.ismount(config['mountpoint']):
134 if not host.umount(config['mountpoint'], persist=True):
135 raise VolumeConfigurationError()
136
137
138def managed_mounts():
139 '''List of all mounted managed volumes'''
140 return filter(lambda mount: mount[0].startswith(MOUNT_BASE), host.mounts())
141
142
143def configure_volume(before_change=lambda: None, after_change=lambda: None):
144 '''Set up storage (or don't) according to the charm's volume configuration.
145 Returns the mount point or "ephemeral". before_change and after_change
146 are optional functions to be called if the volume configuration changes.
147 '''
148
149 config = get_config()
150 if not config:
151 hookenv.log('Failed to read volume configuration', hookenv.CRITICAL)
152 raise VolumeConfigurationError()
153
154 if config['ephemeral']:
155 if os.path.ismount(config['mountpoint']):
156 before_change()
157 unmount_volume(config)
158 after_change()
159 return 'ephemeral'
160 else:
161 # persistent storage
162 if os.path.ismount(config['mountpoint']):
163 mounts = dict(managed_mounts())
164 if mounts.get(config['mountpoint']) != config['device']:
165 before_change()
166 unmount_volume(config)
167 mount_volume(config)
168 after_change()
169 else:
170 before_change()
171 mount_volume(config)
172 after_change()
173 return config['mountpoint']
diff --git a/hooks/charmhelpers/contrib/database/__init__.py b/hooks/charmhelpers/contrib/database/__init__.py
174deleted file mode 1006440deleted file mode 100644
index 64fac9d..0000000
--- a/hooks/charmhelpers/contrib/database/__init__.py
+++ /dev/null
@@ -1,11 +0,0 @@
1# Licensed under the Apache License, Version 2.0 (the "License");
2# you may not use this file except in compliance with the License.
3# You may obtain a copy of the License at
4#
5# http://www.apache.org/licenses/LICENSE-2.0
6#
7# Unless required by applicable law or agreed to in writing, software
8# distributed under the License is distributed on an "AS IS" BASIS,
9# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10# See the License for the specific language governing permissions and
11# limitations under the License.
diff --git a/hooks/charmhelpers/contrib/database/mysql.py b/hooks/charmhelpers/contrib/database/mysql.py
12deleted file mode 1006440deleted file mode 100644
index e5494c1..0000000
--- a/hooks/charmhelpers/contrib/database/mysql.py
+++ /dev/null
@@ -1,575 +0,0 @@
1# Licensed under the Apache License, Version 2.0 (the "License");
2# you may not use this file except in compliance with the License.
3# You may obtain a copy of the License at
4#
5# http://www.apache.org/licenses/LICENSE-2.0
6#
7# Unless required by applicable law or agreed to in writing, software
8# distributed under the License is distributed on an "AS IS" BASIS,
9# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10# See the License for the specific language governing permissions and
11# limitations under the License.
12
13"""Helper for working with a MySQL database"""
14import json
15import re
16import sys
17import platform
18import os
19import glob
20import six
21
22# from string import upper
23
24from charmhelpers.core.host import (
25 CompareHostReleases,
26 lsb_release,
27 mkdir,
28 pwgen,
29 write_file
30)
31from charmhelpers.core.hookenv import (
32 config as config_get,
33 relation_get,
34 related_units,
35 unit_get,
36 log,
37 DEBUG,
38 INFO,
39 WARNING,
40 leader_get,
41 leader_set,
42 is_leader,
43)
44from charmhelpers.fetch import (
45 apt_install,
46 apt_update,
47 filter_installed_packages,
48)
49from charmhelpers.contrib.network.ip import get_host_ip
50
51try:
52 import MySQLdb
53except ImportError:
54 apt_update(fatal=True)
55 if six.PY2:
56 apt_install(filter_installed_packages(['python-mysqldb']), fatal=True)
57 else:
58 apt_install(filter_installed_packages(['python3-mysqldb']), fatal=True)
59 import MySQLdb
60
61
62class MySQLSetPasswordError(Exception):
63 pass
64
65
66class MySQLHelper(object):
67
68 def __init__(self, rpasswdf_template, upasswdf_template, host='localhost',
69 migrate_passwd_to_leader_storage=True,
70 delete_ondisk_passwd_file=True):
71 self.host = host
72 # Password file path templates
73 self.root_passwd_file_template = rpasswdf_template
74 self.user_passwd_file_template = upasswdf_template
75
76 self.migrate_passwd_to_leader_storage = migrate_passwd_to_leader_storage
77 # If we migrate we have the option to delete local copy of root passwd
78 self.delete_ondisk_passwd_file = delete_ondisk_passwd_file
79 self.connection = None
80
81 def connect(self, user='root', password=None):
82 log("Opening db connection for %s@%s" % (user, self.host), level=DEBUG)
83 self.connection = MySQLdb.connect(user=user, host=self.host,
84 passwd=password)
85
86 def database_exists(self, db_name):
87 cursor = self.connection.cursor()
88 try:
89 cursor.execute("SHOW DATABASES")
90 databases = [i[0] for i in cursor.fetchall()]
91 finally:
92 cursor.close()
93
94 return db_name in databases
95
96 def create_database(self, db_name):
97 cursor = self.connection.cursor()
98 try:
99 cursor.execute("CREATE DATABASE `{}` CHARACTER SET UTF8"
100 .format(db_name))
101 finally:
102 cursor.close()
103
104 def grant_exists(self, db_name, db_user, remote_ip):
105 cursor = self.connection.cursor()
106 priv_string = "GRANT ALL PRIVILEGES ON `{}`.* " \
107 "TO '{}'@'{}'".format(db_name, db_user, remote_ip)
108 try:
109 cursor.execute("SHOW GRANTS for '{}'@'{}'".format(db_user,
110 remote_ip))
111 grants = [i[0] for i in cursor.fetchall()]
112 except MySQLdb.OperationalError:
113 return False
114 finally:
115 cursor.close()
116
117 # TODO: review for different grants
118 return priv_string in grants
119
120 def create_grant(self, db_name, db_user, remote_ip, password):
121 cursor = self.connection.cursor()
122 try:
123 # TODO: review for different grants
124 cursor.execute("GRANT ALL PRIVILEGES ON `{}`.* TO '{}'@'{}' "
125 "IDENTIFIED BY '{}'".format(db_name,
126 db_user,
127 remote_ip,
128 password))
129 finally:
130 cursor.close()
131
132 def create_admin_grant(self, db_user, remote_ip, password):
133 cursor = self.connection.cursor()
134 try:
135 cursor.execute("GRANT ALL PRIVILEGES ON *.* TO '{}'@'{}' "
136 "IDENTIFIED BY '{}'".format(db_user,
137 remote_ip,
138 password))
139 finally:
140 cursor.close()
141
142 def cleanup_grant(self, db_user, remote_ip):
143 cursor = self.connection.cursor()
144 try:
145 cursor.execute("DROP FROM mysql.user WHERE user='{}' "
146 "AND HOST='{}'".format(db_user,
147 remote_ip))
148 finally:
149 cursor.close()
150
151 def flush_priviledges(self):
152 cursor = self.connection.cursor()
153 try:
154 cursor.execute("FLUSH PRIVILEGES")
155 finally:
156 cursor.close()
157
158 def execute(self, sql):
159 """Execute arbitary SQL against the database."""
160 cursor = self.connection.cursor()
161 try:
162 cursor.execute(sql)
163 finally:
164 cursor.close()
165
166 def select(self, sql):
167 """
168 Execute arbitrary SQL select query against the database
169 and return the results.
170
171 :param sql: SQL select query to execute
172 :type sql: string
173 :returns: SQL select query result
174 :rtype: list of lists
175 :raises: MySQLdb.Error
176 """
177 cursor = self.connection.cursor()
178 try:
179 cursor.execute(sql)
180 results = [list(i) for i in cursor.fetchall()]
181 finally:
182 cursor.close()
183 return results
184
185 def migrate_passwords_to_leader_storage(self, excludes=None):
186 """Migrate any passwords storage on disk to leader storage."""
187 if not is_leader():
188 log("Skipping password migration as not the lead unit",
189 level=DEBUG)
190 return
191 dirname = os.path.dirname(self.root_passwd_file_template)
192 path = os.path.join(dirname, '*.passwd')
193 for f in glob.glob(path):
194 if excludes and f in excludes:
195 log("Excluding %s from leader storage migration" % (f),
196 level=DEBUG)
197 continue
198
199 key = os.path.basename(f)
200 with open(f, 'r') as passwd:
201 _value = passwd.read().strip()
202
203 try:
204 leader_set(settings={key: _value})
205
206 if self.delete_ondisk_passwd_file:
207 os.unlink(f)
208 except ValueError:
209 # NOTE cluster relation not yet ready - skip for now
210 pass
211
212 def get_mysql_password_on_disk(self, username=None, password=None):
213 """Retrieve, generate or store a mysql password for the provided
214 username on disk."""
215 if username:
216 template = self.user_passwd_file_template
217 passwd_file = template.format(username)
218 else:
219 passwd_file = self.root_passwd_file_template
220
221 _password = None
222 if os.path.exists(passwd_file):
223 log("Using existing password file '%s'" % passwd_file, level=DEBUG)
224 with open(passwd_file, 'r') as passwd:
225 _password = passwd.read().strip()
226 else:
227 log("Generating new password file '%s'" % passwd_file, level=DEBUG)
228 if not os.path.isdir(os.path.dirname(passwd_file)):
229 # NOTE: need to ensure this is not mysql root dir (which needs
230 # to be mysql readable)
231 mkdir(os.path.dirname(passwd_file), owner='root', group='root',
232 perms=0o770)
233 # Force permissions - for some reason the chmod in makedirs
234 # fails
235 os.chmod(os.path.dirname(passwd_file), 0o770)
236
237 _password = password or pwgen(length=32)
238 write_file(passwd_file, _password, owner='root', group='root',
239 perms=0o660)
240
241 return _password
242
243 def passwd_keys(self, username):
244 """Generator to return keys used to store passwords in peer store.
245
246 NOTE: we support both legacy and new format to support mysql
247 charm prior to refactor. This is necessary to avoid LP 1451890.
248 """
249 keys = []
250 if username == 'mysql':
251 log("Bad username '%s'" % (username), level=WARNING)
252
253 if username:
254 # IMPORTANT: *newer* format must be returned first
255 keys.append('mysql-%s.passwd' % (username))
256 keys.append('%s.passwd' % (username))
257 else:
258 keys.append('mysql.passwd')
259
260 for key in keys:
261 yield key
262
263 def get_mysql_password(self, username=None, password=None):
264 """Retrieve, generate or store a mysql password for the provided
265 username using peer relation cluster."""
266 excludes = []
267
268 # First check peer relation.
269 try:
270 for key in self.passwd_keys(username):
271 _password = leader_get(key)
272 if _password:
273 break
274
275 # If root password available don't update peer relation from local
276 if _password and not username:
277 excludes.append(self.root_passwd_file_template)
278
279 except ValueError:
280 # cluster relation is not yet started; use on-disk
281 _password = None
282
283 # If none available, generate new one
284 if not _password:
285 _password = self.get_mysql_password_on_disk(username, password)
286
287 # Put on wire if required
288 if self.migrate_passwd_to_leader_storage:
289 self.migrate_passwords_to_leader_storage(excludes=excludes)
290
291 return _password
292
293 def get_mysql_root_password(self, password=None):
294 """Retrieve or generate mysql root password for service units."""
295 return self.get_mysql_password(username=None, password=password)
296
297 def set_mysql_password(self, username, password):
298 """Update a mysql password for the provided username changing the
299 leader settings
300
301 To update root's password pass `None` in the username
302 """
303
304 if username is None:
305 username = 'root'
306
307 # get root password via leader-get, it may be that in the past (when
308 # changes to root-password were not supported) the user changed the
309 # password, so leader-get is more reliable source than
310 # config.previous('root-password').
311 rel_username = None if username == 'root' else username
312 cur_passwd = self.get_mysql_password(rel_username)
313
314 # password that needs to be set
315 new_passwd = password
316
317 # update password for all users (e.g. root@localhost, root@::1, etc)
318 try:
319 self.connect(user=username, password=cur_passwd)
320 cursor = self.connection.cursor()
321 except MySQLdb.OperationalError as ex:
322 raise MySQLSetPasswordError(('Cannot connect using password in '
323 'leader settings (%s)') % ex, ex)
324
325 try:
326 # NOTE(freyes): Due to skip-name-resolve root@$HOSTNAME account
327 # fails when using SET PASSWORD so using UPDATE against the
328 # mysql.user table is needed, but changes to this table are not
329 # replicated across the cluster, so this update needs to run in
330 # all the nodes. More info at
331 # http://galeracluster.com/documentation-webpages/userchanges.html
332 release = CompareHostReleases(lsb_release()['DISTRIB_CODENAME'])
333 if release < 'bionic':
334 SQL_UPDATE_PASSWD = ("UPDATE mysql.user SET password = "
335 "PASSWORD( %s ) WHERE user = %s;")
336 else:
337 # PXC 5.7 (introduced in Bionic) uses authentication_string
338 SQL_UPDATE_PASSWD = ("UPDATE mysql.user SET "
339 "authentication_string = "
340 "PASSWORD( %s ) WHERE user = %s;")
341 cursor.execute(SQL_UPDATE_PASSWD, (new_passwd, username))
342 cursor.execute('FLUSH PRIVILEGES;')
343 self.connection.commit()
344 except MySQLdb.OperationalError as ex:
345 raise MySQLSetPasswordError('Cannot update password: %s' % str(ex),
346 ex)
347 finally:
348 cursor.close()
349
350 # check the password was changed
351 try:
352 self.connect(user=username, password=new_passwd)
353 self.execute('select 1;')
354 except MySQLdb.OperationalError as ex:
355 raise MySQLSetPasswordError(('Cannot connect using new password: '
356 '%s') % str(ex), ex)
357
358 if not is_leader():
359 log('Only the leader can set a new password in the relation',
360 level=DEBUG)
361 return
362
363 for key in self.passwd_keys(rel_username):
364 _password = leader_get(key)
365 if _password:
366 log('Updating password for %s (%s)' % (key, rel_username),
367 level=DEBUG)
368 leader_set(settings={key: new_passwd})
369
370 def set_mysql_root_password(self, password):
371 self.set_mysql_password('root', password)
372
373 def normalize_address(self, hostname):
374 """Ensure that address returned is an IP address (i.e. not fqdn)"""
375 if config_get('prefer-ipv6'):
376 # TODO: add support for ipv6 dns
377 return hostname
378
379 if hostname != unit_get('private-address'):
380 return get_host_ip(hostname, fallback=hostname)
381
382 # Otherwise assume localhost
383 return '127.0.0.1'
384
385 def get_allowed_units(self, database, username, relation_id=None):
386 """Get list of units with access grants for database with username.
387
388 This is typically used to provide shared-db relations with a list of
389 which units have been granted access to the given database.
390 """
391 self.connect(password=self.get_mysql_root_password())
392 allowed_units = set()
393 for unit in related_units(relation_id):
394 settings = relation_get(rid=relation_id, unit=unit)
395 # First check for setting with prefix, then without
396 for attr in ["%s_hostname" % (database), 'hostname']:
397 hosts = settings.get(attr, None)
398 if hosts:
399 break
400
401 if hosts:
402 # hostname can be json-encoded list of hostnames
403 try:
404 hosts = json.loads(hosts)
405 except ValueError:
406 hosts = [hosts]
407 else:
408 hosts = [settings['private-address']]
409
410 if hosts:
411 for host in hosts:
412 host = self.normalize_address(host)
413 if self.grant_exists(database, username, host):
414 log("Grant exists for host '%s' on db '%s'" %
415 (host, database), level=DEBUG)
416 if unit not in allowed_units:
417 allowed_units.add(unit)
418 else:
419 log("Grant does NOT exist for host '%s' on db '%s'" %
420 (host, database), level=DEBUG)
421 else:
422 log("No hosts found for grant check", level=INFO)
423
424 return allowed_units
425
426 def configure_db(self, hostname, database, username, admin=False):
427 """Configure access to database for username from hostname."""
428 self.connect(password=self.get_mysql_root_password())
429 if not self.database_exists(database):
430 self.create_database(database)
431
432 remote_ip = self.normalize_address(hostname)
433 password = self.get_mysql_password(username)
434 if not self.grant_exists(database, username, remote_ip):
435 if not admin:
436 self.create_grant(database, username, remote_ip, password)
437 else:
438 self.create_admin_grant(username, remote_ip, password)
439 self.flush_priviledges()
440
441 return password
442
443
444class PerconaClusterHelper(object):
445
446 # Going for the biggest page size to avoid wasted bytes.
447 # InnoDB page size is 16MB
448
449 DEFAULT_PAGE_SIZE = 16 * 1024 * 1024
450 DEFAULT_INNODB_BUFFER_FACTOR = 0.50
451 DEFAULT_INNODB_BUFFER_SIZE_MAX = 512 * 1024 * 1024
452
453 # Validation and lookups for InnoDB configuration
454 INNODB_VALID_BUFFERING_VALUES = [
455 'none',
456 'inserts',
457 'deletes',
458 'changes',
459 'purges',
460 'all'
461 ]
462 INNODB_FLUSH_CONFIG_VALUES = {
463 'fast': 2,
464 'safest': 1,
465 'unsafe': 0,
466 }
467
468 def human_to_bytes(self, human):
469 """Convert human readable configuration options to bytes."""
470 num_re = re.compile('^[0-9]+$')
471 if num_re.match(human):
472 return human
473
474 factors = {
475 'K': 1024,
476 'M': 1048576,
477 'G': 1073741824,
478 'T': 1099511627776
479 }
480 modifier = human[-1]
481 if modifier in factors:
482 return int(human[:-1]) * factors[modifier]
483
484 if modifier == '%':
485 total_ram = self.human_to_bytes(self.get_mem_total())
486 if self.is_32bit_system() and total_ram > self.sys_mem_limit():
487 total_ram = self.sys_mem_limit()
488 factor = int(human[:-1]) * 0.01
489 pctram = total_ram * factor
490 return int(pctram - (pctram % self.DEFAULT_PAGE_SIZE))
491
492 raise ValueError("Can only convert K,M,G, or T")
493
494 def is_32bit_system(self):
495 """Determine whether system is 32 or 64 bit."""
496 try:
497 return sys.maxsize < 2 ** 32
498 except OverflowError:
499 return False
500
501 def sys_mem_limit(self):
502 """Determine the default memory limit for the current service unit."""
503 if platform.machine() in ['armv7l']:
504 _mem_limit = self.human_to_bytes('2700M') # experimentally determined
505 else:
506 # Limit for x86 based 32bit systems
507 _mem_limit = self.human_to_bytes('4G')
508
509 return _mem_limit
510
511 def get_mem_total(self):
512 """Calculate the total memory in the current service unit."""
513 with open('/proc/meminfo') as meminfo_file:
514 for line in meminfo_file:
515 key, mem = line.split(':', 2)
516 if key == 'MemTotal':
517 mtot, modifier = mem.strip().split(' ')
518 return '%s%s' % (mtot, modifier[0].upper())
519
520 def parse_config(self):
521 """Parse charm configuration and calculate values for config files."""
522 config = config_get()
523 mysql_config = {}
524 if 'max-connections' in config:
525 mysql_config['max_connections'] = config['max-connections']
526
527 if 'wait-timeout' in config:
528 mysql_config['wait_timeout'] = config['wait-timeout']
529
530 if 'innodb-flush-log-at-trx-commit' in config:
531 mysql_config['innodb_flush_log_at_trx_commit'] = \
532 config['innodb-flush-log-at-trx-commit']
533 elif 'tuning-level' in config:
534 mysql_config['innodb_flush_log_at_trx_commit'] = \
535 self.INNODB_FLUSH_CONFIG_VALUES.get(config['tuning-level'], 1)
536
537 if ('innodb-change-buffering' in config and
538 config['innodb-change-buffering'] in self.INNODB_VALID_BUFFERING_VALUES):
539 mysql_config['innodb_change_buffering'] = config['innodb-change-buffering']
540
541 if 'innodb-io-capacity' in config:
542 mysql_config['innodb_io_capacity'] = config['innodb-io-capacity']
543
544 # Set a sane default key_buffer size
545 mysql_config['key_buffer'] = self.human_to_bytes('32M')
546 total_memory = self.human_to_bytes(self.get_mem_total())
547
548 dataset_bytes = config.get('dataset-size', None)
549 innodb_buffer_pool_size = config.get('innodb-buffer-pool-size', None)
550
551 if innodb_buffer_pool_size:
552 innodb_buffer_pool_size = self.human_to_bytes(
553 innodb_buffer_pool_size)
554 elif dataset_bytes:
555 log("Option 'dataset-size' has been deprecated, please use"
556 "innodb_buffer_pool_size option instead", level="WARN")
557 innodb_buffer_pool_size = self.human_to_bytes(
558 dataset_bytes)
559 else:
560 # NOTE(jamespage): pick the smallest of 50% of RAM or 512MB
561 # to ensure that deployments in containers
562 # without constraints don't try to consume
563 # silly amounts of memory.
564 innodb_buffer_pool_size = min(
565 int(total_memory * self.DEFAULT_INNODB_BUFFER_FACTOR),
566 self.DEFAULT_INNODB_BUFFER_SIZE_MAX
567 )
568
569 if innodb_buffer_pool_size > total_memory:
570 log("innodb_buffer_pool_size; {} is greater than system available memory:{}".format(
571 innodb_buffer_pool_size,
572 total_memory), level='WARN')
573
574 mysql_config['innodb_buffer_pool_size'] = innodb_buffer_pool_size
575 return mysql_config
diff --git a/hooks/charmhelpers/contrib/hahelpers/__init__.py b/hooks/charmhelpers/contrib/hahelpers/__init__.py
576deleted file mode 1006440deleted file mode 100644
index d7567b8..0000000
--- a/hooks/charmhelpers/contrib/hahelpers/__init__.py
+++ /dev/null
@@ -1,13 +0,0 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
diff --git a/hooks/charmhelpers/contrib/hahelpers/apache.py b/hooks/charmhelpers/contrib/hahelpers/apache.py
14deleted file mode 1006440deleted file mode 100644
index 605a1be..0000000
--- a/hooks/charmhelpers/contrib/hahelpers/apache.py
+++ /dev/null
@@ -1,96 +0,0 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15#
16# Copyright 2012 Canonical Ltd.
17#
18# This file is sourced from lp:openstack-charm-helpers
19#
20# Authors:
21# James Page <james.page@ubuntu.com>
22# Adam Gandelman <adamg@ubuntu.com>
23#
24
25import os
26import subprocess
27
28from charmhelpers.core.hookenv import (
29 config as config_get,
30 relation_get,
31 relation_ids,
32 related_units as relation_list,
33 log,
34 INFO,
35)
36
37
38def get_cert(cn=None):
39 # TODO: deal with multiple https endpoints via charm config
40 cert = config_get('ssl_cert')
41 key = config_get('ssl_key')
42 if not (cert and key):
43 log("Inspecting identity-service relations for SSL certificate.",
44 level=INFO)
45 cert = key = None
46 if cn:
47 ssl_cert_attr = 'ssl_cert_{}'.format(cn)
48 ssl_key_attr = 'ssl_key_{}'.format(cn)
49 else:
50 ssl_cert_attr = 'ssl_cert'
51 ssl_key_attr = 'ssl_key'
52 for r_id in relation_ids('identity-service'):
53 for unit in relation_list(r_id):
54 if not cert:
55 cert = relation_get(ssl_cert_attr,
56 rid=r_id, unit=unit)
57 if not key:
58 key = relation_get(ssl_key_attr,
59 rid=r_id, unit=unit)
60 return (cert, key)
61
62
63def get_ca_cert():
64 ca_cert = config_get('ssl_ca')
65 if ca_cert is None:
66 log("Inspecting identity-service relations for CA SSL certificate.",
67 level=INFO)
68 for r_id in (relation_ids('identity-service') +
69 relation_ids('identity-credentials')):
70 for unit in relation_list(r_id):
71 if ca_cert is None:
72 ca_cert = relation_get('ca_cert',
73 rid=r_id, unit=unit)
74 return ca_cert
75
76
77def retrieve_ca_cert(cert_file):
78 cert = None
79 if os.path.isfile(cert_file):
80 with open(cert_file, 'rb') as crt:
81 cert = crt.read()
82 return cert
83
84
85def install_ca_cert(ca_cert):
86 if ca_cert:
87 cert_file = ('/usr/local/share/ca-certificates/'
88 'keystone_juju_ca_cert.crt')
89 old_cert = retrieve_ca_cert(cert_file)
90 if old_cert and old_cert == ca_cert:
91 log("CA cert is the same as installed version", level=INFO)
92 else:
93 log("Installing new CA cert", level=INFO)
94 with open(cert_file, 'wb') as crt:
95 crt.write(ca_cert)
96 subprocess.check_call(['update-ca-certificates', '--fresh'])
diff --git a/hooks/charmhelpers/contrib/hahelpers/cluster.py b/hooks/charmhelpers/contrib/hahelpers/cluster.py
97deleted file mode 1006440deleted file mode 100644
index 4a737e2..0000000
--- a/hooks/charmhelpers/contrib/hahelpers/cluster.py
+++ /dev/null
@@ -1,406 +0,0 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15#
16# Copyright 2012 Canonical Ltd.
17#
18# Authors:
19# James Page <james.page@ubuntu.com>
20# Adam Gandelman <adamg@ubuntu.com>
21#
22
23"""
24Helpers for clustering and determining "cluster leadership" and other
25clustering-related helpers.
26"""
27
28import subprocess
29import os
30import time
31
32from socket import gethostname as get_unit_hostname
33
34import six
35
36from charmhelpers.core.hookenv import (
37 log,
38 relation_ids,
39 related_units as relation_list,
40 relation_get,
41 config as config_get,
42 INFO,
43 DEBUG,
44 WARNING,
45 unit_get,
46 is_leader as juju_is_leader,
47 status_set,
48)
49from charmhelpers.core.host import (
50 modulo_distribution,
51)
52from charmhelpers.core.decorators import (
53 retry_on_exception,
54)
55from charmhelpers.core.strutils import (
56 bool_from_string,
57)
58
59DC_RESOURCE_NAME = 'DC'
60
61
62class HAIncompleteConfig(Exception):
63 pass
64
65
66class HAIncorrectConfig(Exception):
67 pass
68
69
70class CRMResourceNotFound(Exception):
71 pass
72
73
74class CRMDCNotFound(Exception):
75 pass
76
77
78def is_elected_leader(resource):
79 """
80 Returns True if the charm executing this is the elected cluster leader.
81
82 It relies on two mechanisms to determine leadership:
83 1. If juju is sufficiently new and leadership election is supported,
84 the is_leader command will be used.
85 2. If the charm is part of a corosync cluster, call corosync to
86 determine leadership.
87 3. If the charm is not part of a corosync cluster, the leader is
88 determined as being "the alive unit with the lowest unit numer". In
89 other words, the oldest surviving unit.
90 """
91 try:
92 return juju_is_leader()
93 except NotImplementedError:
94 log('Juju leadership election feature not enabled'
95 ', using fallback support',
96 level=WARNING)
97
98 if is_clustered():
99 if not is_crm_leader(resource):
100 log('Deferring action to CRM leader.', level=INFO)
101 return False
102 else:
103 peers = peer_units()
104 if peers and not oldest_peer(peers):
105 log('Deferring action to oldest service unit.', level=INFO)
106 return False
107 return True
108
109
110def is_clustered():
111 for r_id in (relation_ids('ha') or []):
112 for unit in (relation_list(r_id) or []):
113 clustered = relation_get('clustered',
114 rid=r_id,
115 unit=unit)
116 if clustered:
117 return True
118 return False
119
120
121def is_crm_dc():
122 """
123 Determine leadership by querying the pacemaker Designated Controller
124 """
125 cmd = ['crm', 'status']
126 try:
127 status = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
128 if not isinstance(status, six.text_type):
129 status = six.text_type(status, "utf-8")
130 except subprocess.CalledProcessError as ex:
131 raise CRMDCNotFound(str(ex))
132
133 current_dc = ''
134 for line in status.split('\n'):
135 if line.startswith('Current DC'):
136 # Current DC: juju-lytrusty-machine-2 (168108163) - partition with quorum
137 current_dc = line.split(':')[1].split()[0]
138 if current_dc == get_unit_hostname():
139 return True
140 elif current_dc == 'NONE':
141 raise CRMDCNotFound('Current DC: NONE')
142
143 return False
144
145
146@retry_on_exception(5, base_delay=2,
147 exc_type=(CRMResourceNotFound, CRMDCNotFound))
148def is_crm_leader(resource, retry=False):
149 """
150 Returns True if the charm calling this is the elected corosync leader,
151 as returned by calling the external "crm" command.
152
153 We allow this operation to be retried to avoid the possibility of getting a
154 false negative. See LP #1396246 for more info.
155 """
156 if resource == DC_RESOURCE_NAME:
157 return is_crm_dc()
158 cmd = ['crm', 'resource', 'show', resource]
159 try:
160 status = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
161 if not isinstance(status, six.text_type):
162 status = six.text_type(status, "utf-8")
163 except subprocess.CalledProcessError:
164 status = None
165
166 if status and get_unit_hostname() in status:
167 return True
168
169 if status and "resource %s is NOT running" % (resource) in status:
170 raise CRMResourceNotFound("CRM resource %s not found" % (resource))
171
172 return False
173
174
175def is_leader(resource):
176 log("is_leader is deprecated. Please consider using is_crm_leader "
177 "instead.", level=WARNING)
178 return is_crm_leader(resource)
179
180
181def peer_units(peer_relation="cluster"):
182 peers = []
183 for r_id in (relation_ids(peer_relation) or []):
184 for unit in (relation_list(r_id) or []):
185 peers.append(unit)
186 return peers
187
188
189def peer_ips(peer_relation='cluster', addr_key='private-address'):
190 '''Return a dict of peers and their private-address'''
191 peers = {}
192 for r_id in relation_ids(peer_relation):
193 for unit in relation_list(r_id):
194 peers[unit] = relation_get(addr_key, rid=r_id, unit=unit)
195 return peers
196
197
198def oldest_peer(peers):
199 """Determines who the oldest peer is by comparing unit numbers."""
200 local_unit_no = int(os.getenv('JUJU_UNIT_NAME').split('/')[1])
201 for peer in peers:
202 remote_unit_no = int(peer.split('/')[1])
203 if remote_unit_no < local_unit_no:
204 return False
205 return True
206
207
208def eligible_leader(resource):
209 log("eligible_leader is deprecated. Please consider using "
210 "is_elected_leader instead.", level=WARNING)
211 return is_elected_leader(resource)
212
213
214def https():
215 '''
216 Determines whether enough data has been provided in configuration
217 or relation data to configure HTTPS
218 .
219 returns: boolean
220 '''
221 use_https = config_get('use-https')
222 if use_https and bool_from_string(use_https):
223 return True
224 if config_get('ssl_cert') and config_get('ssl_key'):
225 return True
226 for r_id in relation_ids('certificates'):
227 for unit in relation_list(r_id):
228 ca = relation_get('ca', rid=r_id, unit=unit)
229 if ca:
230 return True
231 for r_id in relation_ids('identity-service'):
232 for unit in relation_list(r_id):
233 # TODO - needs fixing for new helper as ssl_cert/key suffixes with CN
234 rel_state = [
235 relation_get('https_keystone', rid=r_id, unit=unit),
236 relation_get('ca_cert', rid=r_id, unit=unit),
237 ]
238 # NOTE: works around (LP: #1203241)
239 if (None not in rel_state) and ('' not in rel_state):
240 return True
241 return False
242
243
244def determine_api_port(public_port, singlenode_mode=False):
245 '''
246 Determine correct API server listening port based on
247 existence of HTTPS reverse proxy and/or haproxy.
248
249 public_port: int: standard public port for given service
250
251 singlenode_mode: boolean: Shuffle ports when only a single unit is present
252
253 returns: int: the correct listening port for the API service
254 '''
255 i = 0
256 if singlenode_mode:
257 i += 1
258 elif len(peer_units()) > 0 or is_clustered():
259 i += 1
260 if https():
261 i += 1
262 return public_port - (i * 10)
263
264
265def determine_apache_port(public_port, singlenode_mode=False):
266 '''
267 Description: Determine correct apache listening port based on public IP +
268 state of the cluster.
269
270 public_port: int: standard public port for given service
271
272 singlenode_mode: boolean: Shuffle ports when only a single unit is present
273
274 returns: int: the correct listening port for the HAProxy service
275 '''
276 i = 0
277 if singlenode_mode:
278 i += 1
279 elif len(peer_units()) > 0 or is_clustered():
280 i += 1
281 return public_port - (i * 10)
282
283
284def get_hacluster_config(exclude_keys=None):
285 '''
286 Obtains all relevant configuration from charm configuration required
287 for initiating a relation to hacluster:
288
289 ha-bindiface, ha-mcastport, vip, os-internal-hostname,
290 os-admin-hostname, os-public-hostname, os-access-hostname
291
292 param: exclude_keys: list of setting key(s) to be excluded.
293 returns: dict: A dict containing settings keyed by setting name.
294 raises: HAIncompleteConfig if settings are missing or incorrect.
295 '''
296 settings = ['ha-bindiface', 'ha-mcastport', 'vip', 'os-internal-hostname',
297 'os-admin-hostname', 'os-public-hostname', 'os-access-hostname']
298 conf = {}
299 for setting in settings:
300 if exclude_keys and setting in exclude_keys:
301 continue
302
303 conf[setting] = config_get(setting)
304
305 if not valid_hacluster_config():
306 raise HAIncorrectConfig('Insufficient or incorrect config data to '
307 'configure hacluster.')
308 return conf
309
310
311def valid_hacluster_config():
312 '''
313 Check that either vip or dns-ha is set. If dns-ha then one of os-*-hostname
314 must be set.
315
316 Note: ha-bindiface and ha-macastport both have defaults and will always
317 be set. We only care that either vip or dns-ha is set.
318
319 :returns: boolean: valid config returns true.
320 raises: HAIncompatibileConfig if settings conflict.
321 raises: HAIncompleteConfig if settings are missing.
322 '''
323 vip = config_get('vip')
324 dns = config_get('dns-ha')
325 if not(bool(vip) ^ bool(dns)):
326 msg = ('HA: Either vip or dns-ha must be set but not both in order to '
327 'use high availability')
328 status_set('blocked', msg)
329 raise HAIncorrectConfig(msg)
330
331 # If dns-ha then one of os-*-hostname must be set
332 if dns:
333 dns_settings = ['os-internal-hostname', 'os-admin-hostname',
334 'os-public-hostname', 'os-access-hostname']
335 # At this point it is unknown if one or all of the possible
336 # network spaces are in HA. Validate at least one is set which is
337 # the minimum required.
338 for setting in dns_settings:
339 if config_get(setting):
340 log('DNS HA: At least one hostname is set {}: {}'
341 ''.format(setting, config_get(setting)),
342 level=DEBUG)
343 return True
344
345 msg = ('DNS HA: At least one os-*-hostname(s) must be set to use '
346 'DNS HA')
347 status_set('blocked', msg)
348 raise HAIncompleteConfig(msg)
349
350 log('VIP HA: VIP is set {}'.format(vip), level=DEBUG)
351 return True
352
353
354def canonical_url(configs, vip_setting='vip'):
355 '''
356 Returns the correct HTTP URL to this host given the state of HTTPS
357 configuration and hacluster.
358
359 :configs : OSTemplateRenderer: A config tempating object to inspect for
360 a complete https context.
361
362 :vip_setting: str: Setting in charm config that specifies
363 VIP address.
364 '''
365 scheme = 'http'
366 if 'https' in configs.complete_contexts():
367 scheme = 'https'
368 if is_clustered():
369 addr = config_get(vip_setting)
370 else:
371 addr = unit_get('private-address')
372 return '%s://%s' % (scheme, addr)
373
374
375def distributed_wait(modulo=None, wait=None, operation_name='operation'):
376 ''' Distribute operations by waiting based on modulo_distribution
377
378 If modulo and or wait are not set, check config_get for those values.
379 If config values are not set, default to modulo=3 and wait=30.
380
381 :param modulo: int The modulo number creates the group distribution
382 :param wait: int The constant time wait value
383 :param operation_name: string Operation name for status message
384 i.e. 'restart'
385 :side effect: Calls config_get()
386 :side effect: Calls log()
387 :side effect: Calls status_set()
388 :side effect: Calls time.sleep()
389 '''
390 if modulo is None:
391 modulo = config_get('modulo-nodes') or 3
392 if wait is None:
393 wait = config_get('known-wait') or 30
394 if juju_is_leader():
395 # The leader should never wait
396 calculated_wait = 0
397 else:
398 # non_zero_wait=True guarantees the non-leader who gets modulo 0
399 # will still wait
400 calculated_wait = modulo_distribution(modulo=modulo, wait=wait,
401 non_zero_wait=True)
402 msg = "Waiting {} seconds for {} ...".format(calculated_wait,
403 operation_name)
404 log(msg, DEBUG)
405 status_set('maintenance', msg)
406 time.sleep(calculated_wait)
diff --git a/hooks/charmhelpers/contrib/hardening/README.hardening.md b/hooks/charmhelpers/contrib/hardening/README.hardening.md
407deleted file mode 1006440deleted file mode 100644
index 91280c0..0000000
--- a/hooks/charmhelpers/contrib/hardening/README.hardening.md
+++ /dev/null
@@ -1,38 +0,0 @@
1# Juju charm-helpers hardening library
2
3## Description
4
5This library provides multiple implementations of system and application
6hardening that conform to the standards of http://hardening.io/.
7
8Current implementations include:
9
10 * OS
11 * SSH
12 * MySQL
13 * Apache
14
15## Requirements
16
17* Juju Charms
18
19## Usage
20
211. Synchronise this library into your charm and add the harden() decorator
22 (from contrib.hardening.harden) to any functions or methods you want to use
23 to trigger hardening of your application/system.
24
252. Add a config option called 'harden' to your charm config.yaml and set it to
26 a space-delimited list of hardening modules you want to run e.g. "os ssh"
27
283. Override any config defaults (contrib.hardening.defaults) by adding a file
29 called hardening.yaml to your charm root containing the name(s) of the
30 modules whose settings you want override at root level and then any settings
31 with overrides e.g.
32
33 os:
34 general:
35 desktop_enable: True
36
374. Now just run your charm as usual and hardening will be applied each time the
38 hook runs.
diff --git a/hooks/charmhelpers/contrib/hardening/__init__.py b/hooks/charmhelpers/contrib/hardening/__init__.py
39deleted file mode 1006440deleted file mode 100644
index 30a3e94..0000000
--- a/hooks/charmhelpers/contrib/hardening/__init__.py
+++ /dev/null
@@ -1,13 +0,0 @@
1# Copyright 2016 Canonical Limited.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
diff --git a/hooks/charmhelpers/contrib/hardening/apache/__init__.py b/hooks/charmhelpers/contrib/hardening/apache/__init__.py
14deleted file mode 1006440deleted file mode 100644
index 58bebd8..0000000
--- a/hooks/charmhelpers/contrib/hardening/apache/__init__.py
+++ /dev/null
@@ -1,17 +0,0 @@
1# Copyright 2016 Canonical Limited.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15from os import path
16
17TEMPLATES_DIR = path.join(path.dirname(__file__), 'templates')
diff --git a/hooks/charmhelpers/contrib/hardening/apache/checks/__init__.py b/hooks/charmhelpers/contrib/hardening/apache/checks/__init__.py
18deleted file mode 1006440deleted file mode 100644
index 3bc2ebd..0000000
--- a/hooks/charmhelpers/contrib/hardening/apache/checks/__init__.py
+++ /dev/null
@@ -1,29 +0,0 @@
1# Copyright 2016 Canonical Limited.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15from charmhelpers.core.hookenv import (
16 log,
17 DEBUG,
18)
19from charmhelpers.contrib.hardening.apache.checks import config
20
21
22def run_apache_checks():
23 log("Starting Apache hardening checks.", level=DEBUG)
24 checks = config.get_audits()
25 for check in checks:
26 log("Running '%s' check" % (check.__class__.__name__), level=DEBUG)
27 check.ensure_compliance()
28
29 log("Apache hardening checks complete.", level=DEBUG)
diff --git a/hooks/charmhelpers/contrib/hardening/apache/checks/config.py b/hooks/charmhelpers/contrib/hardening/apache/checks/config.py
30deleted file mode 1006440deleted file mode 100644
index 06482aa..0000000
--- a/hooks/charmhelpers/contrib/hardening/apache/checks/config.py
+++ /dev/null
@@ -1,101 +0,0 @@
1# Copyright 2016 Canonical Limited.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15import os
16import re
17import subprocess
18
19
20from charmhelpers.core.hookenv import (
21 log,
22 INFO,
23)
24from charmhelpers.contrib.hardening.audits.file import (
25 FilePermissionAudit,
26 DirectoryPermissionAudit,
27 NoReadWriteForOther,
28 TemplatedFile,
29 DeletedFile
30)
31from charmhelpers.contrib.hardening.audits.apache import DisabledModuleAudit
32from charmhelpers.contrib.hardening.apache import TEMPLATES_DIR
33from charmhelpers.contrib.hardening import utils
34
35
36def get_audits():
37 """Get Apache hardening config audits.
38
39 :returns: dictionary of audits
40 """
41 if subprocess.call(['which', 'apache2'], stdout=subprocess.PIPE) != 0:
42 log("Apache server does not appear to be installed on this node - "
43 "skipping apache hardening", level=INFO)
44 return []
45
46 context = ApacheConfContext()
47 settings = utils.get_settings('apache')
48 audits = [
49 FilePermissionAudit(paths=os.path.join(
50 settings['common']['apache_dir'], 'apache2.conf'),
51 user='root', group='root', mode=0o0640),
52
53 TemplatedFile(os.path.join(settings['common']['apache_dir'],
54 'mods-available/alias.conf'),
55 context,
56 TEMPLATES_DIR,
57 mode=0o0640,
58 user='root',
59 service_actions=[{'service': 'apache2',
60 'actions': ['restart']}]),
61
62 TemplatedFile(os.path.join(settings['common']['apache_dir'],
63 'conf-enabled/99-hardening.conf'),
64 context,
65 TEMPLATES_DIR,
66 mode=0o0640,
67 user='root',
68 service_actions=[{'service': 'apache2',
69 'actions': ['restart']}]),
70
71 DirectoryPermissionAudit(settings['common']['apache_dir'],
72 user='root',
73 group='root',
74 mode=0o0750),
75
76 DisabledModuleAudit(settings['hardening']['modules_to_disable']),
77
78 NoReadWriteForOther(settings['common']['apache_dir']),
79
80 DeletedFile(['/var/www/html/index.html'])
81 ]
82
83 return audits
84
85
86class ApacheConfContext(object):
87 """Defines the set of key/value pairs to set in a apache config file.
88
89 This context, when called, will return a dictionary containing the
90 key/value pairs of setting to specify in the
91 /etc/apache/conf-enabled/hardening.conf file.
92 """
93 def __call__(self):
94 settings = utils.get_settings('apache')
95 ctxt = settings['hardening']
96
97 out = subprocess.check_output(['apache2', '-v'])
98 ctxt['apache_version'] = re.search(r'.+version: Apache/(.+?)\s.+',
99 out).group(1)
100 ctxt['apache_icondir'] = '/usr/share/apache2/icons/'
101 return ctxt
diff --git a/hooks/charmhelpers/contrib/hardening/apache/templates/99-hardening.conf b/hooks/charmhelpers/contrib/hardening/apache/templates/99-hardening.conf
102deleted file mode 1006440deleted file mode 100644
index 22b6804..0000000
--- a/hooks/charmhelpers/contrib/hardening/apache/templates/99-hardening.conf
+++ /dev/null
@@ -1,32 +0,0 @@
1###############################################################################
2# WARNING: This configuration file is maintained by Juju. Local changes may
3# be overwritten.
4###############################################################################
5
6<Location / >
7 <LimitExcept {{ allowed_http_methods }} >
8 # http://httpd.apache.org/docs/2.4/upgrading.html
9 {% if apache_version > '2.2' -%}
10 Require all granted
11 {% else -%}
12 Order Allow,Deny
13 Deny from all
14 {% endif %}
15 </LimitExcept>
16</Location>
17
18<Directory />
19 Options -Indexes -FollowSymLinks
20 AllowOverride None
21</Directory>
22
23<Directory /var/www/>
24 Options -Indexes -FollowSymLinks
25 AllowOverride None
26</Directory>
27
28TraceEnable {{ traceenable }}
29ServerTokens {{ servertokens }}
30
31SSLHonorCipherOrder {{ honor_cipher_order }}
32SSLCipherSuite {{ cipher_suite }}
diff --git a/hooks/charmhelpers/contrib/hardening/apache/templates/alias.conf b/hooks/charmhelpers/contrib/hardening/apache/templates/alias.conf
33deleted file mode 1006440deleted file mode 100644
index e46a58a..0000000
--- a/hooks/charmhelpers/contrib/hardening/apache/templates/alias.conf
+++ /dev/null
@@ -1,31 +0,0 @@
1###############################################################################
2# WARNING: This configuration file is maintained by Juju. Local changes may
3# be overwritten.
4###############################################################################
5<IfModule alias_module>
6 #
7 # Aliases: Add here as many aliases as you need (with no limit). The format is
8 # Alias fakename realname
9 #
10 # Note that if you include a trailing / on fakename then the server will
11 # require it to be present in the URL. So "/icons" isn't aliased in this
12 # example, only "/icons/". If the fakename is slash-terminated, then the
13 # realname must also be slash terminated, and if the fakename omits the
14 # trailing slash, the realname must also omit it.
15 #
16 # We include the /icons/ alias for FancyIndexed directory listings. If
17 # you do not use FancyIndexing, you may comment this out.
18 #
19 Alias /icons/ "{{ apache_icondir }}/"
20
21 <Directory "{{ apache_icondir }}">
22 Options -Indexes -MultiViews -FollowSymLinks
23 AllowOverride None
24{% if apache_version == '2.4' -%}
25 Require all granted
26{% else -%}
27 Order allow,deny
28 Allow from all
29{% endif %}
30 </Directory>
31</IfModule>
diff --git a/hooks/charmhelpers/contrib/hardening/audits/__init__.py b/hooks/charmhelpers/contrib/hardening/audits/__init__.py
32deleted file mode 1006440deleted file mode 100644
index 6dd5b05..0000000
--- a/hooks/charmhelpers/contrib/hardening/audits/__init__.py
+++ /dev/null
@@ -1,54 +0,0 @@
1# Copyright 2016 Canonical Limited.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15
16class BaseAudit(object): # NO-QA
17 """Base class for hardening checks.
18
19 The lifecycle of a hardening check is to first check to see if the system
20 is in compliance for the specified check. If it is not in compliance, the
21 check method will return a value which will be supplied to the.
22 """
23 def __init__(self, *args, **kwargs):
24 self.unless = kwargs.get('unless', None)
25 super(BaseAudit, self).__init__()
26
27 def ensure_compliance(self):
28 """Checks to see if the current hardening check is in compliance or
29 not.
30
31 If the check that is performed is not in compliance, then an exception
32 should be raised.
33 """
34 pass
35
36 def _take_action(self):
37 """Determines whether to perform the action or not.
38
39 Checks whether or not an action should be taken. This is determined by
40 the truthy value for the unless parameter. If unless is a callback
41 method, it will be invoked with no parameters in order to determine
42 whether or not the action should be taken. Otherwise, the truthy value
43 of the unless attribute will determine if the action should be
44 performed.
45 """
46 # Do the action if there isn't an unless override.
47 if self.unless is None:
48 return True
49
50 # Invoke the callback if there is one.
51 if hasattr(self.unless, '__call__'):
52 return not self.unless()
53
54 return not self.unless
diff --git a/hooks/charmhelpers/contrib/hardening/audits/apache.py b/hooks/charmhelpers/contrib/hardening/audits/apache.py
55deleted file mode 1006440deleted file mode 100644
index d32bf44..0000000
--- a/hooks/charmhelpers/contrib/hardening/audits/apache.py
+++ /dev/null
@@ -1,98 +0,0 @@
1# Copyright 2016 Canonical Limited.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15import re
16import subprocess
17
18from six import string_types
19
20from charmhelpers.core.hookenv import (
21 log,
22 INFO,
23 ERROR,
24)
25
26from charmhelpers.contrib.hardening.audits import BaseAudit
27
28
29class DisabledModuleAudit(BaseAudit):
30 """Audits Apache2 modules.
31
32 Determines if the apache2 modules are enabled. If the modules are enabled
33 then they are removed in the ensure_compliance.
34 """
35 def __init__(self, modules):
36 if modules is None:
37 self.modules = []
38 elif isinstance(modules, string_types):
39 self.modules = [modules]
40 else:
41 self.modules = modules
42
43 def ensure_compliance(self):
44 """Ensures that the modules are not loaded."""
45 if not self.modules:
46 return
47
48 try:
49 loaded_modules = self._get_loaded_modules()
50 non_compliant_modules = []
51 for module in self.modules:
52 if module in loaded_modules:
53 log("Module '%s' is enabled but should not be." %
54 (module), level=INFO)
55 non_compliant_modules.append(module)
56
57 if len(non_compliant_modules) == 0:
58 return
59
60 for module in non_compliant_modules:
61 self._disable_module(module)
62 self._restart_apache()
63 except subprocess.CalledProcessError as e:
64 log('Error occurred auditing apache module compliance. '
65 'This may have been already reported. '
66 'Output is: %s' % e.output, level=ERROR)
67
68 @staticmethod
69 def _get_loaded_modules():
70 """Returns the modules which are enabled in Apache."""
71 output = subprocess.check_output(['apache2ctl', '-M'])
72 modules = []
73 for line in output.splitlines():
74 # Each line of the enabled module output looks like:
75 # module_name (static|shared)
76 # Plus a header line at the top of the output which is stripped
77 # out by the regex.
78 matcher = re.search(r'^ (\S*)_module (\S*)', line)
79 if matcher:
80 modules.append(matcher.group(1))
81 return modules
82
83 @staticmethod
84 def _disable_module(module):
85 """Disables the specified module in Apache."""
86 try:
87 subprocess.check_call(['a2dismod', module])
88 except subprocess.CalledProcessError as e:
89 # Note: catch error here to allow the attempt of disabling
90 # multiple modules in one go rather than failing after the
91 # first module fails.
92 log('Error occurred disabling module %s. '
93 'Output is: %s' % (module, e.output), level=ERROR)
94
95 @staticmethod
96 def _restart_apache():
97 """Restarts the apache process"""
98 subprocess.check_output(['service', 'apache2', 'restart'])
diff --git a/hooks/charmhelpers/contrib/hardening/audits/apt.py b/hooks/charmhelpers/contrib/hardening/audits/apt.py
99deleted file mode 1006440deleted file mode 100644
index 3dc14e3..0000000
--- a/hooks/charmhelpers/contrib/hardening/audits/apt.py
+++ /dev/null
@@ -1,103 +0,0 @@
1# Copyright 2016 Canonical Limited.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15from __future__ import absolute_import # required for external apt import
16from apt import apt_pkg
17from six import string_types
18
19from charmhelpers.fetch import (
20 apt_cache,
21 apt_purge
22)
23from charmhelpers.core.hookenv import (
24 log,
25 DEBUG,
26 WARNING,
27)
28from charmhelpers.contrib.hardening.audits import BaseAudit
29
30
31class AptConfig(BaseAudit):
32
33 def __init__(self, config, **kwargs):
34 self.config = config
35
36 def verify_config(self):
37 apt_pkg.init()
38 for cfg in self.config:
39 value = apt_pkg.config.get(cfg['key'], cfg.get('default', ''))
40 if value and value != cfg['expected']:
41 log("APT config '%s' has unexpected value '%s' "
42 "(expected='%s')" %
43 (cfg['key'], value, cfg['expected']), level=WARNING)
44
45 def ensure_compliance(self):
46 self.verify_config()
47
48
49class RestrictedPackages(BaseAudit):
50 """Class used to audit restricted packages on the system."""
51
52 def __init__(self, pkgs, **kwargs):
53 super(RestrictedPackages, self).__init__(**kwargs)
54 if isinstance(pkgs, string_types) or not hasattr(pkgs, '__iter__'):
55 self.pkgs = [pkgs]
56 else:
57 self.pkgs = pkgs
58
59 def ensure_compliance(self):
60 cache = apt_cache()
61
62 for p in self.pkgs:
63 if p not in cache:
64 continue
65
66 pkg = cache[p]
67 if not self.is_virtual_package(pkg):
68 if not pkg.current_ver:
69 log("Package '%s' is not installed." % pkg.name,
70 level=DEBUG)
71 continue
72 else:
73 log("Restricted package '%s' is installed" % pkg.name,
74 level=WARNING)
75 self.delete_package(cache, pkg)
76 else:
77 log("Checking restricted virtual package '%s' provides" %
78 pkg.name, level=DEBUG)
79 self.delete_package(cache, pkg)
80
81 def delete_package(self, cache, pkg):
82 """Deletes the package from the system.
83
84 Deletes the package form the system, properly handling virtual
85 packages.
86
87 :param cache: the apt cache
88 :param pkg: the package to remove
89 """
90 if self.is_virtual_package(pkg):
91 log("Package '%s' appears to be virtual - purging provides" %
92 pkg.name, level=DEBUG)
93 for _p in pkg.provides_list:
94 self.delete_package(cache, _p[2].parent_pkg)
95 elif not pkg.current_ver:
96 log("Package '%s' not installed" % pkg.name, level=DEBUG)
97 return
98 else:
99 log("Purging package '%s'" % pkg.name, level=DEBUG)
100 apt_purge(pkg.name)
101
102 def is_virtual_package(self, pkg):
103 return pkg.has_provides and not pkg.has_versions
diff --git a/hooks/charmhelpers/contrib/hardening/audits/file.py b/hooks/charmhelpers/contrib/hardening/audits/file.py
104deleted file mode 1006440deleted file mode 100644
index 257c635..0000000
--- a/hooks/charmhelpers/contrib/hardening/audits/file.py
+++ /dev/null
@@ -1,550 +0,0 @@
1# Copyright 2016 Canonical Limited.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15import grp
16import os
17import pwd
18import re
19
20from subprocess import (
21 CalledProcessError,
22 check_output,
23 check_call,
24)
25from traceback import format_exc
26from six import string_types
27from stat import (
28 S_ISGID,
29 S_ISUID
30)
31
32from charmhelpers.core.hookenv import (
33 log,
34 DEBUG,
35 INFO,
36 WARNING,
37 ERROR,
38)
39from charmhelpers.core import unitdata
40from charmhelpers.core.host import file_hash
41from charmhelpers.contrib.hardening.audits import BaseAudit
42from charmhelpers.contrib.hardening.templating import (
43 get_template_path,
44 render_and_write,
45)
46from charmhelpers.contrib.hardening import utils
47
48
49class BaseFileAudit(BaseAudit):
50 """Base class for file audits.
51
52 Provides api stubs for compliance check flow that must be used by any class
53 that implemented this one.
54 """
55
56 def __init__(self, paths, always_comply=False, *args, **kwargs):
57 """
58 :param paths: string path of list of paths of files we want to apply
59 compliance checks are criteria to.
60 :param always_comply: if true compliance criteria is always applied
61 else compliance is skipped for non-existent
62 paths.
63 """
64 super(BaseFileAudit, self).__init__(*args, **kwargs)
65 self.always_comply = always_comply
66 if isinstance(paths, string_types) or not hasattr(paths, '__iter__'):
67 self.paths = [paths]
68 else:
69 self.paths = paths
70
71 def ensure_compliance(self):
72 """Ensure that the all registered files comply to registered criteria.
73 """
74 for p in self.paths:
75 if os.path.exists(p):
76 if self.is_compliant(p):
77 continue
78
79 log('File %s is not in compliance.' % p, level=INFO)
80 else:
81 if not self.always_comply:
82 log("Non-existent path '%s' - skipping compliance check"
83 % (p), level=INFO)
84 continue
85
86 if self._take_action():
87 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: