Merge lp:~blr/charms/precise/squid-forwardproxy/fix-unit-state into lp:~canonical-launchpad-branches/charms/precise/squid-forwardproxy/trunk
- Precise Pangolin (12.04)
- fix-unit-state
- Merge into trunk
Proposed by
Kit Randel
Status: | Merged |
---|---|
Merged at revision: | 35 |
Proposed branch: | lp:~blr/charms/precise/squid-forwardproxy/fix-unit-state |
Merge into: | lp:~canonical-launchpad-branches/charms/precise/squid-forwardproxy/trunk |
Diff against target: |
3974 lines (+3760/-43) 20 files modified
Makefile (+13/-0) charm-helpers.yaml (+4/-0) hooks/charmhelpers/__init__.py (+38/-0) hooks/charmhelpers/core/__init__.py (+15/-0) hooks/charmhelpers/core/decorators.py (+57/-0) hooks/charmhelpers/core/files.py (+45/-0) hooks/charmhelpers/core/fstab.py (+134/-0) hooks/charmhelpers/core/hookenv.py (+978/-0) hooks/charmhelpers/core/host.py (+641/-0) hooks/charmhelpers/core/hugepage.py (+71/-0) hooks/charmhelpers/core/kernel.py (+68/-0) hooks/charmhelpers/core/services/__init__.py (+18/-0) hooks/charmhelpers/core/services/base.py (+353/-0) hooks/charmhelpers/core/services/helpers.py (+292/-0) hooks/charmhelpers/core/strutils.py (+72/-0) hooks/charmhelpers/core/sysctl.py (+56/-0) hooks/charmhelpers/core/templating.py (+81/-0) hooks/charmhelpers/core/unitdata.py (+521/-0) hooks/hooks.py (+50/-43) scripts/charm_helpers_sync.py (+253/-0) |
To merge this branch: | bzr merge lp:~blr/charms/precise/squid-forwardproxy/fix-unit-state |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
William Grant | code | Approve | |
Review via email: mp+280650@code.launchpad.net |
Commit message
Manage local unit state with charmhelpers unitdata.kv.
Description of the change
This branch correctly manages the persistence of hook states 'start' and 'relation-
To post a comment you must log in.
Revision history for this message
William Grant (wgrant) : | # |
review:
Needs Fixing
(code)
- 63. By Kit Randel
-
Unset hook_start unitdata on stop hook.
- 64. By Kit Randel
-
Reloading squid3 is redundant given fallthrough restart case.
- 65. By Kit Randel
-
Remove unnecessary unitdata keys 'state_
delayed_ start' and 'hook_auth_ helper_ joined' . - 66. By Kit Randel
-
* Remove potential vector for symlink attack.
* Update charmhelpers. - 67. By Kit Randel
-
Factor out test for delayed service start.
- 68. By Kit Randel
-
Move invalid squid configuration CRITICAL log to service_
squid3( 'check' ).
Revision history for this message
William Grant (wgrant) : | # |
review:
Approve
(code)
- 69. By Kit Randel
-
Unset hook_start unitdata key, regardless of service status.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === added file 'Makefile' | |||
2 | --- Makefile 1970-01-01 00:00:00 +0000 | |||
3 | +++ Makefile 2015-12-17 05:44:32 +0000 | |||
4 | @@ -0,0 +1,13 @@ | |||
5 | 1 | # -*- mode: makefile -*- | ||
6 | 2 | |||
7 | 3 | PYTHON := /usr/bin/env python | ||
8 | 4 | |||
9 | 5 | all: sync | ||
10 | 6 | |||
11 | 7 | sync: | ||
12 | 8 | @bzr cat lp:charm-helpers/tools/charm_helpers_sync/charm_helpers_sync.py > scripts/charm_helpers_sync.py | ||
13 | 9 | @echo "Syncing charmhelpers..." | ||
14 | 10 | @mkdir -p hooks/charmhelpers | ||
15 | 11 | @$(PYTHON) scripts/charm_helpers_sync.py -c charm-helpers.yaml | ||
16 | 12 | |||
17 | 13 | |||
18 | 0 | 14 | ||
19 | === added file 'charm-helpers.yaml' | |||
20 | --- charm-helpers.yaml 1970-01-01 00:00:00 +0000 | |||
21 | +++ charm-helpers.yaml 2015-12-17 05:44:32 +0000 | |||
22 | @@ -0,0 +1,4 @@ | |||
23 | 1 | destination: hooks/charmhelpers | ||
24 | 2 | branch: lp:charm-helpers | ||
25 | 3 | include: | ||
26 | 4 | - core | ||
27 | 0 | \ No newline at end of file | 5 | \ No newline at end of file |
28 | 1 | 6 | ||
29 | === added directory 'hooks/charmhelpers' | |||
30 | === added file 'hooks/charmhelpers/__init__.py' | |||
31 | --- hooks/charmhelpers/__init__.py 1970-01-01 00:00:00 +0000 | |||
32 | +++ hooks/charmhelpers/__init__.py 2015-12-17 05:44:32 +0000 | |||
33 | @@ -0,0 +1,38 @@ | |||
34 | 1 | # Copyright 2014-2015 Canonical Limited. | ||
35 | 2 | # | ||
36 | 3 | # This file is part of charm-helpers. | ||
37 | 4 | # | ||
38 | 5 | # charm-helpers is free software: you can redistribute it and/or modify | ||
39 | 6 | # it under the terms of the GNU Lesser General Public License version 3 as | ||
40 | 7 | # published by the Free Software Foundation. | ||
41 | 8 | # | ||
42 | 9 | # charm-helpers is distributed in the hope that it will be useful, | ||
43 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
44 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
45 | 12 | # GNU Lesser General Public License for more details. | ||
46 | 13 | # | ||
47 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
48 | 15 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
49 | 16 | |||
50 | 17 | # Bootstrap charm-helpers, installing its dependencies if necessary using | ||
51 | 18 | # only standard libraries. | ||
52 | 19 | import subprocess | ||
53 | 20 | import sys | ||
54 | 21 | |||
55 | 22 | try: | ||
56 | 23 | import six # flake8: noqa | ||
57 | 24 | except ImportError: | ||
58 | 25 | if sys.version_info.major == 2: | ||
59 | 26 | subprocess.check_call(['apt-get', 'install', '-y', 'python-six']) | ||
60 | 27 | else: | ||
61 | 28 | subprocess.check_call(['apt-get', 'install', '-y', 'python3-six']) | ||
62 | 29 | import six # flake8: noqa | ||
63 | 30 | |||
64 | 31 | try: | ||
65 | 32 | import yaml # flake8: noqa | ||
66 | 33 | except ImportError: | ||
67 | 34 | if sys.version_info.major == 2: | ||
68 | 35 | subprocess.check_call(['apt-get', 'install', '-y', 'python-yaml']) | ||
69 | 36 | else: | ||
70 | 37 | subprocess.check_call(['apt-get', 'install', '-y', 'python3-yaml']) | ||
71 | 38 | import yaml # flake8: noqa | ||
72 | 0 | 39 | ||
73 | === added directory 'hooks/charmhelpers/core' | |||
74 | === added file 'hooks/charmhelpers/core/__init__.py' | |||
75 | --- hooks/charmhelpers/core/__init__.py 1970-01-01 00:00:00 +0000 | |||
76 | +++ hooks/charmhelpers/core/__init__.py 2015-12-17 05:44:32 +0000 | |||
77 | @@ -0,0 +1,15 @@ | |||
78 | 1 | # Copyright 2014-2015 Canonical Limited. | ||
79 | 2 | # | ||
80 | 3 | # This file is part of charm-helpers. | ||
81 | 4 | # | ||
82 | 5 | # charm-helpers is free software: you can redistribute it and/or modify | ||
83 | 6 | # it under the terms of the GNU Lesser General Public License version 3 as | ||
84 | 7 | # published by the Free Software Foundation. | ||
85 | 8 | # | ||
86 | 9 | # charm-helpers is distributed in the hope that it will be useful, | ||
87 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
88 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
89 | 12 | # GNU Lesser General Public License for more details. | ||
90 | 13 | # | ||
91 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
92 | 15 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
93 | 0 | 16 | ||
94 | === added file 'hooks/charmhelpers/core/decorators.py' | |||
95 | --- hooks/charmhelpers/core/decorators.py 1970-01-01 00:00:00 +0000 | |||
96 | +++ hooks/charmhelpers/core/decorators.py 2015-12-17 05:44:32 +0000 | |||
97 | @@ -0,0 +1,57 @@ | |||
98 | 1 | # Copyright 2014-2015 Canonical Limited. | ||
99 | 2 | # | ||
100 | 3 | # This file is part of charm-helpers. | ||
101 | 4 | # | ||
102 | 5 | # charm-helpers is free software: you can redistribute it and/or modify | ||
103 | 6 | # it under the terms of the GNU Lesser General Public License version 3 as | ||
104 | 7 | # published by the Free Software Foundation. | ||
105 | 8 | # | ||
106 | 9 | # charm-helpers is distributed in the hope that it will be useful, | ||
107 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
108 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
109 | 12 | # GNU Lesser General Public License for more details. | ||
110 | 13 | # | ||
111 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
112 | 15 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
113 | 16 | |||
114 | 17 | # | ||
115 | 18 | # Copyright 2014 Canonical Ltd. | ||
116 | 19 | # | ||
117 | 20 | # Authors: | ||
118 | 21 | # Edward Hope-Morley <opentastic@gmail.com> | ||
119 | 22 | # | ||
120 | 23 | |||
121 | 24 | import time | ||
122 | 25 | |||
123 | 26 | from charmhelpers.core.hookenv import ( | ||
124 | 27 | log, | ||
125 | 28 | INFO, | ||
126 | 29 | ) | ||
127 | 30 | |||
128 | 31 | |||
129 | 32 | def retry_on_exception(num_retries, base_delay=0, exc_type=Exception): | ||
130 | 33 | """If the decorated function raises exception exc_type, allow num_retries | ||
131 | 34 | retry attempts before raise the exception. | ||
132 | 35 | """ | ||
133 | 36 | def _retry_on_exception_inner_1(f): | ||
134 | 37 | def _retry_on_exception_inner_2(*args, **kwargs): | ||
135 | 38 | retries = num_retries | ||
136 | 39 | multiplier = 1 | ||
137 | 40 | while True: | ||
138 | 41 | try: | ||
139 | 42 | return f(*args, **kwargs) | ||
140 | 43 | except exc_type: | ||
141 | 44 | if not retries: | ||
142 | 45 | raise | ||
143 | 46 | |||
144 | 47 | delay = base_delay * multiplier | ||
145 | 48 | multiplier += 1 | ||
146 | 49 | log("Retrying '%s' %d more times (delay=%s)" % | ||
147 | 50 | (f.__name__, retries, delay), level=INFO) | ||
148 | 51 | retries -= 1 | ||
149 | 52 | if delay: | ||
150 | 53 | time.sleep(delay) | ||
151 | 54 | |||
152 | 55 | return _retry_on_exception_inner_2 | ||
153 | 56 | |||
154 | 57 | return _retry_on_exception_inner_1 | ||
155 | 0 | 58 | ||
156 | === added file 'hooks/charmhelpers/core/files.py' | |||
157 | --- hooks/charmhelpers/core/files.py 1970-01-01 00:00:00 +0000 | |||
158 | +++ hooks/charmhelpers/core/files.py 2015-12-17 05:44:32 +0000 | |||
159 | @@ -0,0 +1,45 @@ | |||
160 | 1 | #!/usr/bin/env python | ||
161 | 2 | # -*- coding: utf-8 -*- | ||
162 | 3 | |||
163 | 4 | # Copyright 2014-2015 Canonical Limited. | ||
164 | 5 | # | ||
165 | 6 | # This file is part of charm-helpers. | ||
166 | 7 | # | ||
167 | 8 | # charm-helpers is free software: you can redistribute it and/or modify | ||
168 | 9 | # it under the terms of the GNU Lesser General Public License version 3 as | ||
169 | 10 | # published by the Free Software Foundation. | ||
170 | 11 | # | ||
171 | 12 | # charm-helpers is distributed in the hope that it will be useful, | ||
172 | 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
173 | 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
174 | 15 | # GNU Lesser General Public License for more details. | ||
175 | 16 | # | ||
176 | 17 | # You should have received a copy of the GNU Lesser General Public License | ||
177 | 18 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
178 | 19 | |||
179 | 20 | __author__ = 'Jorge Niedbalski <niedbalski@ubuntu.com>' | ||
180 | 21 | |||
181 | 22 | import os | ||
182 | 23 | import subprocess | ||
183 | 24 | |||
184 | 25 | |||
185 | 26 | def sed(filename, before, after, flags='g'): | ||
186 | 27 | """ | ||
187 | 28 | Search and replaces the given pattern on filename. | ||
188 | 29 | |||
189 | 30 | :param filename: relative or absolute file path. | ||
190 | 31 | :param before: expression to be replaced (see 'man sed') | ||
191 | 32 | :param after: expression to replace with (see 'man sed') | ||
192 | 33 | :param flags: sed-compatible regex flags in example, to make | ||
193 | 34 | the search and replace case insensitive, specify ``flags="i"``. | ||
194 | 35 | The ``g`` flag is always specified regardless, so you do not | ||
195 | 36 | need to remember to include it when overriding this parameter. | ||
196 | 37 | :returns: If the sed command exit code was zero then return, | ||
197 | 38 | otherwise raise CalledProcessError. | ||
198 | 39 | """ | ||
199 | 40 | expression = r's/{0}/{1}/{2}'.format(before, | ||
200 | 41 | after, flags) | ||
201 | 42 | |||
202 | 43 | return subprocess.check_call(["sed", "-i", "-r", "-e", | ||
203 | 44 | expression, | ||
204 | 45 | os.path.expanduser(filename)]) | ||
205 | 0 | 46 | ||
206 | === added file 'hooks/charmhelpers/core/fstab.py' | |||
207 | --- hooks/charmhelpers/core/fstab.py 1970-01-01 00:00:00 +0000 | |||
208 | +++ hooks/charmhelpers/core/fstab.py 2015-12-17 05:44:32 +0000 | |||
209 | @@ -0,0 +1,134 @@ | |||
210 | 1 | #!/usr/bin/env python | ||
211 | 2 | # -*- coding: utf-8 -*- | ||
212 | 3 | |||
213 | 4 | # Copyright 2014-2015 Canonical Limited. | ||
214 | 5 | # | ||
215 | 6 | # This file is part of charm-helpers. | ||
216 | 7 | # | ||
217 | 8 | # charm-helpers is free software: you can redistribute it and/or modify | ||
218 | 9 | # it under the terms of the GNU Lesser General Public License version 3 as | ||
219 | 10 | # published by the Free Software Foundation. | ||
220 | 11 | # | ||
221 | 12 | # charm-helpers is distributed in the hope that it will be useful, | ||
222 | 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
223 | 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
224 | 15 | # GNU Lesser General Public License for more details. | ||
225 | 16 | # | ||
226 | 17 | # You should have received a copy of the GNU Lesser General Public License | ||
227 | 18 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
228 | 19 | |||
229 | 20 | import io | ||
230 | 21 | import os | ||
231 | 22 | |||
232 | 23 | __author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>' | ||
233 | 24 | |||
234 | 25 | |||
235 | 26 | class Fstab(io.FileIO): | ||
236 | 27 | """This class extends file in order to implement a file reader/writer | ||
237 | 28 | for file `/etc/fstab` | ||
238 | 29 | """ | ||
239 | 30 | |||
240 | 31 | class Entry(object): | ||
241 | 32 | """Entry class represents a non-comment line on the `/etc/fstab` file | ||
242 | 33 | """ | ||
243 | 34 | def __init__(self, device, mountpoint, filesystem, | ||
244 | 35 | options, d=0, p=0): | ||
245 | 36 | self.device = device | ||
246 | 37 | self.mountpoint = mountpoint | ||
247 | 38 | self.filesystem = filesystem | ||
248 | 39 | |||
249 | 40 | if not options: | ||
250 | 41 | options = "defaults" | ||
251 | 42 | |||
252 | 43 | self.options = options | ||
253 | 44 | self.d = int(d) | ||
254 | 45 | self.p = int(p) | ||
255 | 46 | |||
256 | 47 | def __eq__(self, o): | ||
257 | 48 | return str(self) == str(o) | ||
258 | 49 | |||
259 | 50 | def __str__(self): | ||
260 | 51 | return "{} {} {} {} {} {}".format(self.device, | ||
261 | 52 | self.mountpoint, | ||
262 | 53 | self.filesystem, | ||
263 | 54 | self.options, | ||
264 | 55 | self.d, | ||
265 | 56 | self.p) | ||
266 | 57 | |||
267 | 58 | DEFAULT_PATH = os.path.join(os.path.sep, 'etc', 'fstab') | ||
268 | 59 | |||
269 | 60 | def __init__(self, path=None): | ||
270 | 61 | if path: | ||
271 | 62 | self._path = path | ||
272 | 63 | else: | ||
273 | 64 | self._path = self.DEFAULT_PATH | ||
274 | 65 | super(Fstab, self).__init__(self._path, 'rb+') | ||
275 | 66 | |||
276 | 67 | def _hydrate_entry(self, line): | ||
277 | 68 | # NOTE: use split with no arguments to split on any | ||
278 | 69 | # whitespace including tabs | ||
279 | 70 | return Fstab.Entry(*filter( | ||
280 | 71 | lambda x: x not in ('', None), | ||
281 | 72 | line.strip("\n").split())) | ||
282 | 73 | |||
283 | 74 | @property | ||
284 | 75 | def entries(self): | ||
285 | 76 | self.seek(0) | ||
286 | 77 | for line in self.readlines(): | ||
287 | 78 | line = line.decode('us-ascii') | ||
288 | 79 | try: | ||
289 | 80 | if line.strip() and not line.strip().startswith("#"): | ||
290 | 81 | yield self._hydrate_entry(line) | ||
291 | 82 | except ValueError: | ||
292 | 83 | pass | ||
293 | 84 | |||
294 | 85 | def get_entry_by_attr(self, attr, value): | ||
295 | 86 | for entry in self.entries: | ||
296 | 87 | e_attr = getattr(entry, attr) | ||
297 | 88 | if e_attr == value: | ||
298 | 89 | return entry | ||
299 | 90 | return None | ||
300 | 91 | |||
301 | 92 | def add_entry(self, entry): | ||
302 | 93 | if self.get_entry_by_attr('device', entry.device): | ||
303 | 94 | return False | ||
304 | 95 | |||
305 | 96 | self.write((str(entry) + '\n').encode('us-ascii')) | ||
306 | 97 | self.truncate() | ||
307 | 98 | return entry | ||
308 | 99 | |||
309 | 100 | def remove_entry(self, entry): | ||
310 | 101 | self.seek(0) | ||
311 | 102 | |||
312 | 103 | lines = [l.decode('us-ascii') for l in self.readlines()] | ||
313 | 104 | |||
314 | 105 | found = False | ||
315 | 106 | for index, line in enumerate(lines): | ||
316 | 107 | if line.strip() and not line.strip().startswith("#"): | ||
317 | 108 | if self._hydrate_entry(line) == entry: | ||
318 | 109 | found = True | ||
319 | 110 | break | ||
320 | 111 | |||
321 | 112 | if not found: | ||
322 | 113 | return False | ||
323 | 114 | |||
324 | 115 | lines.remove(line) | ||
325 | 116 | |||
326 | 117 | self.seek(0) | ||
327 | 118 | self.write(''.join(lines).encode('us-ascii')) | ||
328 | 119 | self.truncate() | ||
329 | 120 | return True | ||
330 | 121 | |||
331 | 122 | @classmethod | ||
332 | 123 | def remove_by_mountpoint(cls, mountpoint, path=None): | ||
333 | 124 | fstab = cls(path=path) | ||
334 | 125 | entry = fstab.get_entry_by_attr('mountpoint', mountpoint) | ||
335 | 126 | if entry: | ||
336 | 127 | return fstab.remove_entry(entry) | ||
337 | 128 | return False | ||
338 | 129 | |||
339 | 130 | @classmethod | ||
340 | 131 | def add(cls, device, mountpoint, filesystem, options=None, path=None): | ||
341 | 132 | return cls(path=path).add_entry(Fstab.Entry(device, | ||
342 | 133 | mountpoint, filesystem, | ||
343 | 134 | options=options)) | ||
344 | 0 | 135 | ||
345 | === added file 'hooks/charmhelpers/core/hookenv.py' | |||
346 | --- hooks/charmhelpers/core/hookenv.py 1970-01-01 00:00:00 +0000 | |||
347 | +++ hooks/charmhelpers/core/hookenv.py 2015-12-17 05:44:32 +0000 | |||
348 | @@ -0,0 +1,978 @@ | |||
349 | 1 | # Copyright 2014-2015 Canonical Limited. | ||
350 | 2 | # | ||
351 | 3 | # This file is part of charm-helpers. | ||
352 | 4 | # | ||
353 | 5 | # charm-helpers is free software: you can redistribute it and/or modify | ||
354 | 6 | # it under the terms of the GNU Lesser General Public License version 3 as | ||
355 | 7 | # published by the Free Software Foundation. | ||
356 | 8 | # | ||
357 | 9 | # charm-helpers is distributed in the hope that it will be useful, | ||
358 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
359 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
360 | 12 | # GNU Lesser General Public License for more details. | ||
361 | 13 | # | ||
362 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
363 | 15 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
364 | 16 | |||
365 | 17 | "Interactions with the Juju environment" | ||
366 | 18 | # Copyright 2013 Canonical Ltd. | ||
367 | 19 | # | ||
368 | 20 | # Authors: | ||
369 | 21 | # Charm Helpers Developers <juju@lists.ubuntu.com> | ||
370 | 22 | |||
371 | 23 | from __future__ import print_function | ||
372 | 24 | import copy | ||
373 | 25 | from distutils.version import LooseVersion | ||
374 | 26 | from functools import wraps | ||
375 | 27 | import glob | ||
376 | 28 | import os | ||
377 | 29 | import json | ||
378 | 30 | import yaml | ||
379 | 31 | import subprocess | ||
380 | 32 | import sys | ||
381 | 33 | import errno | ||
382 | 34 | import tempfile | ||
383 | 35 | from subprocess import CalledProcessError | ||
384 | 36 | |||
385 | 37 | import six | ||
386 | 38 | if not six.PY3: | ||
387 | 39 | from UserDict import UserDict | ||
388 | 40 | else: | ||
389 | 41 | from collections import UserDict | ||
390 | 42 | |||
391 | 43 | CRITICAL = "CRITICAL" | ||
392 | 44 | ERROR = "ERROR" | ||
393 | 45 | WARNING = "WARNING" | ||
394 | 46 | INFO = "INFO" | ||
395 | 47 | DEBUG = "DEBUG" | ||
396 | 48 | MARKER = object() | ||
397 | 49 | |||
398 | 50 | cache = {} | ||
399 | 51 | |||
400 | 52 | |||
401 | 53 | def cached(func): | ||
402 | 54 | """Cache return values for multiple executions of func + args | ||
403 | 55 | |||
404 | 56 | For example:: | ||
405 | 57 | |||
406 | 58 | @cached | ||
407 | 59 | def unit_get(attribute): | ||
408 | 60 | pass | ||
409 | 61 | |||
410 | 62 | unit_get('test') | ||
411 | 63 | |||
412 | 64 | will cache the result of unit_get + 'test' for future calls. | ||
413 | 65 | """ | ||
414 | 66 | @wraps(func) | ||
415 | 67 | def wrapper(*args, **kwargs): | ||
416 | 68 | global cache | ||
417 | 69 | key = str((func, args, kwargs)) | ||
418 | 70 | try: | ||
419 | 71 | return cache[key] | ||
420 | 72 | except KeyError: | ||
421 | 73 | pass # Drop out of the exception handler scope. | ||
422 | 74 | res = func(*args, **kwargs) | ||
423 | 75 | cache[key] = res | ||
424 | 76 | return res | ||
425 | 77 | wrapper._wrapped = func | ||
426 | 78 | return wrapper | ||
427 | 79 | |||
428 | 80 | |||
429 | 81 | def flush(key): | ||
430 | 82 | """Flushes any entries from function cache where the | ||
431 | 83 | key is found in the function+args """ | ||
432 | 84 | flush_list = [] | ||
433 | 85 | for item in cache: | ||
434 | 86 | if key in item: | ||
435 | 87 | flush_list.append(item) | ||
436 | 88 | for item in flush_list: | ||
437 | 89 | del cache[item] | ||
438 | 90 | |||
439 | 91 | |||
440 | 92 | def log(message, level=None): | ||
441 | 93 | """Write a message to the juju log""" | ||
442 | 94 | command = ['juju-log'] | ||
443 | 95 | if level: | ||
444 | 96 | command += ['-l', level] | ||
445 | 97 | if not isinstance(message, six.string_types): | ||
446 | 98 | message = repr(message) | ||
447 | 99 | command += [message] | ||
448 | 100 | # Missing juju-log should not cause failures in unit tests | ||
449 | 101 | # Send log output to stderr | ||
450 | 102 | try: | ||
451 | 103 | subprocess.call(command) | ||
452 | 104 | except OSError as e: | ||
453 | 105 | if e.errno == errno.ENOENT: | ||
454 | 106 | if level: | ||
455 | 107 | message = "{}: {}".format(level, message) | ||
456 | 108 | message = "juju-log: {}".format(message) | ||
457 | 109 | print(message, file=sys.stderr) | ||
458 | 110 | else: | ||
459 | 111 | raise | ||
460 | 112 | |||
461 | 113 | |||
462 | 114 | class Serializable(UserDict): | ||
463 | 115 | """Wrapper, an object that can be serialized to yaml or json""" | ||
464 | 116 | |||
465 | 117 | def __init__(self, obj): | ||
466 | 118 | # wrap the object | ||
467 | 119 | UserDict.__init__(self) | ||
468 | 120 | self.data = obj | ||
469 | 121 | |||
470 | 122 | def __getattr__(self, attr): | ||
471 | 123 | # See if this object has attribute. | ||
472 | 124 | if attr in ("json", "yaml", "data"): | ||
473 | 125 | return self.__dict__[attr] | ||
474 | 126 | # Check for attribute in wrapped object. | ||
475 | 127 | got = getattr(self.data, attr, MARKER) | ||
476 | 128 | if got is not MARKER: | ||
477 | 129 | return got | ||
478 | 130 | # Proxy to the wrapped object via dict interface. | ||
479 | 131 | try: | ||
480 | 132 | return self.data[attr] | ||
481 | 133 | except KeyError: | ||
482 | 134 | raise AttributeError(attr) | ||
483 | 135 | |||
484 | 136 | def __getstate__(self): | ||
485 | 137 | # Pickle as a standard dictionary. | ||
486 | 138 | return self.data | ||
487 | 139 | |||
488 | 140 | def __setstate__(self, state): | ||
489 | 141 | # Unpickle into our wrapper. | ||
490 | 142 | self.data = state | ||
491 | 143 | |||
492 | 144 | def json(self): | ||
493 | 145 | """Serialize the object to json""" | ||
494 | 146 | return json.dumps(self.data) | ||
495 | 147 | |||
496 | 148 | def yaml(self): | ||
497 | 149 | """Serialize the object to yaml""" | ||
498 | 150 | return yaml.dump(self.data) | ||
499 | 151 | |||
500 | 152 | |||
501 | 153 | def execution_environment(): | ||
502 | 154 | """A convenient bundling of the current execution context""" | ||
503 | 155 | context = {} | ||
504 | 156 | context['conf'] = config() | ||
505 | 157 | if relation_id(): | ||
506 | 158 | context['reltype'] = relation_type() | ||
507 | 159 | context['relid'] = relation_id() | ||
508 | 160 | context['rel'] = relation_get() | ||
509 | 161 | context['unit'] = local_unit() | ||
510 | 162 | context['rels'] = relations() | ||
511 | 163 | context['env'] = os.environ | ||
512 | 164 | return context | ||
513 | 165 | |||
514 | 166 | |||
515 | 167 | def in_relation_hook(): | ||
516 | 168 | """Determine whether we're running in a relation hook""" | ||
517 | 169 | return 'JUJU_RELATION' in os.environ | ||
518 | 170 | |||
519 | 171 | |||
520 | 172 | def relation_type(): | ||
521 | 173 | """The scope for the current relation hook""" | ||
522 | 174 | return os.environ.get('JUJU_RELATION', None) | ||
523 | 175 | |||
524 | 176 | |||
525 | 177 | @cached | ||
526 | 178 | def relation_id(relation_name=None, service_or_unit=None): | ||
527 | 179 | """The relation ID for the current or a specified relation""" | ||
528 | 180 | if not relation_name and not service_or_unit: | ||
529 | 181 | return os.environ.get('JUJU_RELATION_ID', None) | ||
530 | 182 | elif relation_name and service_or_unit: | ||
531 | 183 | service_name = service_or_unit.split('/')[0] | ||
532 | 184 | for relid in relation_ids(relation_name): | ||
533 | 185 | remote_service = remote_service_name(relid) | ||
534 | 186 | if remote_service == service_name: | ||
535 | 187 | return relid | ||
536 | 188 | else: | ||
537 | 189 | raise ValueError('Must specify neither or both of relation_name and service_or_unit') | ||
538 | 190 | |||
539 | 191 | |||
540 | 192 | def local_unit(): | ||
541 | 193 | """Local unit ID""" | ||
542 | 194 | return os.environ['JUJU_UNIT_NAME'] | ||
543 | 195 | |||
544 | 196 | |||
545 | 197 | def remote_unit(): | ||
546 | 198 | """The remote unit for the current relation hook""" | ||
547 | 199 | return os.environ.get('JUJU_REMOTE_UNIT', None) | ||
548 | 200 | |||
549 | 201 | |||
550 | 202 | def service_name(): | ||
551 | 203 | """The name service group this unit belongs to""" | ||
552 | 204 | return local_unit().split('/')[0] | ||
553 | 205 | |||
554 | 206 | |||
555 | 207 | @cached | ||
556 | 208 | def remote_service_name(relid=None): | ||
557 | 209 | """The remote service name for a given relation-id (or the current relation)""" | ||
558 | 210 | if relid is None: | ||
559 | 211 | unit = remote_unit() | ||
560 | 212 | else: | ||
561 | 213 | units = related_units(relid) | ||
562 | 214 | unit = units[0] if units else None | ||
563 | 215 | return unit.split('/')[0] if unit else None | ||
564 | 216 | |||
565 | 217 | |||
566 | 218 | def hook_name(): | ||
567 | 219 | """The name of the currently executing hook""" | ||
568 | 220 | return os.environ.get('JUJU_HOOK_NAME', os.path.basename(sys.argv[0])) | ||
569 | 221 | |||
570 | 222 | |||
571 | 223 | class Config(dict): | ||
572 | 224 | """A dictionary representation of the charm's config.yaml, with some | ||
573 | 225 | extra features: | ||
574 | 226 | |||
575 | 227 | - See which values in the dictionary have changed since the previous hook. | ||
576 | 228 | - For values that have changed, see what the previous value was. | ||
577 | 229 | - Store arbitrary data for use in a later hook. | ||
578 | 230 | |||
579 | 231 | NOTE: Do not instantiate this object directly - instead call | ||
580 | 232 | ``hookenv.config()``, which will return an instance of :class:`Config`. | ||
581 | 233 | |||
582 | 234 | Example usage:: | ||
583 | 235 | |||
584 | 236 | >>> # inside a hook | ||
585 | 237 | >>> from charmhelpers.core import hookenv | ||
586 | 238 | >>> config = hookenv.config() | ||
587 | 239 | >>> config['foo'] | ||
588 | 240 | 'bar' | ||
589 | 241 | >>> # store a new key/value for later use | ||
590 | 242 | >>> config['mykey'] = 'myval' | ||
591 | 243 | |||
592 | 244 | |||
593 | 245 | >>> # user runs `juju set mycharm foo=baz` | ||
594 | 246 | >>> # now we're inside subsequent config-changed hook | ||
595 | 247 | >>> config = hookenv.config() | ||
596 | 248 | >>> config['foo'] | ||
597 | 249 | 'baz' | ||
598 | 250 | >>> # test to see if this val has changed since last hook | ||
599 | 251 | >>> config.changed('foo') | ||
600 | 252 | True | ||
601 | 253 | >>> # what was the previous value? | ||
602 | 254 | >>> config.previous('foo') | ||
603 | 255 | 'bar' | ||
604 | 256 | >>> # keys/values that we add are preserved across hooks | ||
605 | 257 | >>> config['mykey'] | ||
606 | 258 | 'myval' | ||
607 | 259 | |||
608 | 260 | """ | ||
609 | 261 | CONFIG_FILE_NAME = '.juju-persistent-config' | ||
610 | 262 | |||
611 | 263 | def __init__(self, *args, **kw): | ||
612 | 264 | super(Config, self).__init__(*args, **kw) | ||
613 | 265 | self.implicit_save = True | ||
614 | 266 | self._prev_dict = None | ||
615 | 267 | self.path = os.path.join(charm_dir(), Config.CONFIG_FILE_NAME) | ||
616 | 268 | if os.path.exists(self.path): | ||
617 | 269 | self.load_previous() | ||
618 | 270 | atexit(self._implicit_save) | ||
619 | 271 | |||
620 | 272 | def load_previous(self, path=None): | ||
621 | 273 | """Load previous copy of config from disk. | ||
622 | 274 | |||
623 | 275 | In normal usage you don't need to call this method directly - it | ||
624 | 276 | is called automatically at object initialization. | ||
625 | 277 | |||
626 | 278 | :param path: | ||
627 | 279 | |||
628 | 280 | File path from which to load the previous config. If `None`, | ||
629 | 281 | config is loaded from the default location. If `path` is | ||
630 | 282 | specified, subsequent `save()` calls will write to the same | ||
631 | 283 | path. | ||
632 | 284 | |||
633 | 285 | """ | ||
634 | 286 | self.path = path or self.path | ||
635 | 287 | with open(self.path) as f: | ||
636 | 288 | self._prev_dict = json.load(f) | ||
637 | 289 | for k, v in copy.deepcopy(self._prev_dict).items(): | ||
638 | 290 | if k not in self: | ||
639 | 291 | self[k] = v | ||
640 | 292 | |||
641 | 293 | def changed(self, key): | ||
642 | 294 | """Return True if the current value for this key is different from | ||
643 | 295 | the previous value. | ||
644 | 296 | |||
645 | 297 | """ | ||
646 | 298 | if self._prev_dict is None: | ||
647 | 299 | return True | ||
648 | 300 | return self.previous(key) != self.get(key) | ||
649 | 301 | |||
650 | 302 | def previous(self, key): | ||
651 | 303 | """Return previous value for this key, or None if there | ||
652 | 304 | is no previous value. | ||
653 | 305 | |||
654 | 306 | """ | ||
655 | 307 | if self._prev_dict: | ||
656 | 308 | return self._prev_dict.get(key) | ||
657 | 309 | return None | ||
658 | 310 | |||
659 | 311 | def save(self): | ||
660 | 312 | """Save this config to disk. | ||
661 | 313 | |||
662 | 314 | If the charm is using the :mod:`Services Framework <services.base>` | ||
663 | 315 | or :meth:'@hook <Hooks.hook>' decorator, this | ||
664 | 316 | is called automatically at the end of successful hook execution. | ||
665 | 317 | Otherwise, it should be called directly by user code. | ||
666 | 318 | |||
667 | 319 | To disable automatic saves, set ``implicit_save=False`` on this | ||
668 | 320 | instance. | ||
669 | 321 | |||
670 | 322 | """ | ||
671 | 323 | with open(self.path, 'w') as f: | ||
672 | 324 | json.dump(self, f) | ||
673 | 325 | |||
674 | 326 | def _implicit_save(self): | ||
675 | 327 | if self.implicit_save: | ||
676 | 328 | self.save() | ||
677 | 329 | |||
678 | 330 | |||
679 | 331 | @cached | ||
680 | 332 | def config(scope=None): | ||
681 | 333 | """Juju charm configuration""" | ||
682 | 334 | config_cmd_line = ['config-get'] | ||
683 | 335 | if scope is not None: | ||
684 | 336 | config_cmd_line.append(scope) | ||
685 | 337 | config_cmd_line.append('--format=json') | ||
686 | 338 | try: | ||
687 | 339 | config_data = json.loads( | ||
688 | 340 | subprocess.check_output(config_cmd_line).decode('UTF-8')) | ||
689 | 341 | if scope is not None: | ||
690 | 342 | return config_data | ||
691 | 343 | return Config(config_data) | ||
692 | 344 | except ValueError: | ||
693 | 345 | return None | ||
694 | 346 | |||
695 | 347 | |||
696 | 348 | @cached | ||
697 | 349 | def relation_get(attribute=None, unit=None, rid=None): | ||
698 | 350 | """Get relation information""" | ||
699 | 351 | _args = ['relation-get', '--format=json'] | ||
700 | 352 | if rid: | ||
701 | 353 | _args.append('-r') | ||
702 | 354 | _args.append(rid) | ||
703 | 355 | _args.append(attribute or '-') | ||
704 | 356 | if unit: | ||
705 | 357 | _args.append(unit) | ||
706 | 358 | try: | ||
707 | 359 | return json.loads(subprocess.check_output(_args).decode('UTF-8')) | ||
708 | 360 | except ValueError: | ||
709 | 361 | return None | ||
710 | 362 | except CalledProcessError as e: | ||
711 | 363 | if e.returncode == 2: | ||
712 | 364 | return None | ||
713 | 365 | raise | ||
714 | 366 | |||
715 | 367 | |||
716 | 368 | def relation_set(relation_id=None, relation_settings=None, **kwargs): | ||
717 | 369 | """Set relation information for the current unit""" | ||
718 | 370 | relation_settings = relation_settings if relation_settings else {} | ||
719 | 371 | relation_cmd_line = ['relation-set'] | ||
720 | 372 | accepts_file = "--file" in subprocess.check_output( | ||
721 | 373 | relation_cmd_line + ["--help"], universal_newlines=True) | ||
722 | 374 | if relation_id is not None: | ||
723 | 375 | relation_cmd_line.extend(('-r', relation_id)) | ||
724 | 376 | settings = relation_settings.copy() | ||
725 | 377 | settings.update(kwargs) | ||
726 | 378 | for key, value in settings.items(): | ||
727 | 379 | # Force value to be a string: it always should, but some call | ||
728 | 380 | # sites pass in things like dicts or numbers. | ||
729 | 381 | if value is not None: | ||
730 | 382 | settings[key] = "{}".format(value) | ||
731 | 383 | if accepts_file: | ||
732 | 384 | # --file was introduced in Juju 1.23.2. Use it by default if | ||
733 | 385 | # available, since otherwise we'll break if the relation data is | ||
734 | 386 | # too big. Ideally we should tell relation-set to read the data from | ||
735 | 387 | # stdin, but that feature is broken in 1.23.2: Bug #1454678. | ||
736 | 388 | with tempfile.NamedTemporaryFile(delete=False) as settings_file: | ||
737 | 389 | settings_file.write(yaml.safe_dump(settings).encode("utf-8")) | ||
738 | 390 | subprocess.check_call( | ||
739 | 391 | relation_cmd_line + ["--file", settings_file.name]) | ||
740 | 392 | os.remove(settings_file.name) | ||
741 | 393 | else: | ||
742 | 394 | for key, value in settings.items(): | ||
743 | 395 | if value is None: | ||
744 | 396 | relation_cmd_line.append('{}='.format(key)) | ||
745 | 397 | else: | ||
746 | 398 | relation_cmd_line.append('{}={}'.format(key, value)) | ||
747 | 399 | subprocess.check_call(relation_cmd_line) | ||
748 | 400 | # Flush cache of any relation-gets for local unit | ||
749 | 401 | flush(local_unit()) | ||
750 | 402 | |||
751 | 403 | |||
752 | 404 | def relation_clear(r_id=None): | ||
753 | 405 | ''' Clears any relation data already set on relation r_id ''' | ||
754 | 406 | settings = relation_get(rid=r_id, | ||
755 | 407 | unit=local_unit()) | ||
756 | 408 | for setting in settings: | ||
757 | 409 | if setting not in ['public-address', 'private-address']: | ||
758 | 410 | settings[setting] = None | ||
759 | 411 | relation_set(relation_id=r_id, | ||
760 | 412 | **settings) | ||
761 | 413 | |||
762 | 414 | |||
763 | 415 | @cached | ||
764 | 416 | def relation_ids(reltype=None): | ||
765 | 417 | """A list of relation_ids""" | ||
766 | 418 | reltype = reltype or relation_type() | ||
767 | 419 | relid_cmd_line = ['relation-ids', '--format=json'] | ||
768 | 420 | if reltype is not None: | ||
769 | 421 | relid_cmd_line.append(reltype) | ||
770 | 422 | return json.loads( | ||
771 | 423 | subprocess.check_output(relid_cmd_line).decode('UTF-8')) or [] | ||
772 | 424 | return [] | ||
773 | 425 | |||
774 | 426 | |||
775 | 427 | @cached | ||
776 | 428 | def related_units(relid=None): | ||
777 | 429 | """A list of related units""" | ||
778 | 430 | relid = relid or relation_id() | ||
779 | 431 | units_cmd_line = ['relation-list', '--format=json'] | ||
780 | 432 | if relid is not None: | ||
781 | 433 | units_cmd_line.extend(('-r', relid)) | ||
782 | 434 | return json.loads( | ||
783 | 435 | subprocess.check_output(units_cmd_line).decode('UTF-8')) or [] | ||
784 | 436 | |||
785 | 437 | |||
786 | 438 | @cached | ||
787 | 439 | def relation_for_unit(unit=None, rid=None): | ||
788 | 440 | """Get the json represenation of a unit's relation""" | ||
789 | 441 | unit = unit or remote_unit() | ||
790 | 442 | relation = relation_get(unit=unit, rid=rid) | ||
791 | 443 | for key in relation: | ||
792 | 444 | if key.endswith('-list'): | ||
793 | 445 | relation[key] = relation[key].split() | ||
794 | 446 | relation['__unit__'] = unit | ||
795 | 447 | return relation | ||
796 | 448 | |||
797 | 449 | |||
798 | 450 | @cached | ||
799 | 451 | def relations_for_id(relid=None): | ||
800 | 452 | """Get relations of a specific relation ID""" | ||
801 | 453 | relation_data = [] | ||
802 | 454 | relid = relid or relation_ids() | ||
803 | 455 | for unit in related_units(relid): | ||
804 | 456 | unit_data = relation_for_unit(unit, relid) | ||
805 | 457 | unit_data['__relid__'] = relid | ||
806 | 458 | relation_data.append(unit_data) | ||
807 | 459 | return relation_data | ||
808 | 460 | |||
809 | 461 | |||
810 | 462 | @cached | ||
811 | 463 | def relations_of_type(reltype=None): | ||
812 | 464 | """Get relations of a specific type""" | ||
813 | 465 | relation_data = [] | ||
814 | 466 | reltype = reltype or relation_type() | ||
815 | 467 | for relid in relation_ids(reltype): | ||
816 | 468 | for relation in relations_for_id(relid): | ||
817 | 469 | relation['__relid__'] = relid | ||
818 | 470 | relation_data.append(relation) | ||
819 | 471 | return relation_data | ||
820 | 472 | |||
821 | 473 | |||
822 | 474 | @cached | ||
823 | 475 | def metadata(): | ||
824 | 476 | """Get the current charm metadata.yaml contents as a python object""" | ||
825 | 477 | with open(os.path.join(charm_dir(), 'metadata.yaml')) as md: | ||
826 | 478 | return yaml.safe_load(md) | ||
827 | 479 | |||
828 | 480 | |||
829 | 481 | @cached | ||
830 | 482 | def relation_types(): | ||
831 | 483 | """Get a list of relation types supported by this charm""" | ||
832 | 484 | rel_types = [] | ||
833 | 485 | md = metadata() | ||
834 | 486 | for key in ('provides', 'requires', 'peers'): | ||
835 | 487 | section = md.get(key) | ||
836 | 488 | if section: | ||
837 | 489 | rel_types.extend(section.keys()) | ||
838 | 490 | return rel_types | ||
839 | 491 | |||
840 | 492 | |||
841 | 493 | @cached | ||
842 | 494 | def peer_relation_id(): | ||
843 | 495 | '''Get the peers relation id if a peers relation has been joined, else None.''' | ||
844 | 496 | md = metadata() | ||
845 | 497 | section = md.get('peers') | ||
846 | 498 | if section: | ||
847 | 499 | for key in section: | ||
848 | 500 | relids = relation_ids(key) | ||
849 | 501 | if relids: | ||
850 | 502 | return relids[0] | ||
851 | 503 | return None | ||
852 | 504 | |||
853 | 505 | |||
854 | 506 | @cached | ||
855 | 507 | def relation_to_interface(relation_name): | ||
856 | 508 | """ | ||
857 | 509 | Given the name of a relation, return the interface that relation uses. | ||
858 | 510 | |||
859 | 511 | :returns: The interface name, or ``None``. | ||
860 | 512 | """ | ||
861 | 513 | return relation_to_role_and_interface(relation_name)[1] | ||
862 | 514 | |||
863 | 515 | |||
864 | 516 | @cached | ||
865 | 517 | def relation_to_role_and_interface(relation_name): | ||
866 | 518 | """ | ||
867 | 519 | Given the name of a relation, return the role and the name of the interface | ||
868 | 520 | that relation uses (where role is one of ``provides``, ``requires``, or ``peers``). | ||
869 | 521 | |||
870 | 522 | :returns: A tuple containing ``(role, interface)``, or ``(None, None)``. | ||
871 | 523 | """ | ||
872 | 524 | _metadata = metadata() | ||
873 | 525 | for role in ('provides', 'requires', 'peers'): | ||
874 | 526 | interface = _metadata.get(role, {}).get(relation_name, {}).get('interface') | ||
875 | 527 | if interface: | ||
876 | 528 | return role, interface | ||
877 | 529 | return None, None | ||
878 | 530 | |||
879 | 531 | |||
880 | 532 | @cached | ||
881 | 533 | def role_and_interface_to_relations(role, interface_name): | ||
882 | 534 | """ | ||
883 | 535 | Given a role and interface name, return a list of relation names for the | ||
884 | 536 | current charm that use that interface under that role (where role is one | ||
885 | 537 | of ``provides``, ``requires``, or ``peers``). | ||
886 | 538 | |||
887 | 539 | :returns: A list of relation names. | ||
888 | 540 | """ | ||
889 | 541 | _metadata = metadata() | ||
890 | 542 | results = [] | ||
891 | 543 | for relation_name, relation in _metadata.get(role, {}).items(): | ||
892 | 544 | if relation['interface'] == interface_name: | ||
893 | 545 | results.append(relation_name) | ||
894 | 546 | return results | ||
895 | 547 | |||
896 | 548 | |||
897 | 549 | @cached | ||
898 | 550 | def interface_to_relations(interface_name): | ||
899 | 551 | """ | ||
900 | 552 | Given an interface, return a list of relation names for the current | ||
901 | 553 | charm that use that interface. | ||
902 | 554 | |||
903 | 555 | :returns: A list of relation names. | ||
904 | 556 | """ | ||
905 | 557 | results = [] | ||
906 | 558 | for role in ('provides', 'requires', 'peers'): | ||
907 | 559 | results.extend(role_and_interface_to_relations(role, interface_name)) | ||
908 | 560 | return results | ||
909 | 561 | |||
910 | 562 | |||
911 | 563 | @cached | ||
912 | 564 | def charm_name(): | ||
913 | 565 | """Get the name of the current charm as is specified on metadata.yaml""" | ||
914 | 566 | return metadata().get('name') | ||
915 | 567 | |||
916 | 568 | |||
917 | 569 | @cached | ||
918 | 570 | def relations(): | ||
919 | 571 | """Get a nested dictionary of relation data for all related units""" | ||
920 | 572 | rels = {} | ||
921 | 573 | for reltype in relation_types(): | ||
922 | 574 | relids = {} | ||
923 | 575 | for relid in relation_ids(reltype): | ||
924 | 576 | units = {local_unit(): relation_get(unit=local_unit(), rid=relid)} | ||
925 | 577 | for unit in related_units(relid): | ||
926 | 578 | reldata = relation_get(unit=unit, rid=relid) | ||
927 | 579 | units[unit] = reldata | ||
928 | 580 | relids[relid] = units | ||
929 | 581 | rels[reltype] = relids | ||
930 | 582 | return rels | ||
931 | 583 | |||
932 | 584 | |||
933 | 585 | @cached | ||
934 | 586 | def is_relation_made(relation, keys='private-address'): | ||
935 | 587 | ''' | ||
936 | 588 | Determine whether a relation is established by checking for | ||
937 | 589 | presence of key(s). If a list of keys is provided, they | ||
938 | 590 | must all be present for the relation to be identified as made | ||
939 | 591 | ''' | ||
940 | 592 | if isinstance(keys, str): | ||
941 | 593 | keys = [keys] | ||
942 | 594 | for r_id in relation_ids(relation): | ||
943 | 595 | for unit in related_units(r_id): | ||
944 | 596 | context = {} | ||
945 | 597 | for k in keys: | ||
946 | 598 | context[k] = relation_get(k, rid=r_id, | ||
947 | 599 | unit=unit) | ||
948 | 600 | if None not in context.values(): | ||
949 | 601 | return True | ||
950 | 602 | return False | ||
951 | 603 | |||
952 | 604 | |||
953 | 605 | def open_port(port, protocol="TCP"): | ||
954 | 606 | """Open a service network port""" | ||
955 | 607 | _args = ['open-port'] | ||
956 | 608 | _args.append('{}/{}'.format(port, protocol)) | ||
957 | 609 | subprocess.check_call(_args) | ||
958 | 610 | |||
959 | 611 | |||
960 | 612 | def close_port(port, protocol="TCP"): | ||
961 | 613 | """Close a service network port""" | ||
962 | 614 | _args = ['close-port'] | ||
963 | 615 | _args.append('{}/{}'.format(port, protocol)) | ||
964 | 616 | subprocess.check_call(_args) | ||
965 | 617 | |||
966 | 618 | |||
967 | 619 | @cached | ||
968 | 620 | def unit_get(attribute): | ||
969 | 621 | """Get the unit ID for the remote unit""" | ||
970 | 622 | _args = ['unit-get', '--format=json', attribute] | ||
971 | 623 | try: | ||
972 | 624 | return json.loads(subprocess.check_output(_args).decode('UTF-8')) | ||
973 | 625 | except ValueError: | ||
974 | 626 | return None | ||
975 | 627 | |||
976 | 628 | |||
977 | 629 | def unit_public_ip(): | ||
978 | 630 | """Get this unit's public IP address""" | ||
979 | 631 | return unit_get('public-address') | ||
980 | 632 | |||
981 | 633 | |||
982 | 634 | def unit_private_ip(): | ||
983 | 635 | """Get this unit's private IP address""" | ||
984 | 636 | return unit_get('private-address') | ||
985 | 637 | |||
986 | 638 | |||
987 | 639 | @cached | ||
988 | 640 | def storage_get(attribute=None, storage_id=None): | ||
989 | 641 | """Get storage attributes""" | ||
990 | 642 | _args = ['storage-get', '--format=json'] | ||
991 | 643 | if storage_id: | ||
992 | 644 | _args.extend(('-s', storage_id)) | ||
993 | 645 | if attribute: | ||
994 | 646 | _args.append(attribute) | ||
995 | 647 | try: | ||
996 | 648 | return json.loads(subprocess.check_output(_args).decode('UTF-8')) | ||
997 | 649 | except ValueError: | ||
998 | 650 | return None | ||
999 | 651 | |||
1000 | 652 | |||
1001 | 653 | @cached | ||
1002 | 654 | def storage_list(storage_name=None): | ||
1003 | 655 | """List the storage IDs for the unit""" | ||
1004 | 656 | _args = ['storage-list', '--format=json'] | ||
1005 | 657 | if storage_name: | ||
1006 | 658 | _args.append(storage_name) | ||
1007 | 659 | try: | ||
1008 | 660 | return json.loads(subprocess.check_output(_args).decode('UTF-8')) | ||
1009 | 661 | except ValueError: | ||
1010 | 662 | return None | ||
1011 | 663 | except OSError as e: | ||
1012 | 664 | import errno | ||
1013 | 665 | if e.errno == errno.ENOENT: | ||
1014 | 666 | # storage-list does not exist | ||
1015 | 667 | return [] | ||
1016 | 668 | raise | ||
1017 | 669 | |||
1018 | 670 | |||
1019 | 671 | class UnregisteredHookError(Exception): | ||
1020 | 672 | """Raised when an undefined hook is called""" | ||
1021 | 673 | pass | ||
1022 | 674 | |||
1023 | 675 | |||
1024 | 676 | class Hooks(object): | ||
1025 | 677 | """A convenient handler for hook functions. | ||
1026 | 678 | |||
1027 | 679 | Example:: | ||
1028 | 680 | |||
1029 | 681 | hooks = Hooks() | ||
1030 | 682 | |||
1031 | 683 | # register a hook, taking its name from the function name | ||
1032 | 684 | @hooks.hook() | ||
1033 | 685 | def install(): | ||
1034 | 686 | pass # your code here | ||
1035 | 687 | |||
1036 | 688 | # register a hook, providing a custom hook name | ||
1037 | 689 | @hooks.hook("config-changed") | ||
1038 | 690 | def config_changed(): | ||
1039 | 691 | pass # your code here | ||
1040 | 692 | |||
1041 | 693 | if __name__ == "__main__": | ||
1042 | 694 | # execute a hook based on the name the program is called by | ||
1043 | 695 | hooks.execute(sys.argv) | ||
1044 | 696 | """ | ||
1045 | 697 | |||
1046 | 698 | def __init__(self, config_save=None): | ||
1047 | 699 | super(Hooks, self).__init__() | ||
1048 | 700 | self._hooks = {} | ||
1049 | 701 | |||
1050 | 702 | # For unknown reasons, we allow the Hooks constructor to override | ||
1051 | 703 | # config().implicit_save. | ||
1052 | 704 | if config_save is not None: | ||
1053 | 705 | config().implicit_save = config_save | ||
1054 | 706 | |||
1055 | 707 | def register(self, name, function): | ||
1056 | 708 | """Register a hook""" | ||
1057 | 709 | self._hooks[name] = function | ||
1058 | 710 | |||
1059 | 711 | def execute(self, args): | ||
1060 | 712 | """Execute a registered hook based on args[0]""" | ||
1061 | 713 | _run_atstart() | ||
1062 | 714 | hook_name = os.path.basename(args[0]) | ||
1063 | 715 | if hook_name in self._hooks: | ||
1064 | 716 | try: | ||
1065 | 717 | self._hooks[hook_name]() | ||
1066 | 718 | except SystemExit as x: | ||
1067 | 719 | if x.code is None or x.code == 0: | ||
1068 | 720 | _run_atexit() | ||
1069 | 721 | raise | ||
1070 | 722 | _run_atexit() | ||
1071 | 723 | else: | ||
1072 | 724 | raise UnregisteredHookError(hook_name) | ||
1073 | 725 | |||
1074 | 726 | def hook(self, *hook_names): | ||
1075 | 727 | """Decorator, registering them as hooks""" | ||
1076 | 728 | def wrapper(decorated): | ||
1077 | 729 | for hook_name in hook_names: | ||
1078 | 730 | self.register(hook_name, decorated) | ||
1079 | 731 | else: | ||
1080 | 732 | self.register(decorated.__name__, decorated) | ||
1081 | 733 | if '_' in decorated.__name__: | ||
1082 | 734 | self.register( | ||
1083 | 735 | decorated.__name__.replace('_', '-'), decorated) | ||
1084 | 736 | return decorated | ||
1085 | 737 | return wrapper | ||
1086 | 738 | |||
1087 | 739 | |||
1088 | 740 | def charm_dir(): | ||
1089 | 741 | """Return the root directory of the current charm""" | ||
1090 | 742 | return os.environ.get('CHARM_DIR') | ||
1091 | 743 | |||
1092 | 744 | |||
1093 | 745 | @cached | ||
1094 | 746 | def action_get(key=None): | ||
1095 | 747 | """Gets the value of an action parameter, or all key/value param pairs""" | ||
1096 | 748 | cmd = ['action-get'] | ||
1097 | 749 | if key is not None: | ||
1098 | 750 | cmd.append(key) | ||
1099 | 751 | cmd.append('--format=json') | ||
1100 | 752 | action_data = json.loads(subprocess.check_output(cmd).decode('UTF-8')) | ||
1101 | 753 | return action_data | ||
1102 | 754 | |||
1103 | 755 | |||
1104 | 756 | def action_set(values): | ||
1105 | 757 | """Sets the values to be returned after the action finishes""" | ||
1106 | 758 | cmd = ['action-set'] | ||
1107 | 759 | for k, v in list(values.items()): | ||
1108 | 760 | cmd.append('{}={}'.format(k, v)) | ||
1109 | 761 | subprocess.check_call(cmd) | ||
1110 | 762 | |||
1111 | 763 | |||
1112 | 764 | def action_fail(message): | ||
1113 | 765 | """Sets the action status to failed and sets the error message. | ||
1114 | 766 | |||
1115 | 767 | The results set by action_set are preserved.""" | ||
1116 | 768 | subprocess.check_call(['action-fail', message]) | ||
1117 | 769 | |||
1118 | 770 | |||
1119 | 771 | def action_name(): | ||
1120 | 772 | """Get the name of the currently executing action.""" | ||
1121 | 773 | return os.environ.get('JUJU_ACTION_NAME') | ||
1122 | 774 | |||
1123 | 775 | |||
1124 | 776 | def action_uuid(): | ||
1125 | 777 | """Get the UUID of the currently executing action.""" | ||
1126 | 778 | return os.environ.get('JUJU_ACTION_UUID') | ||
1127 | 779 | |||
1128 | 780 | |||
1129 | 781 | def action_tag(): | ||
1130 | 782 | """Get the tag for the currently executing action.""" | ||
1131 | 783 | return os.environ.get('JUJU_ACTION_TAG') | ||
1132 | 784 | |||
1133 | 785 | |||
1134 | 786 | def status_set(workload_state, message): | ||
1135 | 787 | """Set the workload state with a message | ||
1136 | 788 | |||
1137 | 789 | Use status-set to set the workload state with a message which is visible | ||
1138 | 790 | to the user via juju status. If the status-set command is not found then | ||
1139 | 791 | assume this is juju < 1.23 and juju-log the message unstead. | ||
1140 | 792 | |||
1141 | 793 | workload_state -- valid juju workload state. | ||
1142 | 794 | message -- status update message | ||
1143 | 795 | """ | ||
1144 | 796 | valid_states = ['maintenance', 'blocked', 'waiting', 'active'] | ||
1145 | 797 | if workload_state not in valid_states: | ||
1146 | 798 | raise ValueError( | ||
1147 | 799 | '{!r} is not a valid workload state'.format(workload_state) | ||
1148 | 800 | ) | ||
1149 | 801 | cmd = ['status-set', workload_state, message] | ||
1150 | 802 | try: | ||
1151 | 803 | ret = subprocess.call(cmd) | ||
1152 | 804 | if ret == 0: | ||
1153 | 805 | return | ||
1154 | 806 | except OSError as e: | ||
1155 | 807 | if e.errno != errno.ENOENT: | ||
1156 | 808 | raise | ||
1157 | 809 | log_message = 'status-set failed: {} {}'.format(workload_state, | ||
1158 | 810 | message) | ||
1159 | 811 | log(log_message, level='INFO') | ||
1160 | 812 | |||
1161 | 813 | |||
1162 | 814 | def status_get(): | ||
1163 | 815 | """Retrieve the previously set juju workload state and message | ||
1164 | 816 | |||
1165 | 817 | If the status-get command is not found then assume this is juju < 1.23 and | ||
1166 | 818 | return 'unknown', "" | ||
1167 | 819 | |||
1168 | 820 | """ | ||
1169 | 821 | cmd = ['status-get', "--format=json", "--include-data"] | ||
1170 | 822 | try: | ||
1171 | 823 | raw_status = subprocess.check_output(cmd) | ||
1172 | 824 | except OSError as e: | ||
1173 | 825 | if e.errno == errno.ENOENT: | ||
1174 | 826 | return ('unknown', "") | ||
1175 | 827 | else: | ||
1176 | 828 | raise | ||
1177 | 829 | else: | ||
1178 | 830 | status = json.loads(raw_status.decode("UTF-8")) | ||
1179 | 831 | return (status["status"], status["message"]) | ||
1180 | 832 | |||
1181 | 833 | |||
1182 | 834 | def translate_exc(from_exc, to_exc): | ||
1183 | 835 | def inner_translate_exc1(f): | ||
1184 | 836 | @wraps(f) | ||
1185 | 837 | def inner_translate_exc2(*args, **kwargs): | ||
1186 | 838 | try: | ||
1187 | 839 | return f(*args, **kwargs) | ||
1188 | 840 | except from_exc: | ||
1189 | 841 | raise to_exc | ||
1190 | 842 | |||
1191 | 843 | return inner_translate_exc2 | ||
1192 | 844 | |||
1193 | 845 | return inner_translate_exc1 | ||
1194 | 846 | |||
1195 | 847 | |||
1196 | 848 | @translate_exc(from_exc=OSError, to_exc=NotImplementedError) | ||
1197 | 849 | def is_leader(): | ||
1198 | 850 | """Does the current unit hold the juju leadership | ||
1199 | 851 | |||
1200 | 852 | Uses juju to determine whether the current unit is the leader of its peers | ||
1201 | 853 | """ | ||
1202 | 854 | cmd = ['is-leader', '--format=json'] | ||
1203 | 855 | return json.loads(subprocess.check_output(cmd).decode('UTF-8')) | ||
1204 | 856 | |||
1205 | 857 | |||
1206 | 858 | @translate_exc(from_exc=OSError, to_exc=NotImplementedError) | ||
1207 | 859 | def leader_get(attribute=None): | ||
1208 | 860 | """Juju leader get value(s)""" | ||
1209 | 861 | cmd = ['leader-get', '--format=json'] + [attribute or '-'] | ||
1210 | 862 | return json.loads(subprocess.check_output(cmd).decode('UTF-8')) | ||
1211 | 863 | |||
1212 | 864 | |||
1213 | 865 | @translate_exc(from_exc=OSError, to_exc=NotImplementedError) | ||
1214 | 866 | def leader_set(settings=None, **kwargs): | ||
1215 | 867 | """Juju leader set value(s)""" | ||
1216 | 868 | # Don't log secrets. | ||
1217 | 869 | # log("Juju leader-set '%s'" % (settings), level=DEBUG) | ||
1218 | 870 | cmd = ['leader-set'] | ||
1219 | 871 | settings = settings or {} | ||
1220 | 872 | settings.update(kwargs) | ||
1221 | 873 | for k, v in settings.items(): | ||
1222 | 874 | if v is None: | ||
1223 | 875 | cmd.append('{}='.format(k)) | ||
1224 | 876 | else: | ||
1225 | 877 | cmd.append('{}={}'.format(k, v)) | ||
1226 | 878 | subprocess.check_call(cmd) | ||
1227 | 879 | |||
1228 | 880 | |||
1229 | 881 | @translate_exc(from_exc=OSError, to_exc=NotImplementedError) | ||
1230 | 882 | def payload_register(ptype, klass, pid): | ||
1231 | 883 | """ is used while a hook is running to let Juju know that a | ||
1232 | 884 | payload has been started.""" | ||
1233 | 885 | cmd = ['payload-register'] | ||
1234 | 886 | for x in [ptype, klass, pid]: | ||
1235 | 887 | cmd.append(x) | ||
1236 | 888 | subprocess.check_call(cmd) | ||
1237 | 889 | |||
1238 | 890 | |||
1239 | 891 | @translate_exc(from_exc=OSError, to_exc=NotImplementedError) | ||
1240 | 892 | def payload_unregister(klass, pid): | ||
1241 | 893 | """ is used while a hook is running to let Juju know | ||
1242 | 894 | that a payload has been manually stopped. The <class> and <id> provided | ||
1243 | 895 | must match a payload that has been previously registered with juju using | ||
1244 | 896 | payload-register.""" | ||
1245 | 897 | cmd = ['payload-unregister'] | ||
1246 | 898 | for x in [klass, pid]: | ||
1247 | 899 | cmd.append(x) | ||
1248 | 900 | subprocess.check_call(cmd) | ||
1249 | 901 | |||
1250 | 902 | |||
1251 | 903 | @translate_exc(from_exc=OSError, to_exc=NotImplementedError) | ||
1252 | 904 | def payload_status_set(klass, pid, status): | ||
1253 | 905 | """is used to update the current status of a registered payload. | ||
1254 | 906 | The <class> and <id> provided must match a payload that has been previously | ||
1255 | 907 | registered with juju using payload-register. The <status> must be one of the | ||
1256 | 908 | follow: starting, started, stopping, stopped""" | ||
1257 | 909 | cmd = ['payload-status-set'] | ||
1258 | 910 | for x in [klass, pid, status]: | ||
1259 | 911 | cmd.append(x) | ||
1260 | 912 | subprocess.check_call(cmd) | ||
1261 | 913 | |||
1262 | 914 | |||
1263 | 915 | @cached | ||
1264 | 916 | def juju_version(): | ||
1265 | 917 | """Full version string (eg. '1.23.3.1-trusty-amd64')""" | ||
1266 | 918 | # Per https://bugs.launchpad.net/juju-core/+bug/1455368/comments/1 | ||
1267 | 919 | jujud = glob.glob('/var/lib/juju/tools/machine-*/jujud')[0] | ||
1268 | 920 | return subprocess.check_output([jujud, 'version'], | ||
1269 | 921 | universal_newlines=True).strip() | ||
1270 | 922 | |||
1271 | 923 | |||
1272 | 924 | @cached | ||
1273 | 925 | def has_juju_version(minimum_version): | ||
1274 | 926 | """Return True if the Juju version is at least the provided version""" | ||
1275 | 927 | return LooseVersion(juju_version()) >= LooseVersion(minimum_version) | ||
1276 | 928 | |||
1277 | 929 | |||
1278 | 930 | _atexit = [] | ||
1279 | 931 | _atstart = [] | ||
1280 | 932 | |||
1281 | 933 | |||
1282 | 934 | def atstart(callback, *args, **kwargs): | ||
1283 | 935 | '''Schedule a callback to run before the main hook. | ||
1284 | 936 | |||
1285 | 937 | Callbacks are run in the order they were added. | ||
1286 | 938 | |||
1287 | 939 | This is useful for modules and classes to perform initialization | ||
1288 | 940 | and inject behavior. In particular: | ||
1289 | 941 | |||
1290 | 942 | - Run common code before all of your hooks, such as logging | ||
1291 | 943 | the hook name or interesting relation data. | ||
1292 | 944 | - Defer object or module initialization that requires a hook | ||
1293 | 945 | context until we know there actually is a hook context, | ||
1294 | 946 | making testing easier. | ||
1295 | 947 | - Rather than requiring charm authors to include boilerplate to | ||
1296 | 948 | invoke your helper's behavior, have it run automatically if | ||
1297 | 949 | your object is instantiated or module imported. | ||
1298 | 950 | |||
1299 | 951 | This is not at all useful after your hook framework as been launched. | ||
1300 | 952 | ''' | ||
1301 | 953 | global _atstart | ||
1302 | 954 | _atstart.append((callback, args, kwargs)) | ||
1303 | 955 | |||
1304 | 956 | |||
1305 | 957 | def atexit(callback, *args, **kwargs): | ||
1306 | 958 | '''Schedule a callback to run on successful hook completion. | ||
1307 | 959 | |||
1308 | 960 | Callbacks are run in the reverse order that they were added.''' | ||
1309 | 961 | _atexit.append((callback, args, kwargs)) | ||
1310 | 962 | |||
1311 | 963 | |||
1312 | 964 | def _run_atstart(): | ||
1313 | 965 | '''Hook frameworks must invoke this before running the main hook body.''' | ||
1314 | 966 | global _atstart | ||
1315 | 967 | for callback, args, kwargs in _atstart: | ||
1316 | 968 | callback(*args, **kwargs) | ||
1317 | 969 | del _atstart[:] | ||
1318 | 970 | |||
1319 | 971 | |||
1320 | 972 | def _run_atexit(): | ||
1321 | 973 | '''Hook frameworks must invoke this after the main hook body has | ||
1322 | 974 | successfully completed. Do not invoke it if the hook fails.''' | ||
1323 | 975 | global _atexit | ||
1324 | 976 | for callback, args, kwargs in reversed(_atexit): | ||
1325 | 977 | callback(*args, **kwargs) | ||
1326 | 978 | del _atexit[:] | ||
1327 | 0 | 979 | ||
1328 | === added file 'hooks/charmhelpers/core/host.py' | |||
1329 | --- hooks/charmhelpers/core/host.py 1970-01-01 00:00:00 +0000 | |||
1330 | +++ hooks/charmhelpers/core/host.py 2015-12-17 05:44:32 +0000 | |||
1331 | @@ -0,0 +1,641 @@ | |||
1332 | 1 | # Copyright 2014-2015 Canonical Limited. | ||
1333 | 2 | # | ||
1334 | 3 | # This file is part of charm-helpers. | ||
1335 | 4 | # | ||
1336 | 5 | # charm-helpers is free software: you can redistribute it and/or modify | ||
1337 | 6 | # it under the terms of the GNU Lesser General Public License version 3 as | ||
1338 | 7 | # published by the Free Software Foundation. | ||
1339 | 8 | # | ||
1340 | 9 | # charm-helpers is distributed in the hope that it will be useful, | ||
1341 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
1342 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
1343 | 12 | # GNU Lesser General Public License for more details. | ||
1344 | 13 | # | ||
1345 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
1346 | 15 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
1347 | 16 | |||
1348 | 17 | """Tools for working with the host system""" | ||
1349 | 18 | # Copyright 2012 Canonical Ltd. | ||
1350 | 19 | # | ||
1351 | 20 | # Authors: | ||
1352 | 21 | # Nick Moffitt <nick.moffitt@canonical.com> | ||
1353 | 22 | # Matthew Wedgwood <matthew.wedgwood@canonical.com> | ||
1354 | 23 | |||
1355 | 24 | import os | ||
1356 | 25 | import re | ||
1357 | 26 | import pwd | ||
1358 | 27 | import glob | ||
1359 | 28 | import grp | ||
1360 | 29 | import random | ||
1361 | 30 | import string | ||
1362 | 31 | import subprocess | ||
1363 | 32 | import hashlib | ||
1364 | 33 | from contextlib import contextmanager | ||
1365 | 34 | from collections import OrderedDict | ||
1366 | 35 | |||
1367 | 36 | import six | ||
1368 | 37 | |||
1369 | 38 | from .hookenv import log | ||
1370 | 39 | from .fstab import Fstab | ||
1371 | 40 | |||
1372 | 41 | |||
1373 | 42 | def service_start(service_name): | ||
1374 | 43 | """Start a system service""" | ||
1375 | 44 | return service('start', service_name) | ||
1376 | 45 | |||
1377 | 46 | |||
1378 | 47 | def service_stop(service_name): | ||
1379 | 48 | """Stop a system service""" | ||
1380 | 49 | return service('stop', service_name) | ||
1381 | 50 | |||
1382 | 51 | |||
1383 | 52 | def service_restart(service_name): | ||
1384 | 53 | """Restart a system service""" | ||
1385 | 54 | return service('restart', service_name) | ||
1386 | 55 | |||
1387 | 56 | |||
1388 | 57 | def service_reload(service_name, restart_on_failure=False): | ||
1389 | 58 | """Reload a system service, optionally falling back to restart if | ||
1390 | 59 | reload fails""" | ||
1391 | 60 | service_result = service('reload', service_name) | ||
1392 | 61 | if not service_result and restart_on_failure: | ||
1393 | 62 | service_result = service('restart', service_name) | ||
1394 | 63 | return service_result | ||
1395 | 64 | |||
1396 | 65 | |||
1397 | 66 | def service_pause(service_name, init_dir="/etc/init", initd_dir="/etc/init.d"): | ||
1398 | 67 | """Pause a system service. | ||
1399 | 68 | |||
1400 | 69 | Stop it, and prevent it from starting again at boot.""" | ||
1401 | 70 | stopped = True | ||
1402 | 71 | if service_running(service_name): | ||
1403 | 72 | stopped = service_stop(service_name) | ||
1404 | 73 | upstart_file = os.path.join(init_dir, "{}.conf".format(service_name)) | ||
1405 | 74 | sysv_file = os.path.join(initd_dir, service_name) | ||
1406 | 75 | if os.path.exists(upstart_file): | ||
1407 | 76 | override_path = os.path.join( | ||
1408 | 77 | init_dir, '{}.override'.format(service_name)) | ||
1409 | 78 | with open(override_path, 'w') as fh: | ||
1410 | 79 | fh.write("manual\n") | ||
1411 | 80 | elif os.path.exists(sysv_file): | ||
1412 | 81 | subprocess.check_call(["update-rc.d", service_name, "disable"]) | ||
1413 | 82 | else: | ||
1414 | 83 | # XXX: Support SystemD too | ||
1415 | 84 | raise ValueError( | ||
1416 | 85 | "Unable to detect {0} as either Upstart {1} or SysV {2}".format( | ||
1417 | 86 | service_name, upstart_file, sysv_file)) | ||
1418 | 87 | return stopped | ||
1419 | 88 | |||
1420 | 89 | |||
1421 | 90 | def service_resume(service_name, init_dir="/etc/init", | ||
1422 | 91 | initd_dir="/etc/init.d"): | ||
1423 | 92 | """Resume a system service. | ||
1424 | 93 | |||
1425 | 94 | Reenable starting again at boot. Start the service""" | ||
1426 | 95 | upstart_file = os.path.join(init_dir, "{}.conf".format(service_name)) | ||
1427 | 96 | sysv_file = os.path.join(initd_dir, service_name) | ||
1428 | 97 | if os.path.exists(upstart_file): | ||
1429 | 98 | override_path = os.path.join( | ||
1430 | 99 | init_dir, '{}.override'.format(service_name)) | ||
1431 | 100 | if os.path.exists(override_path): | ||
1432 | 101 | os.unlink(override_path) | ||
1433 | 102 | elif os.path.exists(sysv_file): | ||
1434 | 103 | subprocess.check_call(["update-rc.d", service_name, "enable"]) | ||
1435 | 104 | else: | ||
1436 | 105 | # XXX: Support SystemD too | ||
1437 | 106 | raise ValueError( | ||
1438 | 107 | "Unable to detect {0} as either Upstart {1} or SysV {2}".format( | ||
1439 | 108 | service_name, upstart_file, sysv_file)) | ||
1440 | 109 | |||
1441 | 110 | started = service_running(service_name) | ||
1442 | 111 | if not started: | ||
1443 | 112 | started = service_start(service_name) | ||
1444 | 113 | return started | ||
1445 | 114 | |||
1446 | 115 | |||
1447 | 116 | def service(action, service_name): | ||
1448 | 117 | """Control a system service""" | ||
1449 | 118 | cmd = ['service', service_name, action] | ||
1450 | 119 | return subprocess.call(cmd) == 0 | ||
1451 | 120 | |||
1452 | 121 | |||
1453 | 122 | def service_running(service): | ||
1454 | 123 | """Determine whether a system service is running""" | ||
1455 | 124 | try: | ||
1456 | 125 | output = subprocess.check_output( | ||
1457 | 126 | ['service', service, 'status'], | ||
1458 | 127 | stderr=subprocess.STDOUT).decode('UTF-8') | ||
1459 | 128 | except subprocess.CalledProcessError: | ||
1460 | 129 | return False | ||
1461 | 130 | else: | ||
1462 | 131 | if ("start/running" in output or "is running" in output): | ||
1463 | 132 | return True | ||
1464 | 133 | else: | ||
1465 | 134 | return False | ||
1466 | 135 | |||
1467 | 136 | |||
1468 | 137 | def service_available(service_name): | ||
1469 | 138 | """Determine whether a system service is available""" | ||
1470 | 139 | try: | ||
1471 | 140 | subprocess.check_output( | ||
1472 | 141 | ['service', service_name, 'status'], | ||
1473 | 142 | stderr=subprocess.STDOUT).decode('UTF-8') | ||
1474 | 143 | except subprocess.CalledProcessError as e: | ||
1475 | 144 | return b'unrecognized service' not in e.output | ||
1476 | 145 | else: | ||
1477 | 146 | return True | ||
1478 | 147 | |||
1479 | 148 | |||
1480 | 149 | def adduser(username, password=None, shell='/bin/bash', system_user=False, | ||
1481 | 150 | primary_group=None, secondary_groups=None): | ||
1482 | 151 | """ | ||
1483 | 152 | Add a user to the system. | ||
1484 | 153 | |||
1485 | 154 | Will log but otherwise succeed if the user already exists. | ||
1486 | 155 | |||
1487 | 156 | :param str username: Username to create | ||
1488 | 157 | :param str password: Password for user; if ``None``, create a system user | ||
1489 | 158 | :param str shell: The default shell for the user | ||
1490 | 159 | :param bool system_user: Whether to create a login or system user | ||
1491 | 160 | :param str primary_group: Primary group for user; defaults to their username | ||
1492 | 161 | :param list secondary_groups: Optional list of additional groups | ||
1493 | 162 | |||
1494 | 163 | :returns: The password database entry struct, as returned by `pwd.getpwnam` | ||
1495 | 164 | """ | ||
1496 | 165 | try: | ||
1497 | 166 | user_info = pwd.getpwnam(username) | ||
1498 | 167 | log('user {0} already exists!'.format(username)) | ||
1499 | 168 | except KeyError: | ||
1500 | 169 | log('creating user {0}'.format(username)) | ||
1501 | 170 | cmd = ['useradd'] | ||
1502 | 171 | if system_user or password is None: | ||
1503 | 172 | cmd.append('--system') | ||
1504 | 173 | else: | ||
1505 | 174 | cmd.extend([ | ||
1506 | 175 | '--create-home', | ||
1507 | 176 | '--shell', shell, | ||
1508 | 177 | '--password', password, | ||
1509 | 178 | ]) | ||
1510 | 179 | if not primary_group: | ||
1511 | 180 | try: | ||
1512 | 181 | grp.getgrnam(username) | ||
1513 | 182 | primary_group = username # avoid "group exists" error | ||
1514 | 183 | except KeyError: | ||
1515 | 184 | pass | ||
1516 | 185 | if primary_group: | ||
1517 | 186 | cmd.extend(['-g', primary_group]) | ||
1518 | 187 | if secondary_groups: | ||
1519 | 188 | cmd.extend(['-G', ','.join(secondary_groups)]) | ||
1520 | 189 | cmd.append(username) | ||
1521 | 190 | subprocess.check_call(cmd) | ||
1522 | 191 | user_info = pwd.getpwnam(username) | ||
1523 | 192 | return user_info | ||
1524 | 193 | |||
1525 | 194 | |||
1526 | 195 | def user_exists(username): | ||
1527 | 196 | """Check if a user exists""" | ||
1528 | 197 | try: | ||
1529 | 198 | pwd.getpwnam(username) | ||
1530 | 199 | user_exists = True | ||
1531 | 200 | except KeyError: | ||
1532 | 201 | user_exists = False | ||
1533 | 202 | return user_exists | ||
1534 | 203 | |||
1535 | 204 | |||
1536 | 205 | def add_group(group_name, system_group=False): | ||
1537 | 206 | """Add a group to the system""" | ||
1538 | 207 | try: | ||
1539 | 208 | group_info = grp.getgrnam(group_name) | ||
1540 | 209 | log('group {0} already exists!'.format(group_name)) | ||
1541 | 210 | except KeyError: | ||
1542 | 211 | log('creating group {0}'.format(group_name)) | ||
1543 | 212 | cmd = ['addgroup'] | ||
1544 | 213 | if system_group: | ||
1545 | 214 | cmd.append('--system') | ||
1546 | 215 | else: | ||
1547 | 216 | cmd.extend([ | ||
1548 | 217 | '--group', | ||
1549 | 218 | ]) | ||
1550 | 219 | cmd.append(group_name) | ||
1551 | 220 | subprocess.check_call(cmd) | ||
1552 | 221 | group_info = grp.getgrnam(group_name) | ||
1553 | 222 | return group_info | ||
1554 | 223 | |||
1555 | 224 | |||
1556 | 225 | def add_user_to_group(username, group): | ||
1557 | 226 | """Add a user to a group""" | ||
1558 | 227 | cmd = ['gpasswd', '-a', username, group] | ||
1559 | 228 | log("Adding user {} to group {}".format(username, group)) | ||
1560 | 229 | subprocess.check_call(cmd) | ||
1561 | 230 | |||
1562 | 231 | |||
1563 | 232 | def rsync(from_path, to_path, flags='-r', options=None): | ||
1564 | 233 | """Replicate the contents of a path""" | ||
1565 | 234 | options = options or ['--delete', '--executability'] | ||
1566 | 235 | cmd = ['/usr/bin/rsync', flags] | ||
1567 | 236 | cmd.extend(options) | ||
1568 | 237 | cmd.append(from_path) | ||
1569 | 238 | cmd.append(to_path) | ||
1570 | 239 | log(" ".join(cmd)) | ||
1571 | 240 | return subprocess.check_output(cmd).decode('UTF-8').strip() | ||
1572 | 241 | |||
1573 | 242 | |||
1574 | 243 | def symlink(source, destination): | ||
1575 | 244 | """Create a symbolic link""" | ||
1576 | 245 | log("Symlinking {} as {}".format(source, destination)) | ||
1577 | 246 | cmd = [ | ||
1578 | 247 | 'ln', | ||
1579 | 248 | '-sf', | ||
1580 | 249 | source, | ||
1581 | 250 | destination, | ||
1582 | 251 | ] | ||
1583 | 252 | subprocess.check_call(cmd) | ||
1584 | 253 | |||
1585 | 254 | |||
1586 | 255 | def mkdir(path, owner='root', group='root', perms=0o555, force=False): | ||
1587 | 256 | """Create a directory""" | ||
1588 | 257 | log("Making dir {} {}:{} {:o}".format(path, owner, group, | ||
1589 | 258 | perms)) | ||
1590 | 259 | uid = pwd.getpwnam(owner).pw_uid | ||
1591 | 260 | gid = grp.getgrnam(group).gr_gid | ||
1592 | 261 | realpath = os.path.abspath(path) | ||
1593 | 262 | path_exists = os.path.exists(realpath) | ||
1594 | 263 | if path_exists and force: | ||
1595 | 264 | if not os.path.isdir(realpath): | ||
1596 | 265 | log("Removing non-directory file {} prior to mkdir()".format(path)) | ||
1597 | 266 | os.unlink(realpath) | ||
1598 | 267 | os.makedirs(realpath, perms) | ||
1599 | 268 | elif not path_exists: | ||
1600 | 269 | os.makedirs(realpath, perms) | ||
1601 | 270 | os.chown(realpath, uid, gid) | ||
1602 | 271 | os.chmod(realpath, perms) | ||
1603 | 272 | |||
1604 | 273 | |||
1605 | 274 | def write_file(path, content, owner='root', group='root', perms=0o444): | ||
1606 | 275 | """Create or overwrite a file with the contents of a byte string.""" | ||
1607 | 276 | log("Writing file {} {}:{} {:o}".format(path, owner, group, perms)) | ||
1608 | 277 | uid = pwd.getpwnam(owner).pw_uid | ||
1609 | 278 | gid = grp.getgrnam(group).gr_gid | ||
1610 | 279 | with open(path, 'wb') as target: | ||
1611 | 280 | os.fchown(target.fileno(), uid, gid) | ||
1612 | 281 | os.fchmod(target.fileno(), perms) | ||
1613 | 282 | target.write(content) | ||
1614 | 283 | |||
1615 | 284 | |||
1616 | 285 | def fstab_remove(mp): | ||
1617 | 286 | """Remove the given mountpoint entry from /etc/fstab | ||
1618 | 287 | """ | ||
1619 | 288 | return Fstab.remove_by_mountpoint(mp) | ||
1620 | 289 | |||
1621 | 290 | |||
1622 | 291 | def fstab_add(dev, mp, fs, options=None): | ||
1623 | 292 | """Adds the given device entry to the /etc/fstab file | ||
1624 | 293 | """ | ||
1625 | 294 | return Fstab.add(dev, mp, fs, options=options) | ||
1626 | 295 | |||
1627 | 296 | |||
1628 | 297 | def mount(device, mountpoint, options=None, persist=False, filesystem="ext3"): | ||
1629 | 298 | """Mount a filesystem at a particular mountpoint""" | ||
1630 | 299 | cmd_args = ['mount'] | ||
1631 | 300 | if options is not None: | ||
1632 | 301 | cmd_args.extend(['-o', options]) | ||
1633 | 302 | cmd_args.extend([device, mountpoint]) | ||
1634 | 303 | try: | ||
1635 | 304 | subprocess.check_output(cmd_args) | ||
1636 | 305 | except subprocess.CalledProcessError as e: | ||
1637 | 306 | log('Error mounting {} at {}\n{}'.format(device, mountpoint, e.output)) | ||
1638 | 307 | return False | ||
1639 | 308 | |||
1640 | 309 | if persist: | ||
1641 | 310 | return fstab_add(device, mountpoint, filesystem, options=options) | ||
1642 | 311 | return True | ||
1643 | 312 | |||
1644 | 313 | |||
1645 | 314 | def umount(mountpoint, persist=False): | ||
1646 | 315 | """Unmount a filesystem""" | ||
1647 | 316 | cmd_args = ['umount', mountpoint] | ||
1648 | 317 | try: | ||
1649 | 318 | subprocess.check_output(cmd_args) | ||
1650 | 319 | except subprocess.CalledProcessError as e: | ||
1651 | 320 | log('Error unmounting {}\n{}'.format(mountpoint, e.output)) | ||
1652 | 321 | return False | ||
1653 | 322 | |||
1654 | 323 | if persist: | ||
1655 | 324 | return fstab_remove(mountpoint) | ||
1656 | 325 | return True | ||
1657 | 326 | |||
1658 | 327 | |||
1659 | 328 | def mounts(): | ||
1660 | 329 | """Get a list of all mounted volumes as [[mountpoint,device],[...]]""" | ||
1661 | 330 | with open('/proc/mounts') as f: | ||
1662 | 331 | # [['/mount/point','/dev/path'],[...]] | ||
1663 | 332 | system_mounts = [m[1::-1] for m in [l.strip().split() | ||
1664 | 333 | for l in f.readlines()]] | ||
1665 | 334 | return system_mounts | ||
1666 | 335 | |||
1667 | 336 | |||
1668 | 337 | def fstab_mount(mountpoint): | ||
1669 | 338 | """Mount filesystem using fstab""" | ||
1670 | 339 | cmd_args = ['mount', mountpoint] | ||
1671 | 340 | try: | ||
1672 | 341 | subprocess.check_output(cmd_args) | ||
1673 | 342 | except subprocess.CalledProcessError as e: | ||
1674 | 343 | log('Error unmounting {}\n{}'.format(mountpoint, e.output)) | ||
1675 | 344 | return False | ||
1676 | 345 | return True | ||
1677 | 346 | |||
1678 | 347 | |||
1679 | 348 | def file_hash(path, hash_type='md5'): | ||
1680 | 349 | """ | ||
1681 | 350 | Generate a hash checksum of the contents of 'path' or None if not found. | ||
1682 | 351 | |||
1683 | 352 | :param str hash_type: Any hash alrgorithm supported by :mod:`hashlib`, | ||
1684 | 353 | such as md5, sha1, sha256, sha512, etc. | ||
1685 | 354 | """ | ||
1686 | 355 | if os.path.exists(path): | ||
1687 | 356 | h = getattr(hashlib, hash_type)() | ||
1688 | 357 | with open(path, 'rb') as source: | ||
1689 | 358 | h.update(source.read()) | ||
1690 | 359 | return h.hexdigest() | ||
1691 | 360 | else: | ||
1692 | 361 | return None | ||
1693 | 362 | |||
1694 | 363 | |||
1695 | 364 | def path_hash(path): | ||
1696 | 365 | """ | ||
1697 | 366 | Generate a hash checksum of all files matching 'path'. Standard wildcards | ||
1698 | 367 | like '*' and '?' are supported, see documentation for the 'glob' module for | ||
1699 | 368 | more information. | ||
1700 | 369 | |||
1701 | 370 | :return: dict: A { filename: hash } dictionary for all matched files. | ||
1702 | 371 | Empty if none found. | ||
1703 | 372 | """ | ||
1704 | 373 | return { | ||
1705 | 374 | filename: file_hash(filename) | ||
1706 | 375 | for filename in glob.iglob(path) | ||
1707 | 376 | } | ||
1708 | 377 | |||
1709 | 378 | |||
1710 | 379 | def check_hash(path, checksum, hash_type='md5'): | ||
1711 | 380 | """ | ||
1712 | 381 | Validate a file using a cryptographic checksum. | ||
1713 | 382 | |||
1714 | 383 | :param str checksum: Value of the checksum used to validate the file. | ||
1715 | 384 | :param str hash_type: Hash algorithm used to generate `checksum`. | ||
1716 | 385 | Can be any hash alrgorithm supported by :mod:`hashlib`, | ||
1717 | 386 | such as md5, sha1, sha256, sha512, etc. | ||
1718 | 387 | :raises ChecksumError: If the file fails the checksum | ||
1719 | 388 | |||
1720 | 389 | """ | ||
1721 | 390 | actual_checksum = file_hash(path, hash_type) | ||
1722 | 391 | if checksum != actual_checksum: | ||
1723 | 392 | raise ChecksumError("'%s' != '%s'" % (checksum, actual_checksum)) | ||
1724 | 393 | |||
1725 | 394 | |||
1726 | 395 | class ChecksumError(ValueError): | ||
1727 | 396 | pass | ||
1728 | 397 | |||
1729 | 398 | |||
1730 | 399 | def restart_on_change(restart_map, stopstart=False): | ||
1731 | 400 | """Restart services based on configuration files changing | ||
1732 | 401 | |||
1733 | 402 | This function is used a decorator, for example:: | ||
1734 | 403 | |||
1735 | 404 | @restart_on_change({ | ||
1736 | 405 | '/etc/ceph/ceph.conf': [ 'cinder-api', 'cinder-volume' ] | ||
1737 | 406 | '/etc/apache/sites-enabled/*': [ 'apache2' ] | ||
1738 | 407 | }) | ||
1739 | 408 | def config_changed(): | ||
1740 | 409 | pass # your code here | ||
1741 | 410 | |||
1742 | 411 | In this example, the cinder-api and cinder-volume services | ||
1743 | 412 | would be restarted if /etc/ceph/ceph.conf is changed by the | ||
1744 | 413 | ceph_client_changed function. The apache2 service would be | ||
1745 | 414 | restarted if any file matching the pattern got changed, created | ||
1746 | 415 | or removed. Standard wildcards are supported, see documentation | ||
1747 | 416 | for the 'glob' module for more information. | ||
1748 | 417 | """ | ||
1749 | 418 | def wrap(f): | ||
1750 | 419 | def wrapped_f(*args, **kwargs): | ||
1751 | 420 | checksums = {path: path_hash(path) for path in restart_map} | ||
1752 | 421 | f(*args, **kwargs) | ||
1753 | 422 | restarts = [] | ||
1754 | 423 | for path in restart_map: | ||
1755 | 424 | if path_hash(path) != checksums[path]: | ||
1756 | 425 | restarts += restart_map[path] | ||
1757 | 426 | services_list = list(OrderedDict.fromkeys(restarts)) | ||
1758 | 427 | if not stopstart: | ||
1759 | 428 | for service_name in services_list: | ||
1760 | 429 | service('restart', service_name) | ||
1761 | 430 | else: | ||
1762 | 431 | for action in ['stop', 'start']: | ||
1763 | 432 | for service_name in services_list: | ||
1764 | 433 | service(action, service_name) | ||
1765 | 434 | return wrapped_f | ||
1766 | 435 | return wrap | ||
1767 | 436 | |||
1768 | 437 | |||
1769 | 438 | def lsb_release(): | ||
1770 | 439 | """Return /etc/lsb-release in a dict""" | ||
1771 | 440 | d = {} | ||
1772 | 441 | with open('/etc/lsb-release', 'r') as lsb: | ||
1773 | 442 | for l in lsb: | ||
1774 | 443 | k, v = l.split('=') | ||
1775 | 444 | d[k.strip()] = v.strip() | ||
1776 | 445 | return d | ||
1777 | 446 | |||
1778 | 447 | |||
1779 | 448 | def pwgen(length=None): | ||
1780 | 449 | """Generate a random pasword.""" | ||
1781 | 450 | if length is None: | ||
1782 | 451 | # A random length is ok to use a weak PRNG | ||
1783 | 452 | length = random.choice(range(35, 45)) | ||
1784 | 453 | alphanumeric_chars = [ | ||
1785 | 454 | l for l in (string.ascii_letters + string.digits) | ||
1786 | 455 | if l not in 'l0QD1vAEIOUaeiou'] | ||
1787 | 456 | # Use a crypto-friendly PRNG (e.g. /dev/urandom) for making the | ||
1788 | 457 | # actual password | ||
1789 | 458 | random_generator = random.SystemRandom() | ||
1790 | 459 | random_chars = [ | ||
1791 | 460 | random_generator.choice(alphanumeric_chars) for _ in range(length)] | ||
1792 | 461 | return(''.join(random_chars)) | ||
1793 | 462 | |||
1794 | 463 | |||
1795 | 464 | def is_phy_iface(interface): | ||
1796 | 465 | """Returns True if interface is not virtual, otherwise False.""" | ||
1797 | 466 | if interface: | ||
1798 | 467 | sys_net = '/sys/class/net' | ||
1799 | 468 | if os.path.isdir(sys_net): | ||
1800 | 469 | for iface in glob.glob(os.path.join(sys_net, '*')): | ||
1801 | 470 | if '/virtual/' in os.path.realpath(iface): | ||
1802 | 471 | continue | ||
1803 | 472 | |||
1804 | 473 | if interface == os.path.basename(iface): | ||
1805 | 474 | return True | ||
1806 | 475 | |||
1807 | 476 | return False | ||
1808 | 477 | |||
1809 | 478 | |||
1810 | 479 | def get_bond_master(interface): | ||
1811 | 480 | """Returns bond master if interface is bond slave otherwise None. | ||
1812 | 481 | |||
1813 | 482 | NOTE: the provided interface is expected to be physical | ||
1814 | 483 | """ | ||
1815 | 484 | if interface: | ||
1816 | 485 | iface_path = '/sys/class/net/%s' % (interface) | ||
1817 | 486 | if os.path.exists(iface_path): | ||
1818 | 487 | if '/virtual/' in os.path.realpath(iface_path): | ||
1819 | 488 | return None | ||
1820 | 489 | |||
1821 | 490 | master = os.path.join(iface_path, 'master') | ||
1822 | 491 | if os.path.exists(master): | ||
1823 | 492 | master = os.path.realpath(master) | ||
1824 | 493 | # make sure it is a bond master | ||
1825 | 494 | if os.path.exists(os.path.join(master, 'bonding')): | ||
1826 | 495 | return os.path.basename(master) | ||
1827 | 496 | |||
1828 | 497 | return None | ||
1829 | 498 | |||
1830 | 499 | |||
1831 | 500 | def list_nics(nic_type=None): | ||
1832 | 501 | '''Return a list of nics of given type(s)''' | ||
1833 | 502 | if isinstance(nic_type, six.string_types): | ||
1834 | 503 | int_types = [nic_type] | ||
1835 | 504 | else: | ||
1836 | 505 | int_types = nic_type | ||
1837 | 506 | |||
1838 | 507 | interfaces = [] | ||
1839 | 508 | if nic_type: | ||
1840 | 509 | for int_type in int_types: | ||
1841 | 510 | cmd = ['ip', 'addr', 'show', 'label', int_type + '*'] | ||
1842 | 511 | ip_output = subprocess.check_output(cmd).decode('UTF-8') | ||
1843 | 512 | ip_output = ip_output.split('\n') | ||
1844 | 513 | ip_output = (line for line in ip_output if line) | ||
1845 | 514 | for line in ip_output: | ||
1846 | 515 | if line.split()[1].startswith(int_type): | ||
1847 | 516 | matched = re.search('.*: (' + int_type + | ||
1848 | 517 | r'[0-9]+\.[0-9]+)@.*', line) | ||
1849 | 518 | if matched: | ||
1850 | 519 | iface = matched.groups()[0] | ||
1851 | 520 | else: | ||
1852 | 521 | iface = line.split()[1].replace(":", "") | ||
1853 | 522 | |||
1854 | 523 | if iface not in interfaces: | ||
1855 | 524 | interfaces.append(iface) | ||
1856 | 525 | else: | ||
1857 | 526 | cmd = ['ip', 'a'] | ||
1858 | 527 | ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n') | ||
1859 | 528 | ip_output = (line.strip() for line in ip_output if line) | ||
1860 | 529 | |||
1861 | 530 | key = re.compile('^[0-9]+:\s+(.+):') | ||
1862 | 531 | for line in ip_output: | ||
1863 | 532 | matched = re.search(key, line) | ||
1864 | 533 | if matched: | ||
1865 | 534 | iface = matched.group(1) | ||
1866 | 535 | iface = iface.partition("@")[0] | ||
1867 | 536 | if iface not in interfaces: | ||
1868 | 537 | interfaces.append(iface) | ||
1869 | 538 | |||
1870 | 539 | return interfaces | ||
1871 | 540 | |||
1872 | 541 | |||
1873 | 542 | def set_nic_mtu(nic, mtu): | ||
1874 | 543 | '''Set MTU on a network interface''' | ||
1875 | 544 | cmd = ['ip', 'link', 'set', nic, 'mtu', mtu] | ||
1876 | 545 | subprocess.check_call(cmd) | ||
1877 | 546 | |||
1878 | 547 | |||
1879 | 548 | def get_nic_mtu(nic): | ||
1880 | 549 | cmd = ['ip', 'addr', 'show', nic] | ||
1881 | 550 | ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n') | ||
1882 | 551 | mtu = "" | ||
1883 | 552 | for line in ip_output: | ||
1884 | 553 | words = line.split() | ||
1885 | 554 | if 'mtu' in words: | ||
1886 | 555 | mtu = words[words.index("mtu") + 1] | ||
1887 | 556 | return mtu | ||
1888 | 557 | |||
1889 | 558 | |||
1890 | 559 | def get_nic_hwaddr(nic): | ||
1891 | 560 | cmd = ['ip', '-o', '-0', 'addr', 'show', nic] | ||
1892 | 561 | ip_output = subprocess.check_output(cmd).decode('UTF-8') | ||
1893 | 562 | hwaddr = "" | ||
1894 | 563 | words = ip_output.split() | ||
1895 | 564 | if 'link/ether' in words: | ||
1896 | 565 | hwaddr = words[words.index('link/ether') + 1] | ||
1897 | 566 | return hwaddr | ||
1898 | 567 | |||
1899 | 568 | |||
1900 | 569 | def cmp_pkgrevno(package, revno, pkgcache=None): | ||
1901 | 570 | '''Compare supplied revno with the revno of the installed package | ||
1902 | 571 | |||
1903 | 572 | * 1 => Installed revno is greater than supplied arg | ||
1904 | 573 | * 0 => Installed revno is the same as supplied arg | ||
1905 | 574 | * -1 => Installed revno is less than supplied arg | ||
1906 | 575 | |||
1907 | 576 | This function imports apt_cache function from charmhelpers.fetch if | ||
1908 | 577 | the pkgcache argument is None. Be sure to add charmhelpers.fetch if | ||
1909 | 578 | you call this function, or pass an apt_pkg.Cache() instance. | ||
1910 | 579 | ''' | ||
1911 | 580 | import apt_pkg | ||
1912 | 581 | if not pkgcache: | ||
1913 | 582 | from charmhelpers.fetch import apt_cache | ||
1914 | 583 | pkgcache = apt_cache() | ||
1915 | 584 | pkg = pkgcache[package] | ||
1916 | 585 | return apt_pkg.version_compare(pkg.current_ver.ver_str, revno) | ||
1917 | 586 | |||
1918 | 587 | |||
1919 | 588 | @contextmanager | ||
1920 | 589 | def chdir(d): | ||
1921 | 590 | cur = os.getcwd() | ||
1922 | 591 | try: | ||
1923 | 592 | yield os.chdir(d) | ||
1924 | 593 | finally: | ||
1925 | 594 | os.chdir(cur) | ||
1926 | 595 | |||
1927 | 596 | |||
1928 | 597 | def chownr(path, owner, group, follow_links=True, chowntopdir=False): | ||
1929 | 598 | """ | ||
1930 | 599 | Recursively change user and group ownership of files and directories | ||
1931 | 600 | in given path. Doesn't chown path itself by default, only its children. | ||
1932 | 601 | |||
1933 | 602 | :param bool follow_links: Also Chown links if True | ||
1934 | 603 | :param bool chowntopdir: Also chown path itself if True | ||
1935 | 604 | """ | ||
1936 | 605 | uid = pwd.getpwnam(owner).pw_uid | ||
1937 | 606 | gid = grp.getgrnam(group).gr_gid | ||
1938 | 607 | if follow_links: | ||
1939 | 608 | chown = os.chown | ||
1940 | 609 | else: | ||
1941 | 610 | chown = os.lchown | ||
1942 | 611 | |||
1943 | 612 | if chowntopdir: | ||
1944 | 613 | broken_symlink = os.path.lexists(path) and not os.path.exists(path) | ||
1945 | 614 | if not broken_symlink: | ||
1946 | 615 | chown(path, uid, gid) | ||
1947 | 616 | for root, dirs, files in os.walk(path): | ||
1948 | 617 | for name in dirs + files: | ||
1949 | 618 | full = os.path.join(root, name) | ||
1950 | 619 | broken_symlink = os.path.lexists(full) and not os.path.exists(full) | ||
1951 | 620 | if not broken_symlink: | ||
1952 | 621 | chown(full, uid, gid) | ||
1953 | 622 | |||
1954 | 623 | |||
1955 | 624 | def lchownr(path, owner, group): | ||
1956 | 625 | chownr(path, owner, group, follow_links=False) | ||
1957 | 626 | |||
1958 | 627 | |||
1959 | 628 | def get_total_ram(): | ||
1960 | 629 | '''The total amount of system RAM in bytes. | ||
1961 | 630 | |||
1962 | 631 | This is what is reported by the OS, and may be overcommitted when | ||
1963 | 632 | there are multiple containers hosted on the same machine. | ||
1964 | 633 | ''' | ||
1965 | 634 | with open('/proc/meminfo', 'r') as f: | ||
1966 | 635 | for line in f.readlines(): | ||
1967 | 636 | if line: | ||
1968 | 637 | key, value, unit = line.split() | ||
1969 | 638 | if key == 'MemTotal:': | ||
1970 | 639 | assert unit == 'kB', 'Unknown unit' | ||
1971 | 640 | return int(value) * 1024 # Classic, not KiB. | ||
1972 | 641 | raise NotImplementedError() | ||
1973 | 0 | 642 | ||
1974 | === added file 'hooks/charmhelpers/core/hugepage.py' | |||
1975 | --- hooks/charmhelpers/core/hugepage.py 1970-01-01 00:00:00 +0000 | |||
1976 | +++ hooks/charmhelpers/core/hugepage.py 2015-12-17 05:44:32 +0000 | |||
1977 | @@ -0,0 +1,71 @@ | |||
1978 | 1 | # -*- coding: utf-8 -*- | ||
1979 | 2 | |||
1980 | 3 | # Copyright 2014-2015 Canonical Limited. | ||
1981 | 4 | # | ||
1982 | 5 | # This file is part of charm-helpers. | ||
1983 | 6 | # | ||
1984 | 7 | # charm-helpers is free software: you can redistribute it and/or modify | ||
1985 | 8 | # it under the terms of the GNU Lesser General Public License version 3 as | ||
1986 | 9 | # published by the Free Software Foundation. | ||
1987 | 10 | # | ||
1988 | 11 | # charm-helpers is distributed in the hope that it will be useful, | ||
1989 | 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
1990 | 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
1991 | 14 | # GNU Lesser General Public License for more details. | ||
1992 | 15 | # | ||
1993 | 16 | # You should have received a copy of the GNU Lesser General Public License | ||
1994 | 17 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
1995 | 18 | |||
1996 | 19 | import yaml | ||
1997 | 20 | from charmhelpers.core import fstab | ||
1998 | 21 | from charmhelpers.core import sysctl | ||
1999 | 22 | from charmhelpers.core.host import ( | ||
2000 | 23 | add_group, | ||
2001 | 24 | add_user_to_group, | ||
2002 | 25 | fstab_mount, | ||
2003 | 26 | mkdir, | ||
2004 | 27 | ) | ||
2005 | 28 | from charmhelpers.core.strutils import bytes_from_string | ||
2006 | 29 | from subprocess import check_output | ||
2007 | 30 | |||
2008 | 31 | |||
2009 | 32 | def hugepage_support(user, group='hugetlb', nr_hugepages=256, | ||
2010 | 33 | max_map_count=65536, mnt_point='/run/hugepages/kvm', | ||
2011 | 34 | pagesize='2MB', mount=True, set_shmmax=False): | ||
2012 | 35 | """Enable hugepages on system. | ||
2013 | 36 | |||
2014 | 37 | Args: | ||
2015 | 38 | user (str) -- Username to allow access to hugepages to | ||
2016 | 39 | group (str) -- Group name to own hugepages | ||
2017 | 40 | nr_hugepages (int) -- Number of pages to reserve | ||
2018 | 41 | max_map_count (int) -- Number of Virtual Memory Areas a process can own | ||
2019 | 42 | mnt_point (str) -- Directory to mount hugepages on | ||
2020 | 43 | pagesize (str) -- Size of hugepages | ||
2021 | 44 | mount (bool) -- Whether to Mount hugepages | ||
2022 | 45 | """ | ||
2023 | 46 | group_info = add_group(group) | ||
2024 | 47 | gid = group_info.gr_gid | ||
2025 | 48 | add_user_to_group(user, group) | ||
2026 | 49 | if max_map_count < 2 * nr_hugepages: | ||
2027 | 50 | max_map_count = 2 * nr_hugepages | ||
2028 | 51 | sysctl_settings = { | ||
2029 | 52 | 'vm.nr_hugepages': nr_hugepages, | ||
2030 | 53 | 'vm.max_map_count': max_map_count, | ||
2031 | 54 | 'vm.hugetlb_shm_group': gid, | ||
2032 | 55 | } | ||
2033 | 56 | if set_shmmax: | ||
2034 | 57 | shmmax_current = int(check_output(['sysctl', '-n', 'kernel.shmmax'])) | ||
2035 | 58 | shmmax_minsize = bytes_from_string(pagesize) * nr_hugepages | ||
2036 | 59 | if shmmax_minsize > shmmax_current: | ||
2037 | 60 | sysctl_settings['kernel.shmmax'] = shmmax_minsize | ||
2038 | 61 | sysctl.create(yaml.dump(sysctl_settings), '/etc/sysctl.d/10-hugepage.conf') | ||
2039 | 62 | mkdir(mnt_point, owner='root', group='root', perms=0o755, force=False) | ||
2040 | 63 | lfstab = fstab.Fstab() | ||
2041 | 64 | fstab_entry = lfstab.get_entry_by_attr('mountpoint', mnt_point) | ||
2042 | 65 | if fstab_entry: | ||
2043 | 66 | lfstab.remove_entry(fstab_entry) | ||
2044 | 67 | entry = lfstab.Entry('nodev', mnt_point, 'hugetlbfs', | ||
2045 | 68 | 'mode=1770,gid={},pagesize={}'.format(gid, pagesize), 0, 0) | ||
2046 | 69 | lfstab.add_entry(entry) | ||
2047 | 70 | if mount: | ||
2048 | 71 | fstab_mount(mnt_point) | ||
2049 | 0 | 72 | ||
2050 | === added file 'hooks/charmhelpers/core/kernel.py' | |||
2051 | --- hooks/charmhelpers/core/kernel.py 1970-01-01 00:00:00 +0000 | |||
2052 | +++ hooks/charmhelpers/core/kernel.py 2015-12-17 05:44:32 +0000 | |||
2053 | @@ -0,0 +1,68 @@ | |||
2054 | 1 | #!/usr/bin/env python | ||
2055 | 2 | # -*- coding: utf-8 -*- | ||
2056 | 3 | |||
2057 | 4 | # Copyright 2014-2015 Canonical Limited. | ||
2058 | 5 | # | ||
2059 | 6 | # This file is part of charm-helpers. | ||
2060 | 7 | # | ||
2061 | 8 | # charm-helpers is free software: you can redistribute it and/or modify | ||
2062 | 9 | # it under the terms of the GNU Lesser General Public License version 3 as | ||
2063 | 10 | # published by the Free Software Foundation. | ||
2064 | 11 | # | ||
2065 | 12 | # charm-helpers is distributed in the hope that it will be useful, | ||
2066 | 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
2067 | 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
2068 | 15 | # GNU Lesser General Public License for more details. | ||
2069 | 16 | # | ||
2070 | 17 | # You should have received a copy of the GNU Lesser General Public License | ||
2071 | 18 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
2072 | 19 | |||
2073 | 20 | __author__ = "Jorge Niedbalski <jorge.niedbalski@canonical.com>" | ||
2074 | 21 | |||
2075 | 22 | from charmhelpers.core.hookenv import ( | ||
2076 | 23 | log, | ||
2077 | 24 | INFO | ||
2078 | 25 | ) | ||
2079 | 26 | |||
2080 | 27 | from subprocess import check_call, check_output | ||
2081 | 28 | import re | ||
2082 | 29 | |||
2083 | 30 | |||
2084 | 31 | def modprobe(module, persist=True): | ||
2085 | 32 | """Load a kernel module and configure for auto-load on reboot.""" | ||
2086 | 33 | cmd = ['modprobe', module] | ||
2087 | 34 | |||
2088 | 35 | log('Loading kernel module %s' % module, level=INFO) | ||
2089 | 36 | |||
2090 | 37 | check_call(cmd) | ||
2091 | 38 | if persist: | ||
2092 | 39 | with open('/etc/modules', 'r+') as modules: | ||
2093 | 40 | if module not in modules.read(): | ||
2094 | 41 | modules.write(module) | ||
2095 | 42 | |||
2096 | 43 | |||
2097 | 44 | def rmmod(module, force=False): | ||
2098 | 45 | """Remove a module from the linux kernel""" | ||
2099 | 46 | cmd = ['rmmod'] | ||
2100 | 47 | if force: | ||
2101 | 48 | cmd.append('-f') | ||
2102 | 49 | cmd.append(module) | ||
2103 | 50 | log('Removing kernel module %s' % module, level=INFO) | ||
2104 | 51 | return check_call(cmd) | ||
2105 | 52 | |||
2106 | 53 | |||
2107 | 54 | def lsmod(): | ||
2108 | 55 | """Shows what kernel modules are currently loaded""" | ||
2109 | 56 | return check_output(['lsmod'], | ||
2110 | 57 | universal_newlines=True) | ||
2111 | 58 | |||
2112 | 59 | |||
2113 | 60 | def is_module_loaded(module): | ||
2114 | 61 | """Checks if a kernel module is already loaded""" | ||
2115 | 62 | matches = re.findall('^%s[ ]+' % module, lsmod(), re.M) | ||
2116 | 63 | return len(matches) > 0 | ||
2117 | 64 | |||
2118 | 65 | |||
2119 | 66 | def update_initramfs(version='all'): | ||
2120 | 67 | """Updates an initramfs image""" | ||
2121 | 68 | return check_call(["update-initramfs", "-k", version, "-u"]) | ||
2122 | 0 | 69 | ||
2123 | === added directory 'hooks/charmhelpers/core/services' | |||
2124 | === added file 'hooks/charmhelpers/core/services/__init__.py' | |||
2125 | --- hooks/charmhelpers/core/services/__init__.py 1970-01-01 00:00:00 +0000 | |||
2126 | +++ hooks/charmhelpers/core/services/__init__.py 2015-12-17 05:44:32 +0000 | |||
2127 | @@ -0,0 +1,18 @@ | |||
2128 | 1 | # Copyright 2014-2015 Canonical Limited. | ||
2129 | 2 | # | ||
2130 | 3 | # This file is part of charm-helpers. | ||
2131 | 4 | # | ||
2132 | 5 | # charm-helpers is free software: you can redistribute it and/or modify | ||
2133 | 6 | # it under the terms of the GNU Lesser General Public License version 3 as | ||
2134 | 7 | # published by the Free Software Foundation. | ||
2135 | 8 | # | ||
2136 | 9 | # charm-helpers is distributed in the hope that it will be useful, | ||
2137 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
2138 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
2139 | 12 | # GNU Lesser General Public License for more details. | ||
2140 | 13 | # | ||
2141 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
2142 | 15 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
2143 | 16 | |||
2144 | 17 | from .base import * # NOQA | ||
2145 | 18 | from .helpers import * # NOQA | ||
2146 | 0 | 19 | ||
2147 | === added file 'hooks/charmhelpers/core/services/base.py' | |||
2148 | --- hooks/charmhelpers/core/services/base.py 1970-01-01 00:00:00 +0000 | |||
2149 | +++ hooks/charmhelpers/core/services/base.py 2015-12-17 05:44:32 +0000 | |||
2150 | @@ -0,0 +1,353 @@ | |||
2151 | 1 | # Copyright 2014-2015 Canonical Limited. | ||
2152 | 2 | # | ||
2153 | 3 | # This file is part of charm-helpers. | ||
2154 | 4 | # | ||
2155 | 5 | # charm-helpers is free software: you can redistribute it and/or modify | ||
2156 | 6 | # it under the terms of the GNU Lesser General Public License version 3 as | ||
2157 | 7 | # published by the Free Software Foundation. | ||
2158 | 8 | # | ||
2159 | 9 | # charm-helpers is distributed in the hope that it will be useful, | ||
2160 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
2161 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
2162 | 12 | # GNU Lesser General Public License for more details. | ||
2163 | 13 | # | ||
2164 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
2165 | 15 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
2166 | 16 | |||
2167 | 17 | import os | ||
2168 | 18 | import json | ||
2169 | 19 | from inspect import getargspec | ||
2170 | 20 | from collections import Iterable, OrderedDict | ||
2171 | 21 | |||
2172 | 22 | from charmhelpers.core import host | ||
2173 | 23 | from charmhelpers.core import hookenv | ||
2174 | 24 | |||
2175 | 25 | |||
2176 | 26 | __all__ = ['ServiceManager', 'ManagerCallback', | ||
2177 | 27 | 'PortManagerCallback', 'open_ports', 'close_ports', 'manage_ports', | ||
2178 | 28 | 'service_restart', 'service_stop'] | ||
2179 | 29 | |||
2180 | 30 | |||
2181 | 31 | class ServiceManager(object): | ||
2182 | 32 | def __init__(self, services=None): | ||
2183 | 33 | """ | ||
2184 | 34 | Register a list of services, given their definitions. | ||
2185 | 35 | |||
2186 | 36 | Service definitions are dicts in the following formats (all keys except | ||
2187 | 37 | 'service' are optional):: | ||
2188 | 38 | |||
2189 | 39 | { | ||
2190 | 40 | "service": <service name>, | ||
2191 | 41 | "required_data": <list of required data contexts>, | ||
2192 | 42 | "provided_data": <list of provided data contexts>, | ||
2193 | 43 | "data_ready": <one or more callbacks>, | ||
2194 | 44 | "data_lost": <one or more callbacks>, | ||
2195 | 45 | "start": <one or more callbacks>, | ||
2196 | 46 | "stop": <one or more callbacks>, | ||
2197 | 47 | "ports": <list of ports to manage>, | ||
2198 | 48 | } | ||
2199 | 49 | |||
2200 | 50 | The 'required_data' list should contain dicts of required data (or | ||
2201 | 51 | dependency managers that act like dicts and know how to collect the data). | ||
2202 | 52 | Only when all items in the 'required_data' list are populated are the list | ||
2203 | 53 | of 'data_ready' and 'start' callbacks executed. See `is_ready()` for more | ||
2204 | 54 | information. | ||
2205 | 55 | |||
2206 | 56 | The 'provided_data' list should contain relation data providers, most likely | ||
2207 | 57 | a subclass of :class:`charmhelpers.core.services.helpers.RelationContext`, | ||
2208 | 58 | that will indicate a set of data to set on a given relation. | ||
2209 | 59 | |||
2210 | 60 | The 'data_ready' value should be either a single callback, or a list of | ||
2211 | 61 | callbacks, to be called when all items in 'required_data' pass `is_ready()`. | ||
2212 | 62 | Each callback will be called with the service name as the only parameter. | ||
2213 | 63 | After all of the 'data_ready' callbacks are called, the 'start' callbacks | ||
2214 | 64 | are fired. | ||
2215 | 65 | |||
2216 | 66 | The 'data_lost' value should be either a single callback, or a list of | ||
2217 | 67 | callbacks, to be called when a 'required_data' item no longer passes | ||
2218 | 68 | `is_ready()`. Each callback will be called with the service name as the | ||
2219 | 69 | only parameter. After all of the 'data_lost' callbacks are called, | ||
2220 | 70 | the 'stop' callbacks are fired. | ||
2221 | 71 | |||
2222 | 72 | The 'start' value should be either a single callback, or a list of | ||
2223 | 73 | callbacks, to be called when starting the service, after the 'data_ready' | ||
2224 | 74 | callbacks are complete. Each callback will be called with the service | ||
2225 | 75 | name as the only parameter. This defaults to | ||
2226 | 76 | `[host.service_start, services.open_ports]`. | ||
2227 | 77 | |||
2228 | 78 | The 'stop' value should be either a single callback, or a list of | ||
2229 | 79 | callbacks, to be called when stopping the service. If the service is | ||
2230 | 80 | being stopped because it no longer has all of its 'required_data', this | ||
2231 | 81 | will be called after all of the 'data_lost' callbacks are complete. | ||
2232 | 82 | Each callback will be called with the service name as the only parameter. | ||
2233 | 83 | This defaults to `[services.close_ports, host.service_stop]`. | ||
2234 | 84 | |||
2235 | 85 | The 'ports' value should be a list of ports to manage. The default | ||
2236 | 86 | 'start' handler will open the ports after the service is started, | ||
2237 | 87 | and the default 'stop' handler will close the ports prior to stopping | ||
2238 | 88 | the service. | ||
2239 | 89 | |||
2240 | 90 | |||
2241 | 91 | Examples: | ||
2242 | 92 | |||
2243 | 93 | The following registers an Upstart service called bingod that depends on | ||
2244 | 94 | a mongodb relation and which runs a custom `db_migrate` function prior to | ||
2245 | 95 | restarting the service, and a Runit service called spadesd:: | ||
2246 | 96 | |||
2247 | 97 | manager = services.ServiceManager([ | ||
2248 | 98 | { | ||
2249 | 99 | 'service': 'bingod', | ||
2250 | 100 | 'ports': [80, 443], | ||
2251 | 101 | 'required_data': [MongoRelation(), config(), {'my': 'data'}], | ||
2252 | 102 | 'data_ready': [ | ||
2253 | 103 | services.template(source='bingod.conf'), | ||
2254 | 104 | services.template(source='bingod.ini', | ||
2255 | 105 | target='/etc/bingod.ini', | ||
2256 | 106 | owner='bingo', perms=0400), | ||
2257 | 107 | ], | ||
2258 | 108 | }, | ||
2259 | 109 | { | ||
2260 | 110 | 'service': 'spadesd', | ||
2261 | 111 | 'data_ready': services.template(source='spadesd_run.j2', | ||
2262 | 112 | target='/etc/sv/spadesd/run', | ||
2263 | 113 | perms=0555), | ||
2264 | 114 | 'start': runit_start, | ||
2265 | 115 | 'stop': runit_stop, | ||
2266 | 116 | }, | ||
2267 | 117 | ]) | ||
2268 | 118 | manager.manage() | ||
2269 | 119 | """ | ||
2270 | 120 | self._ready_file = os.path.join(hookenv.charm_dir(), 'READY-SERVICES.json') | ||
2271 | 121 | self._ready = None | ||
2272 | 122 | self.services = OrderedDict() | ||
2273 | 123 | for service in services or []: | ||
2274 | 124 | service_name = service['service'] | ||
2275 | 125 | self.services[service_name] = service | ||
2276 | 126 | |||
2277 | 127 | def manage(self): | ||
2278 | 128 | """ | ||
2279 | 129 | Handle the current hook by doing The Right Thing with the registered services. | ||
2280 | 130 | """ | ||
2281 | 131 | hookenv._run_atstart() | ||
2282 | 132 | try: | ||
2283 | 133 | hook_name = hookenv.hook_name() | ||
2284 | 134 | if hook_name == 'stop': | ||
2285 | 135 | self.stop_services() | ||
2286 | 136 | else: | ||
2287 | 137 | self.reconfigure_services() | ||
2288 | 138 | self.provide_data() | ||
2289 | 139 | except SystemExit as x: | ||
2290 | 140 | if x.code is None or x.code == 0: | ||
2291 | 141 | hookenv._run_atexit() | ||
2292 | 142 | hookenv._run_atexit() | ||
2293 | 143 | |||
2294 | 144 | def provide_data(self): | ||
2295 | 145 | """ | ||
2296 | 146 | Set the relation data for each provider in the ``provided_data`` list. | ||
2297 | 147 | |||
2298 | 148 | A provider must have a `name` attribute, which indicates which relation | ||
2299 | 149 | to set data on, and a `provide_data()` method, which returns a dict of | ||
2300 | 150 | data to set. | ||
2301 | 151 | |||
2302 | 152 | The `provide_data()` method can optionally accept two parameters: | ||
2303 | 153 | |||
2304 | 154 | * ``remote_service`` The name of the remote service that the data will | ||
2305 | 155 | be provided to. The `provide_data()` method will be called once | ||
2306 | 156 | for each connected service (not unit). This allows the method to | ||
2307 | 157 | tailor its data to the given service. | ||
2308 | 158 | * ``service_ready`` Whether or not the service definition had all of | ||
2309 | 159 | its requirements met, and thus the ``data_ready`` callbacks run. | ||
2310 | 160 | |||
2311 | 161 | Note that the ``provided_data`` methods are now called **after** the | ||
2312 | 162 | ``data_ready`` callbacks are run. This gives the ``data_ready`` callbacks | ||
2313 | 163 | a chance to generate any data necessary for the providing to the remote | ||
2314 | 164 | services. | ||
2315 | 165 | """ | ||
2316 | 166 | for service_name, service in self.services.items(): | ||
2317 | 167 | service_ready = self.is_ready(service_name) | ||
2318 | 168 | for provider in service.get('provided_data', []): | ||
2319 | 169 | for relid in hookenv.relation_ids(provider.name): | ||
2320 | 170 | units = hookenv.related_units(relid) | ||
2321 | 171 | if not units: | ||
2322 | 172 | continue | ||
2323 | 173 | remote_service = units[0].split('/')[0] | ||
2324 | 174 | argspec = getargspec(provider.provide_data) | ||
2325 | 175 | if len(argspec.args) > 1: | ||
2326 | 176 | data = provider.provide_data(remote_service, service_ready) | ||
2327 | 177 | else: | ||
2328 | 178 | data = provider.provide_data() | ||
2329 | 179 | if data: | ||
2330 | 180 | hookenv.relation_set(relid, data) | ||
2331 | 181 | |||
2332 | 182 | def reconfigure_services(self, *service_names): | ||
2333 | 183 | """ | ||
2334 | 184 | Update all files for one or more registered services, and, | ||
2335 | 185 | if ready, optionally restart them. | ||
2336 | 186 | |||
2337 | 187 | If no service names are given, reconfigures all registered services. | ||
2338 | 188 | """ | ||
2339 | 189 | for service_name in service_names or self.services.keys(): | ||
2340 | 190 | if self.is_ready(service_name): | ||
2341 | 191 | self.fire_event('data_ready', service_name) | ||
2342 | 192 | self.fire_event('start', service_name, default=[ | ||
2343 | 193 | service_restart, | ||
2344 | 194 | manage_ports]) | ||
2345 | 195 | self.save_ready(service_name) | ||
2346 | 196 | else: | ||
2347 | 197 | if self.was_ready(service_name): | ||
2348 | 198 | self.fire_event('data_lost', service_name) | ||
2349 | 199 | self.fire_event('stop', service_name, default=[ | ||
2350 | 200 | manage_ports, | ||
2351 | 201 | service_stop]) | ||
2352 | 202 | self.save_lost(service_name) | ||
2353 | 203 | |||
2354 | 204 | def stop_services(self, *service_names): | ||
2355 | 205 | """ | ||
2356 | 206 | Stop one or more registered services, by name. | ||
2357 | 207 | |||
2358 | 208 | If no service names are given, stops all registered services. | ||
2359 | 209 | """ | ||
2360 | 210 | for service_name in service_names or self.services.keys(): | ||
2361 | 211 | self.fire_event('stop', service_name, default=[ | ||
2362 | 212 | manage_ports, | ||
2363 | 213 | service_stop]) | ||
2364 | 214 | |||
2365 | 215 | def get_service(self, service_name): | ||
2366 | 216 | """ | ||
2367 | 217 | Given the name of a registered service, return its service definition. | ||
2368 | 218 | """ | ||
2369 | 219 | service = self.services.get(service_name) | ||
2370 | 220 | if not service: | ||
2371 | 221 | raise KeyError('Service not registered: %s' % service_name) | ||
2372 | 222 | return service | ||
2373 | 223 | |||
2374 | 224 | def fire_event(self, event_name, service_name, default=None): | ||
2375 | 225 | """ | ||
2376 | 226 | Fire a data_ready, data_lost, start, or stop event on a given service. | ||
2377 | 227 | """ | ||
2378 | 228 | service = self.get_service(service_name) | ||
2379 | 229 | callbacks = service.get(event_name, default) | ||
2380 | 230 | if not callbacks: | ||
2381 | 231 | return | ||
2382 | 232 | if not isinstance(callbacks, Iterable): | ||
2383 | 233 | callbacks = [callbacks] | ||
2384 | 234 | for callback in callbacks: | ||
2385 | 235 | if isinstance(callback, ManagerCallback): | ||
2386 | 236 | callback(self, service_name, event_name) | ||
2387 | 237 | else: | ||
2388 | 238 | callback(service_name) | ||
2389 | 239 | |||
2390 | 240 | def is_ready(self, service_name): | ||
2391 | 241 | """ | ||
2392 | 242 | Determine if a registered service is ready, by checking its 'required_data'. | ||
2393 | 243 | |||
2394 | 244 | A 'required_data' item can be any mapping type, and is considered ready | ||
2395 | 245 | if `bool(item)` evaluates as True. | ||
2396 | 246 | """ | ||
2397 | 247 | service = self.get_service(service_name) | ||
2398 | 248 | reqs = service.get('required_data', []) | ||
2399 | 249 | return all(bool(req) for req in reqs) | ||
2400 | 250 | |||
2401 | 251 | def _load_ready_file(self): | ||
2402 | 252 | if self._ready is not None: | ||
2403 | 253 | return | ||
2404 | 254 | if os.path.exists(self._ready_file): | ||
2405 | 255 | with open(self._ready_file) as fp: | ||
2406 | 256 | self._ready = set(json.load(fp)) | ||
2407 | 257 | else: | ||
2408 | 258 | self._ready = set() | ||
2409 | 259 | |||
2410 | 260 | def _save_ready_file(self): | ||
2411 | 261 | if self._ready is None: | ||
2412 | 262 | return | ||
2413 | 263 | with open(self._ready_file, 'w') as fp: | ||
2414 | 264 | json.dump(list(self._ready), fp) | ||
2415 | 265 | |||
2416 | 266 | def save_ready(self, service_name): | ||
2417 | 267 | """ | ||
2418 | 268 | Save an indicator that the given service is now data_ready. | ||
2419 | 269 | """ | ||
2420 | 270 | self._load_ready_file() | ||
2421 | 271 | self._ready.add(service_name) | ||
2422 | 272 | self._save_ready_file() | ||
2423 | 273 | |||
2424 | 274 | def save_lost(self, service_name): | ||
2425 | 275 | """ | ||
2426 | 276 | Save an indicator that the given service is no longer data_ready. | ||
2427 | 277 | """ | ||
2428 | 278 | self._load_ready_file() | ||
2429 | 279 | self._ready.discard(service_name) | ||
2430 | 280 | self._save_ready_file() | ||
2431 | 281 | |||
2432 | 282 | def was_ready(self, service_name): | ||
2433 | 283 | """ | ||
2434 | 284 | Determine if the given service was previously data_ready. | ||
2435 | 285 | """ | ||
2436 | 286 | self._load_ready_file() | ||
2437 | 287 | return service_name in self._ready | ||
2438 | 288 | |||
2439 | 289 | |||
2440 | 290 | class ManagerCallback(object): | ||
2441 | 291 | """ | ||
2442 | 292 | Special case of a callback that takes the `ServiceManager` instance | ||
2443 | 293 | in addition to the service name. | ||
2444 | 294 | |||
2445 | 295 | Subclasses should implement `__call__` which should accept three parameters: | ||
2446 | 296 | |||
2447 | 297 | * `manager` The `ServiceManager` instance | ||
2448 | 298 | * `service_name` The name of the service it's being triggered for | ||
2449 | 299 | * `event_name` The name of the event that this callback is handling | ||
2450 | 300 | """ | ||
2451 | 301 | def __call__(self, manager, service_name, event_name): | ||
2452 | 302 | raise NotImplementedError() | ||
2453 | 303 | |||
2454 | 304 | |||
2455 | 305 | class PortManagerCallback(ManagerCallback): | ||
2456 | 306 | """ | ||
2457 | 307 | Callback class that will open or close ports, for use as either | ||
2458 | 308 | a start or stop action. | ||
2459 | 309 | """ | ||
2460 | 310 | def __call__(self, manager, service_name, event_name): | ||
2461 | 311 | service = manager.get_service(service_name) | ||
2462 | 312 | new_ports = service.get('ports', []) | ||
2463 | 313 | port_file = os.path.join(hookenv.charm_dir(), '.{}.ports'.format(service_name)) | ||
2464 | 314 | if os.path.exists(port_file): | ||
2465 | 315 | with open(port_file) as fp: | ||
2466 | 316 | old_ports = fp.read().split(',') | ||
2467 | 317 | for old_port in old_ports: | ||
2468 | 318 | if bool(old_port): | ||
2469 | 319 | old_port = int(old_port) | ||
2470 | 320 | if old_port not in new_ports: | ||
2471 | 321 | hookenv.close_port(old_port) | ||
2472 | 322 | with open(port_file, 'w') as fp: | ||
2473 | 323 | fp.write(','.join(str(port) for port in new_ports)) | ||
2474 | 324 | for port in new_ports: | ||
2475 | 325 | if event_name == 'start': | ||
2476 | 326 | hookenv.open_port(port) | ||
2477 | 327 | elif event_name == 'stop': | ||
2478 | 328 | hookenv.close_port(port) | ||
2479 | 329 | |||
2480 | 330 | |||
2481 | 331 | def service_stop(service_name): | ||
2482 | 332 | """ | ||
2483 | 333 | Wrapper around host.service_stop to prevent spurious "unknown service" | ||
2484 | 334 | messages in the logs. | ||
2485 | 335 | """ | ||
2486 | 336 | if host.service_running(service_name): | ||
2487 | 337 | host.service_stop(service_name) | ||
2488 | 338 | |||
2489 | 339 | |||
2490 | 340 | def service_restart(service_name): | ||
2491 | 341 | """ | ||
2492 | 342 | Wrapper around host.service_restart to prevent spurious "unknown service" | ||
2493 | 343 | messages in the logs. | ||
2494 | 344 | """ | ||
2495 | 345 | if host.service_available(service_name): | ||
2496 | 346 | if host.service_running(service_name): | ||
2497 | 347 | host.service_restart(service_name) | ||
2498 | 348 | else: | ||
2499 | 349 | host.service_start(service_name) | ||
2500 | 350 | |||
2501 | 351 | |||
2502 | 352 | # Convenience aliases | ||
2503 | 353 | open_ports = close_ports = manage_ports = PortManagerCallback() | ||
2504 | 0 | 354 | ||
2505 | === added file 'hooks/charmhelpers/core/services/helpers.py' | |||
2506 | --- hooks/charmhelpers/core/services/helpers.py 1970-01-01 00:00:00 +0000 | |||
2507 | +++ hooks/charmhelpers/core/services/helpers.py 2015-12-17 05:44:32 +0000 | |||
2508 | @@ -0,0 +1,292 @@ | |||
2509 | 1 | # Copyright 2014-2015 Canonical Limited. | ||
2510 | 2 | # | ||
2511 | 3 | # This file is part of charm-helpers. | ||
2512 | 4 | # | ||
2513 | 5 | # charm-helpers is free software: you can redistribute it and/or modify | ||
2514 | 6 | # it under the terms of the GNU Lesser General Public License version 3 as | ||
2515 | 7 | # published by the Free Software Foundation. | ||
2516 | 8 | # | ||
2517 | 9 | # charm-helpers is distributed in the hope that it will be useful, | ||
2518 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
2519 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
2520 | 12 | # GNU Lesser General Public License for more details. | ||
2521 | 13 | # | ||
2522 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
2523 | 15 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
2524 | 16 | |||
2525 | 17 | import os | ||
2526 | 18 | import yaml | ||
2527 | 19 | |||
2528 | 20 | from charmhelpers.core import hookenv | ||
2529 | 21 | from charmhelpers.core import host | ||
2530 | 22 | from charmhelpers.core import templating | ||
2531 | 23 | |||
2532 | 24 | from charmhelpers.core.services.base import ManagerCallback | ||
2533 | 25 | |||
2534 | 26 | |||
2535 | 27 | __all__ = ['RelationContext', 'TemplateCallback', | ||
2536 | 28 | 'render_template', 'template'] | ||
2537 | 29 | |||
2538 | 30 | |||
2539 | 31 | class RelationContext(dict): | ||
2540 | 32 | """ | ||
2541 | 33 | Base class for a context generator that gets relation data from juju. | ||
2542 | 34 | |||
2543 | 35 | Subclasses must provide the attributes `name`, which is the name of the | ||
2544 | 36 | interface of interest, `interface`, which is the type of the interface of | ||
2545 | 37 | interest, and `required_keys`, which is the set of keys required for the | ||
2546 | 38 | relation to be considered complete. The data for all interfaces matching | ||
2547 | 39 | the `name` attribute that are complete will used to populate the dictionary | ||
2548 | 40 | values (see `get_data`, below). | ||
2549 | 41 | |||
2550 | 42 | The generated context will be namespaced under the relation :attr:`name`, | ||
2551 | 43 | to prevent potential naming conflicts. | ||
2552 | 44 | |||
2553 | 45 | :param str name: Override the relation :attr:`name`, since it can vary from charm to charm | ||
2554 | 46 | :param list additional_required_keys: Extend the list of :attr:`required_keys` | ||
2555 | 47 | """ | ||
2556 | 48 | name = None | ||
2557 | 49 | interface = None | ||
2558 | 50 | |||
2559 | 51 | def __init__(self, name=None, additional_required_keys=None): | ||
2560 | 52 | if not hasattr(self, 'required_keys'): | ||
2561 | 53 | self.required_keys = [] | ||
2562 | 54 | |||
2563 | 55 | if name is not None: | ||
2564 | 56 | self.name = name | ||
2565 | 57 | if additional_required_keys: | ||
2566 | 58 | self.required_keys.extend(additional_required_keys) | ||
2567 | 59 | self.get_data() | ||
2568 | 60 | |||
2569 | 61 | def __bool__(self): | ||
2570 | 62 | """ | ||
2571 | 63 | Returns True if all of the required_keys are available. | ||
2572 | 64 | """ | ||
2573 | 65 | return self.is_ready() | ||
2574 | 66 | |||
2575 | 67 | __nonzero__ = __bool__ | ||
2576 | 68 | |||
2577 | 69 | def __repr__(self): | ||
2578 | 70 | return super(RelationContext, self).__repr__() | ||
2579 | 71 | |||
2580 | 72 | def is_ready(self): | ||
2581 | 73 | """ | ||
2582 | 74 | Returns True if all of the `required_keys` are available from any units. | ||
2583 | 75 | """ | ||
2584 | 76 | ready = len(self.get(self.name, [])) > 0 | ||
2585 | 77 | if not ready: | ||
2586 | 78 | hookenv.log('Incomplete relation: {}'.format(self.__class__.__name__), hookenv.DEBUG) | ||
2587 | 79 | return ready | ||
2588 | 80 | |||
2589 | 81 | def _is_ready(self, unit_data): | ||
2590 | 82 | """ | ||
2591 | 83 | Helper method that tests a set of relation data and returns True if | ||
2592 | 84 | all of the `required_keys` are present. | ||
2593 | 85 | """ | ||
2594 | 86 | return set(unit_data.keys()).issuperset(set(self.required_keys)) | ||
2595 | 87 | |||
2596 | 88 | def get_data(self): | ||
2597 | 89 | """ | ||
2598 | 90 | Retrieve the relation data for each unit involved in a relation and, | ||
2599 | 91 | if complete, store it in a list under `self[self.name]`. This | ||
2600 | 92 | is automatically called when the RelationContext is instantiated. | ||
2601 | 93 | |||
2602 | 94 | The units are sorted lexographically first by the service ID, then by | ||
2603 | 95 | the unit ID. Thus, if an interface has two other services, 'db:1' | ||
2604 | 96 | and 'db:2', with 'db:1' having two units, 'wordpress/0' and 'wordpress/1', | ||
2605 | 97 | and 'db:2' having one unit, 'mediawiki/0', all of which have a complete | ||
2606 | 98 | set of data, the relation data for the units will be stored in the | ||
2607 | 99 | order: 'wordpress/0', 'wordpress/1', 'mediawiki/0'. | ||
2608 | 100 | |||
2609 | 101 | If you only care about a single unit on the relation, you can just | ||
2610 | 102 | access it as `{{ interface[0]['key'] }}`. However, if you can at all | ||
2611 | 103 | support multiple units on a relation, you should iterate over the list, | ||
2612 | 104 | like:: | ||
2613 | 105 | |||
2614 | 106 | {% for unit in interface -%} | ||
2615 | 107 | {{ unit['key'] }}{% if not loop.last %},{% endif %} | ||
2616 | 108 | {%- endfor %} | ||
2617 | 109 | |||
2618 | 110 | Note that since all sets of relation data from all related services and | ||
2619 | 111 | units are in a single list, if you need to know which service or unit a | ||
2620 | 112 | set of data came from, you'll need to extend this class to preserve | ||
2621 | 113 | that information. | ||
2622 | 114 | """ | ||
2623 | 115 | if not hookenv.relation_ids(self.name): | ||
2624 | 116 | return | ||
2625 | 117 | |||
2626 | 118 | ns = self.setdefault(self.name, []) | ||
2627 | 119 | for rid in sorted(hookenv.relation_ids(self.name)): | ||
2628 | 120 | for unit in sorted(hookenv.related_units(rid)): | ||
2629 | 121 | reldata = hookenv.relation_get(rid=rid, unit=unit) | ||
2630 | 122 | if self._is_ready(reldata): | ||
2631 | 123 | ns.append(reldata) | ||
2632 | 124 | |||
2633 | 125 | def provide_data(self): | ||
2634 | 126 | """ | ||
2635 | 127 | Return data to be relation_set for this interface. | ||
2636 | 128 | """ | ||
2637 | 129 | return {} | ||
2638 | 130 | |||
2639 | 131 | |||
2640 | 132 | class MysqlRelation(RelationContext): | ||
2641 | 133 | """ | ||
2642 | 134 | Relation context for the `mysql` interface. | ||
2643 | 135 | |||
2644 | 136 | :param str name: Override the relation :attr:`name`, since it can vary from charm to charm | ||
2645 | 137 | :param list additional_required_keys: Extend the list of :attr:`required_keys` | ||
2646 | 138 | """ | ||
2647 | 139 | name = 'db' | ||
2648 | 140 | interface = 'mysql' | ||
2649 | 141 | |||
2650 | 142 | def __init__(self, *args, **kwargs): | ||
2651 | 143 | self.required_keys = ['host', 'user', 'password', 'database'] | ||
2652 | 144 | RelationContext.__init__(self, *args, **kwargs) | ||
2653 | 145 | |||
2654 | 146 | |||
2655 | 147 | class HttpRelation(RelationContext): | ||
2656 | 148 | """ | ||
2657 | 149 | Relation context for the `http` interface. | ||
2658 | 150 | |||
2659 | 151 | :param str name: Override the relation :attr:`name`, since it can vary from charm to charm | ||
2660 | 152 | :param list additional_required_keys: Extend the list of :attr:`required_keys` | ||
2661 | 153 | """ | ||
2662 | 154 | name = 'website' | ||
2663 | 155 | interface = 'http' | ||
2664 | 156 | |||
2665 | 157 | def __init__(self, *args, **kwargs): | ||
2666 | 158 | self.required_keys = ['host', 'port'] | ||
2667 | 159 | RelationContext.__init__(self, *args, **kwargs) | ||
2668 | 160 | |||
2669 | 161 | def provide_data(self): | ||
2670 | 162 | return { | ||
2671 | 163 | 'host': hookenv.unit_get('private-address'), | ||
2672 | 164 | 'port': 80, | ||
2673 | 165 | } | ||
2674 | 166 | |||
2675 | 167 | |||
2676 | 168 | class RequiredConfig(dict): | ||
2677 | 169 | """ | ||
2678 | 170 | Data context that loads config options with one or more mandatory options. | ||
2679 | 171 | |||
2680 | 172 | Once the required options have been changed from their default values, all | ||
2681 | 173 | config options will be available, namespaced under `config` to prevent | ||
2682 | 174 | potential naming conflicts (for example, between a config option and a | ||
2683 | 175 | relation property). | ||
2684 | 176 | |||
2685 | 177 | :param list *args: List of options that must be changed from their default values. | ||
2686 | 178 | """ | ||
2687 | 179 | |||
2688 | 180 | def __init__(self, *args): | ||
2689 | 181 | self.required_options = args | ||
2690 | 182 | self['config'] = hookenv.config() | ||
2691 | 183 | with open(os.path.join(hookenv.charm_dir(), 'config.yaml')) as fp: | ||
2692 | 184 | self.config = yaml.load(fp).get('options', {}) | ||
2693 | 185 | |||
2694 | 186 | def __bool__(self): | ||
2695 | 187 | for option in self.required_options: | ||
2696 | 188 | if option not in self['config']: | ||
2697 | 189 | return False | ||
2698 | 190 | current_value = self['config'][option] | ||
2699 | 191 | default_value = self.config[option].get('default') | ||
2700 | 192 | if current_value == default_value: | ||
2701 | 193 | return False | ||
2702 | 194 | if current_value in (None, '') and default_value in (None, ''): | ||
2703 | 195 | return False | ||
2704 | 196 | return True | ||
2705 | 197 | |||
2706 | 198 | def __nonzero__(self): | ||
2707 | 199 | return self.__bool__() | ||
2708 | 200 | |||
2709 | 201 | |||
2710 | 202 | class StoredContext(dict): | ||
2711 | 203 | """ | ||
2712 | 204 | A data context that always returns the data that it was first created with. | ||
2713 | 205 | |||
2714 | 206 | This is useful to do a one-time generation of things like passwords, that | ||
2715 | 207 | will thereafter use the same value that was originally generated, instead | ||
2716 | 208 | of generating a new value each time it is run. | ||
2717 | 209 | """ | ||
2718 | 210 | def __init__(self, file_name, config_data): | ||
2719 | 211 | """ | ||
2720 | 212 | If the file exists, populate `self` with the data from the file. | ||
2721 | 213 | Otherwise, populate with the given data and persist it to the file. | ||
2722 | 214 | """ | ||
2723 | 215 | if os.path.exists(file_name): | ||
2724 | 216 | self.update(self.read_context(file_name)) | ||
2725 | 217 | else: | ||
2726 | 218 | self.store_context(file_name, config_data) | ||
2727 | 219 | self.update(config_data) | ||
2728 | 220 | |||
2729 | 221 | def store_context(self, file_name, config_data): | ||
2730 | 222 | if not os.path.isabs(file_name): | ||
2731 | 223 | file_name = os.path.join(hookenv.charm_dir(), file_name) | ||
2732 | 224 | with open(file_name, 'w') as file_stream: | ||
2733 | 225 | os.fchmod(file_stream.fileno(), 0o600) | ||
2734 | 226 | yaml.dump(config_data, file_stream) | ||
2735 | 227 | |||
2736 | 228 | def read_context(self, file_name): | ||
2737 | 229 | if not os.path.isabs(file_name): | ||
2738 | 230 | file_name = os.path.join(hookenv.charm_dir(), file_name) | ||
2739 | 231 | with open(file_name, 'r') as file_stream: | ||
2740 | 232 | data = yaml.load(file_stream) | ||
2741 | 233 | if not data: | ||
2742 | 234 | raise OSError("%s is empty" % file_name) | ||
2743 | 235 | return data | ||
2744 | 236 | |||
2745 | 237 | |||
2746 | 238 | class TemplateCallback(ManagerCallback): | ||
2747 | 239 | """ | ||
2748 | 240 | Callback class that will render a Jinja2 template, for use as a ready | ||
2749 | 241 | action. | ||
2750 | 242 | |||
2751 | 243 | :param str source: The template source file, relative to | ||
2752 | 244 | `$CHARM_DIR/templates` | ||
2753 | 245 | |||
2754 | 246 | :param str target: The target to write the rendered template to (or None) | ||
2755 | 247 | :param str owner: The owner of the rendered file | ||
2756 | 248 | :param str group: The group of the rendered file | ||
2757 | 249 | :param int perms: The permissions of the rendered file | ||
2758 | 250 | :param partial on_change_action: functools partial to be executed when | ||
2759 | 251 | rendered file changes | ||
2760 | 252 | :param jinja2 loader template_loader: A jinja2 template loader | ||
2761 | 253 | |||
2762 | 254 | :return str: The rendered template | ||
2763 | 255 | """ | ||
2764 | 256 | def __init__(self, source, target, | ||
2765 | 257 | owner='root', group='root', perms=0o444, | ||
2766 | 258 | on_change_action=None, template_loader=None): | ||
2767 | 259 | self.source = source | ||
2768 | 260 | self.target = target | ||
2769 | 261 | self.owner = owner | ||
2770 | 262 | self.group = group | ||
2771 | 263 | self.perms = perms | ||
2772 | 264 | self.on_change_action = on_change_action | ||
2773 | 265 | self.template_loader = template_loader | ||
2774 | 266 | |||
2775 | 267 | def __call__(self, manager, service_name, event_name): | ||
2776 | 268 | pre_checksum = '' | ||
2777 | 269 | if self.on_change_action and os.path.isfile(self.target): | ||
2778 | 270 | pre_checksum = host.file_hash(self.target) | ||
2779 | 271 | service = manager.get_service(service_name) | ||
2780 | 272 | context = {'ctx': {}} | ||
2781 | 273 | for ctx in service.get('required_data', []): | ||
2782 | 274 | context.update(ctx) | ||
2783 | 275 | context['ctx'].update(ctx) | ||
2784 | 276 | |||
2785 | 277 | result = templating.render(self.source, self.target, context, | ||
2786 | 278 | self.owner, self.group, self.perms, | ||
2787 | 279 | template_loader=self.template_loader) | ||
2788 | 280 | if self.on_change_action: | ||
2789 | 281 | if pre_checksum == host.file_hash(self.target): | ||
2790 | 282 | hookenv.log( | ||
2791 | 283 | 'No change detected: {}'.format(self.target), | ||
2792 | 284 | hookenv.DEBUG) | ||
2793 | 285 | else: | ||
2794 | 286 | self.on_change_action() | ||
2795 | 287 | |||
2796 | 288 | return result | ||
2797 | 289 | |||
2798 | 290 | |||
2799 | 291 | # Convenience aliases for templates | ||
2800 | 292 | render_template = template = TemplateCallback | ||
2801 | 0 | 293 | ||
2802 | === added file 'hooks/charmhelpers/core/strutils.py' | |||
2803 | --- hooks/charmhelpers/core/strutils.py 1970-01-01 00:00:00 +0000 | |||
2804 | +++ hooks/charmhelpers/core/strutils.py 2015-12-17 05:44:32 +0000 | |||
2805 | @@ -0,0 +1,72 @@ | |||
2806 | 1 | #!/usr/bin/env python | ||
2807 | 2 | # -*- coding: utf-8 -*- | ||
2808 | 3 | |||
2809 | 4 | # Copyright 2014-2015 Canonical Limited. | ||
2810 | 5 | # | ||
2811 | 6 | # This file is part of charm-helpers. | ||
2812 | 7 | # | ||
2813 | 8 | # charm-helpers is free software: you can redistribute it and/or modify | ||
2814 | 9 | # it under the terms of the GNU Lesser General Public License version 3 as | ||
2815 | 10 | # published by the Free Software Foundation. | ||
2816 | 11 | # | ||
2817 | 12 | # charm-helpers is distributed in the hope that it will be useful, | ||
2818 | 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
2819 | 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
2820 | 15 | # GNU Lesser General Public License for more details. | ||
2821 | 16 | # | ||
2822 | 17 | # You should have received a copy of the GNU Lesser General Public License | ||
2823 | 18 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
2824 | 19 | |||
2825 | 20 | import six | ||
2826 | 21 | import re | ||
2827 | 22 | |||
2828 | 23 | |||
2829 | 24 | def bool_from_string(value): | ||
2830 | 25 | """Interpret string value as boolean. | ||
2831 | 26 | |||
2832 | 27 | Returns True if value translates to True otherwise False. | ||
2833 | 28 | """ | ||
2834 | 29 | if isinstance(value, six.string_types): | ||
2835 | 30 | value = six.text_type(value) | ||
2836 | 31 | else: | ||
2837 | 32 | msg = "Unable to interpret non-string value '%s' as boolean" % (value) | ||
2838 | 33 | raise ValueError(msg) | ||
2839 | 34 | |||
2840 | 35 | value = value.strip().lower() | ||
2841 | 36 | |||
2842 | 37 | if value in ['y', 'yes', 'true', 't', 'on']: | ||
2843 | 38 | return True | ||
2844 | 39 | elif value in ['n', 'no', 'false', 'f', 'off']: | ||
2845 | 40 | return False | ||
2846 | 41 | |||
2847 | 42 | msg = "Unable to interpret string value '%s' as boolean" % (value) | ||
2848 | 43 | raise ValueError(msg) | ||
2849 | 44 | |||
2850 | 45 | |||
2851 | 46 | def bytes_from_string(value): | ||
2852 | 47 | """Interpret human readable string value as bytes. | ||
2853 | 48 | |||
2854 | 49 | Returns int | ||
2855 | 50 | """ | ||
2856 | 51 | BYTE_POWER = { | ||
2857 | 52 | 'K': 1, | ||
2858 | 53 | 'KB': 1, | ||
2859 | 54 | 'M': 2, | ||
2860 | 55 | 'MB': 2, | ||
2861 | 56 | 'G': 3, | ||
2862 | 57 | 'GB': 3, | ||
2863 | 58 | 'T': 4, | ||
2864 | 59 | 'TB': 4, | ||
2865 | 60 | 'P': 5, | ||
2866 | 61 | 'PB': 5, | ||
2867 | 62 | } | ||
2868 | 63 | if isinstance(value, six.string_types): | ||
2869 | 64 | value = six.text_type(value) | ||
2870 | 65 | else: | ||
2871 | 66 | msg = "Unable to interpret non-string value '%s' as boolean" % (value) | ||
2872 | 67 | raise ValueError(msg) | ||
2873 | 68 | matches = re.match("([0-9]+)([a-zA-Z]+)", value) | ||
2874 | 69 | if not matches: | ||
2875 | 70 | msg = "Unable to interpret string value '%s' as bytes" % (value) | ||
2876 | 71 | raise ValueError(msg) | ||
2877 | 72 | return int(matches.group(1)) * (1024 ** BYTE_POWER[matches.group(2)]) | ||
2878 | 0 | 73 | ||
2879 | === added file 'hooks/charmhelpers/core/sysctl.py' | |||
2880 | --- hooks/charmhelpers/core/sysctl.py 1970-01-01 00:00:00 +0000 | |||
2881 | +++ hooks/charmhelpers/core/sysctl.py 2015-12-17 05:44:32 +0000 | |||
2882 | @@ -0,0 +1,56 @@ | |||
2883 | 1 | #!/usr/bin/env python | ||
2884 | 2 | # -*- coding: utf-8 -*- | ||
2885 | 3 | |||
2886 | 4 | # Copyright 2014-2015 Canonical Limited. | ||
2887 | 5 | # | ||
2888 | 6 | # This file is part of charm-helpers. | ||
2889 | 7 | # | ||
2890 | 8 | # charm-helpers is free software: you can redistribute it and/or modify | ||
2891 | 9 | # it under the terms of the GNU Lesser General Public License version 3 as | ||
2892 | 10 | # published by the Free Software Foundation. | ||
2893 | 11 | # | ||
2894 | 12 | # charm-helpers is distributed in the hope that it will be useful, | ||
2895 | 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
2896 | 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
2897 | 15 | # GNU Lesser General Public License for more details. | ||
2898 | 16 | # | ||
2899 | 17 | # You should have received a copy of the GNU Lesser General Public License | ||
2900 | 18 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
2901 | 19 | |||
2902 | 20 | import yaml | ||
2903 | 21 | |||
2904 | 22 | from subprocess import check_call | ||
2905 | 23 | |||
2906 | 24 | from charmhelpers.core.hookenv import ( | ||
2907 | 25 | log, | ||
2908 | 26 | DEBUG, | ||
2909 | 27 | ERROR, | ||
2910 | 28 | ) | ||
2911 | 29 | |||
2912 | 30 | __author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>' | ||
2913 | 31 | |||
2914 | 32 | |||
2915 | 33 | def create(sysctl_dict, sysctl_file): | ||
2916 | 34 | """Creates a sysctl.conf file from a YAML associative array | ||
2917 | 35 | |||
2918 | 36 | :param sysctl_dict: a YAML-formatted string of sysctl options eg "{ 'kernel.max_pid': 1337 }" | ||
2919 | 37 | :type sysctl_dict: str | ||
2920 | 38 | :param sysctl_file: path to the sysctl file to be saved | ||
2921 | 39 | :type sysctl_file: str or unicode | ||
2922 | 40 | :returns: None | ||
2923 | 41 | """ | ||
2924 | 42 | try: | ||
2925 | 43 | sysctl_dict_parsed = yaml.safe_load(sysctl_dict) | ||
2926 | 44 | except yaml.YAMLError: | ||
2927 | 45 | log("Error parsing YAML sysctl_dict: {}".format(sysctl_dict), | ||
2928 | 46 | level=ERROR) | ||
2929 | 47 | return | ||
2930 | 48 | |||
2931 | 49 | with open(sysctl_file, "w") as fd: | ||
2932 | 50 | for key, value in sysctl_dict_parsed.items(): | ||
2933 | 51 | fd.write("{}={}\n".format(key, value)) | ||
2934 | 52 | |||
2935 | 53 | log("Updating sysctl_file: %s values: %s" % (sysctl_file, sysctl_dict_parsed), | ||
2936 | 54 | level=DEBUG) | ||
2937 | 55 | |||
2938 | 56 | check_call(["sysctl", "-p", sysctl_file]) | ||
2939 | 0 | 57 | ||
2940 | === added file 'hooks/charmhelpers/core/templating.py' | |||
2941 | --- hooks/charmhelpers/core/templating.py 1970-01-01 00:00:00 +0000 | |||
2942 | +++ hooks/charmhelpers/core/templating.py 2015-12-17 05:44:32 +0000 | |||
2943 | @@ -0,0 +1,81 @@ | |||
2944 | 1 | # Copyright 2014-2015 Canonical Limited. | ||
2945 | 2 | # | ||
2946 | 3 | # This file is part of charm-helpers. | ||
2947 | 4 | # | ||
2948 | 5 | # charm-helpers is free software: you can redistribute it and/or modify | ||
2949 | 6 | # it under the terms of the GNU Lesser General Public License version 3 as | ||
2950 | 7 | # published by the Free Software Foundation. | ||
2951 | 8 | # | ||
2952 | 9 | # charm-helpers is distributed in the hope that it will be useful, | ||
2953 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
2954 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
2955 | 12 | # GNU Lesser General Public License for more details. | ||
2956 | 13 | # | ||
2957 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
2958 | 15 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
2959 | 16 | |||
2960 | 17 | import os | ||
2961 | 18 | |||
2962 | 19 | from charmhelpers.core import host | ||
2963 | 20 | from charmhelpers.core import hookenv | ||
2964 | 21 | |||
2965 | 22 | |||
2966 | 23 | def render(source, target, context, owner='root', group='root', | ||
2967 | 24 | perms=0o444, templates_dir=None, encoding='UTF-8', template_loader=None): | ||
2968 | 25 | """ | ||
2969 | 26 | Render a template. | ||
2970 | 27 | |||
2971 | 28 | The `source` path, if not absolute, is relative to the `templates_dir`. | ||
2972 | 29 | |||
2973 | 30 | The `target` path should be absolute. It can also be `None`, in which | ||
2974 | 31 | case no file will be written. | ||
2975 | 32 | |||
2976 | 33 | The context should be a dict containing the values to be replaced in the | ||
2977 | 34 | template. | ||
2978 | 35 | |||
2979 | 36 | The `owner`, `group`, and `perms` options will be passed to `write_file`. | ||
2980 | 37 | |||
2981 | 38 | If omitted, `templates_dir` defaults to the `templates` folder in the charm. | ||
2982 | 39 | |||
2983 | 40 | The rendered template will be written to the file as well as being returned | ||
2984 | 41 | as a string. | ||
2985 | 42 | |||
2986 | 43 | Note: Using this requires python-jinja2; if it is not installed, calling | ||
2987 | 44 | this will attempt to use charmhelpers.fetch.apt_install to install it. | ||
2988 | 45 | """ | ||
2989 | 46 | try: | ||
2990 | 47 | from jinja2 import FileSystemLoader, Environment, exceptions | ||
2991 | 48 | except ImportError: | ||
2992 | 49 | try: | ||
2993 | 50 | from charmhelpers.fetch import apt_install | ||
2994 | 51 | except ImportError: | ||
2995 | 52 | hookenv.log('Could not import jinja2, and could not import ' | ||
2996 | 53 | 'charmhelpers.fetch to install it', | ||
2997 | 54 | level=hookenv.ERROR) | ||
2998 | 55 | raise | ||
2999 | 56 | apt_install('python-jinja2', fatal=True) | ||
3000 | 57 | from jinja2 import FileSystemLoader, Environment, exceptions | ||
3001 | 58 | |||
3002 | 59 | if template_loader: | ||
3003 | 60 | template_env = Environment(loader=template_loader) | ||
3004 | 61 | else: | ||
3005 | 62 | if templates_dir is None: | ||
3006 | 63 | templates_dir = os.path.join(hookenv.charm_dir(), 'templates') | ||
3007 | 64 | template_env = Environment(loader=FileSystemLoader(templates_dir)) | ||
3008 | 65 | try: | ||
3009 | 66 | source = source | ||
3010 | 67 | template = template_env.get_template(source) | ||
3011 | 68 | except exceptions.TemplateNotFound as e: | ||
3012 | 69 | hookenv.log('Could not load template %s from %s.' % | ||
3013 | 70 | (source, templates_dir), | ||
3014 | 71 | level=hookenv.ERROR) | ||
3015 | 72 | raise e | ||
3016 | 73 | content = template.render(context) | ||
3017 | 74 | if target is not None: | ||
3018 | 75 | target_dir = os.path.dirname(target) | ||
3019 | 76 | if not os.path.exists(target_dir): | ||
3020 | 77 | # This is a terrible default directory permission, as the file | ||
3021 | 78 | # or its siblings will often contain secrets. | ||
3022 | 79 | host.mkdir(os.path.dirname(target), owner, group, perms=0o755) | ||
3023 | 80 | host.write_file(target, content.encode(encoding), owner, group, perms) | ||
3024 | 81 | return content | ||
3025 | 0 | 82 | ||
3026 | === added file 'hooks/charmhelpers/core/unitdata.py' | |||
3027 | --- hooks/charmhelpers/core/unitdata.py 1970-01-01 00:00:00 +0000 | |||
3028 | +++ hooks/charmhelpers/core/unitdata.py 2015-12-17 05:44:32 +0000 | |||
3029 | @@ -0,0 +1,521 @@ | |||
3030 | 1 | #!/usr/bin/env python | ||
3031 | 2 | # -*- coding: utf-8 -*- | ||
3032 | 3 | # | ||
3033 | 4 | # Copyright 2014-2015 Canonical Limited. | ||
3034 | 5 | # | ||
3035 | 6 | # This file is part of charm-helpers. | ||
3036 | 7 | # | ||
3037 | 8 | # charm-helpers is free software: you can redistribute it and/or modify | ||
3038 | 9 | # it under the terms of the GNU Lesser General Public License version 3 as | ||
3039 | 10 | # published by the Free Software Foundation. | ||
3040 | 11 | # | ||
3041 | 12 | # charm-helpers is distributed in the hope that it will be useful, | ||
3042 | 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
3043 | 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
3044 | 15 | # GNU Lesser General Public License for more details. | ||
3045 | 16 | # | ||
3046 | 17 | # You should have received a copy of the GNU Lesser General Public License | ||
3047 | 18 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
3048 | 19 | # | ||
3049 | 20 | # | ||
3050 | 21 | # Authors: | ||
3051 | 22 | # Kapil Thangavelu <kapil.foss@gmail.com> | ||
3052 | 23 | # | ||
3053 | 24 | """ | ||
3054 | 25 | Intro | ||
3055 | 26 | ----- | ||
3056 | 27 | |||
3057 | 28 | A simple way to store state in units. This provides a key value | ||
3058 | 29 | storage with support for versioned, transactional operation, | ||
3059 | 30 | and can calculate deltas from previous values to simplify unit logic | ||
3060 | 31 | when processing changes. | ||
3061 | 32 | |||
3062 | 33 | |||
3063 | 34 | Hook Integration | ||
3064 | 35 | ---------------- | ||
3065 | 36 | |||
3066 | 37 | There are several extant frameworks for hook execution, including | ||
3067 | 38 | |||
3068 | 39 | - charmhelpers.core.hookenv.Hooks | ||
3069 | 40 | - charmhelpers.core.services.ServiceManager | ||
3070 | 41 | |||
3071 | 42 | The storage classes are framework agnostic, one simple integration is | ||
3072 | 43 | via the HookData contextmanager. It will record the current hook | ||
3073 | 44 | execution environment (including relation data, config data, etc.), | ||
3074 | 45 | setup a transaction and allow easy access to the changes from | ||
3075 | 46 | previously seen values. One consequence of the integration is the | ||
3076 | 47 | reservation of particular keys ('rels', 'unit', 'env', 'config', | ||
3077 | 48 | 'charm_revisions') for their respective values. | ||
3078 | 49 | |||
3079 | 50 | Here's a fully worked integration example using hookenv.Hooks:: | ||
3080 | 51 | |||
3081 | 52 | from charmhelper.core import hookenv, unitdata | ||
3082 | 53 | |||
3083 | 54 | hook_data = unitdata.HookData() | ||
3084 | 55 | db = unitdata.kv() | ||
3085 | 56 | hooks = hookenv.Hooks() | ||
3086 | 57 | |||
3087 | 58 | @hooks.hook | ||
3088 | 59 | def config_changed(): | ||
3089 | 60 | # Print all changes to configuration from previously seen | ||
3090 | 61 | # values. | ||
3091 | 62 | for changed, (prev, cur) in hook_data.conf.items(): | ||
3092 | 63 | print('config changed', changed, | ||
3093 | 64 | 'previous value', prev, | ||
3094 | 65 | 'current value', cur) | ||
3095 | 66 | |||
3096 | 67 | # Get some unit specific bookeeping | ||
3097 | 68 | if not db.get('pkg_key'): | ||
3098 | 69 | key = urllib.urlopen('https://example.com/pkg_key').read() | ||
3099 | 70 | db.set('pkg_key', key) | ||
3100 | 71 | |||
3101 | 72 | # Directly access all charm config as a mapping. | ||
3102 | 73 | conf = db.getrange('config', True) | ||
3103 | 74 | |||
3104 | 75 | # Directly access all relation data as a mapping | ||
3105 | 76 | rels = db.getrange('rels', True) | ||
3106 | 77 | |||
3107 | 78 | if __name__ == '__main__': | ||
3108 | 79 | with hook_data(): | ||
3109 | 80 | hook.execute() | ||
3110 | 81 | |||
3111 | 82 | |||
3112 | 83 | A more basic integration is via the hook_scope context manager which simply | ||
3113 | 84 | manages transaction scope (and records hook name, and timestamp):: | ||
3114 | 85 | |||
3115 | 86 | >>> from unitdata import kv | ||
3116 | 87 | >>> db = kv() | ||
3117 | 88 | >>> with db.hook_scope('install'): | ||
3118 | 89 | ... # do work, in transactional scope. | ||
3119 | 90 | ... db.set('x', 1) | ||
3120 | 91 | >>> db.get('x') | ||
3121 | 92 | 1 | ||
3122 | 93 | |||
3123 | 94 | |||
3124 | 95 | Usage | ||
3125 | 96 | ----- | ||
3126 | 97 | |||
3127 | 98 | Values are automatically json de/serialized to preserve basic typing | ||
3128 | 99 | and complex data struct capabilities (dicts, lists, ints, booleans, etc). | ||
3129 | 100 | |||
3130 | 101 | Individual values can be manipulated via get/set:: | ||
3131 | 102 | |||
3132 | 103 | >>> kv.set('y', True) | ||
3133 | 104 | >>> kv.get('y') | ||
3134 | 105 | True | ||
3135 | 106 | |||
3136 | 107 | # We can set complex values (dicts, lists) as a single key. | ||
3137 | 108 | >>> kv.set('config', {'a': 1, 'b': True'}) | ||
3138 | 109 | |||
3139 | 110 | # Also supports returning dictionaries as a record which | ||
3140 | 111 | # provides attribute access. | ||
3141 | 112 | >>> config = kv.get('config', record=True) | ||
3142 | 113 | >>> config.b | ||
3143 | 114 | True | ||
3144 | 115 | |||
3145 | 116 | |||
3146 | 117 | Groups of keys can be manipulated with update/getrange:: | ||
3147 | 118 | |||
3148 | 119 | >>> kv.update({'z': 1, 'y': 2}, prefix="gui.") | ||
3149 | 120 | >>> kv.getrange('gui.', strip=True) | ||
3150 | 121 | {'z': 1, 'y': 2} | ||
3151 | 122 | |||
3152 | 123 | When updating values, its very helpful to understand which values | ||
3153 | 124 | have actually changed and how have they changed. The storage | ||
3154 | 125 | provides a delta method to provide for this:: | ||
3155 | 126 | |||
3156 | 127 | >>> data = {'debug': True, 'option': 2} | ||
3157 | 128 | >>> delta = kv.delta(data, 'config.') | ||
3158 | 129 | >>> delta.debug.previous | ||
3159 | 130 | None | ||
3160 | 131 | >>> delta.debug.current | ||
3161 | 132 | True | ||
3162 | 133 | >>> delta | ||
3163 | 134 | {'debug': (None, True), 'option': (None, 2)} | ||
3164 | 135 | |||
3165 | 136 | Note the delta method does not persist the actual change, it needs to | ||
3166 | 137 | be explicitly saved via 'update' method:: | ||
3167 | 138 | |||
3168 | 139 | >>> kv.update(data, 'config.') | ||
3169 | 140 | |||
3170 | 141 | Values modified in the context of a hook scope retain historical values | ||
3171 | 142 | associated to the hookname. | ||
3172 | 143 | |||
3173 | 144 | >>> with db.hook_scope('config-changed'): | ||
3174 | 145 | ... db.set('x', 42) | ||
3175 | 146 | >>> db.gethistory('x') | ||
3176 | 147 | [(1, u'x', 1, u'install', u'2015-01-21T16:49:30.038372'), | ||
3177 | 148 | (2, u'x', 42, u'config-changed', u'2015-01-21T16:49:30.038786')] | ||
3178 | 149 | |||
3179 | 150 | """ | ||
3180 | 151 | |||
3181 | 152 | import collections | ||
3182 | 153 | import contextlib | ||
3183 | 154 | import datetime | ||
3184 | 155 | import itertools | ||
3185 | 156 | import json | ||
3186 | 157 | import os | ||
3187 | 158 | import pprint | ||
3188 | 159 | import sqlite3 | ||
3189 | 160 | import sys | ||
3190 | 161 | |||
3191 | 162 | __author__ = 'Kapil Thangavelu <kapil.foss@gmail.com>' | ||
3192 | 163 | |||
3193 | 164 | |||
3194 | 165 | class Storage(object): | ||
3195 | 166 | """Simple key value database for local unit state within charms. | ||
3196 | 167 | |||
3197 | 168 | Modifications are not persisted unless :meth:`flush` is called. | ||
3198 | 169 | |||
3199 | 170 | To support dicts, lists, integer, floats, and booleans values | ||
3200 | 171 | are automatically json encoded/decoded. | ||
3201 | 172 | """ | ||
3202 | 173 | def __init__(self, path=None): | ||
3203 | 174 | self.db_path = path | ||
3204 | 175 | if path is None: | ||
3205 | 176 | if 'UNIT_STATE_DB' in os.environ: | ||
3206 | 177 | self.db_path = os.environ['UNIT_STATE_DB'] | ||
3207 | 178 | else: | ||
3208 | 179 | self.db_path = os.path.join( | ||
3209 | 180 | os.environ.get('CHARM_DIR', ''), '.unit-state.db') | ||
3210 | 181 | self.conn = sqlite3.connect('%s' % self.db_path) | ||
3211 | 182 | self.cursor = self.conn.cursor() | ||
3212 | 183 | self.revision = None | ||
3213 | 184 | self._closed = False | ||
3214 | 185 | self._init() | ||
3215 | 186 | |||
3216 | 187 | def close(self): | ||
3217 | 188 | if self._closed: | ||
3218 | 189 | return | ||
3219 | 190 | self.flush(False) | ||
3220 | 191 | self.cursor.close() | ||
3221 | 192 | self.conn.close() | ||
3222 | 193 | self._closed = True | ||
3223 | 194 | |||
3224 | 195 | def get(self, key, default=None, record=False): | ||
3225 | 196 | self.cursor.execute('select data from kv where key=?', [key]) | ||
3226 | 197 | result = self.cursor.fetchone() | ||
3227 | 198 | if not result: | ||
3228 | 199 | return default | ||
3229 | 200 | if record: | ||
3230 | 201 | return Record(json.loads(result[0])) | ||
3231 | 202 | return json.loads(result[0]) | ||
3232 | 203 | |||
3233 | 204 | def getrange(self, key_prefix, strip=False): | ||
3234 | 205 | """ | ||
3235 | 206 | Get a range of keys starting with a common prefix as a mapping of | ||
3236 | 207 | keys to values. | ||
3237 | 208 | |||
3238 | 209 | :param str key_prefix: Common prefix among all keys | ||
3239 | 210 | :param bool strip: Optionally strip the common prefix from the key | ||
3240 | 211 | names in the returned dict | ||
3241 | 212 | :return dict: A (possibly empty) dict of key-value mappings | ||
3242 | 213 | """ | ||
3243 | 214 | self.cursor.execute("select key, data from kv where key like ?", | ||
3244 | 215 | ['%s%%' % key_prefix]) | ||
3245 | 216 | result = self.cursor.fetchall() | ||
3246 | 217 | |||
3247 | 218 | if not result: | ||
3248 | 219 | return {} | ||
3249 | 220 | if not strip: | ||
3250 | 221 | key_prefix = '' | ||
3251 | 222 | return dict([ | ||
3252 | 223 | (k[len(key_prefix):], json.loads(v)) for k, v in result]) | ||
3253 | 224 | |||
3254 | 225 | def update(self, mapping, prefix=""): | ||
3255 | 226 | """ | ||
3256 | 227 | Set the values of multiple keys at once. | ||
3257 | 228 | |||
3258 | 229 | :param dict mapping: Mapping of keys to values | ||
3259 | 230 | :param str prefix: Optional prefix to apply to all keys in `mapping` | ||
3260 | 231 | before setting | ||
3261 | 232 | """ | ||
3262 | 233 | for k, v in mapping.items(): | ||
3263 | 234 | self.set("%s%s" % (prefix, k), v) | ||
3264 | 235 | |||
3265 | 236 | def unset(self, key): | ||
3266 | 237 | """ | ||
3267 | 238 | Remove a key from the database entirely. | ||
3268 | 239 | """ | ||
3269 | 240 | self.cursor.execute('delete from kv where key=?', [key]) | ||
3270 | 241 | if self.revision and self.cursor.rowcount: | ||
3271 | 242 | self.cursor.execute( | ||
3272 | 243 | 'insert into kv_revisions values (?, ?, ?)', | ||
3273 | 244 | [key, self.revision, json.dumps('DELETED')]) | ||
3274 | 245 | |||
3275 | 246 | def unsetrange(self, keys=None, prefix=""): | ||
3276 | 247 | """ | ||
3277 | 248 | Remove a range of keys starting with a common prefix, from the database | ||
3278 | 249 | entirely. | ||
3279 | 250 | |||
3280 | 251 | :param list keys: List of keys to remove. | ||
3281 | 252 | :param str prefix: Optional prefix to apply to all keys in ``keys`` | ||
3282 | 253 | before removing. | ||
3283 | 254 | """ | ||
3284 | 255 | if keys is not None: | ||
3285 | 256 | keys = ['%s%s' % (prefix, key) for key in keys] | ||
3286 | 257 | self.cursor.execute('delete from kv where key in (%s)' % ','.join(['?'] * len(keys)), keys) | ||
3287 | 258 | if self.revision and self.cursor.rowcount: | ||
3288 | 259 | self.cursor.execute( | ||
3289 | 260 | 'insert into kv_revisions values %s' % ','.join(['(?, ?, ?)'] * len(keys)), | ||
3290 | 261 | list(itertools.chain.from_iterable((key, self.revision, json.dumps('DELETED')) for key in keys))) | ||
3291 | 262 | else: | ||
3292 | 263 | self.cursor.execute('delete from kv where key like ?', | ||
3293 | 264 | ['%s%%' % prefix]) | ||
3294 | 265 | if self.revision and self.cursor.rowcount: | ||
3295 | 266 | self.cursor.execute( | ||
3296 | 267 | 'insert into kv_revisions values (?, ?, ?)', | ||
3297 | 268 | ['%s%%' % prefix, self.revision, json.dumps('DELETED')]) | ||
3298 | 269 | |||
3299 | 270 | def set(self, key, value): | ||
3300 | 271 | """ | ||
3301 | 272 | Set a value in the database. | ||
3302 | 273 | |||
3303 | 274 | :param str key: Key to set the value for | ||
3304 | 275 | :param value: Any JSON-serializable value to be set | ||
3305 | 276 | """ | ||
3306 | 277 | serialized = json.dumps(value) | ||
3307 | 278 | |||
3308 | 279 | self.cursor.execute('select data from kv where key=?', [key]) | ||
3309 | 280 | exists = self.cursor.fetchone() | ||
3310 | 281 | |||
3311 | 282 | # Skip mutations to the same value | ||
3312 | 283 | if exists: | ||
3313 | 284 | if exists[0] == serialized: | ||
3314 | 285 | return value | ||
3315 | 286 | |||
3316 | 287 | if not exists: | ||
3317 | 288 | self.cursor.execute( | ||
3318 | 289 | 'insert into kv (key, data) values (?, ?)', | ||
3319 | 290 | (key, serialized)) | ||
3320 | 291 | else: | ||
3321 | 292 | self.cursor.execute(''' | ||
3322 | 293 | update kv | ||
3323 | 294 | set data = ? | ||
3324 | 295 | where key = ?''', [serialized, key]) | ||
3325 | 296 | |||
3326 | 297 | # Save | ||
3327 | 298 | if not self.revision: | ||
3328 | 299 | return value | ||
3329 | 300 | |||
3330 | 301 | self.cursor.execute( | ||
3331 | 302 | 'select 1 from kv_revisions where key=? and revision=?', | ||
3332 | 303 | [key, self.revision]) | ||
3333 | 304 | exists = self.cursor.fetchone() | ||
3334 | 305 | |||
3335 | 306 | if not exists: | ||
3336 | 307 | self.cursor.execute( | ||
3337 | 308 | '''insert into kv_revisions ( | ||
3338 | 309 | revision, key, data) values (?, ?, ?)''', | ||
3339 | 310 | (self.revision, key, serialized)) | ||
3340 | 311 | else: | ||
3341 | 312 | self.cursor.execute( | ||
3342 | 313 | ''' | ||
3343 | 314 | update kv_revisions | ||
3344 | 315 | set data = ? | ||
3345 | 316 | where key = ? | ||
3346 | 317 | and revision = ?''', | ||
3347 | 318 | [serialized, key, self.revision]) | ||
3348 | 319 | |||
3349 | 320 | return value | ||
3350 | 321 | |||
3351 | 322 | def delta(self, mapping, prefix): | ||
3352 | 323 | """ | ||
3353 | 324 | return a delta containing values that have changed. | ||
3354 | 325 | """ | ||
3355 | 326 | previous = self.getrange(prefix, strip=True) | ||
3356 | 327 | if not previous: | ||
3357 | 328 | pk = set() | ||
3358 | 329 | else: | ||
3359 | 330 | pk = set(previous.keys()) | ||
3360 | 331 | ck = set(mapping.keys()) | ||
3361 | 332 | delta = DeltaSet() | ||
3362 | 333 | |||
3363 | 334 | # added | ||
3364 | 335 | for k in ck.difference(pk): | ||
3365 | 336 | delta[k] = Delta(None, mapping[k]) | ||
3366 | 337 | |||
3367 | 338 | # removed | ||
3368 | 339 | for k in pk.difference(ck): | ||
3369 | 340 | delta[k] = Delta(previous[k], None) | ||
3370 | 341 | |||
3371 | 342 | # changed | ||
3372 | 343 | for k in pk.intersection(ck): | ||
3373 | 344 | c = mapping[k] | ||
3374 | 345 | p = previous[k] | ||
3375 | 346 | if c != p: | ||
3376 | 347 | delta[k] = Delta(p, c) | ||
3377 | 348 | |||
3378 | 349 | return delta | ||
3379 | 350 | |||
3380 | 351 | @contextlib.contextmanager | ||
3381 | 352 | def hook_scope(self, name=""): | ||
3382 | 353 | """Scope all future interactions to the current hook execution | ||
3383 | 354 | revision.""" | ||
3384 | 355 | assert not self.revision | ||
3385 | 356 | self.cursor.execute( | ||
3386 | 357 | 'insert into hooks (hook, date) values (?, ?)', | ||
3387 | 358 | (name or sys.argv[0], | ||
3388 | 359 | datetime.datetime.utcnow().isoformat())) | ||
3389 | 360 | self.revision = self.cursor.lastrowid | ||
3390 | 361 | try: | ||
3391 | 362 | yield self.revision | ||
3392 | 363 | self.revision = None | ||
3393 | 364 | except: | ||
3394 | 365 | self.flush(False) | ||
3395 | 366 | self.revision = None | ||
3396 | 367 | raise | ||
3397 | 368 | else: | ||
3398 | 369 | self.flush() | ||
3399 | 370 | |||
3400 | 371 | def flush(self, save=True): | ||
3401 | 372 | if save: | ||
3402 | 373 | self.conn.commit() | ||
3403 | 374 | elif self._closed: | ||
3404 | 375 | return | ||
3405 | 376 | else: | ||
3406 | 377 | self.conn.rollback() | ||
3407 | 378 | |||
3408 | 379 | def _init(self): | ||
3409 | 380 | self.cursor.execute(''' | ||
3410 | 381 | create table if not exists kv ( | ||
3411 | 382 | key text, | ||
3412 | 383 | data text, | ||
3413 | 384 | primary key (key) | ||
3414 | 385 | )''') | ||
3415 | 386 | self.cursor.execute(''' | ||
3416 | 387 | create table if not exists kv_revisions ( | ||
3417 | 388 | key text, | ||
3418 | 389 | revision integer, | ||
3419 | 390 | data text, | ||
3420 | 391 | primary key (key, revision) | ||
3421 | 392 | )''') | ||
3422 | 393 | self.cursor.execute(''' | ||
3423 | 394 | create table if not exists hooks ( | ||
3424 | 395 | version integer primary key autoincrement, | ||
3425 | 396 | hook text, | ||
3426 | 397 | date text | ||
3427 | 398 | )''') | ||
3428 | 399 | self.conn.commit() | ||
3429 | 400 | |||
3430 | 401 | def gethistory(self, key, deserialize=False): | ||
3431 | 402 | self.cursor.execute( | ||
3432 | 403 | ''' | ||
3433 | 404 | select kv.revision, kv.key, kv.data, h.hook, h.date | ||
3434 | 405 | from kv_revisions kv, | ||
3435 | 406 | hooks h | ||
3436 | 407 | where kv.key=? | ||
3437 | 408 | and kv.revision = h.version | ||
3438 | 409 | ''', [key]) | ||
3439 | 410 | if deserialize is False: | ||
3440 | 411 | return self.cursor.fetchall() | ||
3441 | 412 | return map(_parse_history, self.cursor.fetchall()) | ||
3442 | 413 | |||
3443 | 414 | def debug(self, fh=sys.stderr): | ||
3444 | 415 | self.cursor.execute('select * from kv') | ||
3445 | 416 | pprint.pprint(self.cursor.fetchall(), stream=fh) | ||
3446 | 417 | self.cursor.execute('select * from kv_revisions') | ||
3447 | 418 | pprint.pprint(self.cursor.fetchall(), stream=fh) | ||
3448 | 419 | |||
3449 | 420 | |||
3450 | 421 | def _parse_history(d): | ||
3451 | 422 | return (d[0], d[1], json.loads(d[2]), d[3], | ||
3452 | 423 | datetime.datetime.strptime(d[-1], "%Y-%m-%dT%H:%M:%S.%f")) | ||
3453 | 424 | |||
3454 | 425 | |||
3455 | 426 | class HookData(object): | ||
3456 | 427 | """Simple integration for existing hook exec frameworks. | ||
3457 | 428 | |||
3458 | 429 | Records all unit information, and stores deltas for processing | ||
3459 | 430 | by the hook. | ||
3460 | 431 | |||
3461 | 432 | Sample:: | ||
3462 | 433 | |||
3463 | 434 | from charmhelper.core import hookenv, unitdata | ||
3464 | 435 | |||
3465 | 436 | changes = unitdata.HookData() | ||
3466 | 437 | db = unitdata.kv() | ||
3467 | 438 | hooks = hookenv.Hooks() | ||
3468 | 439 | |||
3469 | 440 | @hooks.hook | ||
3470 | 441 | def config_changed(): | ||
3471 | 442 | # View all changes to configuration | ||
3472 | 443 | for changed, (prev, cur) in changes.conf.items(): | ||
3473 | 444 | print('config changed', changed, | ||
3474 | 445 | 'previous value', prev, | ||
3475 | 446 | 'current value', cur) | ||
3476 | 447 | |||
3477 | 448 | # Get some unit specific bookeeping | ||
3478 | 449 | if not db.get('pkg_key'): | ||
3479 | 450 | key = urllib.urlopen('https://example.com/pkg_key').read() | ||
3480 | 451 | db.set('pkg_key', key) | ||
3481 | 452 | |||
3482 | 453 | if __name__ == '__main__': | ||
3483 | 454 | with changes(): | ||
3484 | 455 | hook.execute() | ||
3485 | 456 | |||
3486 | 457 | """ | ||
3487 | 458 | def __init__(self): | ||
3488 | 459 | self.kv = kv() | ||
3489 | 460 | self.conf = None | ||
3490 | 461 | self.rels = None | ||
3491 | 462 | |||
3492 | 463 | @contextlib.contextmanager | ||
3493 | 464 | def __call__(self): | ||
3494 | 465 | from charmhelpers.core import hookenv | ||
3495 | 466 | hook_name = hookenv.hook_name() | ||
3496 | 467 | |||
3497 | 468 | with self.kv.hook_scope(hook_name): | ||
3498 | 469 | self._record_charm_version(hookenv.charm_dir()) | ||
3499 | 470 | delta_config, delta_relation = self._record_hook(hookenv) | ||
3500 | 471 | yield self.kv, delta_config, delta_relation | ||
3501 | 472 | |||
3502 | 473 | def _record_charm_version(self, charm_dir): | ||
3503 | 474 | # Record revisions.. charm revisions are meaningless | ||
3504 | 475 | # to charm authors as they don't control the revision. | ||
3505 | 476 | # so logic dependnent on revision is not particularly | ||
3506 | 477 | # useful, however it is useful for debugging analysis. | ||
3507 | 478 | charm_rev = open( | ||
3508 | 479 | os.path.join(charm_dir, 'revision')).read().strip() | ||
3509 | 480 | charm_rev = charm_rev or '0' | ||
3510 | 481 | revs = self.kv.get('charm_revisions', []) | ||
3511 | 482 | if charm_rev not in revs: | ||
3512 | 483 | revs.append(charm_rev.strip() or '0') | ||
3513 | 484 | self.kv.set('charm_revisions', revs) | ||
3514 | 485 | |||
3515 | 486 | def _record_hook(self, hookenv): | ||
3516 | 487 | data = hookenv.execution_environment() | ||
3517 | 488 | self.conf = conf_delta = self.kv.delta(data['conf'], 'config') | ||
3518 | 489 | self.rels = rels_delta = self.kv.delta(data['rels'], 'rels') | ||
3519 | 490 | self.kv.set('env', dict(data['env'])) | ||
3520 | 491 | self.kv.set('unit', data['unit']) | ||
3521 | 492 | self.kv.set('relid', data.get('relid')) | ||
3522 | 493 | return conf_delta, rels_delta | ||
3523 | 494 | |||
3524 | 495 | |||
3525 | 496 | class Record(dict): | ||
3526 | 497 | |||
3527 | 498 | __slots__ = () | ||
3528 | 499 | |||
3529 | 500 | def __getattr__(self, k): | ||
3530 | 501 | if k in self: | ||
3531 | 502 | return self[k] | ||
3532 | 503 | raise AttributeError(k) | ||
3533 | 504 | |||
3534 | 505 | |||
3535 | 506 | class DeltaSet(Record): | ||
3536 | 507 | |||
3537 | 508 | __slots__ = () | ||
3538 | 509 | |||
3539 | 510 | |||
3540 | 511 | Delta = collections.namedtuple('Delta', ['previous', 'current']) | ||
3541 | 512 | |||
3542 | 513 | |||
3543 | 514 | _KV = None | ||
3544 | 515 | |||
3545 | 516 | |||
3546 | 517 | def kv(): | ||
3547 | 518 | global _KV | ||
3548 | 519 | if _KV is None: | ||
3549 | 520 | _KV = Storage() | ||
3550 | 521 | return _KV | ||
3551 | 0 | 522 | ||
3552 | === modified file 'hooks/hooks.py' | |||
3553 | --- hooks/hooks.py 2015-12-06 22:48:27 +0000 | |||
3554 | +++ hooks/hooks.py 2015-12-17 05:44:32 +0000 | |||
3555 | @@ -15,6 +15,9 @@ | |||
3556 | 15 | import glob | 15 | import glob |
3557 | 16 | import utils | 16 | import utils |
3558 | 17 | 17 | ||
3559 | 18 | from charmhelpers.core import hookenv | ||
3560 | 19 | from charmhelpers.core import unitdata | ||
3561 | 20 | |||
3562 | 18 | ############################################################################### | 21 | ############################################################################### |
3563 | 19 | # Global variables | 22 | # Global variables |
3564 | 20 | ############################################################################### | 23 | ############################################################################### |
3565 | @@ -22,9 +25,8 @@ | |||
3566 | 22 | default_squid3_config = "%s/squid.conf" % default_squid3_config_dir | 25 | default_squid3_config = "%s/squid.conf" % default_squid3_config_dir |
3567 | 23 | default_squid3_config_cache_dir = "/var/run/squid3" | 26 | default_squid3_config_cache_dir = "/var/run/squid3" |
3568 | 24 | hook_name = os.path.basename(sys.argv[0]) | 27 | hook_name = os.path.basename(sys.argv[0]) |
3572 | 25 | HOOK_START = False | 28 | db = unitdata.kv() |
3573 | 26 | HOOK_AUTH_HELPER_JOINED = False | 29 | db_changes = unitdata.HookData() |
3571 | 27 | STATE_DELAYED_START = False | ||
3574 | 28 | ############################################################################### | 30 | ############################################################################### |
3575 | 29 | # Supporting functions | 31 | # Supporting functions |
3576 | 30 | ############################################################################### | 32 | ############################################################################### |
3577 | @@ -321,24 +323,26 @@ | |||
3578 | 321 | retVal = subprocess.call( | 323 | retVal = subprocess.call( |
3579 | 322 | ['/usr/sbin/squid3', '-f', squid3_config, '-k', 'parse']) | 324 | ['/usr/sbin/squid3', '-f', squid3_config, '-k', 'parse']) |
3580 | 323 | if retVal == 1: | 325 | if retVal == 1: |
3582 | 324 | return(False) | 326 | utils.juju_log('CRITICAL', 'Invalid squid configuration.') |
3583 | 327 | return False | ||
3584 | 325 | elif retVal == 0: | 328 | elif retVal == 0: |
3586 | 326 | return(True) | 329 | return True |
3587 | 327 | else: | 330 | else: |
3589 | 328 | return(False) | 331 | utils.juju_log('CRITICAL', 'Invalid squid configuration.') |
3590 | 332 | return False | ||
3591 | 329 | elif action == 'status': | 333 | elif action == 'status': |
3592 | 330 | status = subprocess.check_output(['status', 'squid3']) | 334 | status = subprocess.check_output(['status', 'squid3']) |
3593 | 331 | if re.search('running', status) is not None: | 335 | if re.search('running', status) is not None: |
3595 | 332 | return(True) | 336 | return True |
3596 | 333 | else: | 337 | else: |
3598 | 334 | return(False) | 338 | return False |
3599 | 335 | elif action in ('start', 'stop', 'reload', 'restart'): | 339 | elif action in ('start', 'stop', 'reload', 'restart'): |
3600 | 336 | utils.juju_log('INFO', 'Requesting %s of squid3 service.' % action) | 340 | utils.juju_log('INFO', 'Requesting %s of squid3 service.' % action) |
3601 | 337 | retVal = subprocess.call([action, 'squid3']) | 341 | retVal = subprocess.call([action, 'squid3']) |
3602 | 338 | if retVal == 0: | 342 | if retVal == 0: |
3604 | 339 | return(True) | 343 | return True |
3605 | 340 | else: | 344 | else: |
3607 | 341 | return(False) | 345 | return False |
3608 | 342 | 346 | ||
3609 | 343 | 347 | ||
3610 | 344 | def update_nrpe_checks(): | 348 | def update_nrpe_checks(): |
3611 | @@ -400,6 +404,22 @@ | |||
3612 | 400 | return (utils.install_unattended('squid3', 'python-jinja2')) | 404 | return (utils.install_unattended('squid3', 'python-jinja2')) |
3613 | 401 | 405 | ||
3614 | 402 | 406 | ||
3615 | 407 | def service_can_start(): | ||
3616 | 408 | # If wait_for_auth_helper is set, wait until squid has started and | ||
3617 | 409 | # the squid-auth-helper relation has been joined. | ||
3618 | 410 | config_data = config_get() | ||
3619 | 411 | if not config_data['wait_for_auth_helper']: | ||
3620 | 412 | return True | ||
3621 | 413 | if hookenv.is_relation_made('squid-auth-helper') and db.get('hook_start'): | ||
3622 | 414 | utils.juju_log('INFO', | ||
3623 | 415 | 'Squid auth helper available, squid may start...') | ||
3624 | 416 | return True | ||
3625 | 417 | else: | ||
3626 | 418 | utils.juju_log('INFO', 'Squid not ready, waiting for auth helper...') | ||
3627 | 419 | service_squid3('stop') | ||
3628 | 420 | return False | ||
3629 | 421 | |||
3630 | 422 | |||
3631 | 403 | def config_changed(): | 423 | def config_changed(): |
3632 | 404 | current_service_ports = get_service_ports() | 424 | current_service_ports = get_service_ports() |
3633 | 405 | construct_squid3_config() | 425 | construct_squid3_config() |
3634 | @@ -408,11 +428,7 @@ | |||
3635 | 408 | updated_service_ports = get_service_ports() | 428 | updated_service_ports = get_service_ports() |
3636 | 409 | update_service_ports(current_service_ports, updated_service_ports) | 429 | update_service_ports(current_service_ports, updated_service_ports) |
3637 | 410 | 430 | ||
3643 | 411 | config_data = config_get() | 431 | if not service_can_start(): |
3639 | 412 | if config_data['wait_for_auth_helper'] and not STATE_DELAYED_START: | ||
3640 | 413 | # unable to parse squid3 configuration without auth helper in | ||
3641 | 414 | # place. | ||
3642 | 415 | utils.juju_log('INFO', 'Squid not started, waiting for auth helper...') | ||
3644 | 416 | return | 432 | return |
3645 | 417 | 433 | ||
3646 | 418 | if service_squid3('check'): | 434 | if service_squid3('check'): |
3647 | @@ -424,36 +440,26 @@ | |||
3648 | 424 | sys.exit(1) | 440 | sys.exit(1) |
3649 | 425 | 441 | ||
3650 | 426 | 442 | ||
3655 | 427 | def start_hook(start=None, auth_helper=None): | 443 | def start_hook(start=None): |
3652 | 428 | global HOOK_START | ||
3653 | 429 | global HOOK_AUTH_HELPER_JOINED | ||
3654 | 430 | |||
3656 | 431 | if start: | 444 | if start: |
3677 | 432 | HOOK_START = True | 445 | db.set('hook_start', True) |
3678 | 433 | if auth_helper: | 446 | |
3679 | 434 | HOOK_AUTH_HELPER_JOINED = True | 447 | if service_can_start(): |
3680 | 435 | 448 | if not service_squid3('check'): | |
3681 | 436 | config_data = config_get() | 449 | sys.exit(1) |
3682 | 437 | if config_data['wait_for_auth_helper']: | 450 | else: |
3683 | 438 | if HOOK_AUTH_HELPER_JOINED and HOOK_START: | 451 | return |
3684 | 439 | utils.juju_log('INFO', 'Squid auth helper available, starting...') | 452 | |
3685 | 440 | if service_squid3('check'): | 453 | if service_squid3('status'): |
3686 | 441 | STATE_DELAYED_START = True | 454 | return(service_squid3('restart')) |
3687 | 442 | else: | 455 | else: |
3688 | 443 | sys.exit(1) | 456 | return(service_squid3('start')) |
3669 | 444 | else: | ||
3670 | 445 | utils.juju_log('INFO', 'Waiting for auth helper...') | ||
3671 | 446 | return | ||
3672 | 447 | |||
3673 | 448 | if service_squid3("status"): | ||
3674 | 449 | return(service_squid3("restart")) | ||
3675 | 450 | else: | ||
3676 | 451 | return(service_squid3("start")) | ||
3689 | 452 | 457 | ||
3690 | 453 | 458 | ||
3691 | 454 | def stop_hook(): | 459 | def stop_hook(): |
3694 | 455 | if service_squid3("status"): | 460 | db.unset('hook_start') |
3695 | 456 | return(service_squid3("stop")) | 461 | if service_squid3('status'): |
3696 | 462 | return(service_squid3('stop')) | ||
3697 | 457 | 463 | ||
3698 | 458 | 464 | ||
3699 | 459 | def proxy_interface(hook_name=None): | 465 | def proxy_interface(hook_name=None): |
3700 | @@ -478,7 +484,7 @@ | |||
3701 | 478 | elif hook_name == "stop": | 484 | elif hook_name == "stop": |
3702 | 479 | stop_hook() | 485 | stop_hook() |
3703 | 480 | elif hook_name == "squid-auth-helper-relation-joined": | 486 | elif hook_name == "squid-auth-helper-relation-joined": |
3705 | 481 | start_hook(auth_helper=True) | 487 | start_hook() |
3706 | 482 | elif hook_name == "cached-website-relation-joined": | 488 | elif hook_name == "cached-website-relation-joined": |
3707 | 483 | proxy_interface("joined") | 489 | proxy_interface("joined") |
3708 | 484 | elif hook_name == "cached-website-relation-changed": | 490 | elif hook_name == "cached-website-relation-changed": |
3709 | @@ -498,4 +504,5 @@ | |||
3710 | 498 | sys.exit(1) | 504 | sys.exit(1) |
3711 | 499 | 505 | ||
3712 | 500 | if __name__ == '__main__': | 506 | if __name__ == '__main__': |
3714 | 501 | main() | 507 | with db_changes(): |
3715 | 508 | main() | ||
3716 | 502 | 509 | ||
3717 | === added directory 'scripts' | |||
3718 | === added file 'scripts/charm_helpers_sync.py' | |||
3719 | --- scripts/charm_helpers_sync.py 1970-01-01 00:00:00 +0000 | |||
3720 | +++ scripts/charm_helpers_sync.py 2015-12-17 05:44:32 +0000 | |||
3721 | @@ -0,0 +1,253 @@ | |||
3722 | 1 | #!/usr/bin/python | ||
3723 | 2 | |||
3724 | 3 | # Copyright 2014-2015 Canonical Limited. | ||
3725 | 4 | # | ||
3726 | 5 | # This file is part of charm-helpers. | ||
3727 | 6 | # | ||
3728 | 7 | # charm-helpers is free software: you can redistribute it and/or modify | ||
3729 | 8 | # it under the terms of the GNU Lesser General Public License version 3 as | ||
3730 | 9 | # published by the Free Software Foundation. | ||
3731 | 10 | # | ||
3732 | 11 | # charm-helpers is distributed in the hope that it will be useful, | ||
3733 | 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
3734 | 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
3735 | 14 | # GNU Lesser General Public License for more details. | ||
3736 | 15 | # | ||
3737 | 16 | # You should have received a copy of the GNU Lesser General Public License | ||
3738 | 17 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
3739 | 18 | |||
3740 | 19 | # Authors: | ||
3741 | 20 | # Adam Gandelman <adamg@ubuntu.com> | ||
3742 | 21 | |||
3743 | 22 | import logging | ||
3744 | 23 | import optparse | ||
3745 | 24 | import os | ||
3746 | 25 | import subprocess | ||
3747 | 26 | import shutil | ||
3748 | 27 | import sys | ||
3749 | 28 | import tempfile | ||
3750 | 29 | import yaml | ||
3751 | 30 | from fnmatch import fnmatch | ||
3752 | 31 | |||
3753 | 32 | import six | ||
3754 | 33 | |||
3755 | 34 | CHARM_HELPERS_BRANCH = 'lp:charm-helpers' | ||
3756 | 35 | |||
3757 | 36 | |||
3758 | 37 | def parse_config(conf_file): | ||
3759 | 38 | if not os.path.isfile(conf_file): | ||
3760 | 39 | logging.error('Invalid config file: %s.' % conf_file) | ||
3761 | 40 | return False | ||
3762 | 41 | return yaml.load(open(conf_file).read()) | ||
3763 | 42 | |||
3764 | 43 | |||
3765 | 44 | def clone_helpers(work_dir, branch): | ||
3766 | 45 | dest = os.path.join(work_dir, 'charm-helpers') | ||
3767 | 46 | logging.info('Checking out %s to %s.' % (branch, dest)) | ||
3768 | 47 | cmd = ['bzr', 'checkout', '--lightweight', branch, dest] | ||
3769 | 48 | subprocess.check_call(cmd) | ||
3770 | 49 | return dest | ||
3771 | 50 | |||
3772 | 51 | |||
3773 | 52 | def _module_path(module): | ||
3774 | 53 | return os.path.join(*module.split('.')) | ||
3775 | 54 | |||
3776 | 55 | |||
3777 | 56 | def _src_path(src, module): | ||
3778 | 57 | return os.path.join(src, 'charmhelpers', _module_path(module)) | ||
3779 | 58 | |||
3780 | 59 | |||
3781 | 60 | def _dest_path(dest, module): | ||
3782 | 61 | return os.path.join(dest, _module_path(module)) | ||
3783 | 62 | |||
3784 | 63 | |||
3785 | 64 | def _is_pyfile(path): | ||
3786 | 65 | return os.path.isfile(path + '.py') | ||
3787 | 66 | |||
3788 | 67 | |||
3789 | 68 | def ensure_init(path): | ||
3790 | 69 | ''' | ||
3791 | 70 | ensure directories leading up to path are importable, omitting | ||
3792 | 71 | parent directory, eg path='/hooks/helpers/foo'/: | ||
3793 | 72 | hooks/ | ||
3794 | 73 | hooks/helpers/__init__.py | ||
3795 | 74 | hooks/helpers/foo/__init__.py | ||
3796 | 75 | ''' | ||
3797 | 76 | for d, dirs, files in os.walk(os.path.join(*path.split('/')[:2])): | ||
3798 | 77 | _i = os.path.join(d, '__init__.py') | ||
3799 | 78 | if not os.path.exists(_i): | ||
3800 | 79 | logging.info('Adding missing __init__.py: %s' % _i) | ||
3801 | 80 | open(_i, 'wb').close() | ||
3802 | 81 | |||
3803 | 82 | |||
3804 | 83 | def sync_pyfile(src, dest): | ||
3805 | 84 | src = src + '.py' | ||
3806 | 85 | src_dir = os.path.dirname(src) | ||
3807 | 86 | logging.info('Syncing pyfile: %s -> %s.' % (src, dest)) | ||
3808 | 87 | if not os.path.exists(dest): | ||
3809 | 88 | os.makedirs(dest) | ||
3810 | 89 | shutil.copy(src, dest) | ||
3811 | 90 | if os.path.isfile(os.path.join(src_dir, '__init__.py')): | ||
3812 | 91 | shutil.copy(os.path.join(src_dir, '__init__.py'), | ||
3813 | 92 | dest) | ||
3814 | 93 | ensure_init(dest) | ||
3815 | 94 | |||
3816 | 95 | |||
3817 | 96 | def get_filter(opts=None): | ||
3818 | 97 | opts = opts or [] | ||
3819 | 98 | if 'inc=*' in opts: | ||
3820 | 99 | # do not filter any files, include everything | ||
3821 | 100 | return None | ||
3822 | 101 | |||
3823 | 102 | def _filter(dir, ls): | ||
3824 | 103 | incs = [opt.split('=').pop() for opt in opts if 'inc=' in opt] | ||
3825 | 104 | _filter = [] | ||
3826 | 105 | for f in ls: | ||
3827 | 106 | _f = os.path.join(dir, f) | ||
3828 | 107 | |||
3829 | 108 | if not os.path.isdir(_f) and not _f.endswith('.py') and incs: | ||
3830 | 109 | if True not in [fnmatch(_f, inc) for inc in incs]: | ||
3831 | 110 | logging.debug('Not syncing %s, does not match include ' | ||
3832 | 111 | 'filters (%s)' % (_f, incs)) | ||
3833 | 112 | _filter.append(f) | ||
3834 | 113 | else: | ||
3835 | 114 | logging.debug('Including file, which matches include ' | ||
3836 | 115 | 'filters (%s): %s' % (incs, _f)) | ||
3837 | 116 | elif (os.path.isfile(_f) and not _f.endswith('.py')): | ||
3838 | 117 | logging.debug('Not syncing file: %s' % f) | ||
3839 | 118 | _filter.append(f) | ||
3840 | 119 | elif (os.path.isdir(_f) and not | ||
3841 | 120 | os.path.isfile(os.path.join(_f, '__init__.py'))): | ||
3842 | 121 | logging.debug('Not syncing directory: %s' % f) | ||
3843 | 122 | _filter.append(f) | ||
3844 | 123 | return _filter | ||
3845 | 124 | return _filter | ||
3846 | 125 | |||
3847 | 126 | |||
3848 | 127 | def sync_directory(src, dest, opts=None): | ||
3849 | 128 | if os.path.exists(dest): | ||
3850 | 129 | logging.debug('Removing existing directory: %s' % dest) | ||
3851 | 130 | shutil.rmtree(dest) | ||
3852 | 131 | logging.info('Syncing directory: %s -> %s.' % (src, dest)) | ||
3853 | 132 | |||
3854 | 133 | shutil.copytree(src, dest, ignore=get_filter(opts)) | ||
3855 | 134 | ensure_init(dest) | ||
3856 | 135 | |||
3857 | 136 | |||
3858 | 137 | def sync(src, dest, module, opts=None): | ||
3859 | 138 | |||
3860 | 139 | # Sync charmhelpers/__init__.py for bootstrap code. | ||
3861 | 140 | sync_pyfile(_src_path(src, '__init__'), dest) | ||
3862 | 141 | |||
3863 | 142 | # Sync other __init__.py files in the path leading to module. | ||
3864 | 143 | m = [] | ||
3865 | 144 | steps = module.split('.')[:-1] | ||
3866 | 145 | while steps: | ||
3867 | 146 | m.append(steps.pop(0)) | ||
3868 | 147 | init = '.'.join(m + ['__init__']) | ||
3869 | 148 | sync_pyfile(_src_path(src, init), | ||
3870 | 149 | os.path.dirname(_dest_path(dest, init))) | ||
3871 | 150 | |||
3872 | 151 | # Sync the module, or maybe a .py file. | ||
3873 | 152 | if os.path.isdir(_src_path(src, module)): | ||
3874 | 153 | sync_directory(_src_path(src, module), _dest_path(dest, module), opts) | ||
3875 | 154 | elif _is_pyfile(_src_path(src, module)): | ||
3876 | 155 | sync_pyfile(_src_path(src, module), | ||
3877 | 156 | os.path.dirname(_dest_path(dest, module))) | ||
3878 | 157 | else: | ||
3879 | 158 | logging.warn('Could not sync: %s. Neither a pyfile or directory, ' | ||
3880 | 159 | 'does it even exist?' % module) | ||
3881 | 160 | |||
3882 | 161 | |||
3883 | 162 | def parse_sync_options(options): | ||
3884 | 163 | if not options: | ||
3885 | 164 | return [] | ||
3886 | 165 | return options.split(',') | ||
3887 | 166 | |||
3888 | 167 | |||
3889 | 168 | def extract_options(inc, global_options=None): | ||
3890 | 169 | global_options = global_options or [] | ||
3891 | 170 | if global_options and isinstance(global_options, six.string_types): | ||
3892 | 171 | global_options = [global_options] | ||
3893 | 172 | if '|' not in inc: | ||
3894 | 173 | return (inc, global_options) | ||
3895 | 174 | inc, opts = inc.split('|') | ||
3896 | 175 | return (inc, parse_sync_options(opts) + global_options) | ||
3897 | 176 | |||
3898 | 177 | |||
3899 | 178 | def sync_helpers(include, src, dest, options=None): | ||
3900 | 179 | if not os.path.isdir(dest): | ||
3901 | 180 | os.makedirs(dest) | ||
3902 | 181 | |||
3903 | 182 | global_options = parse_sync_options(options) | ||
3904 | 183 | |||
3905 | 184 | for inc in include: | ||
3906 | 185 | if isinstance(inc, str): | ||
3907 | 186 | inc, opts = extract_options(inc, global_options) | ||
3908 | 187 | sync(src, dest, inc, opts) | ||
3909 | 188 | elif isinstance(inc, dict): | ||
3910 | 189 | # could also do nested dicts here. | ||
3911 | 190 | for k, v in six.iteritems(inc): | ||
3912 | 191 | if isinstance(v, list): | ||
3913 | 192 | for m in v: | ||
3914 | 193 | inc, opts = extract_options(m, global_options) | ||
3915 | 194 | sync(src, dest, '%s.%s' % (k, inc), opts) | ||
3916 | 195 | |||
3917 | 196 | if __name__ == '__main__': | ||
3918 | 197 | parser = optparse.OptionParser() | ||
3919 | 198 | parser.add_option('-c', '--config', action='store', dest='config', | ||
3920 | 199 | default=None, help='helper config file') | ||
3921 | 200 | parser.add_option('-D', '--debug', action='store_true', dest='debug', | ||
3922 | 201 | default=False, help='debug') | ||
3923 | 202 | parser.add_option('-b', '--branch', action='store', dest='branch', | ||
3924 | 203 | help='charm-helpers bzr branch (overrides config)') | ||
3925 | 204 | parser.add_option('-d', '--destination', action='store', dest='dest_dir', | ||
3926 | 205 | help='sync destination dir (overrides config)') | ||
3927 | 206 | (opts, args) = parser.parse_args() | ||
3928 | 207 | |||
3929 | 208 | if opts.debug: | ||
3930 | 209 | logging.basicConfig(level=logging.DEBUG) | ||
3931 | 210 | else: | ||
3932 | 211 | logging.basicConfig(level=logging.INFO) | ||
3933 | 212 | |||
3934 | 213 | if opts.config: | ||
3935 | 214 | logging.info('Loading charm helper config from %s.' % opts.config) | ||
3936 | 215 | config = parse_config(opts.config) | ||
3937 | 216 | if not config: | ||
3938 | 217 | logging.error('Could not parse config from %s.' % opts.config) | ||
3939 | 218 | sys.exit(1) | ||
3940 | 219 | else: | ||
3941 | 220 | config = {} | ||
3942 | 221 | |||
3943 | 222 | if 'branch' not in config: | ||
3944 | 223 | config['branch'] = CHARM_HELPERS_BRANCH | ||
3945 | 224 | if opts.branch: | ||
3946 | 225 | config['branch'] = opts.branch | ||
3947 | 226 | if opts.dest_dir: | ||
3948 | 227 | config['destination'] = opts.dest_dir | ||
3949 | 228 | |||
3950 | 229 | if 'destination' not in config: | ||
3951 | 230 | logging.error('No destination dir. specified as option or config.') | ||
3952 | 231 | sys.exit(1) | ||
3953 | 232 | |||
3954 | 233 | if 'include' not in config: | ||
3955 | 234 | if not args: | ||
3956 | 235 | logging.error('No modules to sync specified as option or config.') | ||
3957 | 236 | sys.exit(1) | ||
3958 | 237 | config['include'] = [] | ||
3959 | 238 | [config['include'].append(a) for a in args] | ||
3960 | 239 | |||
3961 | 240 | sync_options = None | ||
3962 | 241 | if 'options' in config: | ||
3963 | 242 | sync_options = config['options'] | ||
3964 | 243 | tmpd = tempfile.mkdtemp() | ||
3965 | 244 | try: | ||
3966 | 245 | checkout = clone_helpers(tmpd, config['branch']) | ||
3967 | 246 | sync_helpers(config['include'], checkout, config['destination'], | ||
3968 | 247 | options=sync_options) | ||
3969 | 248 | except Exception as e: | ||
3970 | 249 | logging.error("Could not sync: %s" % e) | ||
3971 | 250 | raise e | ||
3972 | 251 | finally: | ||
3973 | 252 | logging.debug('Cleaning up %s' % tmpd) | ||
3974 | 253 | shutil.rmtree(tmpd) |