#!/usr/bin/perl -w # Version 1.5 Feb 2001 by Yufan Hu yufan@yahoo.com # updown: a general network service monitor # uses a config file to define host, service, test method # expected result and notification address. If test doesn't return # expected result, notify. Sample cfg file (comments allowed): # (I've 'embedded' it in the $Sample_config var for use w/ the -help command) my $Sample_config = ' # updown Sample config file # which host to test host=www.anywhere.com # who to contact email=andy@anywhere.com # where to keep current status status=/tmp/www.anywhere.status # where to put running log file log=/tmp/www.anywhere.status.log # text name for service (for notification only) service=Web Server # test to run: socket host port # can use external commands too command=socket www.anywhere.com 80 # data to send (can have metachars) send=HEAD / HTTP/1.0\n\n # result text to expect (can be an RE) expect=^HTTP.*200 OK # alway_talk > 0 will return status every run; set to 0 (default) for cron always_talk=1 # notification "from name" text (not use hostname/nslookup exe if defined) from_hostname=updown@anywhere.com # mailer e.g. /usr/lib/sendmail (default) mailer=/usr/sbin/sendmail # end-service tag allows one config file to have many services end-service #now repeat all the fields above to define another service '; # # # ver 1.1 Made 'strict' and fixed small spelling/quote errors, 10/00 afb # ver 1.5 Made readConfig handle multiple services in a single config 02/01 afb my $Usage = "Usage: $0 [-test] config_file -test set debugging info on, notify regardless -help print sample config file"; # use Socket; use strict; my $timeout = 30; my $def_log_dir = "/tmp"; my $tmp_dir = "/tmp"; my $debug = 0; my (%status, %service); my $host_cmd = '/gov/bin/host'; # default to silence for cron usages $service{always_talk} = 0; # default mailer $service{mailer} = "/usr/lib/sendmail"; our @RES; die "$Usage" unless( @ARGV ); my $param = ""; if( $ARGV[0] =~ /^-\w+/ ) { die "$Usage\n$Sample_config" unless $ARGV[0] =~ /(test|debug)/; $param = $1; $debug = 10; $service{always_talk} = 1 unless $param eq 'debug'; shift; } #$| = 1; my ($newstatus); my $config_file = shift; if ( ! -s $config_file ) { print "$Usage"; exit(1); } open( CONF, $config_file ) || die "Cannot open config file $config_file: $!\n"; while ( readConfig() == 0 ) { dumpConf() if $debug; readStatus(); dumpStatus() if $debug; $newstatus = checkStatus(); $newstatus = "no status" unless $newstatus; print $newstatus . "\n" if $debug; print "newstatus=[", $newstatus, "]", " oldstatus=[" , $status{status}, "]\n" if $service{always_talk}; if( $newstatus ne $status{status} || $newstatus =~ /^Down:/ || $debug ) { notify( $newstatus ); dumpStatus() if $debug; writeStatus(); } } # while readConfig close CONF; exit(0); sub doLog { open( LOG, ">>$service{log}") or die "can't open log $service{log}: $!"; print LOG scalar(localtime), " @_\n"; close LOG; } sub notify { my($status) = @_; my $oldstatus = $status{status}; $status{status} = $status; if( $status =~ /^Down:/ && $oldstatus =~ /^Down:/ ) { if( time - $status{notified} > 60 && time - $status{notified} < 1800 ) { $status .= ' -- still down'; } else { return; } } ( $status !~ /-- still down/ ) && ($status{notified} = time ); if( $status ne $oldstatus ) { $status{time} = localtime; ( $status =~ /^Up/ ) && ( $status .= " from " . $oldstatus); $service{log} && doLog( $newstatus ); } if( $service{email} && $debug < 5) { my $hostname; unless( $hostname = $service{from_hostname} ) { $hostname = `hostname`; chomp($hostname); $hostname = `$host_cmd $hostname`; #$hostname = `nslookup -s $hostname|grep Name:`; chomp($hostname); } my $msg = "Subject: $status\n" . "\n" . "From $hostname\n" . "$status{service} on $status{host}\n$status\n" . "Since " . $status{time} . "\n"; open( MAIL, "|$service{mailer} $service{email}") or die "MAIL $service{mailer} failed: $!"; print MAIL $msg; close MAIL; } } sub readStatus { %status = (); $status{status} = 'Unknown'; $status{service} = $service{service}; $status{host} = $service{host}; $status{time} = scalar(localtime); $status{notified} = ''; open( STAT, $service{status} ) || return; while( ) { chop; my( $service, $host, $time, $notified, $status ) = split(/\t/); $status{status} = $status if $status; $status{service} = $service{service}; $status{host} = $service{host}; $status{time} = $time ? $time : scalar(localtime); $status{notified} = $notified ? $notified : time; } close STAT; } sub writeStatus { open( S, ">" . $service{status} ) or die "Cannot create status file $service{status}: $!\n"; print S join("\t", $status{service}, $status{host}, $status{time}, $status{notified}, $status{status}), "\n"; close S; } sub doHEAD { require LWP; require LWP::Debug; require LWP::UserAgent; use URI; use URI::Heuristic qw(uf_uri); use HTTP::Status qw(status_message); use HTTP::Date qw(time2str str2time); my $method = "HEAD"; print STDERR "doHEAD doing: $service{command}\n" if $debug; my($host) = $service{command} =~ /head\s+(\S+)/; return "Invalid HEAD command" unless $host; print STDERR "doHEAD: $host\n" if $debug > 3; my ($response); my $request = HTTP::Request->new($method); my $ua = LWP::UserAgent->new(); my $url = "$host"; $url = uf_uri($url); for ( 1 .. 2 ) { eval { local $SIG{ALRM} = sub { die "timeout\n" }; alarm $timeout; print STDERR "Sending: $service{send}\n" if $service{send} and $debug; # Send the request and get a response back from the server $request->url($url); $response = $ua->request($request); }; # eval alarm 0; print STDERR "doHEAD: problem $@ for $host" if $@; last if ($response->is_success); } # foreach try twice??? if ($response->is_success) { # Display headers print STDERR $response->status_line, "\n", $response->headers_as_string, "\n" if $debug; push(@RES, $response->status_line); } else { print STDERR $response->error_as_HTML if $debug; push(@RES, $response->error_as_HTML); return "Down: problem in establishing the connection to $host (" . $response->error_as_HTML . ")" } return 1; } # sub doHEAD sub doSocket { print STDERR "doSocket doing: $service{command}\n" if $debug; my($host, $port) = $service{command} =~ /socket\s*(\S+)\s+(\S+)/; $host && $port || return "Invalid socket command"; print STDERR "doSocket: $host:$port\n" if $debug > 3; eval { local $SIG{ALRM} = sub { die "timeout\n" }; alarm $timeout; my $ip_addr = inet_aton( $host ) || die "unknown remote host: $!"; socket( RES, AF_INET, SOCK_STREAM, 0 ) || die "cannot allocate socket: $!"; connect RES, sockaddr_in( $port, $ip_addr ) || die "cannot connect to $host:$port: $!\n"; select RES; $| = 1; print STDERR "Sending: $service{send}\n" if $service{send} and $debug; print RES $service{send} if $service{send}; print RES $service{send2} if $service{send2}; select STDOUT; }; alarm 0; print STDERR "doSocket: problem $@ for $host:$port" if $@; return "Down: problem in establishing the connection to $host:$port ($@)" if $@; @RES = ; close RES; return 1; } sub doInternal { print STDERR "doInternal trying: $service{command}\n" if $debug; ( $service{command} =~ /^socket/ ) && return doSocket(); ( $service{command} =~ /^head/ ) && return doHEAD(); return 0; } sub checkStatus { # check whether it is an internal command my( $result ) = doInternal(); $result && $result ne "1" && return $result; print STDERR "checkStatus: result: $result\n" if $debug > 3; # do external command. my($tmp) = $tmp_dir . "/" . "updown.$$"; if( !$result ) { # execute the command to do the test my( $cmd ) = $service{command} . " > $tmp 2>&1 " ; if ( $cmd ) { eval { CORE::open( CMD, "| $cmd " ) || return "Cannot execute command " . $service{command} . "$!\n"; select CMD; $| = 1; select STDOUT; print CMD $service{send} if $service{send}; print CMD $service{send2} if $service{send2}; close CMD; }; # check the result if ( -s $tmp ) { open( RES, $tmp) or return "Cannot open result file $tmp: $!\n"; @RES = ; close RES; } # if ( -s $tmp ) } # if $cmd } my $expected = 0; my $unexpected = 0; my $return_string = "Up"; my @errors; my $first_line; #eval #{ # local $SIG{ALRM} = sub { die "alarm\n" ; }; # alarm $timeout; # print "\nReading\n" if $debug; #while( defined($_ = readLine()) ) #while( ) foreach ( @RES ) { $first_line = $_ unless $first_line; warn "line=[", $_, "]\n" if $debug; eval( /$service{expect}/ ) && ($expected = 1); $service{unexpect} && /$service{unexpect}/ && ($unexpected = "$_"); } #}; #alarm 0; #close RES; @RES = (); unlink $tmp unless $debug > 8; if ( $@ ) { push @errors, "Down: Eval error - $@"; } if ( !$expected ) { push @errors, "\"$service{expect}\" expected but did not appear ($first_line)"; } if ( $unexpected ) { push @errors, " \"$service{unexpect}\" unexpected but seen: $unexpected"; } $return_string = "Down: " . join("\n",@errors) if @errors; return $return_string; } sub readLine { my $line = ''; my $char; while( sysread( RES, $char, 1 ) == 1 ) { next if $char eq "\r"; last if $char eq "\n"; $line .= $char; }; $line; } sub readConfig { #my( $conf ) = @_; #$conf || return 1; #open( CONF, $conf ) || die "Cannot open config file $conf: $!\n"; # %service = (); return 1 if eof CONF; undef $service{send}; undef $service{send2}; while() { last if /end-service/; /^\s*#/ && next; /^\s*(\w+)\s*=\s*(.*)\s*$/ || next; $1 && "$2" && ($service{$1} = $2); } #close CONF; my $bad = 0; $service{host} || warn "host must exist" && ($bad = 1); $service{command} || warn "command must exist" && ($bad = 1); $service{expect} || warn "expect must exists" && ($bad =1); $service{log} || ($service{log} = $def_log_dir . "/" . $service{host} . ".log"); $service{status} || ($service{log} = $def_log_dir . "/" . $service{host} . ".status"); $service{send} && do { $service{send} =~ s!\\n!\n!g; $service{send} =~ s!\\r!\r!g; $service{send} =~ s!\^D!\04!g; }; $service{send2} && do { $service{send2} =~ s!\\n!\n!g; $service{send2} =~ s!\\r!\r!g; $service{send2} =~ s!\^D!\04!g; }; return $bad; } sub dumpConf { print "\nConfiguration\n"; foreach my $s ( sort keys %service ) { print "$s = [", $service{$s}, "]\n" if $service{$s}; print "$s = [undefined]\n" unless $service{$s}; } } sub dumpStatus { print "\nStatus\n"; foreach my $s ( sort keys %status ) { print "$s = [", $status{$s}, "]\n" if $status{$s}; } }