Merge lp:~stub/charms/trusty/postgresql/rewrite-charmhelpers into lp:charms/trusty/postgresql
- Trusty Tahr (14.04)
- rewrite-charmhelpers
- Merge into trunk
Proposed by
Stuart Bishop
Status: | Merged |
---|---|
Merged at revision: | 130 |
Proposed branch: | lp:~stub/charms/trusty/postgresql/rewrite-charmhelpers |
Merge into: | lp:charms/trusty/postgresql |
Diff against target: |
1113 lines (+902/-25) 16 files modified
charm-helpers.yaml (+3/-0) hooks/charmhelpers/context.py (+206/-0) hooks/charmhelpers/contrib/__init__.py (+15/-0) hooks/charmhelpers/contrib/charmsupport/__init__.py (+15/-0) hooks/charmhelpers/contrib/charmsupport/nrpe.py (+360/-0) hooks/charmhelpers/core/files.py (+45/-0) hooks/charmhelpers/core/hookenv.py (+17/-1) hooks/charmhelpers/core/host.py (+47/-5) hooks/charmhelpers/core/services/helpers.py (+2/-2) hooks/charmhelpers/core/templating.py (+5/-1) hooks/charmhelpers/fetch/__init__.py (+23/-14) hooks/charmhelpers/fetch/archiveurl.py (+7/-1) hooks/charmhelpers/fetch/giturl.py (+1/-1) hooks/charmhelpers/payload/__init__.py (+17/-0) hooks/charmhelpers/payload/archive.py (+73/-0) hooks/charmhelpers/payload/execd.py (+66/-0) |
To merge this branch: | bzr merge lp:~stub/charms/trusty/postgresql/rewrite-charmhelpers |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Charles Butler (community) | Approve | ||
Review via email: mp+267645@code.launchpad.net |
Commit message
Description of the change
Charmhelpers sync for lp:~stub/charms/trusty/postgresql/rewrite.
This branch contains charmhelpers updates, and not worth reviewing. It serves as a dependant branch to remove the noise from the main merge proposal.
To post a comment you must log in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'charm-helpers.yaml' |
2 | --- charm-helpers.yaml 2015-06-25 11:39:19 +0000 |
3 | +++ charm-helpers.yaml 2015-08-11 11:19:35 +0000 |
4 | @@ -1,6 +1,9 @@ |
5 | destination: hooks/charmhelpers |
6 | branch: lp:~stub/charm-helpers/integration |
7 | include: |
8 | + - context |
9 | - coordinator |
10 | - core |
11 | - fetch |
12 | + - payload |
13 | + - contrib.charmsupport.nrpe |
14 | |
15 | === added file 'hooks/charmhelpers/context.py' |
16 | --- hooks/charmhelpers/context.py 1970-01-01 00:00:00 +0000 |
17 | +++ hooks/charmhelpers/context.py 2015-08-11 11:19:35 +0000 |
18 | @@ -0,0 +1,206 @@ |
19 | +# Copyright 2015 Canonical Limited. |
20 | +# |
21 | +# This file is part of charm-helpers. |
22 | +# |
23 | +# charm-helpers is free software: you can redistribute it and/or modify |
24 | +# it under the terms of the GNU Lesser General Public License version 3 as |
25 | +# published by the Free Software Foundation. |
26 | +# |
27 | +# charm-helpers is distributed in the hope that it will be useful, |
28 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
29 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
30 | +# GNU Lesser General Public License for more details. |
31 | +# |
32 | +# You should have received a copy of the GNU Lesser General Public License |
33 | +# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. |
34 | +''' |
35 | +A Pythonic API to interact with the charm hook environment. |
36 | + |
37 | +:author: Stuart Bishop <stuart.bishop@canonical.com> |
38 | +''' |
39 | + |
40 | +import six |
41 | + |
42 | +from charmhelpers.core import hookenv |
43 | + |
44 | +from collections import OrderedDict |
45 | +if six.PY3: |
46 | + from collections import UserDict # pragma: nocover |
47 | +else: |
48 | + from UserDict import IterableUserDict as UserDict # pragma: nocover |
49 | + |
50 | + |
51 | +class Relations(OrderedDict): |
52 | + '''Mapping relation name -> relation id -> Relation. |
53 | + |
54 | + >>> rels = Relations() |
55 | + >>> rels['sprog']['sprog:12']['client/6']['widget'] |
56 | + 'remote widget' |
57 | + >>> rels['sprog']['sprog:12'].local['widget'] = 'local widget' |
58 | + >>> rels['sprog']['sprog:12'].local['widget'] |
59 | + 'local widget' |
60 | + >>> rels.peer.local['widget'] |
61 | + 'local widget on the peer relation' |
62 | + ''' |
63 | + def __init__(self): |
64 | + super(Relations, self).__init__() |
65 | + for relname in sorted(hookenv.relation_types()): |
66 | + self[relname] = OrderedDict() |
67 | + relids = hookenv.relation_ids(relname) |
68 | + relids.sort(key=lambda x: int(x.split(':', 1)[-1])) |
69 | + for relid in relids: |
70 | + self[relname][relid] = Relation(relid) |
71 | + |
72 | + @property |
73 | + def peer(self): |
74 | + peer_relid = hookenv.peer_relation_id() |
75 | + for rels in self.values(): |
76 | + if peer_relid in rels: |
77 | + return rels[peer_relid] |
78 | + |
79 | + |
80 | +class Relation(OrderedDict): |
81 | + '''Mapping of unit -> remote RelationInfo for a relation. |
82 | + |
83 | + This is an OrderedDict mapping, ordered numerically by |
84 | + by unit number. |
85 | + |
86 | + Also provides access to the local RelationInfo, and peer RelationInfo |
87 | + instances by the 'local' and 'peers' attributes. |
88 | + |
89 | + >>> r = Relation('sprog:12') |
90 | + >>> r.keys() |
91 | + ['client/9', 'client/10'] # Ordered numerically |
92 | + >>> r['client/10']['widget'] # A remote RelationInfo setting |
93 | + 'remote widget' |
94 | + >>> r.local['widget'] # The local RelationInfo setting |
95 | + 'local widget' |
96 | + ''' |
97 | + relid = None # The relation id. |
98 | + relname = None # The relation name (also known as relation type). |
99 | + service = None # The remote service name, if known. |
100 | + local = None # The local end's RelationInfo. |
101 | + peers = None # Map of peer -> RelationInfo. None if no peer relation. |
102 | + |
103 | + def __init__(self, relid): |
104 | + remote_units = hookenv.related_units(relid) |
105 | + remote_units.sort(key=lambda u: int(u.split('/', 1)[-1])) |
106 | + super(Relation, self).__init__((unit, RelationInfo(relid, unit)) |
107 | + for unit in remote_units) |
108 | + |
109 | + self.relname = relid.split(':', 1)[0] |
110 | + self.relid = relid |
111 | + self.local = RelationInfo(relid, hookenv.local_unit()) |
112 | + |
113 | + for relinfo in self.values(): |
114 | + self.service = relinfo.service |
115 | + break |
116 | + |
117 | + # If we have peers, and they have joined both the provided peer |
118 | + # relation and this relation, we can peek at their data too. |
119 | + # This is useful for creating consensus without leadership. |
120 | + peer_relid = hookenv.peer_relation_id() |
121 | + if peer_relid and peer_relid != relid: |
122 | + peers = hookenv.related_units(peer_relid) |
123 | + if peers: |
124 | + peers.sort(key=lambda u: int(u.split('/', 1)[-1])) |
125 | + self.peers = OrderedDict((peer, RelationInfo(relid, peer)) |
126 | + for peer in peers) |
127 | + else: |
128 | + self.peers = OrderedDict() |
129 | + else: |
130 | + self.peers = None |
131 | + |
132 | + def __str__(self): |
133 | + return '{} ({})'.format(self.relid, self.service) |
134 | + |
135 | + |
136 | +class RelationInfo(UserDict): |
137 | + '''The bag of data at an end of a relation. |
138 | + |
139 | + Every unit participating in a relation has a single bag of |
140 | + data associated with that relation. This is that bag. |
141 | + |
142 | + The bag of data for the local unit may be updated. Remote data |
143 | + is immutable and will remain static for the duration of the hook. |
144 | + |
145 | + Changes made to the local units relation data only become visible |
146 | + to other units after the hook completes successfully. If the hook |
147 | + does not complete successfully, the changes are rolled back. |
148 | + |
149 | + Unlike standard Python mappings, setting an item to None is the |
150 | + same as deleting it. |
151 | + |
152 | + >>> relinfo = RelationInfo('db:12') # Default is the local unit. |
153 | + >>> relinfo['user'] = 'fred' |
154 | + >>> relinfo['user'] |
155 | + 'fred' |
156 | + >>> relinfo['user'] = None |
157 | + >>> 'fred' in relinfo |
158 | + False |
159 | + |
160 | + This class wraps hookenv.relation_get and hookenv.relation_set. |
161 | + All caching is left up to these two methods to avoid synchronization |
162 | + issues. Data is only loaded on demand. |
163 | + ''' |
164 | + relid = None # The relation id. |
165 | + relname = None # The relation name (also know as the relation type). |
166 | + unit = None # The unit id. |
167 | + number = None # The unit number (integer). |
168 | + service = None # The service name. |
169 | + |
170 | + def __init__(self, relid, unit): |
171 | + self.relname = relid.split(':', 1)[0] |
172 | + self.relid = relid |
173 | + self.unit = unit |
174 | + self.service, num = self.unit.split('/', 1) |
175 | + self.number = int(num) |
176 | + |
177 | + def __str__(self): |
178 | + return '{} ({})'.format(self.relid, self.unit) |
179 | + |
180 | + @property |
181 | + def data(self): |
182 | + return hookenv.relation_get(rid=self.relid, unit=self.unit) |
183 | + |
184 | + def __setitem__(self, key, value): |
185 | + if self.unit != hookenv.local_unit(): |
186 | + raise TypeError('Attempting to set {} on remote unit {}' |
187 | + ''.format(key, self.unit)) |
188 | + if value is not None and not isinstance(value, six.string_types): |
189 | + # We don't do implicit casting. This would cause simple |
190 | + # types like integers to be read back as strings in subsequent |
191 | + # hooks, and mutable types would require a lot of wrapping |
192 | + # to ensure relation-set gets called when they are mutated. |
193 | + raise ValueError('Only string values allowed') |
194 | + hookenv.relation_set(self.relid, {key: value}) |
195 | + |
196 | + def __delitem__(self, key): |
197 | + # Deleting a key and setting it to null is the same thing in |
198 | + # Juju relations. |
199 | + self[key] = None |
200 | + |
201 | + |
202 | +class Leader(UserDict): |
203 | + def __init__(self): |
204 | + pass # Don't call superclass initializer, as it will nuke self.data |
205 | + |
206 | + @property |
207 | + def data(self): |
208 | + return hookenv.leader_get() |
209 | + |
210 | + def __setitem__(self, key, value): |
211 | + if not hookenv.is_leader(): |
212 | + raise TypeError('Not the leader. Cannot change leader settings.') |
213 | + if value is not None and not isinstance(value, six.string_types): |
214 | + # We don't do implicit casting. This would cause simple |
215 | + # types like integers to be read back as strings in subsequent |
216 | + # hooks, and mutable types would require a lot of wrapping |
217 | + # to ensure leader-set gets called when they are mutated. |
218 | + raise ValueError('Only string values allowed') |
219 | + hookenv.leader_set({key: value}) |
220 | + |
221 | + def __delitem__(self, key): |
222 | + # Deleting a key and setting it to null is the same thing in |
223 | + # Juju leadership settings. |
224 | + self[key] = None |
225 | |
226 | === added directory 'hooks/charmhelpers/contrib' |
227 | === added file 'hooks/charmhelpers/contrib/__init__.py' |
228 | --- hooks/charmhelpers/contrib/__init__.py 1970-01-01 00:00:00 +0000 |
229 | +++ hooks/charmhelpers/contrib/__init__.py 2015-08-11 11:19:35 +0000 |
230 | @@ -0,0 +1,15 @@ |
231 | +# Copyright 2014-2015 Canonical Limited. |
232 | +# |
233 | +# This file is part of charm-helpers. |
234 | +# |
235 | +# charm-helpers is free software: you can redistribute it and/or modify |
236 | +# it under the terms of the GNU Lesser General Public License version 3 as |
237 | +# published by the Free Software Foundation. |
238 | +# |
239 | +# charm-helpers is distributed in the hope that it will be useful, |
240 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
241 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
242 | +# GNU Lesser General Public License for more details. |
243 | +# |
244 | +# You should have received a copy of the GNU Lesser General Public License |
245 | +# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. |
246 | |
247 | === added directory 'hooks/charmhelpers/contrib/charmsupport' |
248 | === added file 'hooks/charmhelpers/contrib/charmsupport/__init__.py' |
249 | --- hooks/charmhelpers/contrib/charmsupport/__init__.py 1970-01-01 00:00:00 +0000 |
250 | +++ hooks/charmhelpers/contrib/charmsupport/__init__.py 2015-08-11 11:19:35 +0000 |
251 | @@ -0,0 +1,15 @@ |
252 | +# Copyright 2014-2015 Canonical Limited. |
253 | +# |
254 | +# This file is part of charm-helpers. |
255 | +# |
256 | +# charm-helpers is free software: you can redistribute it and/or modify |
257 | +# it under the terms of the GNU Lesser General Public License version 3 as |
258 | +# published by the Free Software Foundation. |
259 | +# |
260 | +# charm-helpers is distributed in the hope that it will be useful, |
261 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
262 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
263 | +# GNU Lesser General Public License for more details. |
264 | +# |
265 | +# You should have received a copy of the GNU Lesser General Public License |
266 | +# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. |
267 | |
268 | === added file 'hooks/charmhelpers/contrib/charmsupport/nrpe.py' |
269 | --- hooks/charmhelpers/contrib/charmsupport/nrpe.py 1970-01-01 00:00:00 +0000 |
270 | +++ hooks/charmhelpers/contrib/charmsupport/nrpe.py 2015-08-11 11:19:35 +0000 |
271 | @@ -0,0 +1,360 @@ |
272 | +# Copyright 2014-2015 Canonical Limited. |
273 | +# |
274 | +# This file is part of charm-helpers. |
275 | +# |
276 | +# charm-helpers is free software: you can redistribute it and/or modify |
277 | +# it under the terms of the GNU Lesser General Public License version 3 as |
278 | +# published by the Free Software Foundation. |
279 | +# |
280 | +# charm-helpers is distributed in the hope that it will be useful, |
281 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
282 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
283 | +# GNU Lesser General Public License for more details. |
284 | +# |
285 | +# You should have received a copy of the GNU Lesser General Public License |
286 | +# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. |
287 | + |
288 | +"""Compatibility with the nrpe-external-master charm""" |
289 | +# Copyright 2012 Canonical Ltd. |
290 | +# |
291 | +# Authors: |
292 | +# Matthew Wedgwood <matthew.wedgwood@canonical.com> |
293 | + |
294 | +import subprocess |
295 | +import pwd |
296 | +import grp |
297 | +import os |
298 | +import glob |
299 | +import shutil |
300 | +import re |
301 | +import shlex |
302 | +import yaml |
303 | + |
304 | +from charmhelpers.core.hookenv import ( |
305 | + config, |
306 | + local_unit, |
307 | + log, |
308 | + relation_ids, |
309 | + relation_set, |
310 | + relations_of_type, |
311 | +) |
312 | + |
313 | +from charmhelpers.core.host import service |
314 | + |
315 | +# This module adds compatibility with the nrpe-external-master and plain nrpe |
316 | +# subordinate charms. To use it in your charm: |
317 | +# |
318 | +# 1. Update metadata.yaml |
319 | +# |
320 | +# provides: |
321 | +# (...) |
322 | +# nrpe-external-master: |
323 | +# interface: nrpe-external-master |
324 | +# scope: container |
325 | +# |
326 | +# and/or |
327 | +# |
328 | +# provides: |
329 | +# (...) |
330 | +# local-monitors: |
331 | +# interface: local-monitors |
332 | +# scope: container |
333 | + |
334 | +# |
335 | +# 2. Add the following to config.yaml |
336 | +# |
337 | +# nagios_context: |
338 | +# default: "juju" |
339 | +# type: string |
340 | +# description: | |
341 | +# Used by the nrpe subordinate charms. |
342 | +# A string that will be prepended to instance name to set the host name |
343 | +# in nagios. So for instance the hostname would be something like: |
344 | +# juju-myservice-0 |
345 | +# If you're running multiple environments with the same services in them |
346 | +# this allows you to differentiate between them. |
347 | +# nagios_servicegroups: |
348 | +# default: "" |
349 | +# type: string |
350 | +# description: | |
351 | +# A comma-separated list of nagios servicegroups. |
352 | +# If left empty, the nagios_context will be used as the servicegroup |
353 | +# |
354 | +# 3. Add custom checks (Nagios plugins) to files/nrpe-external-master |
355 | +# |
356 | +# 4. Update your hooks.py with something like this: |
357 | +# |
358 | +# from charmsupport.nrpe import NRPE |
359 | +# (...) |
360 | +# def update_nrpe_config(): |
361 | +# nrpe_compat = NRPE() |
362 | +# nrpe_compat.add_check( |
363 | +# shortname = "myservice", |
364 | +# description = "Check MyService", |
365 | +# check_cmd = "check_http -w 2 -c 10 http://localhost" |
366 | +# ) |
367 | +# nrpe_compat.add_check( |
368 | +# "myservice_other", |
369 | +# "Check for widget failures", |
370 | +# check_cmd = "/srv/myapp/scripts/widget_check" |
371 | +# ) |
372 | +# nrpe_compat.write() |
373 | +# |
374 | +# def config_changed(): |
375 | +# (...) |
376 | +# update_nrpe_config() |
377 | +# |
378 | +# def nrpe_external_master_relation_changed(): |
379 | +# update_nrpe_config() |
380 | +# |
381 | +# def local_monitors_relation_changed(): |
382 | +# update_nrpe_config() |
383 | +# |
384 | +# 5. ln -s hooks.py nrpe-external-master-relation-changed |
385 | +# ln -s hooks.py local-monitors-relation-changed |
386 | + |
387 | + |
388 | +class CheckException(Exception): |
389 | + pass |
390 | + |
391 | + |
392 | +class Check(object): |
393 | + shortname_re = '[A-Za-z0-9-_]+$' |
394 | + service_template = (""" |
395 | +#--------------------------------------------------- |
396 | +# This file is Juju managed |
397 | +#--------------------------------------------------- |
398 | +define service {{ |
399 | + use active-service |
400 | + host_name {nagios_hostname} |
401 | + service_description {nagios_hostname}[{shortname}] """ |
402 | + """{description} |
403 | + check_command check_nrpe!{command} |
404 | + servicegroups {nagios_servicegroup} |
405 | +}} |
406 | +""") |
407 | + |
408 | + def __init__(self, shortname, description, check_cmd): |
409 | + super(Check, self).__init__() |
410 | + # XXX: could be better to calculate this from the service name |
411 | + if not re.match(self.shortname_re, shortname): |
412 | + raise CheckException("shortname must match {}".format( |
413 | + Check.shortname_re)) |
414 | + self.shortname = shortname |
415 | + self.command = "check_{}".format(shortname) |
416 | + # Note: a set of invalid characters is defined by the |
417 | + # Nagios server config |
418 | + # The default is: illegal_object_name_chars=`~!$%^&*"|'<>?,()= |
419 | + self.description = description |
420 | + self.check_cmd = self._locate_cmd(check_cmd) |
421 | + |
422 | + def _locate_cmd(self, check_cmd): |
423 | + search_path = ( |
424 | + '/usr/lib/nagios/plugins', |
425 | + '/usr/local/lib/nagios/plugins', |
426 | + ) |
427 | + parts = shlex.split(check_cmd) |
428 | + for path in search_path: |
429 | + if os.path.exists(os.path.join(path, parts[0])): |
430 | + command = os.path.join(path, parts[0]) |
431 | + if len(parts) > 1: |
432 | + command += " " + " ".join(parts[1:]) |
433 | + return command |
434 | + log('Check command not found: {}'.format(parts[0])) |
435 | + return '' |
436 | + |
437 | + def write(self, nagios_context, hostname, nagios_servicegroups): |
438 | + nrpe_check_file = '/etc/nagios/nrpe.d/{}.cfg'.format( |
439 | + self.command) |
440 | + with open(nrpe_check_file, 'w') as nrpe_check_config: |
441 | + nrpe_check_config.write("# check {}\n".format(self.shortname)) |
442 | + nrpe_check_config.write("command[{}]={}\n".format( |
443 | + self.command, self.check_cmd)) |
444 | + |
445 | + if not os.path.exists(NRPE.nagios_exportdir): |
446 | + log('Not writing service config as {} is not accessible'.format( |
447 | + NRPE.nagios_exportdir)) |
448 | + else: |
449 | + self.write_service_config(nagios_context, hostname, |
450 | + nagios_servicegroups) |
451 | + |
452 | + def write_service_config(self, nagios_context, hostname, |
453 | + nagios_servicegroups): |
454 | + for f in os.listdir(NRPE.nagios_exportdir): |
455 | + if re.search('.*{}.cfg'.format(self.command), f): |
456 | + os.remove(os.path.join(NRPE.nagios_exportdir, f)) |
457 | + |
458 | + templ_vars = { |
459 | + 'nagios_hostname': hostname, |
460 | + 'nagios_servicegroup': nagios_servicegroups, |
461 | + 'description': self.description, |
462 | + 'shortname': self.shortname, |
463 | + 'command': self.command, |
464 | + } |
465 | + nrpe_service_text = Check.service_template.format(**templ_vars) |
466 | + nrpe_service_file = '{}/service__{}_{}.cfg'.format( |
467 | + NRPE.nagios_exportdir, hostname, self.command) |
468 | + with open(nrpe_service_file, 'w') as nrpe_service_config: |
469 | + nrpe_service_config.write(str(nrpe_service_text)) |
470 | + |
471 | + def run(self): |
472 | + subprocess.call(self.check_cmd) |
473 | + |
474 | + |
475 | +class NRPE(object): |
476 | + nagios_logdir = '/var/log/nagios' |
477 | + nagios_exportdir = '/var/lib/nagios/export' |
478 | + nrpe_confdir = '/etc/nagios/nrpe.d' |
479 | + |
480 | + def __init__(self, hostname=None): |
481 | + super(NRPE, self).__init__() |
482 | + self.config = config() |
483 | + self.nagios_context = self.config['nagios_context'] |
484 | + if 'nagios_servicegroups' in self.config and self.config['nagios_servicegroups']: |
485 | + self.nagios_servicegroups = self.config['nagios_servicegroups'] |
486 | + else: |
487 | + self.nagios_servicegroups = self.nagios_context |
488 | + self.unit_name = local_unit().replace('/', '-') |
489 | + if hostname: |
490 | + self.hostname = hostname |
491 | + else: |
492 | + self.hostname = "{}-{}".format(self.nagios_context, self.unit_name) |
493 | + self.checks = [] |
494 | + |
495 | + def add_check(self, *args, **kwargs): |
496 | + self.checks.append(Check(*args, **kwargs)) |
497 | + |
498 | + def write(self): |
499 | + try: |
500 | + nagios_uid = pwd.getpwnam('nagios').pw_uid |
501 | + nagios_gid = grp.getgrnam('nagios').gr_gid |
502 | + except: |
503 | + log("Nagios user not set up, nrpe checks not updated") |
504 | + return |
505 | + |
506 | + if not os.path.exists(NRPE.nagios_logdir): |
507 | + os.mkdir(NRPE.nagios_logdir) |
508 | + os.chown(NRPE.nagios_logdir, nagios_uid, nagios_gid) |
509 | + |
510 | + nrpe_monitors = {} |
511 | + monitors = {"monitors": {"remote": {"nrpe": nrpe_monitors}}} |
512 | + for nrpecheck in self.checks: |
513 | + nrpecheck.write(self.nagios_context, self.hostname, |
514 | + self.nagios_servicegroups) |
515 | + nrpe_monitors[nrpecheck.shortname] = { |
516 | + "command": nrpecheck.command, |
517 | + } |
518 | + |
519 | + service('restart', 'nagios-nrpe-server') |
520 | + |
521 | + monitor_ids = relation_ids("local-monitors") + \ |
522 | + relation_ids("nrpe-external-master") |
523 | + for rid in monitor_ids: |
524 | + relation_set(relation_id=rid, monitors=yaml.dump(monitors)) |
525 | + |
526 | + |
527 | +def get_nagios_hostcontext(relation_name='nrpe-external-master'): |
528 | + """ |
529 | + Query relation with nrpe subordinate, return the nagios_host_context |
530 | + |
531 | + :param str relation_name: Name of relation nrpe sub joined to |
532 | + """ |
533 | + for rel in relations_of_type(relation_name): |
534 | + if 'nagios_hostname' in rel: |
535 | + return rel['nagios_host_context'] |
536 | + |
537 | + |
538 | +def get_nagios_hostname(relation_name='nrpe-external-master'): |
539 | + """ |
540 | + Query relation with nrpe subordinate, return the nagios_hostname |
541 | + |
542 | + :param str relation_name: Name of relation nrpe sub joined to |
543 | + """ |
544 | + for rel in relations_of_type(relation_name): |
545 | + if 'nagios_hostname' in rel: |
546 | + return rel['nagios_hostname'] |
547 | + |
548 | + |
549 | +def get_nagios_unit_name(relation_name='nrpe-external-master'): |
550 | + """ |
551 | + Return the nagios unit name prepended with host_context if needed |
552 | + |
553 | + :param str relation_name: Name of relation nrpe sub joined to |
554 | + """ |
555 | + host_context = get_nagios_hostcontext(relation_name) |
556 | + if host_context: |
557 | + unit = "%s:%s" % (host_context, local_unit()) |
558 | + else: |
559 | + unit = local_unit() |
560 | + return unit |
561 | + |
562 | + |
563 | +def add_init_service_checks(nrpe, services, unit_name): |
564 | + """ |
565 | + Add checks for each service in list |
566 | + |
567 | + :param NRPE nrpe: NRPE object to add check to |
568 | + :param list services: List of services to check |
569 | + :param str unit_name: Unit name to use in check description |
570 | + """ |
571 | + for svc in services: |
572 | + upstart_init = '/etc/init/%s.conf' % svc |
573 | + sysv_init = '/etc/init.d/%s' % svc |
574 | + if os.path.exists(upstart_init): |
575 | + nrpe.add_check( |
576 | + shortname=svc, |
577 | + description='process check {%s}' % unit_name, |
578 | + check_cmd='check_upstart_job %s' % svc |
579 | + ) |
580 | + elif os.path.exists(sysv_init): |
581 | + cronpath = '/etc/cron.d/nagios-service-check-%s' % svc |
582 | + cron_file = ('*/5 * * * * root ' |
583 | + '/usr/local/lib/nagios/plugins/check_exit_status.pl ' |
584 | + '-s /etc/init.d/%s status > ' |
585 | + '/var/lib/nagios/service-check-%s.txt\n' % (svc, |
586 | + svc) |
587 | + ) |
588 | + f = open(cronpath, 'w') |
589 | + f.write(cron_file) |
590 | + f.close() |
591 | + nrpe.add_check( |
592 | + shortname=svc, |
593 | + description='process check {%s}' % unit_name, |
594 | + check_cmd='check_status_file.py -f ' |
595 | + '/var/lib/nagios/service-check-%s.txt' % svc, |
596 | + ) |
597 | + |
598 | + |
599 | +def copy_nrpe_checks(): |
600 | + """ |
601 | + Copy the nrpe checks into place |
602 | + |
603 | + """ |
604 | + NAGIOS_PLUGINS = '/usr/local/lib/nagios/plugins' |
605 | + nrpe_files_dir = os.path.join(os.getenv('CHARM_DIR'), 'hooks', |
606 | + 'charmhelpers', 'contrib', 'openstack', |
607 | + 'files') |
608 | + |
609 | + if not os.path.exists(NAGIOS_PLUGINS): |
610 | + os.makedirs(NAGIOS_PLUGINS) |
611 | + for fname in glob.glob(os.path.join(nrpe_files_dir, "check_*")): |
612 | + if os.path.isfile(fname): |
613 | + shutil.copy2(fname, |
614 | + os.path.join(NAGIOS_PLUGINS, os.path.basename(fname))) |
615 | + |
616 | + |
617 | +def add_haproxy_checks(nrpe, unit_name): |
618 | + """ |
619 | + Add checks for each service in list |
620 | + |
621 | + :param NRPE nrpe: NRPE object to add check to |
622 | + :param str unit_name: Unit name to use in check description |
623 | + """ |
624 | + nrpe.add_check( |
625 | + shortname='haproxy_servers', |
626 | + description='Check HAProxy {%s}' % unit_name, |
627 | + check_cmd='check_haproxy.sh') |
628 | + nrpe.add_check( |
629 | + shortname='haproxy_queue', |
630 | + description='Check HAProxy queue depth {%s}' % unit_name, |
631 | + check_cmd='check_haproxy_queue_depth.sh') |
632 | |
633 | === added file 'hooks/charmhelpers/core/files.py' |
634 | --- hooks/charmhelpers/core/files.py 1970-01-01 00:00:00 +0000 |
635 | +++ hooks/charmhelpers/core/files.py 2015-08-11 11:19:35 +0000 |
636 | @@ -0,0 +1,45 @@ |
637 | +#!/usr/bin/env python |
638 | +# -*- coding: utf-8 -*- |
639 | + |
640 | +# Copyright 2014-2015 Canonical Limited. |
641 | +# |
642 | +# This file is part of charm-helpers. |
643 | +# |
644 | +# charm-helpers is free software: you can redistribute it and/or modify |
645 | +# it under the terms of the GNU Lesser General Public License version 3 as |
646 | +# published by the Free Software Foundation. |
647 | +# |
648 | +# charm-helpers is distributed in the hope that it will be useful, |
649 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
650 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
651 | +# GNU Lesser General Public License for more details. |
652 | +# |
653 | +# You should have received a copy of the GNU Lesser General Public License |
654 | +# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. |
655 | + |
656 | +__author__ = 'Jorge Niedbalski <niedbalski@ubuntu.com>' |
657 | + |
658 | +import os |
659 | +import subprocess |
660 | + |
661 | + |
662 | +def sed(filename, before, after, flags='g'): |
663 | + """ |
664 | + Search and replaces the given pattern on filename. |
665 | + |
666 | + :param filename: relative or absolute file path. |
667 | + :param before: expression to be replaced (see 'man sed') |
668 | + :param after: expression to replace with (see 'man sed') |
669 | + :param flags: sed-compatible regex flags in example, to make |
670 | + the search and replace case insensitive, specify ``flags="i"``. |
671 | + The ``g`` flag is always specified regardless, so you do not |
672 | + need to remember to include it when overriding this parameter. |
673 | + :returns: If the sed command exit code was zero then return, |
674 | + otherwise raise CalledProcessError. |
675 | + """ |
676 | + expression = r's/{0}/{1}/{2}'.format(before, |
677 | + after, flags) |
678 | + |
679 | + return subprocess.check_call(["sed", "-i", "-r", "-e", |
680 | + expression, |
681 | + os.path.expanduser(filename)]) |
682 | |
683 | === modified file 'hooks/charmhelpers/core/hookenv.py' |
684 | --- hooks/charmhelpers/core/hookenv.py 2015-06-25 11:39:19 +0000 |
685 | +++ hooks/charmhelpers/core/hookenv.py 2015-08-11 11:19:35 +0000 |
686 | @@ -21,6 +21,7 @@ |
687 | # Charm Helpers Developers <juju@lists.ubuntu.com> |
688 | |
689 | from __future__ import print_function |
690 | +import copy |
691 | from distutils.version import LooseVersion |
692 | from functools import wraps |
693 | import glob |
694 | @@ -263,7 +264,7 @@ |
695 | self.path = path or self.path |
696 | with open(self.path) as f: |
697 | self._prev_dict = json.load(f) |
698 | - for k, v in self._prev_dict.items(): |
699 | + for k, v in copy.deepcopy(self._prev_dict).items(): |
700 | if k not in self: |
701 | self[k] = v |
702 | |
703 | @@ -468,6 +469,19 @@ |
704 | |
705 | |
706 | @cached |
707 | +def peer_relation_id(): |
708 | + '''Get a peer relation id if a peer relation has been joined, else None.''' |
709 | + md = metadata() |
710 | + section = md.get('peers') |
711 | + if section: |
712 | + for key in section: |
713 | + relids = relation_ids(key) |
714 | + if relids: |
715 | + return relids[0] |
716 | + return None |
717 | + |
718 | + |
719 | +@cached |
720 | def charm_name(): |
721 | """Get the name of the current charm as is specified on metadata.yaml""" |
722 | return metadata().get('name') |
723 | @@ -691,6 +705,7 @@ |
724 | |
725 | def translate_exc(from_exc, to_exc): |
726 | def inner_translate_exc1(f): |
727 | + @wraps(f) |
728 | def inner_translate_exc2(*args, **kwargs): |
729 | try: |
730 | return f(*args, **kwargs) |
731 | @@ -761,6 +776,7 @@ |
732 | |
733 | This is useful for modules and classes to perform initialization |
734 | and inject behavior. In particular: |
735 | + |
736 | - Run common code before all of your hooks, such as logging |
737 | the hook name or interesting relation data. |
738 | - Defer object or module initialization that requires a hook |
739 | |
740 | === modified file 'hooks/charmhelpers/core/host.py' |
741 | --- hooks/charmhelpers/core/host.py 2015-06-25 07:56:30 +0000 |
742 | +++ hooks/charmhelpers/core/host.py 2015-08-11 11:19:35 +0000 |
743 | @@ -63,6 +63,36 @@ |
744 | return service_result |
745 | |
746 | |
747 | +def service_pause(service_name, init_dir=None): |
748 | + """Pause a system service. |
749 | + |
750 | + Stop it, and prevent it from starting again at boot.""" |
751 | + if init_dir is None: |
752 | + init_dir = "/etc/init" |
753 | + stopped = service_stop(service_name) |
754 | + # XXX: Support systemd too |
755 | + override_path = os.path.join( |
756 | + init_dir, '{}.conf.override'.format(service_name)) |
757 | + with open(override_path, 'w') as fh: |
758 | + fh.write("manual\n") |
759 | + return stopped |
760 | + |
761 | + |
762 | +def service_resume(service_name, init_dir=None): |
763 | + """Resume a system service. |
764 | + |
765 | + Reenable starting again at boot. Start the service""" |
766 | + # XXX: Support systemd too |
767 | + if init_dir is None: |
768 | + init_dir = "/etc/init" |
769 | + override_path = os.path.join( |
770 | + init_dir, '{}.conf.override'.format(service_name)) |
771 | + if os.path.exists(override_path): |
772 | + os.unlink(override_path) |
773 | + started = service_start(service_name) |
774 | + return started |
775 | + |
776 | + |
777 | def service(action, service_name): |
778 | """Control a system service""" |
779 | cmd = ['service', service_name, action] |
780 | @@ -140,11 +170,7 @@ |
781 | |
782 | def add_user_to_group(username, group): |
783 | """Add a user to a group""" |
784 | - cmd = [ |
785 | - 'gpasswd', '-a', |
786 | - username, |
787 | - group |
788 | - ] |
789 | + cmd = ['gpasswd', '-a', username, group] |
790 | log("Adding user {} to group {}".format(username, group)) |
791 | subprocess.check_call(cmd) |
792 | |
793 | @@ -466,3 +492,19 @@ |
794 | |
795 | def lchownr(path, owner, group): |
796 | chownr(path, owner, group, follow_links=False) |
797 | + |
798 | + |
799 | +def get_total_ram(): |
800 | + '''The total amount of system RAM in bytes. |
801 | + |
802 | + This is what is reported by the OS, and may be overcommitted when |
803 | + there are multiple containers hosted on the same machine. |
804 | + ''' |
805 | + with open('/proc/meminfo', 'r') as f: |
806 | + for line in f.readlines(): |
807 | + if line: |
808 | + key, value, unit = line.split() |
809 | + if key == 'MemTotal:': |
810 | + assert unit == 'kB', 'Unknown unit' |
811 | + return int(value) * 1024 # Classic, not KiB. |
812 | + raise NotImplementedError() |
813 | |
814 | === modified file 'hooks/charmhelpers/core/services/helpers.py' |
815 | --- hooks/charmhelpers/core/services/helpers.py 2015-06-25 07:56:30 +0000 |
816 | +++ hooks/charmhelpers/core/services/helpers.py 2015-08-11 11:19:35 +0000 |
817 | @@ -239,12 +239,12 @@ |
818 | action. |
819 | |
820 | :param str source: The template source file, relative to |
821 | - `$CHARM_DIR/templates` |
822 | - |
823 | + `$CHARM_DIR/templates` |
824 | :param str target: The target to write the rendered template to |
825 | :param str owner: The owner of the rendered file |
826 | :param str group: The group of the rendered file |
827 | :param int perms: The permissions of the rendered file |
828 | + |
829 | """ |
830 | def __init__(self, source, target, |
831 | owner='root', group='root', perms=0o444): |
832 | |
833 | === modified file 'hooks/charmhelpers/core/templating.py' |
834 | --- hooks/charmhelpers/core/templating.py 2015-06-25 07:56:30 +0000 |
835 | +++ hooks/charmhelpers/core/templating.py 2015-08-11 11:19:35 +0000 |
836 | @@ -64,5 +64,9 @@ |
837 | level=hookenv.ERROR) |
838 | raise e |
839 | content = template.render(context) |
840 | - host.mkdir(os.path.dirname(target), owner, group, perms=0o755) |
841 | + target_dir = os.path.dirname(target) |
842 | + if not os.path.exists(target_dir): |
843 | + # This is a terrible default directory permission, as the file |
844 | + # or its siblings will often contain secrets. |
845 | + host.mkdir(os.path.dirname(target), owner, group, perms=0o755) |
846 | host.write_file(target, content.encode(encoding), owner, group, perms) |
847 | |
848 | === modified file 'hooks/charmhelpers/fetch/__init__.py' |
849 | --- hooks/charmhelpers/fetch/__init__.py 2015-06-25 07:56:30 +0000 |
850 | +++ hooks/charmhelpers/fetch/__init__.py 2015-08-11 11:19:35 +0000 |
851 | @@ -215,19 +215,27 @@ |
852 | _run_apt_command(cmd, fatal) |
853 | |
854 | |
855 | +def apt_mark(packages, mark, fatal=False): |
856 | + """Flag one or more packages using apt-mark""" |
857 | + log("Marking {} as {}".format(packages, mark)) |
858 | + cmd = ['apt-mark', mark] |
859 | + if isinstance(packages, six.string_types): |
860 | + cmd.append(packages) |
861 | + else: |
862 | + cmd.extend(packages) |
863 | + |
864 | + if fatal: |
865 | + subprocess.check_call(cmd, universal_newlines=True) |
866 | + else: |
867 | + subprocess.call(cmd, universal_newlines=True) |
868 | + |
869 | + |
870 | def apt_hold(packages, fatal=False): |
871 | - """Hold one or more packages""" |
872 | - cmd = ['apt-mark', 'hold'] |
873 | - if isinstance(packages, six.string_types): |
874 | - cmd.append(packages) |
875 | - else: |
876 | - cmd.extend(packages) |
877 | - log("Holding {}".format(packages)) |
878 | - |
879 | - if fatal: |
880 | - subprocess.check_call(cmd) |
881 | - else: |
882 | - subprocess.call(cmd) |
883 | + return apt_mark(packages, 'hold', fatal=fatal) |
884 | + |
885 | + |
886 | +def apt_unhold(packages, fatal=False): |
887 | + return apt_mark(packages, 'unhold', fatal=fatal) |
888 | |
889 | |
890 | def add_source(source, key=None): |
891 | @@ -370,8 +378,9 @@ |
892 | for handler in handlers: |
893 | try: |
894 | installed_to = handler.install(source, *args, **kwargs) |
895 | - except UnhandledSource: |
896 | - pass |
897 | + except UnhandledSource as e: |
898 | + log('Install source attempt unsuccessful: {}'.format(e), |
899 | + level='WARNING') |
900 | if not installed_to: |
901 | raise UnhandledSource("No handler found for source {}".format(source)) |
902 | return installed_to |
903 | |
904 | === modified file 'hooks/charmhelpers/fetch/archiveurl.py' |
905 | --- hooks/charmhelpers/fetch/archiveurl.py 2015-06-25 07:56:30 +0000 |
906 | +++ hooks/charmhelpers/fetch/archiveurl.py 2015-08-11 11:19:35 +0000 |
907 | @@ -77,6 +77,8 @@ |
908 | def can_handle(self, source): |
909 | url_parts = self.parse_url(source) |
910 | if url_parts.scheme not in ('http', 'https', 'ftp', 'file'): |
911 | + # XXX: Why is this returning a boolean and a string? It's |
912 | + # doomed to fail since "bool(can_handle('foo://'))" will be True. |
913 | return "Wrong source type" |
914 | if get_archive_handler(self.base_url(source)): |
915 | return True |
916 | @@ -155,7 +157,11 @@ |
917 | else: |
918 | algorithms = hashlib.algorithms_available |
919 | if key in algorithms: |
920 | - check_hash(dld_file, value, key) |
921 | + if len(value) != 1: |
922 | + raise TypeError( |
923 | + "Expected 1 hash value, not %d" % len(value)) |
924 | + expected = value[0] |
925 | + check_hash(dld_file, expected, key) |
926 | if checksum: |
927 | check_hash(dld_file, checksum, hash_type) |
928 | return extract(dld_file, dest) |
929 | |
930 | === modified file 'hooks/charmhelpers/fetch/giturl.py' |
931 | --- hooks/charmhelpers/fetch/giturl.py 2015-06-25 07:56:30 +0000 |
932 | +++ hooks/charmhelpers/fetch/giturl.py 2015-08-11 11:19:35 +0000 |
933 | @@ -67,7 +67,7 @@ |
934 | try: |
935 | self.clone(source, dest_dir, branch, depth) |
936 | except GitCommandError as e: |
937 | - raise UnhandledSource(e.message) |
938 | + raise UnhandledSource(e) |
939 | except OSError as e: |
940 | raise UnhandledSource(e.strerror) |
941 | return dest_dir |
942 | |
943 | === added directory 'hooks/charmhelpers/payload' |
944 | === added file 'hooks/charmhelpers/payload/__init__.py' |
945 | --- hooks/charmhelpers/payload/__init__.py 1970-01-01 00:00:00 +0000 |
946 | +++ hooks/charmhelpers/payload/__init__.py 2015-08-11 11:19:35 +0000 |
947 | @@ -0,0 +1,17 @@ |
948 | +# Copyright 2014-2015 Canonical Limited. |
949 | +# |
950 | +# This file is part of charm-helpers. |
951 | +# |
952 | +# charm-helpers is free software: you can redistribute it and/or modify |
953 | +# it under the terms of the GNU Lesser General Public License version 3 as |
954 | +# published by the Free Software Foundation. |
955 | +# |
956 | +# charm-helpers is distributed in the hope that it will be useful, |
957 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
958 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
959 | +# GNU Lesser General Public License for more details. |
960 | +# |
961 | +# You should have received a copy of the GNU Lesser General Public License |
962 | +# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. |
963 | + |
964 | +"Tools for working with files injected into a charm just before deployment." |
965 | |
966 | === added file 'hooks/charmhelpers/payload/archive.py' |
967 | --- hooks/charmhelpers/payload/archive.py 1970-01-01 00:00:00 +0000 |
968 | +++ hooks/charmhelpers/payload/archive.py 2015-08-11 11:19:35 +0000 |
969 | @@ -0,0 +1,73 @@ |
970 | +# Copyright 2014-2015 Canonical Limited. |
971 | +# |
972 | +# This file is part of charm-helpers. |
973 | +# |
974 | +# charm-helpers is free software: you can redistribute it and/or modify |
975 | +# it under the terms of the GNU Lesser General Public License version 3 as |
976 | +# published by the Free Software Foundation. |
977 | +# |
978 | +# charm-helpers is distributed in the hope that it will be useful, |
979 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
980 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
981 | +# GNU Lesser General Public License for more details. |
982 | +# |
983 | +# You should have received a copy of the GNU Lesser General Public License |
984 | +# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. |
985 | + |
986 | +import os |
987 | +import tarfile |
988 | +import zipfile |
989 | +from charmhelpers.core import ( |
990 | + host, |
991 | + hookenv, |
992 | +) |
993 | + |
994 | + |
995 | +class ArchiveError(Exception): |
996 | + pass |
997 | + |
998 | + |
999 | +def get_archive_handler(archive_name): |
1000 | + if os.path.isfile(archive_name): |
1001 | + if tarfile.is_tarfile(archive_name): |
1002 | + return extract_tarfile |
1003 | + elif zipfile.is_zipfile(archive_name): |
1004 | + return extract_zipfile |
1005 | + else: |
1006 | + # look at the file name |
1007 | + for ext in ('.tar', '.tar.gz', '.tgz', 'tar.bz2', '.tbz2', '.tbz'): |
1008 | + if archive_name.endswith(ext): |
1009 | + return extract_tarfile |
1010 | + for ext in ('.zip', '.jar'): |
1011 | + if archive_name.endswith(ext): |
1012 | + return extract_zipfile |
1013 | + |
1014 | + |
1015 | +def archive_dest_default(archive_name): |
1016 | + archive_file = os.path.basename(archive_name) |
1017 | + return os.path.join(hookenv.charm_dir(), "archives", archive_file) |
1018 | + |
1019 | + |
1020 | +def extract(archive_name, destpath=None): |
1021 | + handler = get_archive_handler(archive_name) |
1022 | + if handler: |
1023 | + if not destpath: |
1024 | + destpath = archive_dest_default(archive_name) |
1025 | + if not os.path.isdir(destpath): |
1026 | + host.mkdir(destpath) |
1027 | + handler(archive_name, destpath) |
1028 | + return destpath |
1029 | + else: |
1030 | + raise ArchiveError("No handler for archive") |
1031 | + |
1032 | + |
1033 | +def extract_tarfile(archive_name, destpath): |
1034 | + "Unpack a tar archive, optionally compressed" |
1035 | + archive = tarfile.open(archive_name) |
1036 | + archive.extractall(destpath) |
1037 | + |
1038 | + |
1039 | +def extract_zipfile(archive_name, destpath): |
1040 | + "Unpack a zip file" |
1041 | + archive = zipfile.ZipFile(archive_name) |
1042 | + archive.extractall(destpath) |
1043 | |
1044 | === added file 'hooks/charmhelpers/payload/execd.py' |
1045 | --- hooks/charmhelpers/payload/execd.py 1970-01-01 00:00:00 +0000 |
1046 | +++ hooks/charmhelpers/payload/execd.py 2015-08-11 11:19:35 +0000 |
1047 | @@ -0,0 +1,66 @@ |
1048 | +#!/usr/bin/env python |
1049 | + |
1050 | +# Copyright 2014-2015 Canonical Limited. |
1051 | +# |
1052 | +# This file is part of charm-helpers. |
1053 | +# |
1054 | +# charm-helpers is free software: you can redistribute it and/or modify |
1055 | +# it under the terms of the GNU Lesser General Public License version 3 as |
1056 | +# published by the Free Software Foundation. |
1057 | +# |
1058 | +# charm-helpers is distributed in the hope that it will be useful, |
1059 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
1060 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1061 | +# GNU Lesser General Public License for more details. |
1062 | +# |
1063 | +# You should have received a copy of the GNU Lesser General Public License |
1064 | +# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. |
1065 | + |
1066 | +import os |
1067 | +import sys |
1068 | +import subprocess |
1069 | +from charmhelpers.core import hookenv |
1070 | + |
1071 | + |
1072 | +def default_execd_dir(): |
1073 | + return os.path.join(os.environ['CHARM_DIR'], 'exec.d') |
1074 | + |
1075 | + |
1076 | +def execd_module_paths(execd_dir=None): |
1077 | + """Generate a list of full paths to modules within execd_dir.""" |
1078 | + if not execd_dir: |
1079 | + execd_dir = default_execd_dir() |
1080 | + |
1081 | + if not os.path.exists(execd_dir): |
1082 | + return |
1083 | + |
1084 | + for subpath in os.listdir(execd_dir): |
1085 | + module = os.path.join(execd_dir, subpath) |
1086 | + if os.path.isdir(module): |
1087 | + yield module |
1088 | + |
1089 | + |
1090 | +def execd_submodule_paths(command, execd_dir=None): |
1091 | + """Generate a list of full paths to the specified command within exec_dir. |
1092 | + """ |
1093 | + for module_path in execd_module_paths(execd_dir): |
1094 | + path = os.path.join(module_path, command) |
1095 | + if os.access(path, os.X_OK) and os.path.isfile(path): |
1096 | + yield path |
1097 | + |
1098 | + |
1099 | +def execd_run(command, execd_dir=None, die_on_error=False, stderr=None): |
1100 | + """Run command for each module within execd_dir which defines it.""" |
1101 | + for submodule_path in execd_submodule_paths(command, execd_dir): |
1102 | + try: |
1103 | + subprocess.check_call(submodule_path, shell=True, stderr=stderr) |
1104 | + except subprocess.CalledProcessError as e: |
1105 | + hookenv.log("Error ({}) running {}. Output: {}".format( |
1106 | + e.returncode, e.cmd, e.output)) |
1107 | + if die_on_error: |
1108 | + sys.exit(e.returncode) |
1109 | + |
1110 | + |
1111 | +def execd_preinstall(execd_dir=None): |
1112 | + """Run charm-pre-install for each module within execd_dir.""" |
1113 | + execd_run('charm-pre-install', execd_dir=execd_dir) |
Stub,
I did a quick sanity check and found some proof/lint errors that were pre-existing. Unrelated to this merge +1 LGTM.
Merged and pushed, thanks for breaking apart the noise.