1#!/usr/bin/perl -w 2 3######################################################################## 4# 5# <@LICENSE> 6# Licensed to the Apache Software Foundation (ASF) under one or more 7# contributor license agreements. See the NOTICE file distributed with 8# this work for additional information regarding copyright ownership. 9# The ASF licenses this file to you under the Apache License, Version 2.0 10# (the "License"); you may not use this file except in compliance with 11# the License. You may obtain a copy of the License at: 12# 13# http://www.apache.org/licenses/LICENSE-2.0 14# 15# Unless required by applicable law or agreed to in writing, software 16# distributed under the License is distributed on an "AS IS" BASIS, 17# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18# See the License for the specific language governing permissions and 19# limitations under the License. 20# </@LICENSE> 21# 22######################################################################## 23 24# Written by Daryl C. W. O'Shea, DOS Technologies <spamassassin@dostech.ca> 25# See perldoc sa-check_spamd for program info. 26 27use strict; 28use warnings; 29use re 'taint'; 30 31my $PREFIX = '@@PREFIX@@'; # substituted at 'make' time 32my $DEF_RULES_DIR = '@@DEF_RULES_DIR@@'; # substituted at 'make' time 33my $LOCAL_RULES_DIR = '@@LOCAL_RULES_DIR@@'; # substituted at 'make' time 34my $LOCAL_STATE_DIR = '@@LOCAL_STATE_DIR@@'; # substituted at 'make' time 35use lib '@@INSTALLSITELIB@@'; # substituted at 'make' time 36 37use Errno qw(EBADF); 38use File::Spec; 39use Config; 40use POSIX qw(locale_h setsid sigprocmask _exit); 41 42POSIX::setlocale(LC_TIME,'C'); 43 44BEGIN { # see comments in "spamassassin.raw" for doco 45 my @bin = File::Spec->splitpath($0); 46 my $bin = ($bin[0] ? File::Spec->catpath(@bin[0..1], '') : $bin[1]) 47 || File::Spec->curdir; 48 49 if (-e $bin.'/lib/Mail/SpamAssassin.pm' 50 || !-e '@@INSTALLSITELIB@@/Mail/SpamAssassin.pm' ) 51 { 52 my $searchrelative; 53 $searchrelative = 1; # disabled during "make install": REMOVEFORINST 54 if ($searchrelative && $bin eq '../' && -e '../blib/lib/Mail/SpamAssassin.pm') 55 { 56 unshift ( @INC, '../blib/lib' ); 57 } else { 58 foreach ( qw(lib ../lib/site_perl 59 ../lib/spamassassin ../share/spamassassin/lib)) 60 { 61 my $dir = File::Spec->catdir( $bin, split ( '/', $_ ) ); 62 if ( -f File::Spec->catfile( $dir, "Mail", "SpamAssassin.pm" ) ) 63 { unshift ( @INC, $dir ); last; } 64 } 65 } 66 } 67} 68 69use Getopt::Long; 70 71use constant HAS_TIME_HIRES => eval { require Time::HiRes; }; 72use constant HAS_MSA_CLIENT => eval { require Mail::SpamAssassin::Client; }; 73use constant HAS_MSA_TIMEOUT => eval { require Mail::SpamAssassin::Timeout; }; 74use Mail::SpamAssassin::Util; 75 76### nagios plugin return codes: 0 OK / 1 Warning / 2 Critical / 3 Unknown ### 77use constant EX_OK => 0; 78use constant EX_WARNING => 1; 79use constant EX_CRITICAL => 2; 80use constant EX_UNKNOWN => 3; 81 82my $VERSION = $Mail::SpamAssassin::VERSION; 83 84my %opt = ( 85 'critical' => undef, 86 'hostname' => undef, 87 'port' => undef, 88 'socketpath' => undef, 89 'timeout' => 45, 90 'verbose' => undef, 91 'warning' => undef, 92); 93 94# Parse the command line 95Getopt::Long::Configure("bundling"); 96GetOptions( 97 'critical|c=s' => \$opt{'critical'}, 98 'help|h|?' => sub { print_usage_and_exit(); }, 99 'hostname|H=s' => \$opt{'hostname'}, 100 'port|p=s' => \$opt{'port'}, 101 'socketpath=s' => \$opt{'socketpath'}, 102 'timeout|t=s' => \$opt{'timeout'}, 103 'verbose|v' => \$opt{'verbose'}, 104 'version|V' => sub { print "sa-check_spamd version $VERSION\n"; exit EX_UNKNOWN; }, 105 'warning|w=s' => \$opt{'warning'}, 106 107) or print_usage_and_exit(); 108 109 110if (defined $opt{'critical'}) { 111 if ($opt{'critical'} =~ /^(\d+(?:\.\d*)?)$/) { 112 $opt{'critical'} = $1; 113 } else { 114 print "SPAMD UNKNOWN: invalid critical config value provided\n"; 115 exit EX_UNKNOWN; 116 } 117} 118 119if (defined $opt{'hostname'}) { 120 if ($opt{'hostname'} =~ /^([A-Za-z0-9_.:-]+)$/) { 121 $opt{'hostname'} = $1; 122 } else { 123 print "SPAMD UNKNOWN: invalid hostname config value provided\n"; 124 exit EX_UNKNOWN; 125 } 126} 127 128if (defined $opt{'port'}) { 129 if ($opt{'port'} =~ /^(\d+)$/) { 130 $opt{'port'} = $1; 131 } else { 132 print "SPAMD UNKNOWN: invalid port config value provided\n"; 133 exit EX_UNKNOWN; 134 } 135} 136 137# TODO: --socketpath isn't checked, suboptimal 138 139if ($opt{'timeout'} =~ /^(\d+(?:\.\d*)?)$/ && $opt{'timeout'} >= 1) { 140 $opt{'timeout'} = $1; 141} else { 142 print "SPAMD UNKNOWN: invalid timeout config value provided\n"; 143 exit EX_UNKNOWN; 144} 145 146if (defined $opt{'warning'}) { 147 if ($opt{'warning'} =~ /^(\d+(?:\.\d*)?)$/) { 148 $opt{'warning'} = $1; 149 } else { 150 print "SPAMD UNKNOWN: invalid warning config value provided\n"; 151 exit EX_UNKNOWN; 152 } 153} 154 155# logic checking 156if (defined $opt{'critical'} && defined $opt{'warning'} && 157 $opt{'critical'} < $opt{'warning'}) { 158 print "SPAMD UNKNOWN: critical value is less than warning value, config not valid\n"; 159 exit EX_UNKNOWN; 160} 161 162if (defined $opt{'critical'} && defined $opt{'timeout'} && 163 $opt{'critical'} > $opt{'timeout'}) { 164 print "SPAMD UNKNOWN: critical value is greater than timeout value, config not valid\n"; 165 exit EX_UNKNOWN; 166} 167 168if (defined $opt{'warning'} && defined $opt{'timeout'} && 169 $opt{'warning'} > $opt{'timeout'}) { 170 print "SPAMD UNKNOWN: warning value is greater than timeout value, config not valid\n"; 171 exit EX_UNKNOWN; 172} 173 174# check to make sure that both TCP and UNIX domain socket info wasn't provided 175if ((defined $opt{'hostname'} || defined $opt{'port'}) && defined $opt{'socketpath'}) { 176 print "SPAMD UNKNOWN: both TCP and UNIX domain socket info provided, only one can be used\n"; 177 exit EX_UNKNOWN; 178} 179 180# if not provided with a spamd service to connect to set some defaults 181unless (defined $opt{'socketpath'}) { 182 $opt{'hostname'} ||= 'localhost'; 183 $opt{'port'} ||= 783; 184} 185 186 187if ($opt{'verbose'}) { 188 print ((HAS_MSA_CLIENT ? "loaded" : "failed to load") ." Mail::SpamAssassin::Client\n"); 189 print ((HAS_MSA_TIMEOUT ? "loaded" : "failed to load") ." Mail::SpamAssassin::Timeout\n"); 190} 191 192# If there's no client available, there's no way to check the service... 193unless (HAS_MSA_CLIENT && HAS_MSA_TIMEOUT) { 194 # Nagios will only display the first line printed. 195 print "SPAMD UNKNOWN: could not load M:SA::Client\n" unless HAS_MSA_CLIENT; 196 print "SPAMD UNKNOWN: could not load M:SA::Timeout\n" unless HAS_MSA_TIMEOUT; 197 print "cannot continue\n" if $opt{'verbose'}; 198 exit EX_UNKNOWN; 199} 200 201 202# untaint the command-line args; since the root user supplied these, and 203# we're not a setuid script, we trust them. This needs to be called explicitly 204foreach my $optkey (keys %opt) { 205 next if ref $opt{$optkey}; 206 Mail::SpamAssassin::Util::untaint_var(\$opt{$optkey}); 207} 208 209 210# If the client connection fails it'll spit out it's own error message which 211# is probably more appropriate than anything we can provide to Nagios ourself. 212# We'll still spit out something later, but Nagios will ignore it since it 213# only uses the first line of output. 214my $client; 215if (defined $opt{'port'}) { 216 $client = new Mail::SpamAssassin::Client({port => $opt{'port'}, 217 host => $opt{'hostname'}}); 218} else { 219 $client = new Mail::SpamAssassin::Client({socketpath => $opt{'socketpath'}}); 220} 221 222# this'd be weird, but totally dependent on the client 223unless (defined $client) { 224 print "SPAMD UNKNOWN: could not create M::SA::Client instance\n"; 225 print "failed to create Mail::SpamAssassin::Client instance\n" if $opt{'verbose'}; 226 exit EX_UNKNOWN; 227} 228 229# until we try a ping, the ping response status is unknown 230my $response = -1; 231print "connecting to spamd for ping\n" if $opt{'verbose'}; 232 233my $timer = Mail::SpamAssassin::Timeout->new({ secs => $opt{'timeout'}}); 234my $t0 = (HAS_TIME_HIRES ? Time::HiRes::time() : time()); 235 236my $err = $timer->run(sub { 237 if ($client->ping()) { 238 $response = 1; 239 } else { 240 $response = 0; 241 } 242}); 243 244my $elapsed = (HAS_TIME_HIRES ? Time::HiRes::time() : time()) - $t0; 245 246 247# a ping response should be most common, we'll handle it first 248if ($response == 1) { 249 # it's possible that we may timeout right after setting the response status to 1 250 # since the timeout value > the critical value, this is a critical state 251 if ((defined $opt{'critical'} && $elapsed > $opt{'critical'}) || $timer->timed_out()) { 252 printf("SPAMD CRITICAL: %.3f second ping response time\n", $elapsed); 253 exit EX_CRITICAL; 254 } 255 256 # warning state will never timeout since that'd be critical (above) 257 if (defined $opt{'warning'} && ($elapsed > $opt{'warning'})) { 258 printf("SPAMD WARNING: %.3f second ping response time\n", $elapsed); 259 exit EX_WARNING; 260 } 261 262 # otherwise we got a timely ping response 263 printf("SPAMD OK: %.3f second ping response time\n", $elapsed); 264 exit EX_OK; 265} 266 267# any way we get a failed ping response is a critical state 268if ($response == 0) { 269 printf("SPAMD CRITICAL: ping failed in %.3f seconds\n", $elapsed); 270 exit EX_CRITICAL; 271} 272 273if ($response == -1) { 274 # this is the common timeout scenario 275 if ($timer->timed_out()) { 276 printf("SPAMD CRITICAL: ping timed out in %.3f seconds\n", $elapsed); 277 exit EX_CRITICAL; 278 } 279 280 # dos: I'll buy lunch for the first person that gets a page about this while 281 # they're sleeping if they come to Midland, ON to get it 282 printf("SPAMD UNKNOWN: assertion! unknown ping response status without timeout after %.3f seconds\n", $elapsed); 283 exit EX_UNKNOWN; 284} 285 286# and some apple pie too 287exit EX_UNKNOWN; 288 289 290############################################################################# 291 292sub print_usage_and_exit { 293 print <<EOF; 294sa-check_spamd version $VERSION 295 296For more details, use "perldoc sa-check_spamd". 297 298Usage: 299 sa-check_spamd [options] 300 301 Options: 302 303 -c secs, --critical=secs Critical ping response threshold 304 -h, -?, --help Print usage message 305 -H hostname, --hostname=hostname Hostname of spamd service to ping 306 -p port, --port=port Port of spamd service to ping 307 --socketpath=path Connect to given UNIX domain socket 308 -t secs, --timeout=secs Max time to wait for a ping response 309 -v, --verbose Verbose debug output 310 -V, --version Output version info 311 -w secs, --warning=secs Warning ping response threshold 312 313EOF 314 315 exit EX_UNKNOWN; 316} 317 318# Don't use a __DATA__ here, it screws up embedded Perl Nagios (ePN) 319 320=head1 NAME 321 322sa-check_spamd - spamd monitoring script for use with Nagios, etc. 323 324=head1 SYNOPSIS 325 326sa-check_spamd [options] 327 328Options: 329 330 -c secs, --critical=secs Critical ping response threshold 331 -h, -?, --help Print usage message 332 -H hostname, --hostname=hostname Hostname of spamd service to ping 333 -p port, --port=port Port of spamd service to ping 334 --socketpath=path Connect to given UNIX domain socket 335 -t secs, --timeout=secs Max time to wait for a ping response 336 -v, --verbose Verbose debug output 337 -V, --version Output version info 338 -w secs, --warning=secs Warning ping response threshold 339 340 341=head1 DESCRIPTION 342 343The purpose of this program is to provide a tool to monitor the status of 344C<spamd> server processes. spamd is the daemonized version of the 345spamassassin executable, both provided in the SpamAssassin distribution. 346 347This program is designed for use, as a plugin, with the Nagios service 348monitoring software available from http://nagios.org. It might be compatible 349with other service monitoring packages. It is also useful as a command line 350utility or as a component of a custom shell script. 351 352=head1 OPTIONS 353 354Options of the long form can be shortened as long as the remain 355unambiguous (i.e. B<--host> can be used instead of B<--hostname>). 356 357=over 4 358 359=item B<-c> I<secs>, B<--critical>=I<secs> 360 361Critical ping response threshold in seconds. If a spamd ping response takes 362longer than the value specified (in seconds) the program will exit with a 363value of 2 to indicate the critical status. 364 365This value must be at least as long as the value specified for B<warning> and 366less than the value specified for B<timeout>. 367 368=item B<-h>, B<-?>, B<--help> 369 370Prints this usage message and exits. 371 372=item B<-H> I<hostname>, B<--hostname>=I<hostname> 373 374The hostname, or IP address, of the spamd service to ping. By default the 375hostname B<localhost> is used. If B<--socketpath> is set this value will be 376ignored. 377 378=item B<-p> I<port>, B<--port>=I<port> 379 380The port of the spamd service to ping. By default port B<783> (the spamd 381default port number) is used. If B<--socketpath> is set this value will be 382ignored. 383 384=item B<--socketpath>=I<path> 385 386Connect to given UNIX domain socket. Use instead of a hostname and TCP port. 387When set, any hostname and TCP port specified will be ignored. 388 389=item B<-t> I<secs>, B<--timeout>=I<secs> 390 391The maximum time to wait for a ping response. Once exceeded the program will 392exit with a value of 2 to indicate the critical status. The default timeout 393value is 45 seconds. The timeout must be no less than 1 second. 394 395This value must be greater than the values specified for both the B<critical> 396and B<warning> values. 397 398=item B<-v>, B<--verbose> 399 400Display verbose debug output on STDOUT. 401 402=item B<-V>, B<--version> 403 404Display version info on STDOUT. 405 406=item B<-w> I<secs>, B<--warning>=I<secs> 407 408Warning ping response threshold in seconds. If a spamd ping response takes 409longer than the value specified (in seconds), and does not exceed the 410B<critical> threshold value, the program will exit with a value of 1 to 411indicate the warning status. 412 413This value must be no longer than the value specified for B<critical> and 414less than the value specified for B<timeout>. 415 416=back 417 418=head1 EXIT CODES 419 420The program will indicate the status of the spamd process being monitored by 421exiting with one of these values: 422 423=over 4 424 425=item C<0> 426 427OK: A spamd ping response was received within all threshold times. 428 429=item C<1> 430 431WARNING: A spamd ping response exceeded the warning threshold but not the 432critical threshold. 433 434=item C<2> 435 436CRITICAL: A spamd ping response exceeded either the critical threshold or the 437timeout value. 438 439=item C<3> 440 441UNKNOWN: An error, probably caused by a missing dependency or an invalid 442configuration parameter being supplied, occurred in the sa-check_spamd program. 443 444=back 445 446=head1 SEE ALSO 447 448spamc(1) 449spamd(1) 450spamassassin(1) 451 452=head1 PREREQUISITES 453 454C<Mail::SpamAssassin> version 3.1.1 or higher (3.1.6 or higher recommended) 455 456=head1 AUTHOR 457 458Daryl C. W. O'Shea, DOS Technologies <spamassassin@dostech.ca> 459 460=head1 COPYRIGHT AND LICENSE 461 462sa-check_spamd is distributed under the Apache License, Version 2.0, as 463described in the file C<LICENSE> included with the Apache SpamAssassin 464distribution and available at http://www.apache.org/licenses/LICENSE-2.0 465 466Copyright (C) 2015 The Apache Software Foundation 467 468=cut 469