Merge lp:~jacekn/charms/precise/mysql/n-e-m-relation into lp:charms/mysql

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
Reviewer Review Type Date Requested Status
Jorge Niedbalski (community) Needs Resubmitting
charmers Pending
Review via email: mp+218785@code.launchpad.net

Description of the change

Added support for nrpe-external-master relation.

To post a comment you must log in.
Revision history for this message
Benjamin Saller (bcsaller) wrote :

+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.

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

Subscribers

People subscribed via source and target branches

to all changes: