Merge lp:~jacekn/charms/precise/swift-storage/n-e-m-with-concat into lp:~openstack-charmers-archive/charms/trusty/swift-storage/next

Proposed by James Page
Status: Superseded
Proposed branch: lp:~jacekn/charms/precise/swift-storage/n-e-m-with-concat
Merge into: lp:~openstack-charmers-archive/charms/trusty/swift-storage/next
Diff against target: 848 lines (+634/-33)
13 files modified
charm-helpers.yaml (+1/-0)
config.yaml (+14/-1)
hooks/charmhelpers/contrib/charmsupport/nrpe.py (+219/-0)
hooks/charmhelpers/contrib/charmsupport/volumes.py (+156/-0)
hooks/swift_storage_hooks.py (+44/-1)
hooks/swift_storage_utils.py (+18/-2)
metadata.yaml (+3/-0)
revision (+1/-1)
scripts/check_swift_storage.py (+136/-0)
templates/050-swift-storage (+24/-0)
templates/rsyncd.conf (+5/-23)
unit_tests/test_swift_storage_relations.py (+10/-3)
unit_tests/test_swift_storage_utils.py (+3/-2)
To merge this branch: bzr merge lp:~jacekn/charms/precise/swift-storage/n-e-m-with-concat
Reviewer Review Type Date Requested Status
James Page Needs Information
Review via email: mp+221692@code.launchpad.net

This proposal supersedes a proposal from 2014-05-16.

This proposal has been superseded by a proposal from 2014-07-31.

Description of the change

This change adds nrpe-external-master support to the charm including basic nagios plugin

To post a comment you must log in.
Revision history for this message
James Page (james-page) wrote :

This merge proposal appears to have more than just the nrpe support - lots of changes around how rsync is managed as well?

Was this intentional? if so I would prefer that they where split out into two MP's to make it easier to test/review.

review: Needs Information
Revision history for this message
Jacek Nykis (jacekn) wrote :

> This merge proposal appears to have more than just the nrpe support - lots of
> changes around how rsync is managed as well?
>
> Was this intentional? if so I would prefer that they where split out into two
> MP's to make it easier to test/review.

Hi James,

Yes it was intentional. The swift-storage charm assumed full control over rsync config which could lead to broken configuration when used with subordinate charms.

So the rsync management changes are prerequisite for n-e-m support because n-e-m needs to be able to add rsync stanzas to the config without destroying swift config (and the swift charm needs to be able to update its config without breaking n-e-m config)

Unmerged revisions

31. By Jacek Nykis

Fixed tests

30. By Jacek Nykis

Added rsync fragment concatenation to the config-changed hook

29. By Ryan Finnie

Enable /etc/rsyncd.d functionality for compatibility with basenode

28. By Jacek Nykis

Fixed bug in check_swift_storage.py

27. By Jacek Nykis

