Merge lp:~1chb1n/charms/trusty/mongodb/sync-fetch-helpers-liberty into lp:charms/trusty/mongodb
- Trusty Tahr (14.04)
- sync-fetch-helpers-liberty
- Merge into trunk
Proposed by
Ryan Beisner
Status: | Merged |
---|---|
Merged at revision: | 76 |
Proposed branch: | lp:~1chb1n/charms/trusty/mongodb/sync-fetch-helpers-liberty |
Merge into: | lp:charms/trusty/mongodb |
Diff against target: |
1620 lines (+890/-143) 14 files modified
hooks/charmhelpers/contrib/charmsupport/nrpe.py (+3/-1) hooks/charmhelpers/contrib/hahelpers/cluster.py (+47/-3) hooks/charmhelpers/contrib/python/packages.py (+30/-5) hooks/charmhelpers/core/files.py (+45/-0) hooks/charmhelpers/core/hookenv.py (+371/-43) hooks/charmhelpers/core/host.py (+148/-24) hooks/charmhelpers/core/hugepage.py (+62/-0) hooks/charmhelpers/core/services/base.py (+43/-19) hooks/charmhelpers/core/services/helpers.py (+30/-6) hooks/charmhelpers/core/strutils.py (+2/-2) hooks/charmhelpers/core/unitdata.py (+62/-18) hooks/charmhelpers/fetch/__init__.py (+32/-15) hooks/charmhelpers/fetch/archiveurl.py (+7/-1) hooks/charmhelpers/fetch/giturl.py (+8/-6) |
To merge this branch: | bzr merge lp:~1chb1n/charms/trusty/mongodb/sync-fetch-helpers-liberty |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Liam Young (community) | Approve | ||
Review via email: mp+268413@code.launchpad.net |
Commit message
Description of the change
Sync hooks/charmhelpers for liberty cloud archive enablement.
Resolves:
2015-08-26 13:00:47 INFO install charmhelpers.
To post a comment you must log in.
Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote : | # |
Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote : | # |
charm_unit_test #7733 mongodb for 1chb1n mp268413
UNIT OK: passed
Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote : | # |
charm_amulet_test #5878 mongodb for 1chb1n mp268413
AMULET OK: passed
Build: http://
- 77. By Ryan Beisner
-
resync all hooks/charmhelpers
Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote : | # |
charm_lint_check #8364 mongodb for 1chb1n mp268413
LINT OK: passed
Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote : | # |
charm_unit_test #7762 mongodb for 1chb1n mp268413
UNIT OK: passed
Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote : | # |
charm_amulet_test #5908 mongodb for 1chb1n mp268413
AMULET OK: passed
Build: http://
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'hooks/charmhelpers/contrib/charmsupport/nrpe.py' |
2 | --- hooks/charmhelpers/contrib/charmsupport/nrpe.py 2015-02-25 00:24:56 +0000 |
3 | +++ hooks/charmhelpers/contrib/charmsupport/nrpe.py 2015-08-19 13:58:57 +0000 |
4 | @@ -247,7 +247,9 @@ |
5 | |
6 | service('restart', 'nagios-nrpe-server') |
7 | |
8 | - for rid in relation_ids("local-monitors"): |
9 | + monitor_ids = relation_ids("local-monitors") + \ |
10 | + relation_ids("nrpe-external-master") |
11 | + for rid in monitor_ids: |
12 | relation_set(relation_id=rid, monitors=yaml.dump(monitors)) |
13 | |
14 | |
15 | |
16 | === modified file 'hooks/charmhelpers/contrib/hahelpers/cluster.py' |
17 | --- hooks/charmhelpers/contrib/hahelpers/cluster.py 2015-02-25 00:24:56 +0000 |
18 | +++ hooks/charmhelpers/contrib/hahelpers/cluster.py 2015-08-19 13:58:57 +0000 |
19 | @@ -44,6 +44,7 @@ |
20 | ERROR, |
21 | WARNING, |
22 | unit_get, |
23 | + is_leader as juju_is_leader |
24 | ) |
25 | from charmhelpers.core.decorators import ( |
26 | retry_on_exception, |
27 | @@ -52,6 +53,8 @@ |
28 | bool_from_string, |
29 | ) |
30 | |
31 | +DC_RESOURCE_NAME = 'DC' |
32 | + |
33 | |
34 | class HAIncompleteConfig(Exception): |
35 | pass |
36 | @@ -61,17 +64,30 @@ |
37 | pass |
38 | |
39 | |
40 | +class CRMDCNotFound(Exception): |
41 | + pass |
42 | + |
43 | + |
44 | def is_elected_leader(resource): |
45 | """ |
46 | Returns True if the charm executing this is the elected cluster leader. |
47 | |
48 | It relies on two mechanisms to determine leadership: |
49 | - 1. If the charm is part of a corosync cluster, call corosync to |
50 | + 1. If juju is sufficiently new and leadership election is supported, |
51 | + the is_leader command will be used. |
52 | + 2. If the charm is part of a corosync cluster, call corosync to |
53 | determine leadership. |
54 | - 2. If the charm is not part of a corosync cluster, the leader is |
55 | + 3. If the charm is not part of a corosync cluster, the leader is |
56 | determined as being "the alive unit with the lowest unit numer". In |
57 | other words, the oldest surviving unit. |
58 | """ |
59 | + try: |
60 | + return juju_is_leader() |
61 | + except NotImplementedError: |
62 | + log('Juju leadership election feature not enabled' |
63 | + ', using fallback support', |
64 | + level=WARNING) |
65 | + |
66 | if is_clustered(): |
67 | if not is_crm_leader(resource): |
68 | log('Deferring action to CRM leader.', level=INFO) |
69 | @@ -95,7 +111,33 @@ |
70 | return False |
71 | |
72 | |
73 | -@retry_on_exception(5, base_delay=2, exc_type=CRMResourceNotFound) |
74 | +def is_crm_dc(): |
75 | + """ |
76 | + Determine leadership by querying the pacemaker Designated Controller |
77 | + """ |
78 | + cmd = ['crm', 'status'] |
79 | + try: |
80 | + status = subprocess.check_output(cmd, stderr=subprocess.STDOUT) |
81 | + if not isinstance(status, six.text_type): |
82 | + status = six.text_type(status, "utf-8") |
83 | + except subprocess.CalledProcessError as ex: |
84 | + raise CRMDCNotFound(str(ex)) |
85 | + |
86 | + current_dc = '' |
87 | + for line in status.split('\n'): |
88 | + if line.startswith('Current DC'): |
89 | + # Current DC: juju-lytrusty-machine-2 (168108163) - partition with quorum |
90 | + current_dc = line.split(':')[1].split()[0] |
91 | + if current_dc == get_unit_hostname(): |
92 | + return True |
93 | + elif current_dc == 'NONE': |
94 | + raise CRMDCNotFound('Current DC: NONE') |
95 | + |
96 | + return False |
97 | + |
98 | + |
99 | +@retry_on_exception(5, base_delay=2, |
100 | + exc_type=(CRMResourceNotFound, CRMDCNotFound)) |
101 | def is_crm_leader(resource, retry=False): |
102 | """ |
103 | Returns True if the charm calling this is the elected corosync leader, |
104 | @@ -104,6 +146,8 @@ |
105 | We allow this operation to be retried to avoid the possibility of getting a |
106 | false negative. See LP #1396246 for more info. |
107 | """ |
108 | + if resource == DC_RESOURCE_NAME: |
109 | + return is_crm_dc() |
110 | cmd = ['crm', 'resource', 'show', resource] |
111 | try: |
112 | status = subprocess.check_output(cmd, stderr=subprocess.STDOUT) |
113 | |
114 | === modified file 'hooks/charmhelpers/contrib/python/packages.py' |
115 | --- hooks/charmhelpers/contrib/python/packages.py 2015-02-25 00:24:56 +0000 |
116 | +++ hooks/charmhelpers/contrib/python/packages.py 2015-08-19 13:58:57 +0000 |
117 | @@ -17,8 +17,11 @@ |
118 | # You should have received a copy of the GNU Lesser General Public License |
119 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. |
120 | |
121 | +import os |
122 | +import subprocess |
123 | + |
124 | from charmhelpers.fetch import apt_install, apt_update |
125 | -from charmhelpers.core.hookenv import log |
126 | +from charmhelpers.core.hookenv import charm_dir, log |
127 | |
128 | try: |
129 | from pip import main as pip_execute |
130 | @@ -33,6 +36,8 @@ |
131 | def parse_options(given, available): |
132 | """Given a set of options, check if available""" |
133 | for key, value in sorted(given.items()): |
134 | + if not value: |
135 | + continue |
136 | if key in available: |
137 | yield "--{0}={1}".format(key, value) |
138 | |
139 | @@ -51,11 +56,15 @@ |
140 | pip_execute(command) |
141 | |
142 | |
143 | -def pip_install(package, fatal=False, upgrade=False, **options): |
144 | +def pip_install(package, fatal=False, upgrade=False, venv=None, **options): |
145 | """Install a python package""" |
146 | - command = ["install"] |
147 | + if venv: |
148 | + venv_python = os.path.join(venv, 'bin/pip') |
149 | + command = [venv_python, "install"] |
150 | + else: |
151 | + command = ["install"] |
152 | |
153 | - available_options = ('proxy', 'src', 'log', "index-url", ) |
154 | + available_options = ('proxy', 'src', 'log', 'index-url', ) |
155 | for option in parse_options(options, available_options): |
156 | command.append(option) |
157 | |
158 | @@ -69,7 +78,10 @@ |
159 | |
160 | log("Installing {} package with options: {}".format(package, |
161 | command)) |
162 | - pip_execute(command) |
163 | + if venv: |
164 | + subprocess.check_call(command) |
165 | + else: |
166 | + pip_execute(command) |
167 | |
168 | |
169 | def pip_uninstall(package, **options): |
170 | @@ -94,3 +106,16 @@ |
171 | """Returns the list of current python installed packages |
172 | """ |
173 | return pip_execute(["list"]) |
174 | + |
175 | + |
176 | +def pip_create_virtualenv(path=None): |
177 | + """Create an isolated Python environment.""" |
178 | + apt_install('python-virtualenv') |
179 | + |
180 | + if path: |
181 | + venv_path = path |
182 | + else: |
183 | + venv_path = os.path.join(charm_dir(), 'venv') |
184 | + |
185 | + if not os.path.exists(venv_path): |
186 | + subprocess.check_call(['virtualenv', venv_path]) |
187 | |
188 | === added file 'hooks/charmhelpers/core/files.py' |
189 | --- hooks/charmhelpers/core/files.py 1970-01-01 00:00:00 +0000 |
190 | +++ hooks/charmhelpers/core/files.py 2015-08-19 13:58:57 +0000 |
191 | @@ -0,0 +1,45 @@ |
192 | +#!/usr/bin/env python |
193 | +# -*- coding: utf-8 -*- |
194 | + |
195 | +# Copyright 2014-2015 Canonical Limited. |
196 | +# |
197 | +# This file is part of charm-helpers. |
198 | +# |
199 | +# charm-helpers is free software: you can redistribute it and/or modify |
200 | +# it under the terms of the GNU Lesser General Public License version 3 as |
201 | +# published by the Free Software Foundation. |
202 | +# |
203 | +# charm-helpers is distributed in the hope that it will be useful, |
204 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
205 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
206 | +# GNU Lesser General Public License for more details. |
207 | +# |
208 | +# You should have received a copy of the GNU Lesser General Public License |
209 | +# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. |
210 | + |
211 | +__author__ = 'Jorge Niedbalski <niedbalski@ubuntu.com>' |
212 | + |
213 | +import os |
214 | +import subprocess |
215 | + |
216 | + |
217 | +def sed(filename, before, after, flags='g'): |
218 | + """ |
219 | + Search and replaces the given pattern on filename. |
220 | + |
221 | + :param filename: relative or absolute file path. |
222 | + :param before: expression to be replaced (see 'man sed') |
223 | + :param after: expression to replace with (see 'man sed') |
224 | + :param flags: sed-compatible regex flags in example, to make |
225 | + the search and replace case insensitive, specify ``flags="i"``. |
226 | + The ``g`` flag is always specified regardless, so you do not |
227 | + need to remember to include it when overriding this parameter. |
228 | + :returns: If the sed command exit code was zero then return, |
229 | + otherwise raise CalledProcessError. |
230 | + """ |
231 | + expression = r's/{0}/{1}/{2}'.format(before, |
232 | + after, flags) |
233 | + |
234 | + return subprocess.check_call(["sed", "-i", "-r", "-e", |
235 | + expression, |
236 | + os.path.expanduser(filename)]) |
237 | |
238 | === modified file 'hooks/charmhelpers/core/hookenv.py' |
239 | --- hooks/charmhelpers/core/hookenv.py 2015-02-25 00:24:56 +0000 |
240 | +++ hooks/charmhelpers/core/hookenv.py 2015-08-19 13:58:57 +0000 |
241 | @@ -20,11 +20,18 @@ |
242 | # Authors: |
243 | # Charm Helpers Developers <juju@lists.ubuntu.com> |
244 | |
245 | +from __future__ import print_function |
246 | +import copy |
247 | +from distutils.version import LooseVersion |
248 | +from functools import wraps |
249 | +import glob |
250 | import os |
251 | import json |
252 | import yaml |
253 | import subprocess |
254 | import sys |
255 | +import errno |
256 | +import tempfile |
257 | from subprocess import CalledProcessError |
258 | |
259 | import six |
260 | @@ -56,15 +63,18 @@ |
261 | |
262 | will cache the result of unit_get + 'test' for future calls. |
263 | """ |
264 | + @wraps(func) |
265 | def wrapper(*args, **kwargs): |
266 | global cache |
267 | key = str((func, args, kwargs)) |
268 | try: |
269 | return cache[key] |
270 | except KeyError: |
271 | - res = func(*args, **kwargs) |
272 | - cache[key] = res |
273 | - return res |
274 | + pass # Drop out of the exception handler scope. |
275 | + res = func(*args, **kwargs) |
276 | + cache[key] = res |
277 | + return res |
278 | + wrapper._wrapped = func |
279 | return wrapper |
280 | |
281 | |
282 | @@ -87,7 +97,18 @@ |
283 | if not isinstance(message, six.string_types): |
284 | message = repr(message) |
285 | command += [message] |
286 | - subprocess.call(command) |
287 | + # Missing juju-log should not cause failures in unit tests |
288 | + # Send log output to stderr |
289 | + try: |
290 | + subprocess.call(command) |
291 | + except OSError as e: |
292 | + if e.errno == errno.ENOENT: |
293 | + if level: |
294 | + message = "{}: {}".format(level, message) |
295 | + message = "juju-log: {}".format(message) |
296 | + print(message, file=sys.stderr) |
297 | + else: |
298 | + raise |
299 | |
300 | |
301 | class Serializable(UserDict): |
302 | @@ -153,9 +174,19 @@ |
303 | return os.environ.get('JUJU_RELATION', None) |
304 | |
305 | |
306 | -def relation_id(): |
307 | - """The relation ID for the current relation hook""" |
308 | - return os.environ.get('JUJU_RELATION_ID', None) |
309 | +@cached |
310 | +def relation_id(relation_name=None, service_or_unit=None): |
311 | + """The relation ID for the current or a specified relation""" |
312 | + if not relation_name and not service_or_unit: |
313 | + return os.environ.get('JUJU_RELATION_ID', None) |
314 | + elif relation_name and service_or_unit: |
315 | + service_name = service_or_unit.split('/')[0] |
316 | + for relid in relation_ids(relation_name): |
317 | + remote_service = remote_service_name(relid) |
318 | + if remote_service == service_name: |
319 | + return relid |
320 | + else: |
321 | + raise ValueError('Must specify neither or both of relation_name and service_or_unit') |
322 | |
323 | |
324 | def local_unit(): |
325 | @@ -165,7 +196,7 @@ |
326 | |
327 | def remote_unit(): |
328 | """The remote unit for the current relation hook""" |
329 | - return os.environ['JUJU_REMOTE_UNIT'] |
330 | + return os.environ.get('JUJU_REMOTE_UNIT', None) |
331 | |
332 | |
333 | def service_name(): |
334 | @@ -173,9 +204,20 @@ |
335 | return local_unit().split('/')[0] |
336 | |
337 | |
338 | +@cached |
339 | +def remote_service_name(relid=None): |
340 | + """The remote service name for a given relation-id (or the current relation)""" |
341 | + if relid is None: |
342 | + unit = remote_unit() |
343 | + else: |
344 | + units = related_units(relid) |
345 | + unit = units[0] if units else None |
346 | + return unit.split('/')[0] if unit else None |
347 | + |
348 | + |
349 | def hook_name(): |
350 | """The name of the currently executing hook""" |
351 | - return os.path.basename(sys.argv[0]) |
352 | + return os.environ.get('JUJU_HOOK_NAME', os.path.basename(sys.argv[0])) |
353 | |
354 | |
355 | class Config(dict): |
356 | @@ -225,23 +267,7 @@ |
357 | self.path = os.path.join(charm_dir(), Config.CONFIG_FILE_NAME) |
358 | if os.path.exists(self.path): |
359 | self.load_previous() |
360 | - |
361 | - def __getitem__(self, key): |
362 | - """For regular dict lookups, check the current juju config first, |
363 | - then the previous (saved) copy. This ensures that user-saved values |
364 | - will be returned by a dict lookup. |
365 | - |
366 | - """ |
367 | - try: |
368 | - return dict.__getitem__(self, key) |
369 | - except KeyError: |
370 | - return (self._prev_dict or {})[key] |
371 | - |
372 | - def keys(self): |
373 | - prev_keys = [] |
374 | - if self._prev_dict is not None: |
375 | - prev_keys = self._prev_dict.keys() |
376 | - return list(set(prev_keys + list(dict.keys(self)))) |
377 | + atexit(self._implicit_save) |
378 | |
379 | def load_previous(self, path=None): |
380 | """Load previous copy of config from disk. |
381 | @@ -260,6 +286,9 @@ |
382 | self.path = path or self.path |
383 | with open(self.path) as f: |
384 | self._prev_dict = json.load(f) |
385 | + for k, v in copy.deepcopy(self._prev_dict).items(): |
386 | + if k not in self: |
387 | + self[k] = v |
388 | |
389 | def changed(self, key): |
390 | """Return True if the current value for this key is different from |
391 | @@ -291,13 +320,13 @@ |
392 | instance. |
393 | |
394 | """ |
395 | - if self._prev_dict: |
396 | - for k, v in six.iteritems(self._prev_dict): |
397 | - if k not in self: |
398 | - self[k] = v |
399 | with open(self.path, 'w') as f: |
400 | json.dump(self, f) |
401 | |
402 | + def _implicit_save(self): |
403 | + if self.implicit_save: |
404 | + self.save() |
405 | + |
406 | |
407 | @cached |
408 | def config(scope=None): |
409 | @@ -340,18 +369,49 @@ |
410 | """Set relation information for the current unit""" |
411 | relation_settings = relation_settings if relation_settings else {} |
412 | relation_cmd_line = ['relation-set'] |
413 | + accepts_file = "--file" in subprocess.check_output( |
414 | + relation_cmd_line + ["--help"], universal_newlines=True) |
415 | if relation_id is not None: |
416 | relation_cmd_line.extend(('-r', relation_id)) |
417 | - for k, v in (list(relation_settings.items()) + list(kwargs.items())): |
418 | - if v is None: |
419 | - relation_cmd_line.append('{}='.format(k)) |
420 | - else: |
421 | - relation_cmd_line.append('{}={}'.format(k, v)) |
422 | - subprocess.check_call(relation_cmd_line) |
423 | + settings = relation_settings.copy() |
424 | + settings.update(kwargs) |
425 | + for key, value in settings.items(): |
426 | + # Force value to be a string: it always should, but some call |
427 | + # sites pass in things like dicts or numbers. |
428 | + if value is not None: |
429 | + settings[key] = "{}".format(value) |
430 | + if accepts_file: |
431 | + # --file was introduced in Juju 1.23.2. Use it by default if |
432 | + # available, since otherwise we'll break if the relation data is |
433 | + # too big. Ideally we should tell relation-set to read the data from |
434 | + # stdin, but that feature is broken in 1.23.2: Bug #1454678. |
435 | + with tempfile.NamedTemporaryFile(delete=False) as settings_file: |
436 | + settings_file.write(yaml.safe_dump(settings).encode("utf-8")) |
437 | + subprocess.check_call( |
438 | + relation_cmd_line + ["--file", settings_file.name]) |
439 | + os.remove(settings_file.name) |
440 | + else: |
441 | + for key, value in settings.items(): |
442 | + if value is None: |
443 | + relation_cmd_line.append('{}='.format(key)) |
444 | + else: |
445 | + relation_cmd_line.append('{}={}'.format(key, value)) |
446 | + subprocess.check_call(relation_cmd_line) |
447 | # Flush cache of any relation-gets for local unit |
448 | flush(local_unit()) |
449 | |
450 | |
451 | +def relation_clear(r_id=None): |
452 | + ''' Clears any relation data already set on relation r_id ''' |
453 | + settings = relation_get(rid=r_id, |
454 | + unit=local_unit()) |
455 | + for setting in settings: |
456 | + if setting not in ['public-address', 'private-address']: |
457 | + settings[setting] = None |
458 | + relation_set(relation_id=r_id, |
459 | + **settings) |
460 | + |
461 | + |
462 | @cached |
463 | def relation_ids(reltype=None): |
464 | """A list of relation_ids""" |
465 | @@ -431,6 +491,63 @@ |
466 | |
467 | |
468 | @cached |
469 | +def relation_to_interface(relation_name): |
470 | + """ |
471 | + Given the name of a relation, return the interface that relation uses. |
472 | + |
473 | + :returns: The interface name, or ``None``. |
474 | + """ |
475 | + return relation_to_role_and_interface(relation_name)[1] |
476 | + |
477 | + |
478 | +@cached |
479 | +def relation_to_role_and_interface(relation_name): |
480 | + """ |
481 | + Given the name of a relation, return the role and the name of the interface |
482 | + that relation uses (where role is one of ``provides``, ``requires``, or ``peer``). |
483 | + |
484 | + :returns: A tuple containing ``(role, interface)``, or ``(None, None)``. |
485 | + """ |
486 | + _metadata = metadata() |
487 | + for role in ('provides', 'requires', 'peer'): |
488 | + interface = _metadata.get(role, {}).get(relation_name, {}).get('interface') |
489 | + if interface: |
490 | + return role, interface |
491 | + return None, None |
492 | + |
493 | + |
494 | +@cached |
495 | +def role_and_interface_to_relations(role, interface_name): |
496 | + """ |
497 | + Given a role and interface name, return a list of relation names for the |
498 | + current charm that use that interface under that role (where role is one |
499 | + of ``provides``, ``requires``, or ``peer``). |
500 | + |
501 | + :returns: A list of relation names. |
502 | + """ |
503 | + _metadata = metadata() |
504 | + results = [] |
505 | + for relation_name, relation in _metadata.get(role, {}).items(): |
506 | + if relation['interface'] == interface_name: |
507 | + results.append(relation_name) |
508 | + return results |
509 | + |
510 | + |
511 | +@cached |
512 | +def interface_to_relations(interface_name): |
513 | + """ |
514 | + Given an interface, return a list of relation names for the current |
515 | + charm that use that interface. |
516 | + |
517 | + :returns: A list of relation names. |
518 | + """ |
519 | + results = [] |
520 | + for role in ('provides', 'requires', 'peer'): |
521 | + results.extend(role_and_interface_to_relations(role, interface_name)) |
522 | + return results |
523 | + |
524 | + |
525 | +@cached |
526 | def charm_name(): |
527 | """Get the name of the current charm as is specified on metadata.yaml""" |
528 | return metadata().get('name') |
529 | @@ -496,6 +613,11 @@ |
530 | return None |
531 | |
532 | |
533 | +def unit_public_ip(): |
534 | + """Get this unit's public IP address""" |
535 | + return unit_get('public-address') |
536 | + |
537 | + |
538 | def unit_private_ip(): |
539 | """Get this unit's private IP address""" |
540 | return unit_get('private-address') |
541 | @@ -528,10 +650,14 @@ |
542 | hooks.execute(sys.argv) |
543 | """ |
544 | |
545 | - def __init__(self, config_save=True): |
546 | + def __init__(self, config_save=None): |
547 | super(Hooks, self).__init__() |
548 | self._hooks = {} |
549 | - self._config_save = config_save |
550 | + |
551 | + # For unknown reasons, we allow the Hooks constructor to override |
552 | + # config().implicit_save. |
553 | + if config_save is not None: |
554 | + config().implicit_save = config_save |
555 | |
556 | def register(self, name, function): |
557 | """Register a hook""" |
558 | @@ -539,13 +665,16 @@ |
559 | |
560 | def execute(self, args): |
561 | """Execute a registered hook based on args[0]""" |
562 | + _run_atstart() |
563 | hook_name = os.path.basename(args[0]) |
564 | if hook_name in self._hooks: |
565 | - self._hooks[hook_name]() |
566 | - if self._config_save: |
567 | - cfg = config() |
568 | - if cfg.implicit_save: |
569 | - cfg.save() |
570 | + try: |
571 | + self._hooks[hook_name]() |
572 | + except SystemExit as x: |
573 | + if x.code is None or x.code == 0: |
574 | + _run_atexit() |
575 | + raise |
576 | + _run_atexit() |
577 | else: |
578 | raise UnregisteredHookError(hook_name) |
579 | |
580 | @@ -566,3 +695,202 @@ |
581 | def charm_dir(): |
582 | """Return the root directory of the current charm""" |
583 | return os.environ.get('CHARM_DIR') |
584 | + |
585 | + |
586 | +@cached |
587 | +def action_get(key=None): |
588 | + """Gets the value of an action parameter, or all key/value param pairs""" |
589 | + cmd = ['action-get'] |
590 | + if key is not None: |
591 | + cmd.append(key) |
592 | + cmd.append('--format=json') |
593 | + action_data = json.loads(subprocess.check_output(cmd).decode('UTF-8')) |
594 | + return action_data |
595 | + |
596 | + |
597 | +def action_set(values): |
598 | + """Sets the values to be returned after the action finishes""" |
599 | + cmd = ['action-set'] |
600 | + for k, v in list(values.items()): |
601 | + cmd.append('{}={}'.format(k, v)) |
602 | + subprocess.check_call(cmd) |
603 | + |
604 | + |
605 | +def action_fail(message): |
606 | + """Sets the action status to failed and sets the error message. |
607 | + |
608 | + The results set by action_set are preserved.""" |
609 | + subprocess.check_call(['action-fail', message]) |
610 | + |
611 | + |
612 | +def action_name(): |
613 | + """Get the name of the currently executing action.""" |
614 | + return os.environ.get('JUJU_ACTION_NAME') |
615 | + |
616 | + |
617 | +def action_uuid(): |
618 | + """Get the UUID of the currently executing action.""" |
619 | + return os.environ.get('JUJU_ACTION_UUID') |
620 | + |
621 | + |
622 | +def action_tag(): |
623 | + """Get the tag for the currently executing action.""" |
624 | + return os.environ.get('JUJU_ACTION_TAG') |
625 | + |
626 | + |
627 | +def status_set(workload_state, message): |
628 | + """Set the workload state with a message |
629 | + |
630 | + Use status-set to set the workload state with a message which is visible |
631 | + to the user via juju status. If the status-set command is not found then |
632 | + assume this is juju < 1.23 and juju-log the message unstead. |
633 | + |
634 | + workload_state -- valid juju workload state. |
635 | + message -- status update message |
636 | + """ |
637 | + valid_states = ['maintenance', 'blocked', 'waiting', 'active'] |
638 | + if workload_state not in valid_states: |
639 | + raise ValueError( |
640 | + '{!r} is not a valid workload state'.format(workload_state) |
641 | + ) |
642 | + cmd = ['status-set', workload_state, message] |
643 | + try: |
644 | + ret = subprocess.call(cmd) |
645 | + if ret == 0: |
646 | + return |
647 | + except OSError as e: |
648 | + if e.errno != errno.ENOENT: |
649 | + raise |
650 | + log_message = 'status-set failed: {} {}'.format(workload_state, |
651 | + message) |
652 | + log(log_message, level='INFO') |
653 | + |
654 | + |
655 | +def status_get(): |
656 | + """Retrieve the previously set juju workload state |
657 | + |
658 | + If the status-set command is not found then assume this is juju < 1.23 and |
659 | + return 'unknown' |
660 | + """ |
661 | + cmd = ['status-get'] |
662 | + try: |
663 | + raw_status = subprocess.check_output(cmd, universal_newlines=True) |
664 | + status = raw_status.rstrip() |
665 | + return status |
666 | + except OSError as e: |
667 | + if e.errno == errno.ENOENT: |
668 | + return 'unknown' |
669 | + else: |
670 | + raise |
671 | + |
672 | + |
673 | +def translate_exc(from_exc, to_exc): |
674 | + def inner_translate_exc1(f): |
675 | + def inner_translate_exc2(*args, **kwargs): |
676 | + try: |
677 | + return f(*args, **kwargs) |
678 | + except from_exc: |
679 | + raise to_exc |
680 | + |
681 | + return inner_translate_exc2 |
682 | + |
683 | + return inner_translate_exc1 |
684 | + |
685 | + |
686 | +@translate_exc(from_exc=OSError, to_exc=NotImplementedError) |
687 | +def is_leader(): |
688 | + """Does the current unit hold the juju leadership |
689 | + |
690 | + Uses juju to determine whether the current unit is the leader of its peers |
691 | + """ |
692 | + cmd = ['is-leader', '--format=json'] |
693 | + return json.loads(subprocess.check_output(cmd).decode('UTF-8')) |
694 | + |
695 | + |
696 | +@translate_exc(from_exc=OSError, to_exc=NotImplementedError) |
697 | +def leader_get(attribute=None): |
698 | + """Juju leader get value(s)""" |
699 | + cmd = ['leader-get', '--format=json'] + [attribute or '-'] |
700 | + return json.loads(subprocess.check_output(cmd).decode('UTF-8')) |
701 | + |
702 | + |
703 | +@translate_exc(from_exc=OSError, to_exc=NotImplementedError) |
704 | +def leader_set(settings=None, **kwargs): |
705 | + """Juju leader set value(s)""" |
706 | + # Don't log secrets. |
707 | + # log("Juju leader-set '%s'" % (settings), level=DEBUG) |
708 | + cmd = ['leader-set'] |
709 | + settings = settings or {} |
710 | + settings.update(kwargs) |
711 | + for k, v in settings.items(): |
712 | + if v is None: |
713 | + cmd.append('{}='.format(k)) |
714 | + else: |
715 | + cmd.append('{}={}'.format(k, v)) |
716 | + subprocess.check_call(cmd) |
717 | + |
718 | + |
719 | +@cached |
720 | +def juju_version(): |
721 | + """Full version string (eg. '1.23.3.1-trusty-amd64')""" |
722 | + # Per https://bugs.launchpad.net/juju-core/+bug/1455368/comments/1 |
723 | + jujud = glob.glob('/var/lib/juju/tools/machine-*/jujud')[0] |
724 | + return subprocess.check_output([jujud, 'version'], |
725 | + universal_newlines=True).strip() |
726 | + |
727 | + |
728 | +@cached |
729 | +def has_juju_version(minimum_version): |
730 | + """Return True if the Juju version is at least the provided version""" |
731 | + return LooseVersion(juju_version()) >= LooseVersion(minimum_version) |
732 | + |
733 | + |
734 | +_atexit = [] |
735 | +_atstart = [] |
736 | + |
737 | + |
738 | +def atstart(callback, *args, **kwargs): |
739 | + '''Schedule a callback to run before the main hook. |
740 | + |
741 | + Callbacks are run in the order they were added. |
742 | + |
743 | + This is useful for modules and classes to perform initialization |
744 | + and inject behavior. In particular: |
745 | + |
746 | + - Run common code before all of your hooks, such as logging |
747 | + the hook name or interesting relation data. |
748 | + - Defer object or module initialization that requires a hook |
749 | + context until we know there actually is a hook context, |
750 | + making testing easier. |
751 | + - Rather than requiring charm authors to include boilerplate to |
752 | + invoke your helper's behavior, have it run automatically if |
753 | + your object is instantiated or module imported. |
754 | + |
755 | + This is not at all useful after your hook framework as been launched. |
756 | + ''' |
757 | + global _atstart |
758 | + _atstart.append((callback, args, kwargs)) |
759 | + |
760 | + |
761 | +def atexit(callback, *args, **kwargs): |
762 | + '''Schedule a callback to run on successful hook completion. |
763 | + |
764 | + Callbacks are run in the reverse order that they were added.''' |
765 | + _atexit.append((callback, args, kwargs)) |
766 | + |
767 | + |
768 | +def _run_atstart(): |
769 | + '''Hook frameworks must invoke this before running the main hook body.''' |
770 | + global _atstart |
771 | + for callback, args, kwargs in _atstart: |
772 | + callback(*args, **kwargs) |
773 | + del _atstart[:] |
774 | + |
775 | + |
776 | +def _run_atexit(): |
777 | + '''Hook frameworks must invoke this after the main hook body has |
778 | + successfully completed. Do not invoke it if the hook fails.''' |
779 | + global _atexit |
780 | + for callback, args, kwargs in reversed(_atexit): |
781 | + callback(*args, **kwargs) |
782 | + del _atexit[:] |
783 | |
784 | === modified file 'hooks/charmhelpers/core/host.py' |
785 | --- hooks/charmhelpers/core/host.py 2015-02-25 00:24:56 +0000 |
786 | +++ hooks/charmhelpers/core/host.py 2015-08-19 13:58:57 +0000 |
787 | @@ -24,6 +24,7 @@ |
788 | import os |
789 | import re |
790 | import pwd |
791 | +import glob |
792 | import grp |
793 | import random |
794 | import string |
795 | @@ -62,6 +63,36 @@ |
796 | return service_result |
797 | |
798 | |
799 | +def service_pause(service_name, init_dir=None): |
800 | + """Pause a system service. |
801 | + |
802 | + Stop it, and prevent it from starting again at boot.""" |
803 | + if init_dir is None: |
804 | + init_dir = "/etc/init" |
805 | + stopped = service_stop(service_name) |
806 | + # XXX: Support systemd too |
807 | + override_path = os.path.join( |
808 | + init_dir, '{}.override'.format(service_name)) |
809 | + with open(override_path, 'w') as fh: |
810 | + fh.write("manual\n") |
811 | + return stopped |
812 | + |
813 | + |
814 | +def service_resume(service_name, init_dir=None): |
815 | + """Resume a system service. |
816 | + |
817 | + Reenable starting again at boot. Start the service""" |
818 | + # XXX: Support systemd too |
819 | + if init_dir is None: |
820 | + init_dir = "/etc/init" |
821 | + override_path = os.path.join( |
822 | + init_dir, '{}.override'.format(service_name)) |
823 | + if os.path.exists(override_path): |
824 | + os.unlink(override_path) |
825 | + started = service_start(service_name) |
826 | + return started |
827 | + |
828 | + |
829 | def service(action, service_name): |
830 | """Control a system service""" |
831 | cmd = ['service', service_name, action] |
832 | @@ -90,7 +121,7 @@ |
833 | ['service', service_name, 'status'], |
834 | stderr=subprocess.STDOUT).decode('UTF-8') |
835 | except subprocess.CalledProcessError as e: |
836 | - return 'unrecognized service' not in e.output |
837 | + return b'unrecognized service' not in e.output |
838 | else: |
839 | return True |
840 | |
841 | @@ -117,6 +148,16 @@ |
842 | return user_info |
843 | |
844 | |
845 | +def user_exists(username): |
846 | + """Check if a user exists""" |
847 | + try: |
848 | + pwd.getpwnam(username) |
849 | + user_exists = True |
850 | + except KeyError: |
851 | + user_exists = False |
852 | + return user_exists |
853 | + |
854 | + |
855 | def add_group(group_name, system_group=False): |
856 | """Add a group to the system""" |
857 | try: |
858 | @@ -139,11 +180,7 @@ |
859 | |
860 | def add_user_to_group(username, group): |
861 | """Add a user to a group""" |
862 | - cmd = [ |
863 | - 'gpasswd', '-a', |
864 | - username, |
865 | - group |
866 | - ] |
867 | + cmd = ['gpasswd', '-a', username, group] |
868 | log("Adding user {} to group {}".format(username, group)) |
869 | subprocess.check_call(cmd) |
870 | |
871 | @@ -253,6 +290,17 @@ |
872 | return system_mounts |
873 | |
874 | |
875 | +def fstab_mount(mountpoint): |
876 | + """Mount filesystem using fstab""" |
877 | + cmd_args = ['mount', mountpoint] |
878 | + try: |
879 | + subprocess.check_output(cmd_args) |
880 | + except subprocess.CalledProcessError as e: |
881 | + log('Error unmounting {}\n{}'.format(mountpoint, e.output)) |
882 | + return False |
883 | + return True |
884 | + |
885 | + |
886 | def file_hash(path, hash_type='md5'): |
887 | """ |
888 | Generate a hash checksum of the contents of 'path' or None if not found. |
889 | @@ -269,6 +317,21 @@ |
890 | return None |
891 | |
892 | |
893 | +def path_hash(path): |
894 | + """ |
895 | + Generate a hash checksum of all files matching 'path'. Standard wildcards |
896 | + like '*' and '?' are supported, see documentation for the 'glob' module for |
897 | + more information. |
898 | + |
899 | + :return: dict: A { filename: hash } dictionary for all matched files. |
900 | + Empty if none found. |
901 | + """ |
902 | + return { |
903 | + filename: file_hash(filename) |
904 | + for filename in glob.iglob(path) |
905 | + } |
906 | + |
907 | + |
908 | def check_hash(path, checksum, hash_type='md5'): |
909 | """ |
910 | Validate a file using a cryptographic checksum. |
911 | @@ -296,23 +359,25 @@ |
912 | |
913 | @restart_on_change({ |
914 | '/etc/ceph/ceph.conf': [ 'cinder-api', 'cinder-volume' ] |
915 | + '/etc/apache/sites-enabled/*': [ 'apache2' ] |
916 | }) |
917 | - def ceph_client_changed(): |
918 | + def config_changed(): |
919 | pass # your code here |
920 | |
921 | In this example, the cinder-api and cinder-volume services |
922 | would be restarted if /etc/ceph/ceph.conf is changed by the |
923 | - ceph_client_changed function. |
924 | + ceph_client_changed function. The apache2 service would be |
925 | + restarted if any file matching the pattern got changed, created |
926 | + or removed. Standard wildcards are supported, see documentation |
927 | + for the 'glob' module for more information. |
928 | """ |
929 | def wrap(f): |
930 | def wrapped_f(*args, **kwargs): |
931 | - checksums = {} |
932 | - for path in restart_map: |
933 | - checksums[path] = file_hash(path) |
934 | + checksums = {path: path_hash(path) for path in restart_map} |
935 | f(*args, **kwargs) |
936 | restarts = [] |
937 | for path in restart_map: |
938 | - if checksums[path] != file_hash(path): |
939 | + if path_hash(path) != checksums[path]: |
940 | restarts += restart_map[path] |
941 | services_list = list(OrderedDict.fromkeys(restarts)) |
942 | if not stopstart: |
943 | @@ -339,34 +404,93 @@ |
944 | def pwgen(length=None): |
945 | """Generate a random pasword.""" |
946 | if length is None: |
947 | + # A random length is ok to use a weak PRNG |
948 | length = random.choice(range(35, 45)) |
949 | alphanumeric_chars = [ |
950 | l for l in (string.ascii_letters + string.digits) |
951 | if l not in 'l0QD1vAEIOUaeiou'] |
952 | + # Use a crypto-friendly PRNG (e.g. /dev/urandom) for making the |
953 | + # actual password |
954 | + random_generator = random.SystemRandom() |
955 | random_chars = [ |
956 | - random.choice(alphanumeric_chars) for _ in range(length)] |
957 | + random_generator.choice(alphanumeric_chars) for _ in range(length)] |
958 | return(''.join(random_chars)) |
959 | |
960 | |
961 | -def list_nics(nic_type): |
962 | +def is_phy_iface(interface): |
963 | + """Returns True if interface is not virtual, otherwise False.""" |
964 | + if interface: |
965 | + sys_net = '/sys/class/net' |
966 | + if os.path.isdir(sys_net): |
967 | + for iface in glob.glob(os.path.join(sys_net, '*')): |
968 | + if '/virtual/' in os.path.realpath(iface): |
969 | + continue |
970 | + |
971 | + if interface == os.path.basename(iface): |
972 | + return True |
973 | + |
974 | + return False |
975 | + |
976 | + |
977 | +def get_bond_master(interface): |
978 | + """Returns bond master if interface is bond slave otherwise None. |
979 | + |
980 | + NOTE: the provided interface is expected to be physical |
981 | + """ |
982 | + if interface: |
983 | + iface_path = '/sys/class/net/%s' % (interface) |
984 | + if os.path.exists(iface_path): |
985 | + if '/virtual/' in os.path.realpath(iface_path): |
986 | + return None |
987 | + |
988 | + master = os.path.join(iface_path, 'master') |
989 | + if os.path.exists(master): |
990 | + master = os.path.realpath(master) |
991 | + # make sure it is a bond master |
992 | + if os.path.exists(os.path.join(master, 'bonding')): |
993 | + return os.path.basename(master) |
994 | + |
995 | + return None |
996 | + |
997 | + |
998 | +def list_nics(nic_type=None): |
999 | '''Return a list of nics of given type(s)''' |
1000 | if isinstance(nic_type, six.string_types): |
1001 | int_types = [nic_type] |
1002 | else: |
1003 | int_types = nic_type |
1004 | + |
1005 | interfaces = [] |
1006 | - for int_type in int_types: |
1007 | - cmd = ['ip', 'addr', 'show', 'label', int_type + '*'] |
1008 | + if nic_type: |
1009 | + for int_type in int_types: |
1010 | + cmd = ['ip', 'addr', 'show', 'label', int_type + '*'] |
1011 | + ip_output = subprocess.check_output(cmd).decode('UTF-8') |
1012 | + ip_output = ip_output.split('\n') |
1013 | + ip_output = (line for line in ip_output if line) |
1014 | + for line in ip_output: |
1015 | + if line.split()[1].startswith(int_type): |
1016 | + matched = re.search('.*: (' + int_type + |
1017 | + r'[0-9]+\.[0-9]+)@.*', line) |
1018 | + if matched: |
1019 | + iface = matched.groups()[0] |
1020 | + else: |
1021 | + iface = line.split()[1].replace(":", "") |
1022 | + |
1023 | + if iface not in interfaces: |
1024 | + interfaces.append(iface) |
1025 | + else: |
1026 | + cmd = ['ip', 'a'] |
1027 | ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n') |
1028 | - ip_output = (line for line in ip_output if line) |
1029 | + ip_output = (line.strip() for line in ip_output if line) |
1030 | + |
1031 | + key = re.compile('^[0-9]+:\s+(.+):') |
1032 | for line in ip_output: |
1033 | - if line.split()[1].startswith(int_type): |
1034 | - matched = re.search('.*: (' + int_type + r'[0-9]+\.[0-9]+)@.*', line) |
1035 | - if matched: |
1036 | - interface = matched.groups()[0] |
1037 | - else: |
1038 | - interface = line.split()[1].replace(":", "") |
1039 | - interfaces.append(interface) |
1040 | + matched = re.search(key, line) |
1041 | + if matched: |
1042 | + iface = matched.group(1) |
1043 | + iface = iface.partition("@")[0] |
1044 | + if iface not in interfaces: |
1045 | + interfaces.append(iface) |
1046 | |
1047 | return interfaces |
1048 | |
1049 | |
1050 | === added file 'hooks/charmhelpers/core/hugepage.py' |
1051 | --- hooks/charmhelpers/core/hugepage.py 1970-01-01 00:00:00 +0000 |
1052 | +++ hooks/charmhelpers/core/hugepage.py 2015-08-19 13:58:57 +0000 |
1053 | @@ -0,0 +1,62 @@ |
1054 | +# -*- coding: utf-8 -*- |
1055 | + |
1056 | +# Copyright 2014-2015 Canonical Limited. |
1057 | +# |
1058 | +# This file is part of charm-helpers. |
1059 | +# |
1060 | +# charm-helpers is free software: you can redistribute it and/or modify |
1061 | +# it under the terms of the GNU Lesser General Public License version 3 as |
1062 | +# published by the Free Software Foundation. |
1063 | +# |
1064 | +# charm-helpers is distributed in the hope that it will be useful, |
1065 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
1066 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1067 | +# GNU Lesser General Public License for more details. |
1068 | +# |
1069 | +# You should have received a copy of the GNU Lesser General Public License |
1070 | +# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. |
1071 | + |
1072 | +import yaml |
1073 | +from charmhelpers.core import fstab |
1074 | +from charmhelpers.core import sysctl |
1075 | +from charmhelpers.core.host import ( |
1076 | + add_group, |
1077 | + add_user_to_group, |
1078 | + fstab_mount, |
1079 | + mkdir, |
1080 | +) |
1081 | + |
1082 | + |
1083 | +def hugepage_support(user, group='hugetlb', nr_hugepages=256, |
1084 | + max_map_count=65536, mnt_point='/run/hugepages/kvm', |
1085 | + pagesize='2MB', mount=True): |
1086 | + """Enable hugepages on system. |
1087 | + |
1088 | + Args: |
1089 | + user (str) -- Username to allow access to hugepages to |
1090 | + group (str) -- Group name to own hugepages |
1091 | + nr_hugepages (int) -- Number of pages to reserve |
1092 | + max_map_count (int) -- Number of Virtual Memory Areas a process can own |
1093 | + mnt_point (str) -- Directory to mount hugepages on |
1094 | + pagesize (str) -- Size of hugepages |
1095 | + mount (bool) -- Whether to Mount hugepages |
1096 | + """ |
1097 | + group_info = add_group(group) |
1098 | + gid = group_info.gr_gid |
1099 | + add_user_to_group(user, group) |
1100 | + sysctl_settings = { |
1101 | + 'vm.nr_hugepages': nr_hugepages, |
1102 | + 'vm.max_map_count': max_map_count, |
1103 | + 'vm.hugetlb_shm_group': gid, |
1104 | + } |
1105 | + sysctl.create(yaml.dump(sysctl_settings), '/etc/sysctl.d/10-hugepage.conf') |
1106 | + mkdir(mnt_point, owner='root', group='root', perms=0o755, force=False) |
1107 | + lfstab = fstab.Fstab() |
1108 | + fstab_entry = lfstab.get_entry_by_attr('mountpoint', mnt_point) |
1109 | + if fstab_entry: |
1110 | + lfstab.remove_entry(fstab_entry) |
1111 | + entry = lfstab.Entry('nodev', mnt_point, 'hugetlbfs', |
1112 | + 'mode=1770,gid={},pagesize={}'.format(gid, pagesize), 0, 0) |
1113 | + lfstab.add_entry(entry) |
1114 | + if mount: |
1115 | + fstab_mount(mnt_point) |
1116 | |
1117 | === modified file 'hooks/charmhelpers/core/services/base.py' |
1118 | --- hooks/charmhelpers/core/services/base.py 2015-02-25 00:24:56 +0000 |
1119 | +++ hooks/charmhelpers/core/services/base.py 2015-08-19 13:58:57 +0000 |
1120 | @@ -15,9 +15,9 @@ |
1121 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. |
1122 | |
1123 | import os |
1124 | -import re |
1125 | import json |
1126 | -from collections import Iterable |
1127 | +from inspect import getargspec |
1128 | +from collections import Iterable, OrderedDict |
1129 | |
1130 | from charmhelpers.core import host |
1131 | from charmhelpers.core import hookenv |
1132 | @@ -119,7 +119,7 @@ |
1133 | """ |
1134 | self._ready_file = os.path.join(hookenv.charm_dir(), 'READY-SERVICES.json') |
1135 | self._ready = None |
1136 | - self.services = {} |
1137 | + self.services = OrderedDict() |
1138 | for service in services or []: |
1139 | service_name = service['service'] |
1140 | self.services[service_name] = service |
1141 | @@ -128,15 +128,18 @@ |
1142 | """ |
1143 | Handle the current hook by doing The Right Thing with the registered services. |
1144 | """ |
1145 | - hook_name = hookenv.hook_name() |
1146 | - if hook_name == 'stop': |
1147 | - self.stop_services() |
1148 | - else: |
1149 | - self.provide_data() |
1150 | - self.reconfigure_services() |
1151 | - cfg = hookenv.config() |
1152 | - if cfg.implicit_save: |
1153 | - cfg.save() |
1154 | + hookenv._run_atstart() |
1155 | + try: |
1156 | + hook_name = hookenv.hook_name() |
1157 | + if hook_name == 'stop': |
1158 | + self.stop_services() |
1159 | + else: |
1160 | + self.reconfigure_services() |
1161 | + self.provide_data() |
1162 | + except SystemExit as x: |
1163 | + if x.code is None or x.code == 0: |
1164 | + hookenv._run_atexit() |
1165 | + hookenv._run_atexit() |
1166 | |
1167 | def provide_data(self): |
1168 | """ |
1169 | @@ -145,15 +148,36 @@ |
1170 | A provider must have a `name` attribute, which indicates which relation |
1171 | to set data on, and a `provide_data()` method, which returns a dict of |
1172 | data to set. |
1173 | + |
1174 | + The `provide_data()` method can optionally accept two parameters: |
1175 | + |
1176 | + * ``remote_service`` The name of the remote service that the data will |
1177 | + be provided to. The `provide_data()` method will be called once |
1178 | + for each connected service (not unit). This allows the method to |
1179 | + tailor its data to the given service. |
1180 | + * ``service_ready`` Whether or not the service definition had all of |
1181 | + its requirements met, and thus the ``data_ready`` callbacks run. |
1182 | + |
1183 | + Note that the ``provided_data`` methods are now called **after** the |
1184 | + ``data_ready`` callbacks are run. This gives the ``data_ready`` callbacks |
1185 | + a chance to generate any data necessary for the providing to the remote |
1186 | + services. |
1187 | """ |
1188 | - hook_name = hookenv.hook_name() |
1189 | - for service in self.services.values(): |
1190 | + for service_name, service in self.services.items(): |
1191 | + service_ready = self.is_ready(service_name) |
1192 | for provider in service.get('provided_data', []): |
1193 | - if re.match(r'{}-relation-(joined|changed)'.format(provider.name), hook_name): |
1194 | - data = provider.provide_data() |
1195 | - _ready = provider._is_ready(data) if hasattr(provider, '_is_ready') else data |
1196 | - if _ready: |
1197 | - hookenv.relation_set(None, data) |
1198 | + for relid in hookenv.relation_ids(provider.name): |
1199 | + units = hookenv.related_units(relid) |
1200 | + if not units: |
1201 | + continue |
1202 | + remote_service = units[0].split('/')[0] |
1203 | + argspec = getargspec(provider.provide_data) |
1204 | + if len(argspec.args) > 1: |
1205 | + data = provider.provide_data(remote_service, service_ready) |
1206 | + else: |
1207 | + data = provider.provide_data() |
1208 | + if data: |
1209 | + hookenv.relation_set(relid, data) |
1210 | |
1211 | def reconfigure_services(self, *service_names): |
1212 | """ |
1213 | |
1214 | === modified file 'hooks/charmhelpers/core/services/helpers.py' |
1215 | --- hooks/charmhelpers/core/services/helpers.py 2015-02-25 00:24:56 +0000 |
1216 | +++ hooks/charmhelpers/core/services/helpers.py 2015-08-19 13:58:57 +0000 |
1217 | @@ -16,7 +16,9 @@ |
1218 | |
1219 | import os |
1220 | import yaml |
1221 | + |
1222 | from charmhelpers.core import hookenv |
1223 | +from charmhelpers.core import host |
1224 | from charmhelpers.core import templating |
1225 | |
1226 | from charmhelpers.core.services.base import ManagerCallback |
1227 | @@ -45,12 +47,14 @@ |
1228 | """ |
1229 | name = None |
1230 | interface = None |
1231 | - required_keys = [] |
1232 | |
1233 | def __init__(self, name=None, additional_required_keys=None): |
1234 | + if not hasattr(self, 'required_keys'): |
1235 | + self.required_keys = [] |
1236 | + |
1237 | if name is not None: |
1238 | self.name = name |
1239 | - if additional_required_keys is not None: |
1240 | + if additional_required_keys: |
1241 | self.required_keys.extend(additional_required_keys) |
1242 | self.get_data() |
1243 | |
1244 | @@ -134,7 +138,10 @@ |
1245 | """ |
1246 | name = 'db' |
1247 | interface = 'mysql' |
1248 | - required_keys = ['host', 'user', 'password', 'database'] |
1249 | + |
1250 | + def __init__(self, *args, **kwargs): |
1251 | + self.required_keys = ['host', 'user', 'password', 'database'] |
1252 | + RelationContext.__init__(self, *args, **kwargs) |
1253 | |
1254 | |
1255 | class HttpRelation(RelationContext): |
1256 | @@ -146,7 +153,10 @@ |
1257 | """ |
1258 | name = 'website' |
1259 | interface = 'http' |
1260 | - required_keys = ['host', 'port'] |
1261 | + |
1262 | + def __init__(self, *args, **kwargs): |
1263 | + self.required_keys = ['host', 'port'] |
1264 | + RelationContext.__init__(self, *args, **kwargs) |
1265 | |
1266 | def provide_data(self): |
1267 | return { |
1268 | @@ -231,28 +241,42 @@ |
1269 | action. |
1270 | |
1271 | :param str source: The template source file, relative to |
1272 | - `$CHARM_DIR/templates` |
1273 | + `$CHARM_DIR/templates` |
1274 | |
1275 | :param str target: The target to write the rendered template to |
1276 | :param str owner: The owner of the rendered file |
1277 | :param str group: The group of the rendered file |
1278 | :param int perms: The permissions of the rendered file |
1279 | + :param partial on_change_action: functools partial to be executed when |
1280 | + rendered file changes |
1281 | """ |
1282 | def __init__(self, source, target, |
1283 | - owner='root', group='root', perms=0o444): |
1284 | + owner='root', group='root', perms=0o444, |
1285 | + on_change_action=None): |
1286 | self.source = source |
1287 | self.target = target |
1288 | self.owner = owner |
1289 | self.group = group |
1290 | self.perms = perms |
1291 | + self.on_change_action = on_change_action |
1292 | |
1293 | def __call__(self, manager, service_name, event_name): |
1294 | + pre_checksum = '' |
1295 | + if self.on_change_action and os.path.isfile(self.target): |
1296 | + pre_checksum = host.file_hash(self.target) |
1297 | service = manager.get_service(service_name) |
1298 | context = {} |
1299 | for ctx in service.get('required_data', []): |
1300 | context.update(ctx) |
1301 | templating.render(self.source, self.target, context, |
1302 | self.owner, self.group, self.perms) |
1303 | + if self.on_change_action: |
1304 | + if pre_checksum == host.file_hash(self.target): |
1305 | + hookenv.log( |
1306 | + 'No change detected: {}'.format(self.target), |
1307 | + hookenv.DEBUG) |
1308 | + else: |
1309 | + self.on_change_action() |
1310 | |
1311 | |
1312 | # Convenience aliases for templates |
1313 | |
1314 | === modified file 'hooks/charmhelpers/core/strutils.py' |
1315 | --- hooks/charmhelpers/core/strutils.py 2015-02-25 00:24:56 +0000 |
1316 | +++ hooks/charmhelpers/core/strutils.py 2015-08-19 13:58:57 +0000 |
1317 | @@ -33,9 +33,9 @@ |
1318 | |
1319 | value = value.strip().lower() |
1320 | |
1321 | - if value in ['y', 'yes', 'true', 't']: |
1322 | + if value in ['y', 'yes', 'true', 't', 'on']: |
1323 | return True |
1324 | - elif value in ['n', 'no', 'false', 'f']: |
1325 | + elif value in ['n', 'no', 'false', 'f', 'off']: |
1326 | return False |
1327 | |
1328 | msg = "Unable to interpret string value '%s' as boolean" % (value) |
1329 | |
1330 | === modified file 'hooks/charmhelpers/core/unitdata.py' |
1331 | --- hooks/charmhelpers/core/unitdata.py 2015-02-25 00:24:56 +0000 |
1332 | +++ hooks/charmhelpers/core/unitdata.py 2015-08-19 13:58:57 +0000 |
1333 | @@ -152,6 +152,7 @@ |
1334 | import collections |
1335 | import contextlib |
1336 | import datetime |
1337 | +import itertools |
1338 | import json |
1339 | import os |
1340 | import pprint |
1341 | @@ -164,8 +165,7 @@ |
1342 | class Storage(object): |
1343 | """Simple key value database for local unit state within charms. |
1344 | |
1345 | - Modifications are automatically committed at hook exit. That's |
1346 | - currently regardless of exit code. |
1347 | + Modifications are not persisted unless :meth:`flush` is called. |
1348 | |
1349 | To support dicts, lists, integer, floats, and booleans values |
1350 | are automatically json encoded/decoded. |
1351 | @@ -173,8 +173,11 @@ |
1352 | def __init__(self, path=None): |
1353 | self.db_path = path |
1354 | if path is None: |
1355 | - self.db_path = os.path.join( |
1356 | - os.environ.get('CHARM_DIR', ''), '.unit-state.db') |
1357 | + if 'UNIT_STATE_DB' in os.environ: |
1358 | + self.db_path = os.environ['UNIT_STATE_DB'] |
1359 | + else: |
1360 | + self.db_path = os.path.join( |
1361 | + os.environ.get('CHARM_DIR', ''), '.unit-state.db') |
1362 | self.conn = sqlite3.connect('%s' % self.db_path) |
1363 | self.cursor = self.conn.cursor() |
1364 | self.revision = None |
1365 | @@ -189,15 +192,8 @@ |
1366 | self.conn.close() |
1367 | self._closed = True |
1368 | |
1369 | - def _scoped_query(self, stmt, params=None): |
1370 | - if params is None: |
1371 | - params = [] |
1372 | - return stmt, params |
1373 | - |
1374 | def get(self, key, default=None, record=False): |
1375 | - self.cursor.execute( |
1376 | - *self._scoped_query( |
1377 | - 'select data from kv where key=?', [key])) |
1378 | + self.cursor.execute('select data from kv where key=?', [key]) |
1379 | result = self.cursor.fetchone() |
1380 | if not result: |
1381 | return default |
1382 | @@ -206,33 +202,81 @@ |
1383 | return json.loads(result[0]) |
1384 | |
1385 | def getrange(self, key_prefix, strip=False): |
1386 | - stmt = "select key, data from kv where key like '%s%%'" % key_prefix |
1387 | - self.cursor.execute(*self._scoped_query(stmt)) |
1388 | + """ |
1389 | + Get a range of keys starting with a common prefix as a mapping of |
1390 | + keys to values. |
1391 | + |
1392 | + :param str key_prefix: Common prefix among all keys |
1393 | + :param bool strip: Optionally strip the common prefix from the key |
1394 | + names in the returned dict |
1395 | + :return dict: A (possibly empty) dict of key-value mappings |
1396 | + """ |
1397 | + self.cursor.execute("select key, data from kv where key like ?", |
1398 | + ['%s%%' % key_prefix]) |
1399 | result = self.cursor.fetchall() |
1400 | |
1401 | if not result: |
1402 | - return None |
1403 | + return {} |
1404 | if not strip: |
1405 | key_prefix = '' |
1406 | return dict([ |
1407 | (k[len(key_prefix):], json.loads(v)) for k, v in result]) |
1408 | |
1409 | def update(self, mapping, prefix=""): |
1410 | + """ |
1411 | + Set the values of multiple keys at once. |
1412 | + |
1413 | + :param dict mapping: Mapping of keys to values |
1414 | + :param str prefix: Optional prefix to apply to all keys in `mapping` |
1415 | + before setting |
1416 | + """ |
1417 | for k, v in mapping.items(): |
1418 | self.set("%s%s" % (prefix, k), v) |
1419 | |
1420 | def unset(self, key): |
1421 | + """ |
1422 | + Remove a key from the database entirely. |
1423 | + """ |
1424 | self.cursor.execute('delete from kv where key=?', [key]) |
1425 | if self.revision and self.cursor.rowcount: |
1426 | self.cursor.execute( |
1427 | 'insert into kv_revisions values (?, ?, ?)', |
1428 | [key, self.revision, json.dumps('DELETED')]) |
1429 | |
1430 | + def unsetrange(self, keys=None, prefix=""): |
1431 | + """ |
1432 | + Remove a range of keys starting with a common prefix, from the database |
1433 | + entirely. |
1434 | + |
1435 | + :param list keys: List of keys to remove. |
1436 | + :param str prefix: Optional prefix to apply to all keys in ``keys`` |
1437 | + before removing. |
1438 | + """ |
1439 | + if keys is not None: |
1440 | + keys = ['%s%s' % (prefix, key) for key in keys] |
1441 | + self.cursor.execute('delete from kv where key in (%s)' % ','.join(['?'] * len(keys)), keys) |
1442 | + if self.revision and self.cursor.rowcount: |
1443 | + self.cursor.execute( |
1444 | + 'insert into kv_revisions values %s' % ','.join(['(?, ?, ?)'] * len(keys)), |
1445 | + list(itertools.chain.from_iterable((key, self.revision, json.dumps('DELETED')) for key in keys))) |
1446 | + else: |
1447 | + self.cursor.execute('delete from kv where key like ?', |
1448 | + ['%s%%' % prefix]) |
1449 | + if self.revision and self.cursor.rowcount: |
1450 | + self.cursor.execute( |
1451 | + 'insert into kv_revisions values (?, ?, ?)', |
1452 | + ['%s%%' % prefix, self.revision, json.dumps('DELETED')]) |
1453 | + |
1454 | def set(self, key, value): |
1455 | + """ |
1456 | + Set a value in the database. |
1457 | + |
1458 | + :param str key: Key to set the value for |
1459 | + :param value: Any JSON-serializable value to be set |
1460 | + """ |
1461 | serialized = json.dumps(value) |
1462 | |
1463 | - self.cursor.execute( |
1464 | - 'select data from kv where key=?', [key]) |
1465 | + self.cursor.execute('select data from kv where key=?', [key]) |
1466 | exists = self.cursor.fetchone() |
1467 | |
1468 | # Skip mutations to the same value |
1469 | @@ -443,7 +487,7 @@ |
1470 | data = hookenv.execution_environment() |
1471 | self.conf = conf_delta = self.kv.delta(data['conf'], 'config') |
1472 | self.rels = rels_delta = self.kv.delta(data['rels'], 'rels') |
1473 | - self.kv.set('env', data['env']) |
1474 | + self.kv.set('env', dict(data['env'])) |
1475 | self.kv.set('unit', data['unit']) |
1476 | self.kv.set('relid', data.get('relid')) |
1477 | return conf_delta, rels_delta |
1478 | |
1479 | === modified file 'hooks/charmhelpers/fetch/__init__.py' |
1480 | --- hooks/charmhelpers/fetch/__init__.py 2015-02-25 00:24:56 +0000 |
1481 | +++ hooks/charmhelpers/fetch/__init__.py 2015-08-19 13:58:57 +0000 |
1482 | @@ -90,6 +90,14 @@ |
1483 | 'kilo/proposed': 'trusty-proposed/kilo', |
1484 | 'trusty-kilo/proposed': 'trusty-proposed/kilo', |
1485 | 'trusty-proposed/kilo': 'trusty-proposed/kilo', |
1486 | + # Liberty |
1487 | + 'liberty': 'trusty-updates/liberty', |
1488 | + 'trusty-liberty': 'trusty-updates/liberty', |
1489 | + 'trusty-liberty/updates': 'trusty-updates/liberty', |
1490 | + 'trusty-updates/liberty': 'trusty-updates/liberty', |
1491 | + 'liberty/proposed': 'trusty-proposed/liberty', |
1492 | + 'trusty-liberty/proposed': 'trusty-proposed/liberty', |
1493 | + 'trusty-proposed/liberty': 'trusty-proposed/liberty', |
1494 | } |
1495 | |
1496 | # The order of this list is very important. Handlers should be listed in from |
1497 | @@ -158,7 +166,7 @@ |
1498 | |
1499 | def apt_cache(in_memory=True): |
1500 | """Build and return an apt cache""" |
1501 | - import apt_pkg |
1502 | + from apt import apt_pkg |
1503 | apt_pkg.init() |
1504 | if in_memory: |
1505 | apt_pkg.config.set("Dir::Cache::pkgcache", "") |
1506 | @@ -215,19 +223,27 @@ |
1507 | _run_apt_command(cmd, fatal) |
1508 | |
1509 | |
1510 | +def apt_mark(packages, mark, fatal=False): |
1511 | + """Flag one or more packages using apt-mark""" |
1512 | + cmd = ['apt-mark', mark] |
1513 | + if isinstance(packages, six.string_types): |
1514 | + cmd.append(packages) |
1515 | + else: |
1516 | + cmd.extend(packages) |
1517 | + log("Holding {}".format(packages)) |
1518 | + |
1519 | + if fatal: |
1520 | + subprocess.check_call(cmd, universal_newlines=True) |
1521 | + else: |
1522 | + subprocess.call(cmd, universal_newlines=True) |
1523 | + |
1524 | + |
1525 | def apt_hold(packages, fatal=False): |
1526 | - """Hold one or more packages""" |
1527 | - cmd = ['apt-mark', 'hold'] |
1528 | - if isinstance(packages, six.string_types): |
1529 | - cmd.append(packages) |
1530 | - else: |
1531 | - cmd.extend(packages) |
1532 | - log("Holding {}".format(packages)) |
1533 | - |
1534 | - if fatal: |
1535 | - subprocess.check_call(cmd) |
1536 | - else: |
1537 | - subprocess.call(cmd) |
1538 | + return apt_mark(packages, 'hold', fatal=fatal) |
1539 | + |
1540 | + |
1541 | +def apt_unhold(packages, fatal=False): |
1542 | + return apt_mark(packages, 'unhold', fatal=fatal) |
1543 | |
1544 | |
1545 | def add_source(source, key=None): |
1546 | @@ -370,8 +386,9 @@ |
1547 | for handler in handlers: |
1548 | try: |
1549 | installed_to = handler.install(source, *args, **kwargs) |
1550 | - except UnhandledSource: |
1551 | - pass |
1552 | + except UnhandledSource as e: |
1553 | + log('Install source attempt unsuccessful: {}'.format(e), |
1554 | + level='WARNING') |
1555 | if not installed_to: |
1556 | raise UnhandledSource("No handler found for source {}".format(source)) |
1557 | return installed_to |
1558 | |
1559 | === modified file 'hooks/charmhelpers/fetch/archiveurl.py' |
1560 | --- hooks/charmhelpers/fetch/archiveurl.py 2015-02-25 00:24:56 +0000 |
1561 | +++ hooks/charmhelpers/fetch/archiveurl.py 2015-08-19 13:58:57 +0000 |
1562 | @@ -77,6 +77,8 @@ |
1563 | def can_handle(self, source): |
1564 | url_parts = self.parse_url(source) |
1565 | if url_parts.scheme not in ('http', 'https', 'ftp', 'file'): |
1566 | + # XXX: Why is this returning a boolean and a string? It's |
1567 | + # doomed to fail since "bool(can_handle('foo://'))" will be True. |
1568 | return "Wrong source type" |
1569 | if get_archive_handler(self.base_url(source)): |
1570 | return True |
1571 | @@ -155,7 +157,11 @@ |
1572 | else: |
1573 | algorithms = hashlib.algorithms_available |
1574 | if key in algorithms: |
1575 | - check_hash(dld_file, value, key) |
1576 | + if len(value) != 1: |
1577 | + raise TypeError( |
1578 | + "Expected 1 hash value, not %d" % len(value)) |
1579 | + expected = value[0] |
1580 | + check_hash(dld_file, expected, key) |
1581 | if checksum: |
1582 | check_hash(dld_file, checksum, hash_type) |
1583 | return extract(dld_file, dest) |
1584 | |
1585 | === modified file 'hooks/charmhelpers/fetch/giturl.py' |
1586 | --- hooks/charmhelpers/fetch/giturl.py 2015-02-25 00:24:56 +0000 |
1587 | +++ hooks/charmhelpers/fetch/giturl.py 2015-08-19 13:58:57 +0000 |
1588 | @@ -45,14 +45,16 @@ |
1589 | else: |
1590 | return True |
1591 | |
1592 | - def clone(self, source, dest, branch): |
1593 | + def clone(self, source, dest, branch, depth=None): |
1594 | if not self.can_handle(source): |
1595 | raise UnhandledSource("Cannot handle {}".format(source)) |
1596 | |
1597 | - repo = Repo.clone_from(source, dest) |
1598 | - repo.git.checkout(branch) |
1599 | + if depth: |
1600 | + Repo.clone_from(source, dest, branch=branch, depth=depth) |
1601 | + else: |
1602 | + Repo.clone_from(source, dest, branch=branch) |
1603 | |
1604 | - def install(self, source, branch="master", dest=None): |
1605 | + def install(self, source, branch="master", dest=None, depth=None): |
1606 | url_parts = self.parse_url(source) |
1607 | branch_name = url_parts.path.strip("/").split("/")[-1] |
1608 | if dest: |
1609 | @@ -63,9 +65,9 @@ |
1610 | if not os.path.exists(dest_dir): |
1611 | mkdir(dest_dir, perms=0o755) |
1612 | try: |
1613 | - self.clone(source, dest_dir, branch) |
1614 | + self.clone(source, dest_dir, branch, depth) |
1615 | except GitCommandError as e: |
1616 | - raise UnhandledSource(e.message) |
1617 | + raise UnhandledSource(e) |
1618 | except OSError as e: |
1619 | raise UnhandledSource(e.strerror) |
1620 | return dest_dir |
charm_lint_check #8336 mongodb for 1chb1n mp268413
LINT OK: passed
Build: http:// 10.245. 162.77: 8080/job/ charm_lint_ check/8336/