#!/usr/bin/python # Copyright 2010, 2011 Peter Palfrader # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. import datetime import dateutil.parser import optparse import os import re import sys import glob parser = optparse.OptionParser() parser.set_usage("%prog [options] ") parser.add_option("-q", "--quiet", dest="quiet", default=False, action="store_true", help="Silent mode (exit code only)") parser.add_option("-w", "--warn", metavar="AGE", dest="warn", help="Warn if backup older than (default: 28h)") parser.add_option("-c", "--critical", metavar="AGE", dest="critical", help="Warn if backup older than (default: 72h)") (options, args) = parser.parse_args() if len(args) == 0: parser.print_help() sys.exit(1) log = [] exitcode = 0 backup_per_code = {} def code_to_prefix(code): if code == 3: return 'UNKNOWN: ' elif code == 2: return 'CRITICAL: ' elif code == 1: return 'WARNING: ' elif code == 0: return 'OK: ' else: raise ValueError def convert_time(s, default_unit='h'): m = re.match('([0-9]+)([smhdw])?$', s) if m is None: raise ValueError ticks = int(m.group(1)) unit = m.group(2) if unit is None: unit = default_unit if unit == 's': None elif unit == 'm': ticks *= 60 elif unit == 'h': ticks *= 60*60 elif unit == 'd': ticks *= 60*60*24 elif unit == 'w': ticks *= 60*60*24*7 else: raise ValueError return ticks def record(code, base, msg): global exitcode if not code in backup_per_code: backup_per_code[code] = [] backup_per_code[code].append(base) prefix = code_to_prefix(code) log.append("%s%s: %s"%(prefix, base, msg)) if code > exitcode: exitcode = code def get_ts_from_fn(base, fn): m = re.match('current_mirror\.(.*)\.data$', fn) if m is None: record(2, base, "backup wedged - cannot parse current mirrors filename '%s'."%(fn)) return None try: ts = dateutil.parser.parse(m.group(1)) except ValueError: record(2, base, "backup wedged - cannot parse datestring '%s' from filename '%s'."%(m.group(1), fn)) return None return ts if options.warn is None: options.warn = '28' if options.critical is None: options.critical = '72' options.warn = datetime.timedelta(seconds = convert_time(options.warn)) options.critical = datetime.timedelta(seconds = convert_time(options.critical)) for base in args: data_dir = os.path.join(base, 'rdiff-backup-data') if not os.path.exists(base): record(2, base, "not a directory") continue if not os.path.isdir(data_dir): record(2, base, "does not seem to be an rdiff-backup backup directory") continue os.chdir(data_dir) current_mirrors = glob.glob('current_mirror.*.data') if len(current_mirrors) == 0: record(2, base, "no current backups? (current_mirror.* not found)") continue elif len(current_mirrors) == 2: # one backup is currently running, presumably current_mirrors.sort() elif len(current_mirrors) == 1: # only one backup is current, good. None else: record(2, base, "backup wedged - too many current_mirror.* files") continue ts = get_ts_from_fn(base, current_mirrors[0]) age = datetime.datetime.now(ts.tzinfo) - ts if age > options.critical: record(2, base, "backup is old (%s)."%(age)) continue elif age > options.warn: record(1, base, "backup is old (%s)."%(age)) continue else: record(0, base, "backup is current (age: %s)."%(age)) continue def record(code, base, msg): if not code in backup_per_code: backup_per_code[code] = [] backup_per_code[code].append(base) report = [] keys = backup_per_code.keys() keys.sort(reverse=True) for code in keys: prefix = code_to_prefix(code) report.append( prefix + ', '.join(backup_per_code[code]) ) if not options.quiet: print '; '.join(report) for l in log: print l sys.exit(exitcode)