#!/usr/bin/perl =head1 NAME check_cups - monitor CUPS queues =head1 AUTHOR Steve Huff =head1 SYNOPSIS Polls a CUPS server to discover queues, then reports on queue status. =head1 REQUIRES Perl5.004, strict, warnings, Data::Dumper, Nagios::Plugin, Net::CUPS, Date::Manip, POSIX =head1 EXPORTS Nothing =head1 DESCRIPTION C polls the specified CUPS server and discovers print queues. It tests against a queue length threshold and an age of the oldest job in the queue threshold. Performance data (queue length and maximum queue age) is provided. Future development plans include the option to specify the number of expected print queues. =cut # these need to be outside the BEGIN use strict; use warnings; # see this page for an explanation regarding the levels of warnings: # http://search.cpan.org/~rgarcia/perl-5.6.2/pod/perllexwarn.pod no warnings qw( redefine prototype ); BEGIN { # use Opsview libs use lib '/usr/local/nagios/perl/lib'; use lib '/usr/local/nagios/lib'; use Nagios::Plugin; use Data::Dumper; use Date::Manip; use POSIX qw( strftime ); use Net::CUPS; # reregister interrupt $SIG{INT} = \&DoInterrupt; } ## end BEGIN ######################################################################## # IMPORTANT VARIABLES # # default values my( $timeout, $verbose ); my $WARNINGJOBS = 6; my $CRITICALJOBS = 10; my $WARNINGAGE = '20 minutes'; my $CRITICALAGE = '1 hour'; # ######################################################################## # # MAIN # # instantiate the Nagios::Plugin object my( $usagemsg ) = < [-w ] [-c ] [-W ] [-C ] [-t timeout] [-v|d] [-h] USAGE my( $blurb ) = <). LICENSE my( $plugin ) = Nagios::Plugin->new( shortname => 'check_cups', usage => $usagemsg, version => '0.2', blurb => $blurb, license => $license, ); =head2 Plugin Metadata Metadata is documented in L and L. Other available options of interest include C, C, and C. =cut # add the additional options $plugin->add_arg( spec => 'hostname|H=s', help => [ "Hostname to query", "IP address to query", ], label => [ 'HOSTNAME', 'IP' ], required => 1, ); # queued jobs $plugin->add_arg( spec => 'warning|w=i', help => "Warning threshold in number of queued jobs (default $WARNINGJOBS)", default => $WARNINGJOBS, label => [ 'QUEUED JOBS' ], required => 0, ); $plugin->add_arg( spec => 'critical|c=i', help => "Critical threshold in number of queued jobs (default $CRITICALJOBS)", default => $CRITICALJOBS, label => [ 'QUEUED JOBS' ], required => 0, ); # job age $plugin->add_arg( spec => 'warningage|W=s', help => "Warning threshold of job age (default '$WARNINGAGE')", default => $WARNINGAGE, label => [ 'TIME SPECIFICATION' ], required => 0, ); $plugin->add_arg( spec => 'criticalage|C=s', help => "Critical threshold of job age (default '$CRITICALAGE')", default => $CRITICALAGE, label => [ 'TIME SPECIFICATION' ], required => 0, ); =head2 Plugin Options Refer to L for option specifications - they differ slightly from pure L-style option specifications. A number of standard options (C<--help>, C<--version>, C<--usage>, C<--timeout>, and C<--verbose>) are implemented by default, thanks to the arguments passed to Cnew()>, and do not need to be specified. Moreover, if you attempt to override them, you will simply create two such options, which produces confusing C<--help> output and unexpected behavior. =cut # parse options $plugin->getopts; my( $opts ) = $plugin->opts; # declare variables my( $server, $queues, $warningjobs, $criticaljobs, $warningage, $criticalage ); # every variable must have a value $server = $opts->get( 'hostname' ); $timeout = $opts->get( 'timeout' ); # See above: option provided by default $verbose = $opts->get( 'verbose' ); # See above: option provided by default $warningjobs = $opts->get( 'warning' ); $criticaljobs = $opts->get( 'critical' ); $warningage = $opts->get( 'warningage' ); $criticalage = $opts->get( 'criticalage' ); # sanity check defined( $server ) or $plugin->nagios_die( "No value provided for --hostname!" ); # parse time differentials my( $nowdate ) = ParseDate( 'now' ); my( $warningdelta ) = ParseDateDelta( $warningage ); unless( defined( $warningdelta ) ) { $plugin->nagios_die( "'$warningage' is not a valid time specification!" ); } my( $criticaldelta ) = ParseDateDelta( $criticalage ); unless( defined( $criticaldelta ) ) { $plugin->nagios_die( "'$criticalage' is not a valid time specification!" ); } my( $warningminutes ) = sprintf( '%d', Delta_Format( $warningdelta, 'approx', 0, '%mt' ) ); my( $criticalminutes ) = sprintf( '%d', Delta_Format( $criticaldelta, 'approx', 0, '%mt' ) ); # critical thresholds must be greater than warning thresholds if ( $warningjobs >= $criticaljobs ) { $plugin->nagios_die( "Job number warning threshold ($warningjobs) must be less than critical threshold ($criticaljobs)." ); } if ( $warningminutes >= $criticalminutes ) { $plugin->nagios_die( "Job age warning threshold ($warningage) must be less than critical threshold ($criticalage)." ); } =head2 Testing and Exit Status The basic methodology of a Nagios plugin is as follows: =over =item 1. Run some sort of test. =item 2. Validate the result against provided (or default) thresholds. =item 3. Exit with the appropriate error code, optionally outputting text. =back Running the test is up to you. The function to use for validating the result is C (see L and L for details and specifications), which returns a value appropriate for use as an exit code. The functions to use for exiting are C and C. C is the general-purpose exit function, while C should be used for any unexpected exits and indicates that something went wrong with the plugin's operation. =head3 Return String The plugin return string performs two functions: =over =item 1. It communicates in human-readable format more details about what exactly has gone wrong (e.g. "Disk usage on '/var' is at 92% (threshold 90%)"). =item 2. It (optionally) communicates in machine-readable format performance data which can be aggregated by tools such as Nagiosgraph (e.g. '| time=0.042745s;5.000000;10.000000;0.000000 size=2162B;;;0'). See L for details and specifications; the function to use is C. =back The plugin string will automatically be populated with the plugin name and the exit status (e.g. "check_snmp_disk OK - "); everything after the "- " is what you provide with the message arguments to C or C. C takes care of appending the "|" and properly formatting performance data. =cut # begin tracking status and message my( $status, $message ) = ( OK, '' ); # do some test, with a timeout alarm $timeout; # the heavy lifting is done in Nagios::Plugin::Getopt my( $result ) = doTest( $server ); alarm 0; # validate the result, queue by queue my( %ok, %warning, %critical, %unknown ); debug( "Analyzing results..." ); foreach my $queuename ( keys( %{$result} ) ) { # FIXME - allow per-queue overrides my( $queue ) = $result->{$queuename}; my( $age, $jobs ) = ( $queue->{delta}, $queue->{numjobs} ); # sanitize values defined( $age ) or $age = 0; defined( $jobs ) or $jobs = 0; my( $agestatus ) = $plugin->check_threshold( check => $age, warning => $warningminutes, critical => $criticalminutes, ); my( $jobsstatus ) = $plugin->check_threshold( check => $jobs, warning => $warningjobs, critical => $criticaljobs, ); # sort the queue appropriately if ( $agestatus == CRITICAL or $jobsstatus == CRITICAL ) { $critical{$queuename} = { age => $age, jobs => $jobs }; } elsif ( $agestatus == WARNING or $jobsstatus == WARNING ) { $warning{$queuename} = { age => $age, jobs => $jobs }; } elsif ( $agestatus == UNKNOWN or $jobsstatus == UNKNOWN ) { $unknown{$queuename} = { age => $age, jobs => $jobs }; } else { $ok{$queuename} = { age => $age, jobs => $jobs }; } # add performance data $plugin->add_perfdata( label => $queuename . "_jobs", value => $jobs, uom => undef, warning => $warningjobs, critical => $criticaljobs, ); # add performance data $plugin->add_perfdata( label => $queuename . "_age", value => $age, uom => undef, warning => $warningminutes, critical => $criticalminutes, ); } # figure out our status if ( scalar( keys( %critical ) ) ) { debug( Data::Dumper->Dump( [\%critical], [qw(*critical)] ), 3 ); $status = CRITICAL; foreach my $queue ( sort( keys( %critical ) ) ) { $message .= "$queue ( "; my( $age, $jobs ) = ( $critical{$queue}->{age}, $critical{$queue}->{jobs} ); my( $prettyage ) = prettyDelta( ParseDateDelta( "$age minutes" ) ); my( @messages ); if ( $age > $criticalminutes ) { push( @messages, "job $prettyage old" ); } if ( $jobs > $criticaljobs ) { push( @messages, "$jobs queued jobs" ); } $message .= join( ',', @messages ); $message .= ' ) '; } } elsif ( scalar( keys( %warning ) ) ) { debug( Data::Dumper->Dump( [\%warning], [qw(*warning)] ), 3 ); $status = WARNING; foreach my $queue ( sort( keys( %warning ) ) ) { $message .= "$queue ( "; my( $age, $jobs ) = ( $warning{$queue}->{age}, $warning{$queue}->{jobs} ); my( $prettyage ) = prettyDelta( parseDateDelta( "$age minutes" ) ); my( @messages ); if ( $age > $warningminutes ) { push( @messages, "job $prettyage old" ); } if ( $jobs > $warningjobs ) { push( @messages, "$jobs queued jobs" ); } $message .= join( ',', @messages ); $message .= ' ) '; } } elsif ( scalar( keys( %unknown ) ) ) { debug( Data::Dumper->Dump( [\%unknown], [qw(*unknown)] ), 3 ); $status = UNKNOWN; $message .= 'Unable to determine status: '; $message .= join( ',', sort( keys( %unknown ) ) ); } else { debug( Data::Dumper->Dump( [\%ok], [qw(*ok)] ), 3 ); $message .= 'All queues within parameters.'; } # exit with appropriate return values $plugin->nagios_exit( $status, $message ); ################################################################################ # # # doTest - poll server for printers # # ################################################################################ sub doTest( $ ) { my( $host ) = @_; my( %queues ); debug( "Creating Net::CUPS object...", 2 ); my( $cups ) = Net::CUPS->new(); unless ( defined( $cups ) ) { debug( "Unable to create Net::CUPS object: $!" ); return( undef ); } debug( "Setting server to '$host'...", 2 ); $cups->setServer( $host ); debug( "Polling for queues...", 1 ); foreach my $queue ( $cups->getDestinations() ) { my( $name ) = $queue->getName(); if ( defined( $name ) ) { $queues{$name} = { destination => $queue }; debug( "Found queue '$name'.", 3 ); } else { debug( "Unable to determine queue name!" ); } } debug( "Parsing queues...", 1 ); my( $now ) = strftime( '%s', localtime() ); foreach my $queue ( keys( %queues ) ) { my( $destination ) = $queues{$queue}->{destination}; my( @jobs ) = $destination->getJobs( 0, 0 ); ( @jobs ) or ( @jobs ) = (); foreach my $jobid ( @jobs ) { my( $job ) = \%{$destination->getJob( $jobid )}; $queues{$queue}->{jobs}->{$jobid} = $job; my( $delta ) = deltaMinutes( $job->{creation_time}, $now ); # track the largest delta in the queue if ( ( ! exists( $queues{$queue}->{delta} ) ) || ( $delta > $queues{$queue}->{delta} ) ) { $queues{$queue}->{delta} = $delta; } } $queues{$queue}->{numjobs} = keys( %{$queues{$queue}->{jobs}} ); } debug( Data::Dumper->Dump( [\%queues], [qw(*queues)] ), 3 ); return( \%queues ); } ################################################################################ # # # deltaMinutes - determine the delta in days between two datestamps # # ################################################################################ sub deltaMinutes( $$ ) { my( $lesser, $greater ) = sort( @_ ); debug( "Received '$lesser', '$greater'.", 3 ); my( $lesserdate ) = ParseDateString( "epoch $lesser" ); my( $greaterdate ) = ParseDateString( "epoch $greater" ); my( $delta ) = DateCalc( $lesserdate, $greaterdate ); my( $deltaminutes ) = sprintf( '%d', Delta_Format( $delta, 'approx', 0, '%mt' ) ); defined( $deltaminutes ) or $deltaminutes = 0; debug( "Delta: $deltaminutes minutes.", 3 ); return( $deltaminutes ); } ################################################################################ # # # prettyDelta - display time delta in sensible format # # ################################################################################ sub prettyDelta( $ ) { my( $delta ) = @_; debug( "Delta: '$delta'", 3 ); # parse the delta my( $days, $hours, $minutes ) = Delta_Format( $delta, 'approx', 0, ( qw( %dh %hv %mv ) ) ); debug( "\$days: $days\n\$hours: $hours\n\$minutes: $minutes", 3 ); # assemble the string my( $pretty ); # a day or more? if ( $days ) { $pretty .= "$days day"; if ( $days > 1 ) { $pretty .= "s"; } } # hours? if ( $hours ) { if ( $days ) { if ( $minutes ) { $pretty .= ", "; } else { $pretty .= " and "; } } $pretty .= "$hours hour"; if ( $hours > 1 ) { $pretty .= "s"; } } # minutes? if ( $minutes ) { if ( $days && $hours ) { $pretty .= ","; } if ( $days || $hours ) { $pretty .= " and "; } $pretty .= "$minutes minute"; if ( $minutes > 1 ) { $pretty .= "s"; } } debug( "\$pretty: '$pretty'", 3 ); return( $pretty ); } ################################################################################ # # # debug - print debug info # # ################################################################################ sub debug( $;$ ) { my( $message, $level ) = @_; defined( $level ) or $level = 1; if ( $verbose >= $level ) { chomp $message; print "$message\n"; } } ################################################################################ # # # DoInterrupt - handle SIGINT and clean up # # ################################################################################ sub DoInterrupt { $plugin->nagios_die( "Interrupt received" ); }