Merge lp:~jacekn/charms/precise/mysql/n-e-m-relation into lp:charms/mysql
- Precise Pangolin (12.04)
- n-e-m-relation
- Merge into trunk
Proposed by
Jacek Nykis
Status: | Merged |
---|---|
Merged at revision: | 125 |
Proposed branch: | lp:~jacekn/charms/precise/mysql/n-e-m-relation |
Merge into: | lp:charms/mysql |
Diff against target: |
510 lines (+436/-2) 8 files modified
config.yaml (+10/-0) hooks/charmhelpers/contrib/charmsupport/nrpe.py (+219/-0) hooks/charmhelpers/contrib/charmsupport/volumes.py (+156/-0) hooks/charmhelpers/fetch/__init__.py (+1/-1) hooks/config-changed (+4/-0) hooks/nrpe_relations.py (+42/-0) metadata.yaml (+3/-0) revision (+1/-1) |
To merge this branch: | bzr merge lp:~jacekn/charms/precise/mysql/n-e-m-relation |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Jorge Niedbalski (community) | Needs Resubmitting | ||
charmers | Pending | ||
Review via email: mp+218785@code.launchpad.net |
Commit message
Description of the change
Added support for nrpe-external-
To post a comment you must log in.
Revision history for this message
Benjamin Saller (bcsaller) wrote : | # |
Revision history for this message
Jorge Niedbalski (niedbalski) wrote : | # |
Hello Jacek,
Thanks for your submission. I have only a minor code observation ( commented on the diff ) , also the charm proof command is warning about config.yml keys (key, source, ) because they don't have default values, on which case default values should be : "".
Please review this few changes prior to get my +1.
review:
Needs Resubmitting
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'config.yaml' |
2 | --- config.yaml 2014-04-17 11:29:24 +0000 |
3 | +++ config.yaml 2014-05-08 11:31:23 +0000 |
4 | @@ -94,3 +94,13 @@ |
5 | description: | |
6 | Key ID to import to the apt keyring to support use with arbitary source |
7 | configuration from outside of Launchpad archives or PPA's. |
8 | + nagios_context: |
9 | + default: "juju" |
10 | + type: string |
11 | + description: | |
12 | + Used by the nrpe-external-master subordinate charm. |
13 | + A string that will be prepended to instance name to set the host name |
14 | + in nagios. So for instance the hostname would be something like: |
15 | + juju-myservice-0 |
16 | + If you're running multiple environments with the same services in them |
17 | + this allows you to differentiate between them. |
18 | |
19 | === added directory 'hooks/charmhelpers/contrib' |
20 | === added file 'hooks/charmhelpers/contrib/__init__.py' |
21 | === added directory 'hooks/charmhelpers/contrib/charmsupport' |
22 | === added file 'hooks/charmhelpers/contrib/charmsupport/__init__.py' |
23 | === added file 'hooks/charmhelpers/contrib/charmsupport/nrpe.py' |
24 | --- hooks/charmhelpers/contrib/charmsupport/nrpe.py 1970-01-01 00:00:00 +0000 |
25 | +++ hooks/charmhelpers/contrib/charmsupport/nrpe.py 2014-05-08 11:31:23 +0000 |
26 | @@ -0,0 +1,219 @@ |
27 | +"""Compatibility with the nrpe-external-master charm""" |
28 | +# Copyright 2012 Canonical Ltd. |
29 | +# |
30 | +# Authors: |
31 | +# Matthew Wedgwood <matthew.wedgwood@canonical.com> |
32 | + |
33 | +import subprocess |
34 | +import pwd |
35 | +import grp |
36 | +import os |
37 | +import re |
38 | +import shlex |
39 | +import yaml |
40 | + |
41 | +from charmhelpers.core.hookenv import ( |
42 | + config, |
43 | + local_unit, |
44 | + log, |
45 | + relation_ids, |
46 | + relation_set, |
47 | +) |
48 | + |
49 | +from charmhelpers.core.host import service |
50 | + |
51 | +# This module adds compatibility with the nrpe-external-master and plain nrpe |
52 | +# subordinate charms. To use it in your charm: |
53 | +# |
54 | +# 1. Update metadata.yaml |
55 | +# |
56 | +# provides: |
57 | +# (...) |
58 | +# nrpe-external-master: |
59 | +# interface: nrpe-external-master |
60 | +# scope: container |
61 | +# |
62 | +# and/or |
63 | +# |
64 | +# provides: |
65 | +# (...) |
66 | +# local-monitors: |
67 | +# interface: local-monitors |
68 | +# scope: container |
69 | + |
70 | +# |
71 | +# 2. Add the following to config.yaml |
72 | +# |
73 | +# nagios_context: |
74 | +# default: "juju" |
75 | +# type: string |
76 | +# description: | |
77 | +# Used by the nrpe subordinate charms. |
78 | +# A string that will be prepended to instance name to set the host name |
79 | +# in nagios. So for instance the hostname would be something like: |
80 | +# juju-myservice-0 |
81 | +# If you're running multiple environments with the same services in them |
82 | +# this allows you to differentiate between them. |
83 | +# |
84 | +# 3. Add custom checks (Nagios plugins) to files/nrpe-external-master |
85 | +# |
86 | +# 4. Update your hooks.py with something like this: |
87 | +# |
88 | +# from charmsupport.nrpe import NRPE |
89 | +# (...) |
90 | +# def update_nrpe_config(): |
91 | +# nrpe_compat = NRPE() |
92 | +# nrpe_compat.add_check( |
93 | +# shortname = "myservice", |
94 | +# description = "Check MyService", |
95 | +# check_cmd = "check_http -w 2 -c 10 http://localhost" |
96 | +# ) |
97 | +# nrpe_compat.add_check( |
98 | +# "myservice_other", |
99 | +# "Check for widget failures", |
100 | +# check_cmd = "/srv/myapp/scripts/widget_check" |
101 | +# ) |
102 | +# nrpe_compat.write() |
103 | +# |
104 | +# def config_changed(): |
105 | +# (...) |
106 | +# update_nrpe_config() |
107 | +# |
108 | +# def nrpe_external_master_relation_changed(): |
109 | +# update_nrpe_config() |
110 | +# |
111 | +# def local_monitors_relation_changed(): |
112 | +# update_nrpe_config() |
113 | +# |
114 | +# 5. ln -s hooks.py nrpe-external-master-relation-changed |
115 | +# ln -s hooks.py local-monitors-relation-changed |
116 | + |
117 | + |
118 | +class CheckException(Exception): |
119 | + pass |
120 | + |
121 | + |
122 | +class Check(object): |
123 | + shortname_re = '[A-Za-z0-9-_]+$' |
124 | + service_template = (""" |
125 | +#--------------------------------------------------- |
126 | +# This file is Juju managed |
127 | +#--------------------------------------------------- |
128 | +define service {{ |
129 | + use active-service |
130 | + host_name {nagios_hostname} |
131 | + service_description {nagios_hostname}[{shortname}] """ |
132 | + """{description} |
133 | + check_command check_nrpe!{command} |
134 | + servicegroups {nagios_servicegroup} |
135 | +}} |
136 | +""") |
137 | + |
138 | + def __init__(self, shortname, description, check_cmd): |
139 | + super(Check, self).__init__() |
140 | + # XXX: could be better to calculate this from the service name |
141 | + if not re.match(self.shortname_re, shortname): |
142 | + raise CheckException("shortname must match {}".format( |
143 | + Check.shortname_re)) |
144 | + self.shortname = shortname |
145 | + self.command = "check_{}".format(shortname) |
146 | + # Note: a set of invalid characters is defined by the |
147 | + # Nagios server config |
148 | + # The default is: illegal_object_name_chars=`~!$%^&*"|'<>?,()= |
149 | + self.description = description |
150 | + self.check_cmd = self._locate_cmd(check_cmd) |
151 | + |
152 | + def _locate_cmd(self, check_cmd): |
153 | + search_path = ( |
154 | + '/usr/lib/nagios/plugins', |
155 | + '/usr/local/lib/nagios/plugins', |
156 | + ) |
157 | + parts = shlex.split(check_cmd) |
158 | + for path in search_path: |
159 | + if os.path.exists(os.path.join(path, parts[0])): |
160 | + command = os.path.join(path, parts[0]) |
161 | + if len(parts) > 1: |
162 | + command += " " + " ".join(parts[1:]) |
163 | + return command |
164 | + log('Check command not found: {}'.format(parts[0])) |
165 | + return '' |
166 | + |
167 | + def write(self, nagios_context, hostname): |
168 | + nrpe_check_file = '/etc/nagios/nrpe.d/{}.cfg'.format( |
169 | + self.command) |
170 | + with open(nrpe_check_file, 'w') as nrpe_check_config: |
171 | + nrpe_check_config.write("# check {}\n".format(self.shortname)) |
172 | + nrpe_check_config.write("command[{}]={}\n".format( |
173 | + self.command, self.check_cmd)) |
174 | + |
175 | + if not os.path.exists(NRPE.nagios_exportdir): |
176 | + log('Not writing service config as {} is not accessible'.format( |
177 | + NRPE.nagios_exportdir)) |
178 | + else: |
179 | + self.write_service_config(nagios_context, hostname) |
180 | + |
181 | + def write_service_config(self, nagios_context, hostname): |
182 | + for f in os.listdir(NRPE.nagios_exportdir): |
183 | + if re.search('.*{}.cfg'.format(self.command), f): |
184 | + os.remove(os.path.join(NRPE.nagios_exportdir, f)) |
185 | + |
186 | + templ_vars = { |
187 | + 'nagios_hostname': hostname, |
188 | + 'nagios_servicegroup': nagios_context, |
189 | + 'description': self.description, |
190 | + 'shortname': self.shortname, |
191 | + 'command': self.command, |
192 | + } |
193 | + nrpe_service_text = Check.service_template.format(**templ_vars) |
194 | + nrpe_service_file = '{}/service__{}_{}.cfg'.format( |
195 | + NRPE.nagios_exportdir, hostname, self.command) |
196 | + with open(nrpe_service_file, 'w') as nrpe_service_config: |
197 | + nrpe_service_config.write(str(nrpe_service_text)) |
198 | + |
199 | + def run(self): |
200 | + subprocess.call(self.check_cmd) |
201 | + |
202 | + |
203 | +class NRPE(object): |
204 | + nagios_logdir = '/var/log/nagios' |
205 | + nagios_exportdir = '/var/lib/nagios/export' |
206 | + nrpe_confdir = '/etc/nagios/nrpe.d' |
207 | + |
208 | + def __init__(self, hostname=None): |
209 | + super(NRPE, self).__init__() |
210 | + self.config = config() |
211 | + self.nagios_context = self.config['nagios_context'] |
212 | + self.unit_name = local_unit().replace('/', '-') |
213 | + if hostname: |
214 | + self.hostname = hostname |
215 | + else: |
216 | + self.hostname = "{}-{}".format(self.nagios_context, self.unit_name) |
217 | + self.checks = [] |
218 | + |
219 | + def add_check(self, *args, **kwargs): |
220 | + self.checks.append(Check(*args, **kwargs)) |
221 | + |
222 | + def write(self): |
223 | + try: |
224 | + nagios_uid = pwd.getpwnam('nagios').pw_uid |
225 | + nagios_gid = grp.getgrnam('nagios').gr_gid |
226 | + except: |
227 | + log("Nagios user not set up, nrpe checks not updated") |
228 | + return |
229 | + |
230 | + if not os.path.exists(NRPE.nagios_logdir): |
231 | + os.mkdir(NRPE.nagios_logdir) |
232 | + os.chown(NRPE.nagios_logdir, nagios_uid, nagios_gid) |
233 | + |
234 | + nrpe_monitors = {} |
235 | + monitors = {"monitors": {"remote": {"nrpe": nrpe_monitors}}} |
236 | + for nrpecheck in self.checks: |
237 | + nrpecheck.write(self.nagios_context, self.hostname) |
238 | + nrpe_monitors[nrpecheck.shortname] = { |
239 | + "command": nrpecheck.command, |
240 | + } |
241 | + |
242 | + service('restart', 'nagios-nrpe-server') |
243 | + |
244 | + for rid in relation_ids("local-monitors"): |
245 | + relation_set(relation_id=rid, monitors=yaml.dump(monitors)) |
246 | |
247 | === added file 'hooks/charmhelpers/contrib/charmsupport/volumes.py' |
248 | --- hooks/charmhelpers/contrib/charmsupport/volumes.py 1970-01-01 00:00:00 +0000 |
249 | +++ hooks/charmhelpers/contrib/charmsupport/volumes.py 2014-05-08 11:31:23 +0000 |
250 | @@ -0,0 +1,156 @@ |
251 | +''' |
252 | +Functions for managing volumes in juju units. One volume is supported per unit. |
253 | +Subordinates may have their own storage, provided it is on its own partition. |
254 | + |
255 | +Configuration stanzas: |
256 | + volume-ephemeral: |
257 | + type: boolean |
258 | + default: true |
259 | + description: > |
260 | + If false, a volume is mounted as sepecified in "volume-map" |
261 | + If true, ephemeral storage will be used, meaning that log data |
262 | + will only exist as long as the machine. YOU HAVE BEEN WARNED. |
263 | + volume-map: |
264 | + type: string |
265 | + default: {} |
266 | + description: > |
267 | + YAML map of units to device names, e.g: |
268 | + "{ rsyslog/0: /dev/vdb, rsyslog/1: /dev/vdb }" |
269 | + Service units will raise a configure-error if volume-ephemeral |
270 | + is 'true' and no volume-map value is set. Use 'juju set' to set a |
271 | + value and 'juju resolved' to complete configuration. |
272 | + |
273 | +Usage: |
274 | + from charmsupport.volumes import configure_volume, VolumeConfigurationError |
275 | + from charmsupport.hookenv import log, ERROR |
276 | + def post_mount_hook(): |
277 | + stop_service('myservice') |
278 | + def post_mount_hook(): |
279 | + start_service('myservice') |
280 | + |
281 | + if __name__ == '__main__': |
282 | + try: |
283 | + configure_volume(before_change=pre_mount_hook, |
284 | + after_change=post_mount_hook) |
285 | + except VolumeConfigurationError: |
286 | + log('Storage could not be configured', ERROR) |
287 | +''' |
288 | + |
289 | +# XXX: Known limitations |
290 | +# - fstab is neither consulted nor updated |
291 | + |
292 | +import os |
293 | +from charmhelpers.core import hookenv |
294 | +from charmhelpers.core import host |
295 | +import yaml |
296 | + |
297 | + |
298 | +MOUNT_BASE = '/srv/juju/volumes' |
299 | + |
300 | + |
301 | +class VolumeConfigurationError(Exception): |
302 | + '''Volume configuration data is missing or invalid''' |
303 | + pass |
304 | + |
305 | + |
306 | +def get_config(): |
307 | + '''Gather and sanity-check volume configuration data''' |
308 | + volume_config = {} |
309 | + config = hookenv.config() |
310 | + |
311 | + errors = False |
312 | + |
313 | + if config.get('volume-ephemeral') in (True, 'True', 'true', 'Yes', 'yes'): |
314 | + volume_config['ephemeral'] = True |
315 | + else: |
316 | + volume_config['ephemeral'] = False |
317 | + |
318 | + try: |
319 | + volume_map = yaml.safe_load(config.get('volume-map', '{}')) |
320 | + except yaml.YAMLError as e: |
321 | + hookenv.log("Error parsing YAML volume-map: {}".format(e), |
322 | + hookenv.ERROR) |
323 | + errors = True |
324 | + if volume_map is None: |
325 | + # probably an empty string |
326 | + volume_map = {} |
327 | + elif not isinstance(volume_map, dict): |
328 | + hookenv.log("Volume-map should be a dictionary, not {}".format( |
329 | + type(volume_map))) |
330 | + errors = True |
331 | + |
332 | + volume_config['device'] = volume_map.get(os.environ['JUJU_UNIT_NAME']) |
333 | + if volume_config['device'] and volume_config['ephemeral']: |
334 | + # asked for ephemeral storage but also defined a volume ID |
335 | + hookenv.log('A volume is defined for this unit, but ephemeral ' |
336 | + 'storage was requested', hookenv.ERROR) |
337 | + errors = True |
338 | + elif not volume_config['device'] and not volume_config['ephemeral']: |
339 | + # asked for permanent storage but did not define volume ID |
340 | + hookenv.log('Ephemeral storage was requested, but there is no volume ' |
341 | + 'defined for this unit.', hookenv.ERROR) |
342 | + errors = True |
343 | + |
344 | + unit_mount_name = hookenv.local_unit().replace('/', '-') |
345 | + volume_config['mountpoint'] = os.path.join(MOUNT_BASE, unit_mount_name) |
346 | + |
347 | + if errors: |
348 | + return None |
349 | + return volume_config |
350 | + |
351 | + |
352 | +def mount_volume(config): |
353 | + if os.path.exists(config['mountpoint']): |
354 | + if not os.path.isdir(config['mountpoint']): |
355 | + hookenv.log('Not a directory: {}'.format(config['mountpoint'])) |
356 | + raise VolumeConfigurationError() |
357 | + else: |
358 | + host.mkdir(config['mountpoint']) |
359 | + if os.path.ismount(config['mountpoint']): |
360 | + unmount_volume(config) |
361 | + if not host.mount(config['device'], config['mountpoint'], persist=True): |
362 | + raise VolumeConfigurationError() |
363 | + |
364 | + |
365 | +def unmount_volume(config): |
366 | + if os.path.ismount(config['mountpoint']): |
367 | + if not host.umount(config['mountpoint'], persist=True): |
368 | + raise VolumeConfigurationError() |
369 | + |
370 | + |
371 | +def managed_mounts(): |
372 | + '''List of all mounted managed volumes''' |
373 | + return filter(lambda mount: mount[0].startswith(MOUNT_BASE), host.mounts()) |
374 | + |
375 | + |
376 | +def configure_volume(before_change=lambda: None, after_change=lambda: None): |
377 | + '''Set up storage (or don't) according to the charm's volume configuration. |
378 | + Returns the mount point or "ephemeral". before_change and after_change |
379 | + are optional functions to be called if the volume configuration changes. |
380 | + ''' |
381 | + |
382 | + config = get_config() |
383 | + if not config: |
384 | + hookenv.log('Failed to read volume configuration', hookenv.CRITICAL) |
385 | + raise VolumeConfigurationError() |
386 | + |
387 | + if config['ephemeral']: |
388 | + if os.path.ismount(config['mountpoint']): |
389 | + before_change() |
390 | + unmount_volume(config) |
391 | + after_change() |
392 | + return 'ephemeral' |
393 | + else: |
394 | + # persistent storage |
395 | + if os.path.ismount(config['mountpoint']): |
396 | + mounts = dict(managed_mounts()) |
397 | + if mounts.get(config['mountpoint']) != config['device']: |
398 | + before_change() |
399 | + unmount_volume(config) |
400 | + mount_volume(config) |
401 | + after_change() |
402 | + else: |
403 | + before_change() |
404 | + mount_volume(config) |
405 | + after_change() |
406 | + return config['mountpoint'] |
407 | |
408 | === modified file 'hooks/charmhelpers/fetch/__init__.py' |
409 | --- hooks/charmhelpers/fetch/__init__.py 2014-04-17 10:53:00 +0000 |
410 | +++ hooks/charmhelpers/fetch/__init__.py 2014-05-08 11:31:23 +0000 |
411 | @@ -184,7 +184,7 @@ |
412 | apt.write(PROPOSED_POCKET.format(release)) |
413 | if key: |
414 | subprocess.check_call(['apt-key', 'adv', '--keyserver', |
415 | - 'keyserver.ubuntu.com', '--recv', |
416 | + 'hkp://keyserver.ubuntu.com:80', '--recv', |
417 | key]) |
418 | |
419 | |
420 | |
421 | === modified file 'hooks/config-changed' |
422 | --- hooks/config-changed 2014-04-17 10:52:14 +0000 |
423 | +++ hooks/config-changed 2014-05-08 11:31:23 +0000 |
424 | @@ -14,6 +14,7 @@ |
425 | add_source, |
426 | apt_update |
427 | ) |
428 | +from charmhelpers.core.hookenv import relations_of_type |
429 | |
430 | |
431 | # Add archive source if provided |
432 | @@ -377,3 +378,6 @@ |
433 | except CalledProcessError: |
434 | check_call(['juju-log', '-l', 'INFO', 'Restart failed, trying again']) |
435 | check_call(['service', 'mysql', 'restart']) |
436 | +if relations_of_type('nrpe-external-master'): |
437 | + import nrpe_relations |
438 | + nrpe_relations.update_nrpe_checks() |
439 | |
440 | === added symlink 'hooks/nrpe-external-master-relation-changed' |
441 | === target is u'nrpe_relations.py' |
442 | === added symlink 'hooks/nrpe-external-master-relation-joined' |
443 | === target is u'nrpe_relations.py' |
444 | === added file 'hooks/nrpe_relations.py' |
445 | --- hooks/nrpe_relations.py 1970-01-01 00:00:00 +0000 |
446 | +++ hooks/nrpe_relations.py 2014-05-08 11:31:23 +0000 |
447 | @@ -0,0 +1,42 @@ |
448 | +#!/usr/bin/env python |
449 | + |
450 | +import sys |
451 | +from charmhelpers.core.hookenv import ( |
452 | + log, |
453 | + relations_of_type, |
454 | + Hooks, UnregisteredHookError |
455 | +) |
456 | +from charmhelpers.contrib.charmsupport.nrpe import NRPE |
457 | + |
458 | + |
459 | +hooks = Hooks() |
460 | + |
461 | + |
462 | +def update_nrpe_checks(): |
463 | + log('Refreshing nrpe checks') |
464 | + # Find out if nrpe set nagios_hostname |
465 | + hostname = None |
466 | + for rel in relations_of_type('nrpe-external-master'): |
467 | + if 'nagios_hostname' in rel: |
468 | + hostname = rel['nagios_hostname'] |
469 | + break |
470 | + nrpe = NRPE(hostname=hostname) |
471 | + nrpe.add_check( |
472 | + shortname='mysql_proc', |
473 | + description='Check MySQL process', |
474 | + check_cmd='check_procs -c 1:1 -C mysqld' |
475 | + ) |
476 | + nrpe.write() |
477 | + |
478 | + |
479 | +@hooks.hook('nrpe-external-master-relation-changed') |
480 | +@hooks.hook('nrpe-external-master-relation-joined') |
481 | +def add_nrpe_relation(): |
482 | + update_nrpe_checks() |
483 | + |
484 | + |
485 | +if __name__ == '__main__': |
486 | + try: |
487 | + hooks.execute(sys.argv) |
488 | + except UnregisteredHookError as e: |
489 | + log('Unknown hook {} - skipping.'.format(e)) |
490 | |
491 | === modified file 'metadata.yaml' |
492 | --- metadata.yaml 2013-07-08 13:40:00 +0000 |
493 | +++ metadata.yaml 2014-05-08 11:31:23 +0000 |
494 | @@ -24,6 +24,9 @@ |
495 | local-monitors: |
496 | interface: local-monitors |
497 | scope: container |
498 | + nrpe-external-master: |
499 | + interface: nrpe-external-master |
500 | + scope: container |
501 | peers: |
502 | cluster: |
503 | interface: mysql-ha |
504 | |
505 | === modified file 'revision' |
506 | --- revision 2014-02-19 10:13:17 +0000 |
507 | +++ revision 2014-05-08 11:31:23 +0000 |
508 | @@ -1,1 +1,1 @@ |
509 | -312 |
510 | +321 |
+1 Thanks for updating the deps and fixing this.
While not directly your issues the charm still only passes proof with warnings and there are no tests, either for the main charm or these new additions. I'd encourage testing around some of the new code at a minimum.