#!/usr/bin/python # queries a bacula database for the last backup of a given host # Copyright 2010, 2011, 2013 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 optparse import psycopg2 import psycopg2.extras import re import sys codes = { 'UNKNOWN': 3, 'CRITICAL': 2, 'WARNING': 1, 'OK': 0 } 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 parser = optparse.OptionParser() parser.set_usage("%prog [options] []") 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)") parser.add_option("-d", "--db-connect-string", metavar="connect-string", dest="db", help="Database connect string") parser.add_option("-D", "--db-connect-string-file", metavar="FILE", dest="dbfile", default='/etc/nagios/bacula-database', help="File to read database connect string from (/etc/nagios/bacula-database)") (options, args) = parser.parse_args() if len(args) == 1: host = args[0] level = None elif len(args) == 2: host = args[0] level = args[1] else: parser.print_help() sys.exit(codes['UNKNOWN']) if options.warn is None: options.warn = '28' if options.critical is None: options.critical = '72' options.warn = convert_time(options.warn) options.critical = convert_time(options.critical) if options.db is not None: pass elif options.dbfile is not None: options.db = open(options.dbfile).read().rstrip() else: print >>sys.stderr, "Need one of -d or -D." sys.exit(codes['UNKNOWN']) query = "SELECT min(extract('epoch' from (CURRENT_TIMESTAMP - realendtime))) AS age FROM job WHERE name=%(host)s AND jobstatus='T'" params = { 'host': host } if level is not None: query += " AND level=%(level)s" params['level'] = level else: level = 'any' conn = psycopg2.connect(options.db) cursor = conn.cursor(cursor_factory=psycopg2.extras.DictCursor) cursor.execute(query, params) records = cursor.fetchall() if len(records) == 0 or records[0][0] is None: print "CRITICAL: No backups of %s/%s."%(host, level) sys.exit(codes['CRITICAL']) elif len(records) > 1: print "UNKNOWN: got too many records back from query." sys.exit(codes['UNKNOWN']) elif records[0]['age'] > options.critical: print "CRITICAL: Last backup of %s/%s is %.2f days old."%(host, level, float(records[0]['age'])/3600/24) sys.exit(codes['CRITICAL']) elif records[0]['age'] > options.warn: print "WARN: Last backup of %s/%s is %.2f days old."%(host, level, float(records[0]['age'])/3600/24) sys.exit(codes['WARNING']) else: print "OK: Last backup of %s/%s is %.2f days old."%(host, level, float(records[0]['age'])/3600/24) sys.exit(codes['OK'])