#!/usr/bin/perl # # Copyright (c) 2013, Michael Hanselmann # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # - Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # - Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # - Neither the name of Michael Hanselmann nor the names of any contributor may # be used to endorse or promote products derived from this software without # specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. use strict; use warnings; use Try::Tiny; use POSIX qw(ctime); use Getopt::Std; use File::Basename; use Foundation; use constant OK => 0; use constant WARNING => 1; use constant CRITICAL => 2; use constant UNKNOWN => 3; use constant BACKUPD_STRINGS => '/System/Library/CoreServices/backupd.bundle/' . 'Contents/Resources/English.lproj/Localizable.strings'; my $result_plist_path = '/var/db/.TimeMachine.Results.plist'; my $VERSION = '0.1.1'; $Getopt::Std::STANDARD_HELP_VERSION = 1; sub HELP_MESSAGE { my ($fh) = @_; my $name = basename($0); print $fh "Usage: $name -w -c \n", "\n", " -w Seconds since last backup to issue warning\n", " -c Seconds since last backup for critical status\n", "\n"; } sub VERSION_MESSAGE { my ($fh) = @_; my $name = basename($0); print $fh "$name version $VERSION\n"; } # Use UTF-8 output binmode(STDOUT, ':utf8'); binmode(STDERR, ':utf8'); sub load_plist { my ($path) = @_; my $dict = NSDictionary->dictionaryWithContentsOfFile_($path); unless ($$dict) { die "Couldn't load results from '$path'\n"; } return Foundation::perlRefFromObjectRef($dict); } sub parse_date { my ($date) = @_; my $obj = NSDate->dateWithString_($date); unless ($$obj) { die "Couldn't parse date string '$date'\n"; } return $obj->timeIntervalSince1970; } sub lookup_error { my ($code) = @_; return try { my $strings = load_plist(BACKUPD_STRINGS); my $header = $strings->{"RESULTCODE_${code}_HEADER"}; my $msg = $strings->{"RESULTCODE_${code}_MESSAGE"}; if (defined $header) { chomp $header; $header =~ s/\s+/ /g; } if (defined $msg) { chomp $msg; $msg =~ s/\s+/ /g; } if (defined $header and defined $msg) { "$header: $msg"; } elsif (defined $msg) { $msg; } else { die; } } catch { "Unknown error code $code"; }; } sub _exit { my ($code, $prefix, $msg) = @_; chomp $msg; print "$prefix: $msg\n"; exit $code; } sub exit_unknown { _exit(UNKNOWN, 'UNKNOWN', @_); } sub exit_critical { _exit(CRITICAL, 'CRITICAL', @_); } sub exit_warning { _exit(WARNING, 'WARNING', @_); } sub exit_ok { _exit(OK, 'OK', @_); } sub main { my $warning_age; my $critical_age; my %opts; getopts('w:c:', \%opts) or exit UNKNOWN; if (defined $opts{w}) { $warning_age = $opts{w}; } else { exit_unknown("Missing warning age (-w)"); } if (defined $opts{c}) { $critical_age = $opts{c}; } else { exit_unknown("Missing critical age (-w)"); } if ($warning_age =~ m/^\d+/) { $warning_age = int($warning_age); } else { exit_unknown("Warning age is not a number: $warning_age"); } if ($critical_age =~ m/^\d+/) { $critical_age = int($critical_age); } else { exit_unknown("Critical age is not a number: $critical_age"); } if ($critical_age < $warning_age) { exit_unknown("Critical age must be equal or larger than warning age"); } # Try loading data my $data = try { load_plist($result_plist_path); } catch { exit_critical($_); }; my $result_str = $data->{RESULT}; my $completed_str = $data->{BACKUP_COMPLETED_DATE}; unless (defined $result_str) { exit_critical('Unable to find result code'); } unless ($result_str =~ m/^\d+$/) { exit_critical("Result code not a number: $result_str"); } my $result_code = int($result_str); if ($result_code != 0) { exit_critical(lookup_error($result_code)); } unless (defined $completed_str) { exit_critical('Unable to find completion date'); } my $completed = try { parse_date($completed_str); } catch { exit_critical($_); }; my $now = time; chomp (my $fmtcompleted = ctime($completed)); if ($completed < ($now - $critical_age)) { exit_critical("Backup is older than $critical_age seconds ($fmtcompleted)"); } elsif ($completed < ($now - $warning_age)) { exit_warning("Backup is older than $warning_age seconds ($fmtcompleted)"); } else { exit_ok("Backup finished at " . ctime($completed)); } } main; # vim: set sw=2 sts=2 et foldmethod=marker :