Added nrpe-external-master hook support

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 2014-03-25 17:05:07 +0000
3+++ charm-helpers.yaml 2014-06-02 09:35:40 +0000
4@@ -9,3 +9,4 @@
5 - apache
6 - cluster
7 - payload.execd
8+ - contrib.charmsupport
9
10=== modified file 'config.yaml'
11--- config.yaml 2012-12-19 23:09:13 +0000
12+++ config.yaml 2014-06-02 09:35:40 +0000
13@@ -49,4 +49,17 @@
14 default: 6002
15 type: int
16 description: Listening port of the swift-account-server.
17-
18+ nagios-check-params:
19+ default: "-m -r 60 180 10 20"
20+ type: string
21+ description: String appended to nagios check
22+ nagios_context:
23+ default: "juju"
24+ type: string
25+ description: |
26+ Used by the nrpe-external-master subordinate charm.
27+ A string that will be prepended to instance name to set the host name
28+ in nagios. So for instance the hostname would be something like:
29+ juju-myservice-0
30+ If you're running multiple environments with the same services in them
31+ this allows you to differentiate between them.
32
33=== added directory 'hooks/charmhelpers/contrib/charmsupport'
34=== added file 'hooks/charmhelpers/contrib/charmsupport/__init__.py'
35=== added file 'hooks/charmhelpers/contrib/charmsupport/nrpe.py'
36--- hooks/charmhelpers/contrib/charmsupport/nrpe.py 1970-01-01 00:00:00 +0000
37+++ hooks/charmhelpers/contrib/charmsupport/nrpe.py 2014-06-02 09:35:40 +0000
38@@ -0,0 +1,219 @@
39+"""Compatibility with the nrpe-external-master charm"""
40+# Copyright 2012 Canonical Ltd.
41+#
42+# Authors:
43+# Matthew Wedgwood <matthew.wedgwood@canonical.com>
44+
45+import subprocess
46+import pwd
47+import grp
48+import os
49+import re
50+import shlex
51+import yaml
52+
53+from charmhelpers.core.hookenv import (
54+ config,
55+ local_unit,
56+ log,
57+ relation_ids,
58+ relation_set,
59+)
60+
61+from charmhelpers.core.host import service
62+
63+# This module adds compatibility with the nrpe-external-master and plain nrpe
64+# subordinate charms. To use it in your charm:
65+#
66+# 1. Update metadata.yaml
67+#
68+# provides:
69+# (...)
70+# nrpe-external-master:
71+# interface: nrpe-external-master
72+# scope: container
73+#
74+# and/or
75+#
76+# provides:
77+# (...)
78+# local-monitors:
79+# interface: local-monitors
80+# scope: container
81+
82+#
83+# 2. Add the following to config.yaml
84+#
85+# nagios_context:
86+# default: "juju"
87+# type: string
88+# description: |
89+# Used by the nrpe subordinate charms.
90+# A string that will be prepended to instance name to set the host name
91+# in nagios. So for instance the hostname would be something like:
92+# juju-myservice-0
93+# If you're running multiple environments with the same services in them
94+# this allows you to differentiate between them.
95+#
96+# 3. Add custom checks (Nagios plugins) to files/nrpe-external-master
97+#
98+# 4. Update your hooks.py with something like this:
99+#
100+# from charmsupport.nrpe import NRPE
101+# (...)
102+# def update_nrpe_config():
103+# nrpe_compat = NRPE()
104+# nrpe_compat.add_check(
105+# shortname = "myservice",
106+# description = "Check MyService",
107+# check_cmd = "check_http -w 2 -c 10 http://localhost"
108+# )
109+# nrpe_compat.add_check(
110+# "myservice_other",
111+# "Check for widget failures",
112+# check_cmd = "/srv/myapp/scripts/widget_check"
113+# )
114+# nrpe_compat.write()
115+#
116+# def config_changed():
117+# (...)
118+# update_nrpe_config()
119+#
120+# def nrpe_external_master_relation_changed():
121+# update_nrpe_config()
122+#
123+# def local_monitors_relation_changed():
124+# update_nrpe_config()
125+#
126+# 5. ln -s hooks.py nrpe-external-master-relation-changed
127+# ln -s hooks.py local-monitors-relation-changed
128+
129+
130+class CheckException(Exception):
131+ pass
132+
133+
134+class Check(object):
135+ shortname_re = '[A-Za-z0-9-_]+$'
136+ service_template = ("""
137+#---------------------------------------------------
138+# This file is Juju managed
139+#---------------------------------------------------
140+define service {{
141+ use active-service
142+ host_name {nagios_hostname}
143+ service_description {nagios_hostname}[{shortname}] """
144+ """{description}
145+ check_command check_nrpe!{command}
146+ servicegroups {nagios_servicegroup}
147+}}
148+""")
149+
150+ def __init__(self, shortname, description, check_cmd):
151+ super(Check, self).__init__()
152+ # XXX: could be better to calculate this from the service name
153+ if not re.match(self.shortname_re, shortname):
154+ raise CheckException("shortname must match {}".format(
155+ Check.shortname_re))
156+ self.shortname = shortname
157+ self.command = "check_{}".format(shortname)
158+ # Note: a set of invalid characters is defined by the
159+ # Nagios server config
160+ # The default is: illegal_object_name_chars=`~!$%^&*"|'<>?,()=
161+ self.description = description
162+ self.check_cmd = self._locate_cmd(check_cmd)
163+
164+ def _locate_cmd(self, check_cmd):
165+ search_path = (
166+ '/usr/lib/nagios/plugins',
167+ '/usr/local/lib/nagios/plugins',
168+ )
169+ parts = shlex.split(check_cmd)
170+ for path in search_path:
171+ if os.path.exists(os.path.join(path, parts[0])):
172+ command = os.path.join(path, parts[0])
173+ if len(parts) > 1:
174+ command += " " + " ".join(parts[1:])
175+ return command
176+ log('Check command not found: {}'.format(parts[0]))
177+ return ''
178+
179+ def write(self, nagios_context, hostname):
180+ nrpe_check_file = '/etc/nagios/nrpe.d/{}.cfg'.format(
181+ self.command)
182+ with open(nrpe_check_file, 'w') as nrpe_check_config:
183+ nrpe_check_config.write("# check {}\n".format(self.shortname))
184+ nrpe_check_config.write("command[{}]={}\n".format(
185+ self.command, self.check_cmd))
186+
187+ if not os.path.exists(NRPE.nagios_exportdir):
188+ log('Not writing service config as {} is not accessible'.format(
189+ NRPE.nagios_exportdir))
190+ else:
191+ self.write_service_config(nagios_context, hostname)
192+
193+ def write_service_config(self, nagios_context, hostname):
194+ for f in os.listdir(NRPE.nagios_exportdir):
195+ if re.search('.*{}.cfg'.format(self.command), f):
196+ os.remove(os.path.join(NRPE.nagios_exportdir, f))
197+
198+ templ_vars = {
199+ 'nagios_hostname': hostname,
200+ 'nagios_servicegroup': nagios_context,
201+ 'description': self.description,
202+ 'shortname': self.shortname,
203+ 'command': self.command,
204+ }
205+ nrpe_service_text = Check.service_template.format(**templ_vars)
206+ nrpe_service_file = '{}/service__{}_{}.cfg'.format(
207+ NRPE.nagios_exportdir, hostname, self.command)
208+ with open(nrpe_service_file, 'w') as nrpe_service_config:
209+ nrpe_service_config.write(str(nrpe_service_text))
210+
211+ def run(self):
212+ subprocess.call(self.check_cmd)
213+
214+
215+class NRPE(object):
216+ nagios_logdir = '/var/log/nagios'
217+ nagios_exportdir = '/var/lib/nagios/export'
218+ nrpe_confdir = '/etc/nagios/nrpe.d'
219+
220+ def __init__(self, hostname=None):
221+ super(NRPE, self).__init__()
222+ self.config = config()
223+ self.nagios_context = self.config['nagios_context']
224+ self.unit_name = local_unit().replace('/', '-')
225+ if hostname:
226+ self.hostname = hostname
227+ else:
228+ self.hostname = "{}-{}".format(self.nagios_context, self.unit_name)
229+ self.checks = []
230+
231+ def add_check(self, *args, **kwargs):
232+ self.checks.append(Check(*args, **kwargs))
233+
234+ def write(self):
235+ try:
236+ nagios_uid = pwd.getpwnam('nagios').pw_uid
237+ nagios_gid = grp.getgrnam('nagios').gr_gid
238+ except:
239+ log("Nagios user not set up, nrpe checks not updated")
240+ return
241+
242+ if not os.path.exists(NRPE.nagios_logdir):
243+ os.mkdir(NRPE.nagios_logdir)
244+ os.chown(NRPE.nagios_logdir, nagios_uid, nagios_gid)
245+
246+ nrpe_monitors = {}
247+ monitors = {"monitors": {"remote": {"nrpe": nrpe_monitors}}}
248+ for nrpecheck in self.checks:
249+ nrpecheck.write(self.nagios_context, self.hostname)
250+ nrpe_monitors[nrpecheck.shortname] = {
251+ "command": nrpecheck.command,
252+ }
253+
254+ service('restart', 'nagios-nrpe-server')
255+
256+ for rid in relation_ids("local-monitors"):
257+ relation_set(relation_id=rid, monitors=yaml.dump(monitors))
258
259=== added file 'hooks/charmhelpers/contrib/charmsupport/volumes.py'
260--- hooks/charmhelpers/contrib/charmsupport/volumes.py 1970-01-01 00:00:00 +0000
261+++ hooks/charmhelpers/contrib/charmsupport/volumes.py 2014-06-02 09:35:40 +0000
262@@ -0,0 +1,156 @@
263+'''
264+Functions for managing volumes in juju units. One volume is supported per unit.
265+Subordinates may have their own storage, provided it is on its own partition.
266+
267+Configuration stanzas:
268+ volume-ephemeral:
269+ type: boolean
270+ default: true
271+ description: >
272+ If false, a volume is mounted as sepecified in "volume-map"
273+ If true, ephemeral storage will be used, meaning that log data
274+ will only exist as long as the machine. YOU HAVE BEEN WARNED.
275+ volume-map:
276+ type: string
277+ default: {}
278+ description: >
279+ YAML map of units to device names, e.g:
280+ "{ rsyslog/0: /dev/vdb, rsyslog/1: /dev/vdb }"
281+ Service units will raise a configure-error if volume-ephemeral
282+ is 'true' and no volume-map value is set. Use 'juju set' to set a
283+ value and 'juju resolved' to complete configuration.
284+
285+Usage:
286+ from charmsupport.volumes import configure_volume, VolumeConfigurationError
287+ from charmsupport.hookenv import log, ERROR
288+ def post_mount_hook():
289+ stop_service('myservice')
290+ def post_mount_hook():
291+ start_service('myservice')
292+
293+ if __name__ == '__main__':
294+ try:
295+ configure_volume(before_change=pre_mount_hook,
296+ after_change=post_mount_hook)
297+ except VolumeConfigurationError:
298+ log('Storage could not be configured', ERROR)
299+'''
300+
301+# XXX: Known limitations
302+# - fstab is neither consulted nor updated
303+
304+import os
305+from charmhelpers.core import hookenv
306+from charmhelpers.core import host
307+import yaml
308+
309+
310+MOUNT_BASE = '/srv/juju/volumes'
311+
312+
313+class VolumeConfigurationError(Exception):
314+ '''Volume configuration data is missing or invalid'''
315+ pass
316+
317+
318+def get_config():
319+ '''Gather and sanity-check volume configuration data'''
320+ volume_config = {}
321+ config = hookenv.config()
322+
323+ errors = False
324+
325+ if config.get('volume-ephemeral') in (True, 'True', 'true', 'Yes', 'yes'):
326+ volume_config['ephemeral'] = True
327+ else:
328+ volume_config['ephemeral'] = False
329+
330+ try:
331+ volume_map = yaml.safe_load(config.get('volume-map', '{}'))
332+ except yaml.YAMLError as e:
333+ hookenv.log("Error parsing YAML volume-map: {}".format(e),
334+ hookenv.ERROR)
335+ errors = True
336+ if volume_map is None:
337+ # probably an empty string
338+ volume_map = {}
339+ elif not isinstance(volume_map, dict):
340+ hookenv.log("Volume-map should be a dictionary, not {}".format(
341+ type(volume_map)))
342+ errors = True
343+
344+ volume_config['device'] = volume_map.get(os.environ['JUJU_UNIT_NAME'])
345+ if volume_config['device'] and volume_config['ephemeral']:
346+ # asked for ephemeral storage but also defined a volume ID
347+ hookenv.log('A volume is defined for this unit, but ephemeral '
348+ 'storage was requested', hookenv.ERROR)
349+ errors = True
350+ elif not volume_config['device'] and not volume_config['ephemeral']:
351+ # asked for permanent storage but did not define volume ID
352+ hookenv.log('Ephemeral storage was requested, but there is no volume '
353+ 'defined for this unit.', hookenv.ERROR)
354+ errors = True
355+
356+ unit_mount_name = hookenv.local_unit().replace('/', '-')
357+ volume_config['mountpoint'] = os.path.join(MOUNT_BASE, unit_mount_name)
358+
359+ if errors:
360+ return None
361+ return volume_config
362+
363+
364+def mount_volume(config):
365+ if os.path.exists(config['mountpoint']):
366+ if not os.path.isdir(config['mountpoint']):
367+ hookenv.log('Not a directory: {}'.format(config['mountpoint']))
368+ raise VolumeConfigurationError()
369+ else:
370+ host.mkdir(config['mountpoint'])
371+ if os.path.ismount(config['mountpoint']):
372+ unmount_volume(config)
373+ if not host.mount(config['device'], config['mountpoint'], persist=True):
374+ raise VolumeConfigurationError()
375+
376+
377+def unmount_volume(config):
378+ if os.path.ismount(config['mountpoint']):
379+ if not host.umount(config['mountpoint'], persist=True):
380+ raise VolumeConfigurationError()
381+
382+
383+def managed_mounts():
384+ '''List of all mounted managed volumes'''
385+ return filter(lambda mount: mount[0].startswith(MOUNT_BASE), host.mounts())
386+
387+
388+def configure_volume(before_change=lambda: None, after_change=lambda: None):
389+ '''Set up storage (or don't) according to the charm's volume configuration.
390+ Returns the mount point or "ephemeral". before_change and after_change
391+ are optional functions to be called if the volume configuration changes.
392+ '''
393+
394+ config = get_config()
395+ if not config:
396+ hookenv.log('Failed to read volume configuration', hookenv.CRITICAL)
397+ raise VolumeConfigurationError()
398+
399+ if config['ephemeral']:
400+ if os.path.ismount(config['mountpoint']):
401+ before_change()
402+ unmount_volume(config)
403+ after_change()
404+ return 'ephemeral'
405+ else:
406+ # persistent storage
407+ if os.path.ismount(config['mountpoint']):
408+ mounts = dict(managed_mounts())
409+ if mounts.get(config['mountpoint']) != config['device']:
410+ before_change()
411+ unmount_volume(config)
412+ mount_volume(config)
413+ after_change()
414+ else:
415+ before_change()
416+ mount_volume(config)
417+ after_change()
418+ return config['mountpoint']
419
420=== added symlink 'hooks/nrpe-external-master-relation-changed'
421=== target is u'swift_storage_hooks.py'
422=== added symlink 'hooks/nrpe-external-master-relation-joined'
423=== target is u'swift_storage_hooks.py'
424=== modified file 'hooks/swift_storage_hooks.py'
425--- hooks/swift_storage_hooks.py 2013-09-27 16:33:06 +0000
426+++ hooks/swift_storage_hooks.py 2014-06-02 09:35:40 +0000
427@@ -13,6 +13,7 @@
428 register_configs,
429 save_script_rc,
430 setup_storage,
431+ concat_rsync_fragments,
432 )
433
434 from charmhelpers.core.hookenv import (
435@@ -21,10 +22,11 @@
436 log,
437 relation_get,
438 relation_set,
439+ relations_of_type,
440 )
441
442 from charmhelpers.fetch import apt_install, apt_update
443-from charmhelpers.core.host import restart_on_change
444+from charmhelpers.core.host import restart_on_change, rsync
445 from charmhelpers.payload.execd import execd_preinstall
446
447 from charmhelpers.contrib.openstack.utils import (
448@@ -32,8 +34,11 @@
449 openstack_upgrade_available,
450 )
451
452+from charmhelpers.contrib.charmsupport.nrpe import NRPE
453+
454 hooks = Hooks()
455 CONFIGS = register_configs()
456+NAGIOS_PLUGINS = '/usr/local/lib/nagios/plugins'
457
458
459 @hooks.hook()
460@@ -52,7 +57,22 @@
461 if openstack_upgrade_available('swift'):
462 do_openstack_upgrade(configs=CONFIGS)
463 CONFIGS.write_all()
464+
465+ # If basenode is not installed and managing rsyncd.conf, replicate
466+ # its core functionality. Otherwise concat files
467+ if not os.path.exists('/etc/rsyncd.d/001-basenode'):
468+ with open('templates/rsyncd.conf') as _in:
469+ rsync_header = _in.read()
470+ with open('/etc/rsyncd.d/050-swift-storage') as _in:
471+ rsync_fragment = _in.read()
472+ with open('/etc/rsyncd.conf', 'w') as out:
473+ out.write(rsync_header + rsync_fragment)
474+ else:
475+ concat_rsync_fragments()
476+
477 save_script_rc()
478+ if relations_of_type('nrpe-external-master'):
479+ nrpe_relation()
480
481
482 @hooks.hook()
483@@ -80,6 +100,29 @@
484 fetch_swift_rings(rings_url)
485
486
487+@hooks.hook('nrpe-external-master-relation-joined')
488+@hooks.hook('nrpe-external-master-relation-changed')
489+def nrpe_relation():
490+ log('Refreshing nrpe checks')
491+ rsync(os.path.join(os.getenv('CHARM_DIR'), 'scripts',
492+ 'check_swift_storage.py'),
493+ os.path.join(NAGIOS_PLUGINS, 'check_swift_storage.py'))
494+ # Find out if nrpe set nagios_hostname
495+ hostname = None
496+ for rel in relations_of_type('nrpe-external-master'):
497+ if 'nagios_hostname' in rel:
498+ hostname = rel['nagios_hostname']
499+ break
500+ nrpe = NRPE(hostname=hostname)
501+ nrpe.add_check(
502+ shortname='swift_storage',
503+ description='Check swift storage',
504+ check_cmd='check_swift_storage.py {}'.format(
505+ config('nagios-check-params'))
506+ )
507+ nrpe.write()
508+
509+
510 def main():
511 try:
512 hooks.execute(sys.argv)
513
514=== modified file 'hooks/swift_storage_utils.py'
515--- hooks/swift_storage_utils.py 2014-04-07 14:50:34 +0000
516+++ hooks/swift_storage_utils.py 2014-06-02 09:35:40 +0000
517@@ -70,7 +70,7 @@
518 ]
519
520 RESTART_MAP = {
521- '/etc/rsyncd.conf': ['rsync'],
522+ '/etc/rsyncd.d/050-swift-storage': ['rsync'],
523 '/etc/swift/account-server.conf': ACCOUNT_SVCS,
524 '/etc/swift/container-server.conf': CONTAINER_SVCS,
525 '/etc/swift/object-server.conf': OBJECT_SVCS,
526@@ -90,6 +90,11 @@
527 ]
528 [mkdir(d, owner='swift', group='swift') for d in dirs
529 if not os.path.isdir(d)]
530+ root_dirs = [
531+ '/etc/rsyncd.d',
532+ ]
533+ [mkdir(d, owner='root', group='root') for d in root_dirs
534+ if not os.path.isdir(d)]
535
536
537 def register_configs():
538@@ -98,7 +103,7 @@
539 openstack_release=release)
540 configs.register('/etc/swift/swift.conf',
541 [SwiftStorageContext()])
542- configs.register('/etc/rsyncd.conf',
543+ configs.register('/etc/rsyncd.d/050-swift-storage',
544 [RsyncContext()])
545 for server in ['account', 'object', 'container']:
546 configs.register('/etc/swift/%s-server.conf' % server,
547@@ -209,3 +214,14 @@
548 'OPENSTACK_URL_%s' % svc: url,
549 })
550 _save_script_rc(**env_vars)
551+
552+
553+def concat_rsync_fragments():
554+ log('Concatenating rsyncd.d fragments')
555+ rsyncd_dir = '/etc/rsyncd.d'
556+ rsyncd_conf = ""
557+ for filename in sorted(os.listdir(rsyncd_dir)):
558+ with open(os.path.join(rsyncd_dir, filename), 'r') as fragment:
559+ rsyncd_conf += fragment.read()
560+ with open('/etc/rsyncd.conf', 'w') as f:
561+ f.write(rsyncd_conf)
562
563=== modified file 'metadata.yaml'
564--- metadata.yaml 2013-07-11 20:45:19 +0000
565+++ metadata.yaml 2014-06-02 09:35:40 +0000
566@@ -8,3 +8,6 @@
567 provides:
568 swift-storage:
569 interface: swift
570+ nrpe-external-master:
571+ interface: nrpe-external-master
572+ scope: container
573
574=== modified file 'revision'
575--- revision 2013-07-19 21:13:59 +0000
576+++ revision 2014-06-02 09:35:40 +0000
577@@ -1,1 +1,1 @@
578-90
579+100
580\ No newline at end of file
581
582=== added file 'scripts/check_swift_storage.py'
583--- scripts/check_swift_storage.py 1970-01-01 00:00:00 +0000
584+++ scripts/check_swift_storage.py 2014-06-02 09:35:40 +0000
585@@ -0,0 +1,136 @@
586+#!/usr/bin/env python
587+
588+# Copyright (C) 2014 Canonical
589+# All Rights Reserved
590+# Author: Jacek Nykis
591+
592+import sys
593+import json
594+import urllib2
595+import argparse
596+import hashlib
597+import datetime
598+
599+STATUS_OK = 0
600+STATUS_WARN = 1
601+STATUS_CRIT = 2
602+STATUS_UNKNOWN = 3
603+
604+
605+def generate_md5(filename):
606+ with open(filename, 'rb') as f:
607+ md5 = hashlib.md5()
608+ buffer = f.read(2 ** 20)
609+ while buffer:
610+ md5.update(buffer)
611+ buffer = f.read(2 ** 20)
612+ return md5.hexdigest()
613+
614+
615+def check_md5(base_url):
616+ url = base_url + "ringmd5"
617+ ringfiles = ["/etc/swift/object.ring.gz",
618+ "/etc/swift/account.ring.gz",
619+ "/etc/swift/container.ring.gz"]
620+ results = []
621+ try:
622+ data = urllib2.urlopen(url).read()
623+ j = json.loads(data)
624+ except urllib2.URLError:
625+ return [(STATUS_UNKNOWN, "Can't open url: {}".format(url))]
626+ except ValueError:
627+ return [(STATUS_UNKNOWN, "Can't parse status data")]
628+
629+ for ringfile in ringfiles:
630+ try:
631+ if generate_md5(ringfile) != j[ringfile]:
632+ results.append((STATUS_CRIT,
633+ "Ringfile {} MD5 sum mismatch".format(ringfile)))
634+ except IOError:
635+ results.append(
636+ (STATUS_UNKNOWN, "Can't open ringfile {}".format(ringfile)))
637+ if results:
638+ return results
639+ else:
640+ return [(STATUS_OK, "OK")]
641+
642+
643+def check_replication(base_url, limits):
644+ types = ["account", "object", "container"]
645+ results = []
646+ for repl in types:
647+ url = base_url + "replication/" + repl
648+ try:
649+ data = urllib2.urlopen(url).read()
650+ j = json.loads(data)
651+ except urllib2.URLError:
652+ results.append((STATUS_UNKNOWN, "Can't open url: {}".format(url)))
653+ continue
654+ except ValueError:
655+ results.append((STATUS_UNKNOWN, "Can't parse status data"))
656+ continue
657+
658+ if "object_replication_last" in j:
659+ repl_last = datetime.datetime.fromtimestamp(j["object_replication_last"])
660+ else:
661+ repl_last = datetime.datetime.fromtimestamp(j["replication_last"])
662+ delta = datetime.datetime.now() - repl_last
663+ if delta.seconds >= limits[1]:
664+ results.append((STATUS_CRIT,
665+ "'{}' replication lag is {} seconds".format(repl, delta.seconds)))
666+ elif delta.seconds >= limits[0]:
667+ results.append((STATUS_WARN,
668+ "'{}' replication lag is {} seconds".format(repl, delta.seconds)))
669+ if "replication_stats" in j:
670+ errors = j["replication_stats"]["failure"]
671+ if errors >= limits[3]:
672+ results.append(
673+ (STATUS_CRIT, "{} replication failures".format(errors)))
674+ elif errors >= limits[2]:
675+ results.append(
676+ (STATUS_WARN, "{} replication failures".format(errors)))
677+ if results:
678+ return results
679+ else:
680+ return [(STATUS_OK, "OK")]
681+
682+
683+if __name__ == '__main__':
684+ parser = argparse.ArgumentParser(description='Check swift-storage health')
685+ parser.add_argument('-H', '--host', dest='host', default='localhost',
686+ help='Hostname to query')
687+ parser.add_argument('-p', '--port', dest='port', default='6000',
688+ type=int, help='Port number')
689+ parser.add_argument('-r', '--replication', dest='check_replication',
690+ type=int, nargs=4, help='Check replication status',
691+ metavar=('lag_warn', 'lag_crit', 'failures_warn', 'failures_crit'))
692+ parser.add_argument('-m', '--md5', dest='check_md5', action='store_true',
693+ help='Compare server rings md5sum with local copy')
694+ args = parser.parse_args()
695+
696+ if not args.check_replication and not args.check_md5:
697+ print "You must use -r or -m switch"
698+ sys.exit(STATUS_UNKNOWN)
699+
700+ base_url = "http://{}:{}/recon/".format(args.host, args.port)
701+ results = []
702+ if args.check_replication:
703+ results.extend(check_replication(base_url, args.check_replication))
704+ if args.check_md5:
705+ results.extend(check_md5(base_url))
706+
707+ crits = ';'.join([i[1] for i in results if i[0] == STATUS_CRIT])
708+ warns = ';'.join([i[1] for i in results if i[0] == STATUS_WARN])
709+ unknowns = ';'.join([i[1] for i in results if i[0] == STATUS_UNKNOWN])
710+ if crits:
711+ print "CRITICAL: " + crits
712+ sys.exit(STATUS_CRIT)
713+ elif warns:
714+ print "WARNING: " + warns
715+ sys.exit(STATUS_WARN)
716+ elif unknowns:
717+ print "UNKNOWN: " + unknowns
718+ sys.exit(STATUS_UNKNOWN)
719+ else:
720+ print "OK"
721+ sys.exit(0)
722
723=== added file 'templates/050-swift-storage'
724--- templates/050-swift-storage 1970-01-01 00:00:00 +0000
725+++ templates/050-swift-storage 2014-06-02 09:35:40 +0000
726@@ -0,0 +1,24 @@
727+[account]
728+uid = swift
729+gid = swift
730+max connections = 2
731+path = /srv/node/
732+read only = false
733+lock file = /var/lock/account.lock
734+
735+[container]
736+uid = swift
737+gid = swift
738+max connections = 2
739+path = /srv/node/
740+read only = false
741+lock file = /var/lock/container.lock
742+
743+[object]
744+uid = swift
745+gid = swift
746+max connections = 2
747+path = /srv/node/
748+read only = false
749+lock file = /var/lock/object.lock
750+
751
752=== modified file 'templates/rsyncd.conf'
753--- templates/rsyncd.conf 2013-07-19 19:52:45 +0000
754+++ templates/rsyncd.conf 2014-06-02 09:35:40 +0000
755@@ -1,23 +1,5 @@
756-uid = swift
757-gid = swift
758-log file = /var/log/rsyncd.log
759-pid file = /var/run/rsyncd.pid
760-address = {{ local_ip }}
761-
762-[account]
763-max connections = 2
764-path = /srv/node/
765-read only = false
766-lock file = /var/lock/account.lock
767-
768-[container]
769-max connections = 2
770-path = /srv/node/
771-read only = false
772-lock file = /var/lock/container.lock
773-
774-[object]
775-max connections = 2
776-path = /srv/node/
777-read only = false
778-lock file = /var/lock/object.lock
779+uid = nobody
780+gid = nogroup
781+syslog facility = daemon
782+socket options = SO_KEEPALIVE
783+
784
785=== modified file 'unit_tests/test_swift_storage_relations.py'
786--- unit_tests/test_swift_storage_relations.py 2013-09-27 16:33:06 +0000
787+++ unit_tests/test_swift_storage_relations.py 2014-06-02 09:35:40 +0000
788@@ -1,6 +1,6 @@
789 from mock import patch, MagicMock
790
791-from test_utils import CharmTestCase
792+from test_utils import CharmTestCase, patch_open
793
794 import swift_storage_utils as utils
795
796@@ -21,6 +21,7 @@
797 'log',
798 'relation_set',
799 'relation_get',
800+ 'relations_of_type',
801 # charmhelpers.core.host
802 'apt_update',
803 'apt_install',
804@@ -60,13 +61,19 @@
805
806 def test_config_changed_no_upgrade_available(self):
807 self.openstack_upgrade_available.return_value = False
808- hooks.config_changed()
809+ self.relations_of_type.return_value = False
810+ with patch_open() as (_open, _file):
811+ _file.read.return_value = "foo"
812+ hooks.config_changed()
813 self.assertFalse(self.do_openstack_upgrade.called)
814 self.assertTrue(self.CONFIGS.write_all.called)
815
816 def test_config_changed_upgrade_available(self):
817 self.openstack_upgrade_available.return_value = True
818- hooks.config_changed()
819+ self.relations_of_type.return_value = False
820+ with patch_open() as (_open, _file):
821+ _file.read.return_value = "foo"
822+ hooks.config_changed()
823 self.assertTrue(self.do_openstack_upgrade.called)
824 self.assertTrue(self.CONFIGS.write_all.called)
825
826
827=== modified file 'unit_tests/test_swift_storage_utils.py'
828--- unit_tests/test_swift_storage_utils.py 2014-03-20 13:50:49 +0000
829+++ unit_tests/test_swift_storage_utils.py 2014-06-02 09:35:40 +0000
830@@ -74,7 +74,8 @@
831 ex_dirs = [
832 call('/etc/swift', owner='swift', group='swift'),
833 call('/var/cache/swift', owner='swift', group='swift'),
834- call('/srv/node', owner='swift', group='swift')
835+ call('/srv/node', owner='swift', group='swift'),
836+ call('/etc/rsyncd.d', owner='root', group='root')
837 ]
838 self.assertEquals(ex_dirs, self.mkdir.call_args_list)
839
840@@ -196,7 +197,7 @@
841 openstack_release='grizzly')
842 ex = [
843 call('/etc/swift/swift.conf', ['swift_server_context']),
844- call('/etc/rsyncd.conf', ['rsync_context']),
845+ call('/etc/rsyncd.d/050-swift-storage', ['rsync_context']),
846 call('/etc/swift/account-server.conf', ['swift_context']),
847 call('/etc/swift/object-server.conf', ['swift_context']),
848 call('/etc/swift/container-server.conf', ['swift_context'])

Subscribers

People subscribed via source and target branches