check_backupninja_duplicity.py nagios script
authorvarac <varacanero@zeromail.org>
Thu, 24 Jan 2013 11:08:24 +0000 (12:08 +0100)
committervarac <varacanero@zeromail.org>
Thu, 24 Jan 2013 11:08:24 +0000 (12:08 +0100)
files/nagios_plugins/duplicity/README.md [new file with mode: 0644]
files/nagios_plugins/duplicity/backupninja_duplicity_freshness.sh [new file with mode: 0644]
files/nagios_plugins/duplicity/check_backupninja_duplicity.py [new file with mode: 0644]
manifests/nagios_plugin/duplicity.pp [new file with mode: 0644]

diff --git a/files/nagios_plugins/duplicity/README.md b/files/nagios_plugins/duplicity/README.md
new file mode 100644 (file)
index 0000000..1cd349a
--- /dev/null
@@ -0,0 +1,24 @@
+duplicity-backup-status
+=======================
+
+Backupninja generates duplicity configfiles, this nagios plugin can check their freshness. Currently only the config files generated by backupninja can be parsed and we depend on that.
+
+## Prerequisites
+
+Make sure you have python-argparse installed (yes an extra dependency, getopt doubles the amount of code, so I gave up on that). The Python script will look for the duplicity_freshness.sh shell script in /usr/local/lib/nagios/plugins/ or /usr/lib/nagios/plugins/ make sure you copy it there and make executable. 
+
+## Getting started
+
+Run the python script from your nagios. Don't forget to specify some extras like when warnings or criticalities should be emerged.
+
+-  -w WARNINC   Number of hours allowed for incremential backup warning level default 28
+-  -W WARNFULL  Number of hours allowed for incremential backup critical level default 40
+-  -c CRITINC   Number of days allowed for full backup warning level default 52
+-  -C CRITFULL  Number of days allowed for full backup critical level default 60
+
+
+## TODO:
+
+- make it cuter, tidy up
+- make it more robust
+- support other config backends as backupninja - this can be done by writing more scripts like backupninja_duplicity_freshness.sh and parsing an extra parameter
diff --git a/files/nagios_plugins/duplicity/backupninja_duplicity_freshness.sh b/files/nagios_plugins/duplicity/backupninja_duplicity_freshness.sh
new file mode 100644 (file)
index 0000000..ea694d0
--- /dev/null
@@ -0,0 +1,268 @@
+# -*- mode: sh; sh-basic-offset: 3; indent-tabs-mode: nil; -*-
+# vim: set filetype=sh sw=3 sts=3 expandtab autoindent:
+
+# Load backupninja library/helpers, because why reinventing the wheel? [Because my wheels weren't round]
+# some duplication is to be expected
+# this is only supposed to work with duplicity
+
+## Functions
+# simple lowercase function
+function tolower() {
+   echo "$1" | tr '[:upper:]' '[:lower:]'
+}
+
+# we grab the current time once, since processing
+# all the configs might take more than an hour.
+nowtime=`LC_ALL=C date +%H`
+nowday=`LC_ALL=C date +%d`
+nowdayofweek=`LC_ALL=C date +%A`
+nowdayofweek=`tolower "$nowdayofweek"`
+
+conffile="/etc/backupninja.conf"
+
+# find $libdirectory
+libdirectory=`grep '^libdirectory' $conffile | /usr/bin/awk '{print $3}'`
+if [ -z "$libdirectory" ]; then
+   if [ -d "/usr/lib/backupninja" ]; then
+      libdirectory="/usr/lib/backupninja"
+   else
+      echo "Could not find entry 'libdirectory' in $conffile."
+      fatal "Could not find entry 'libdirectory' in $conffile."
+   fi
+else
+   if [ ! -d "$libdirectory" ]; then
+      echo "Lib directory $libdirectory not found."
+      fatal "Lib directory $libdirectory not found."
+   fi
+fi
+
+. $libdirectory/tools
+
+setfile $conffile
+
+# get global config options (second param is the default)
+getconf configdirectory /etc/backup.d
+getconf scriptdirectory /usr/share/backupninja
+getconf reportdirectory
+getconf reportemail
+getconf reporthost
+getconf reportspace
+getconf reportsuccess yes
+getconf reportinfo no
+getconf reportuser
+getconf reportwarning yes
+getconf loglevel 3
+getconf when "Everyday at 01:00"
+defaultwhen=$when
+getconf logfile /var/log/backupninja.log
+getconf usecolors "yes"
+getconf SLAPCAT /usr/sbin/slapcat
+getconf LDAPSEARCH /usr/bin/ldapsearch
+getconf RDIFFBACKUP /usr/bin/rdiff-backup
+getconf CSTREAM /usr/bin/cstream
+getconf MYSQLADMIN /usr/bin/mysqladmin
+getconf MYSQL /usr/bin/mysql
+getconf MYSQLHOTCOPY /usr/bin/mysqlhotcopy
+getconf MYSQLDUMP /usr/bin/mysqldump
+getconf PGSQLDUMP /usr/bin/pg_dump
+getconf PGSQLDUMPALL /usr/bin/pg_dumpall
+getconf PGSQLUSER postgres
+getconf GZIP /bin/gzip
+getconf GZIP_OPTS --rsyncable
+getconf RSYNC /usr/bin/rsync
+getconf admingroup root
+
+if [ ! -d "$configdirectory" ]; then
+   echo "Configuration directory '$configdirectory' not found."
+   fatal "Configuration directory '$configdirectory' not found."
+fi
+
+# get the duplicity configuration
+function get_dupconf(){
+   setfile $1
+   getconf options
+   getconf testconnect yes
+   getconf nicelevel 0
+   getconf tmpdir
+   
+   setsection gpg
+   getconf password
+   getconf sign no
+   getconf encryptkey
+   getconf signkey
+   
+   setsection source
+   getconf include
+   getconf vsnames all
+   getconf vsinclude
+   getconf exclude
+   
+   setsection dest
+   getconf incremental yes
+   getconf increments 30
+   getconf keep 60
+   getconf keepincroffulls all
+   getconf desturl
+   getconf awsaccesskeyid
+   getconf awssecretaccesskey
+   getconf cfusername
+   getconf cfapikey
+   getconf cfauthurl
+   getconf ftp_password
+   getconf sshoptions
+   getconf bandwidthlimit 0
+   getconf desthost
+   getconf destdir
+   getconf destuser
+   destdir=${destdir%/}
+}
+
+### some voodoo to mangle the correct commands
+
+function mangle_cli(){
+
+   execstr_options="$options "
+   execstr_source=
+   if [ -n "$desturl" ]; then
+      [ -z "$destuser" ] || warning 'the configured destuser is ignored since desturl is set'
+      [ -z "$desthost" ] || warning 'the configured desthost is ignored since desturl is set'
+      [ -z "$destdir" ] || warning 'the configured destdir is ignored since desturl is set'
+      execstr_serverpart="$desturl"
+   else
+      execstr_serverpart="scp://$destuser@$desthost/$destdir"
+   fi
+   
+   
+   ### Symmetric or asymmetric (public/private key pair) encryption
+   if [ -n "$encryptkey" ]; then
+      execstr_options="${execstr_options} --encrypt-key $encryptkey"
+   fi
+   
+   ### Data signing (or not)
+   if [ "$sign" == yes ]; then
+      # duplicity is not able to sign data when using symmetric encryption
+      [ -n "$encryptkey" ] || fatal "The encryptkey option must be set when signing."
+      # if needed, initialize signkey to a value that is not empty (checked above)
+      [ -n "$signkey" ] || signkey="$encryptkey"
+      execstr_options="${execstr_options} --sign-key $signkey"
+   fi
+   
+   ### Temporary directory
+   precmd=
+   if [ -n "$tmpdir" ]; then
+      if [ ! -d "$tmpdir" ]; then
+         #info "Temporary directory ($tmpdir) does not exist, creating it."
+         mkdir -p "$tmpdir"
+         [ $? -eq 0 ] || fatal "Could not create temporary directory ($tmpdir)."
+         chmod 0700 "$tmpdir"
+      fi
+      #info "Using $tmpdir as TMPDIR"
+      precmd="${precmd}TMPDIR=$tmpdir "
+   fi
+   
+   ### Source
+   
+   set -o noglob
+   
+   # excludes
+   SAVEIFS=$IFS
+   IFS=$(echo -en "\n\b")
+   for i in $exclude; do
+      str="${i//__star__/*}"
+      execstr_source="${execstr_source} --exclude '$str'"
+   done
+   IFS=$SAVEIFS
+   
+   # includes
+   SAVEIFS=$IFS
+   IFS=$(echo -en "\n\b")
+   for i in $include; do
+      [ "$i" != "/" ] || fatal "Sorry, you cannot use 'include = /'"
+      str="${i//__star__/*}"
+      execstr_source="${execstr_source} --include '$str'"
+   done
+   IFS=$SAVEIFS
+   
+   set +o noglob
+   
+   execstr_options="${execstr_options} --ssh-options '$sshoptions'"
+   if [ "$bandwidthlimit" != 0 ]; then
+      [ -z "$desturl" ] || warning 'The bandwidthlimit option is not used when desturl is set.'
+      execstr_precmd="trickle -s -d $bandwidthlimit -u $bandwidthlimit"
+   fi
+}
+
+function findlastdates(){
+   outputfile=$1
+   lastfull=0
+   lastinc=0
+   backuptime=0
+   
+   while read line; do
+      atime=0
+      arr=()
+      sort=''
+      test=$(echo $line|awk '{if (NF == 7); if ($1 == "Full" || $1 == "Incremental") {print $4, $3, $6, $5}}'  )
+   
+      if [ -n "$test"  ]; then
+         backuptime=$(date -u -d "$test" +%s)
+   
+         arr=($(echo $line|awk '{print $1, $2, $3, $4, $5, $6}'))
+         if [ ${arr[0]} == "Incremental" ] && [ "$lastinc" -lt "$backuptime" ] ; then
+            lastinc=$backuptime
+         elif [ ${arr[0]} == "Full" ] && [ "$lastfull" -lt "$backuptime" ] ; then
+            lastfull=$backuptime
+         fi
+   
+      fi
+   
+   done < $outputfile
+      # a full backup can be seen as incremental too
+      lastinc=$(echo $lastinc | awk 'max=="" || $1 > max {max=$1} END{ print max}')
+}
+
+function check_status() {
+   grep -q 'No orphaned or incomplete backup sets found.' $1
+   if [ $? -ne 0 ] ; then
+     exit 2
+   fi
+}
+
+##
+## this function handles the freshness check of a backup action
+##
+
+function process_action() {
+   local file="$1"
+   local suffix="$2"
+   setfile $file
+   get_dupconf $1
+   mangle_cli
+   
+   outputfile=`maketemp backupout`
+   export PASSPHRASE=$password
+   export FTP_PASSWORD=$ftp_password
+   output=` su -c \
+            "$execstr_precmd duplicity $execstr_options collection-status $execstr_serverpart >$outputfile 2>&1"`
+   #echo "$execstr_precmd duplicity $execstr_options collection-status $execstr_serverpart" >$outputfile
+   exit_code=$?
+   echo -n $outputfile
+
+   #check_status
+   #findlastdates
+}
+
+files=`find $configdirectory -follow -mindepth 1 -maxdepth 1 -type f ! -name '.*.swp' | sort -n`
+
+for file in $files; do
+   [ -f "$file" ] || continue
+   suffix="${file##*.}"
+   base=`basename $file`
+   if [ "${base:0:1}" == "0" -o "$suffix" == "disabled" ]; then
+      continue
+   fi
+   if [ -e "$scriptdirectory/$suffix" -a "$suffix" == "dup" ]; then
+      process_action $file $suffix
+   fi
+done
+
diff --git a/files/nagios_plugins/duplicity/check_backupninja_duplicity.py b/files/nagios_plugins/duplicity/check_backupninja_duplicity.py
new file mode 100644 (file)
index 0000000..5deeccb
--- /dev/null
@@ -0,0 +1,118 @@
+#!/usr/bin/env python
+
+# Inspired by Arne Schwabe <arne-nagios@rfc2549.org> [with BSD license]
+# Inspired by backupninja [that's gpl some version]
+# minor changes by someon who doesn't understand all the license quirks
+
+from subprocess import Popen,PIPE
+import sys
+import time
+import os
+import argparse
+import getopt
+
+def main():
+    # getopt = much more writing
+
+    parser = argparse.ArgumentParser(description='Nagios Duplicity status checker')
+
+    parser.add_argument("-w", dest="warninc", default=28, type=int, 
+                        help="Number of hours allowed for incremential backup warning level")
+    parser.add_argument("-W", dest="warnfull", default=40, type=int, 
+                        help="Number of hours allowed for incremential backup critical level")
+
+    parser.add_argument("-c", dest="critinc", default=52, type=int, 
+                        help="Number of days allowed for full backup warning level")
+
+    parser.add_argument("-C", dest="critfull", default=60, type=int, 
+                        help="Number of days allowed for full backup critical level")
+
+    args = parser.parse_args()
+    
+    okay = 0
+
+    #f = open ('/tmp/tmp.q5Mqui6nVr/backupout.amDtCIcW', 'r')
+    #output = f.read()
+    
+    # *sigh* check_output is from python 2.7 and onwards. Debian, upgrade yourself.
+    #output , err = check_output(['/root/freshness.sh'])
+
+    if os.path.isfile("/usr/lib/nagios/plugins/backupninja_duplicity_freshness.sh") and os.access("/usr/lib/nagios/plugins/backupninja_duplicity_freshness.sh", os.X_OK):
+      checkstatus, err = Popen(['/bin/bash', '/usr/lib/nagios/plugins/backupninja_duplicity_freshness.sh'], stdout=PIPE, stderr=PIPE, env={'HOME': '/root', 'PATH': os.environ['PATH']}).communicate()
+    elif os.path.isfile("/usr/local/lib/nagios/plugins/backupninja_duplicity_freshness.sh") and os.access("/usr/local/lib/nagios/plugins/backupninja_duplicity_freshness.sh", os.X_OK):
+      checkstatus, err = Popen(['/bin/bash', '/usr/local/lib/nagios/plugins/backupninja_duplicity_freshness.sh'], stdout=PIPE, stderr=PIPE, env={'HOME': '/root', 'PATH': os.environ['PATH']}).communicate()
+
+    # Don't use exec(), popen(), etc. to execute external commands without explicity using the full path of the external program.  Hijacked search path could be problematic.
+    #checkstatus, err = Popen(['/bin/bash', './freshness.sh'], stdout=PIPE, stderr=PIPE, env={'HOME': '/root', 'PATH': os.environ['PATH']}).communicate()
+
+    f = open (checkstatus)
+    output = f.read()
+
+    lastfull, lastinc = findlastdates(output)
+
+    sincelastfull = time.time() - lastfull 
+    sincelastinc  =  time.time() - lastinc 
+
+    msg = "OK: "
+    
+    if sincelastfull > (args.warnfull * 24 * 3600) or sincelastinc > (args.warninc * 3600):
+        okay = 1
+        msg = "WARNING: "
+    
+    if sincelastfull > (args.critfull * 24 * 3600) or sincelastinc > (args.critinc * 3600):
+        okay = 2
+        msg = "CRITICAL: "
+
+    if not checkoutput(output):
+        okay = max(okay,1)
+        msg = "WARNING: duplicity output: %s " % repr(output)
+
+    if err:
+        okay=2
+        msg = "Unexpected output: %s, " % repr(err)
+
+    print msg, "last full %s ago, last incremential %s ago|lastfull=%d, lastinc=%d" % ( formattime(sincelastfull), formattime(sincelastinc), sincelastfull, sincelastinc)
+    sys.exit(okay)
+
+def checkoutput(output):
+    if output.find("No orphaned or incomplete backup sets found.")==-1:
+        return False
+
+    return True
+
+def formattime(seconds):
+    days = seconds / (3600 * 24)
+    hours = seconds / 3600 % 24
+
+    if days:
+        return "%d days %d hours" % (days,hours)
+    else:
+        return "%d hours" % hours
+
+
+def findlastdates(output):
+    lastfull =0
+    lastinc = 0
+
+    for line in output.split("\n"):
+        parts = line.split()
+
+        # ['Incremental', 'Sun', 'Oct', '31', '03:00:04', '2010', '1']
+        if len (parts) == 7 and parts[0] in ["Full","Incremental"]:
+            foo = time.strptime(" ".join(parts[1:6]),"%a %b %d %H:%M:%S %Y")
+    
+            backuptime =  time.mktime(foo)
+    
+            if parts[0] == "Incremental" and lastinc < backuptime:
+                lastinc = backuptime
+            elif parts[0] == "Full" and lastfull < backuptime:
+                lastfull = backuptime
+        
+
+    # Count a full backup as incremental backup
+    lastinc = max(lastfull,lastinc)
+    return (lastfull, lastinc)
+       
+
+if __name__=='__main__':
+   main()
diff --git a/manifests/nagios_plugin/duplicity.pp b/manifests/nagios_plugin/duplicity.pp
new file mode 100644 (file)
index 0000000..93b0989
--- /dev/null
@@ -0,0 +1,22 @@
+class backupninja::nagios_plugin::duplicity {
+  case ::operatingsystem {
+    'Debian': { package { 'python-argparse': ensure => installed, } }
+    'Ubuntu': { package { 'python-argh':     ensure => installed, } }
+    default:  {
+      notify {'Backupninja-Duplicity Nagios check needs python-argparse to be installed !':}  }
+  }
+
+  nagios::plugin { 'check_backupninja_duplicity.py':
+    source => 'backupninja/nagios_plugins/duplicity/check_backupninja_duplicity.py'
+  }
+
+  # deploy helper script
+  file { '/usr/lib/nagios/plugins/backupninja_duplicity_freshness.sh':
+    source => 'puppet:///modules/backupninja/nagios_plugins/duplicity/backupninja_duplicity_freshness.sh',
+    mode   => '0755',
+    owner  => 'nagios',
+    group  => 'nagios',
+  }
+
+}
+