#!/usr/bin/perl -T # $RedRiver: check_hw_sensors,v 1.31 2009/11/10 18:24:56 andrew Exp $ ######################################################################## # check_hw_sensors *** A nagios check for OpenBSD sysctl hw.sensors # # 2006.05.01 #*#*# andrew fresh ######################################################################## use strict; use warnings; %ENV = (); use constant NAGIOS_OUTPUT => 1; my $License = <<'EOL'; Copyright (c) 2009 Andrew Fresh Permission to use, copy, modify, and distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. EOL use POSIX; use Config; use lib "/usr/local/libexec/nagios"; use utils qw($TIMEOUT %ERRORS &support); use Getopt::Long; Getopt::Long::Configure('bundling'); my $PROGNAME = "check_hw_sensors"; my $SYSCTL = '/sbin/sysctl'; my $GETCAP = '/usr/bin/getcap'; my $BASE = 'hw.sensors'; my $DEFAULT_CONFIG = '/etc/sensorsd.conf'; my $OSVer = $Config{'osvers'} || 0; my $state = 'UNKNOWN'; # tells whether the it is warning, critical, or OK my %states; # This stores the count of states; my $opt_V; my $opt_h; my $ignore_status; my $filename; my $sensor; my $warning; my $critical; my $CHECK_SENSOR = $BASE; my %CHECKS; my @SENSORS; #Option checking my $status = GetOptions( "version|V" => \$opt_V, "help|h" => \$opt_h, "ignore-status|i" => \$ignore_status, "filename|f:s" => \$filename, "sensor|s=s" => \$sensor, "warning|w=s" => \$warning, "critical|c=s" => \$critical, ); if ( $status == 0 ) { print_help(); exit $ERRORS{'OK'}; } if ($opt_V) { print_revision( $PROGNAME, '$Revision: 1.32 $ ' ); exit $ERRORS{'OK'}; } if ($opt_h) { print_help(); exit $ERRORS{'OK'}; } # set the default this way so it only happens if someone typed -f or --filename $filename = $DEFAULT_CONFIG if ( defined $filename && $filename eq '' ); # Stuff is output in this file by print_sensor() # http://www.openbsd.org/cgi-bin/cvsweb/src/sbin/sysctl/sysctl.c my @Type_Map = ( { type => 'temp', regex => qr/\sdegC$/, }, { type => 'fanrpm', regex => qr/\sRPM$/, }, { type => 'volts_dc', regex => qr/\sV\sDC$/, }, { type => 'amps', regex => qr/\sA$/, }, { type => 'watthour', regex => qr/\sWh$/, }, { type => 'amphour', regex => qr/\sAh$/, }, { type => 'indicator', regex => qr/^(On|Off)$/, }, { type => 'integer', regex => qr/\sraw$/, }, { type => 'percent', regex => qr/\s\%$/, }, { type => 'lux', regex => qr/\slx$/, }, { type => 'drive', regex => qr/^drive\s/, }, { type => 'timedelta', regex => qr/\ssecs$/, }, ); if ( defined $sensor ) { if ( $sensor !~ /^$BASE/ ) { $sensor = $BASE . '.' . $sensor; } $CHECK_SENSOR = $sensor; $CHECKS{$sensor}{'warn'} = $warning; $CHECKS{$sensor}{'crit'} = $critical; } elsif ( defined $filename ) { %CHECKS = parse_file($filename); } open my $sysctl, "-|", $SYSCTL, $CHECK_SENSOR or die "Couldn't open sysctl: $!"; while (<$sysctl>) { chomp; my ( $id, $output ) = split /=/xms; my @s = split /\./xms, $id; my @o = split /,\s*/xms, $output; my ( $type, $source, $descr, $data, $status ); $source = $o[0]; $descr = $o[1]; if ( $OSVer >= 4.1 ) { $data = $o[0]; if ( $data =~ s/\s+\((.*)\).*$//xms ) { $descr = $1; } $status = $o[1]; ( $source, $type ) = $id =~ /([^\.]+)\.([^\.]+?)\d+$/xms; } elsif ( $OSVer >= 4.0 ) { $data = $o[2]; $status = $o[3]; foreach my $t (@Type_Map) { if ( $data =~ /$t->{'regex'}/xms ) { $type = $t->{'type'}; last; } } } else { $data = $o[-1]; $status = $o[2] if @o == 5; $type = $o[-2]; } $type ||= 'unknown'; push @SENSORS, { id => $id, output => $output, source => $source, description => $descr, status => $status, type => $type, data => $data, }; } close $sysctl; foreach my $sensor (@SENSORS) { my ( $r, $data ); if ( exists $CHECKS{ $sensor->{id} } ) { $r = check_sensor( $sensor, $CHECKS{ $sensor->{id} } ); $data = $sensor->{id} . '=' . $sensor->{output}; } elsif ( !$ignore_status && $sensor->{status} ) { $r = check_sensor( $sensor, { STATUS => 1 } ); $data = $sensor->{id} . '=' . $sensor->{output}; } else { # ignore this sensor, theoretically you could do the check and # that would show unknown sensors. } if ( defined $r ) { push @{ $states{$r} }, $data; } } my $have_results = 0; $state = 'OK'; foreach my $error ( sort { $ERRORS{$b} <=> $ERRORS{$a} } keys %ERRORS ) { if ( exists $states{$error} ) { $have_results++; $state = $error if $ERRORS{$state} < $ERRORS{$error}; if (NAGIOS_OUTPUT) { print "$error (" . scalar( @{ $states{$error} } ) . ")"; if ( $error ne 'OK' ) { print '
'; print map {" - $_
"} @{ $states{$error} }; } } else { print "$error (" . scalar( @{ $states{$error} } ) . "):\n"; foreach ( @{ $states{$error} } ) { print " $_\n"; } } } } if ( $have_results == 0 ) { print "No results found\n"; } exit $ERRORS{$state}; sub parse_file { my $filename = shift; my %contents; die "file '$filename' does not exist." unless -e $filename; open my $fh, '-|', $GETCAP, '-a', '-f', $filename or die "Couldn't open FILE '$GETCAP -a -f $filename': $!"; while (<$fh>) { chomp; my ( $key, @c ) = split /\:/; foreach (@c) { my ( $k, $v ) = split /\=/; if ( lc($k) eq 'ignore' ) { $contents{$key}{'IGNORE'} = 1; } elsif ( lc($k) eq 'status' ) { $contents{$key}{'STATUS'} = 1; } else { $contents{$key}{$k} = $v; } } } close $fh; return %contents; } sub parse_check { my $check = shift; return unless $check; return 'STATUS' if $check->{'STATUS'}; return 'IGNORE' if $check->{'IGNORE'}; foreach my $code ( 'crit', 'warn' ) { if ( defined $check->{$code} && $check->{$code} =~ /:/ ) { if ( my ( $low, $high ) = split /:/, $check->{$code} ) { $check->{ $code . '.low' } = $low if length $low; $check->{ $code . '.high' } = $high if length $high; } delete $check->{$code}; } foreach my $severity ( 'low', 'high' ) { if ( defined $check->{$severity} ) { $check->{ $code . '.' . $severity } = $check->{$severity} if !defined $check->{ $code . '.' . $severity }; } } no warnings 'uninitialized'; $check->{$code} = [ split /,\s*/, $check->{$code} ]; } return $check; } sub check_sensor { my $sensor = shift; my $check = shift; my $result = 'UNKNOWN'; my %errors = ( 'warn' => 'WARNING', 'crit' => 'CRITICAL', ); return $result unless ref $sensor eq 'HASH'; $check = parse_check($check) if $check; if ( !$check ) { return $result; } elsif ( $check eq 'STATUS' ) { # It looks like doing this should be safe, from # src/sbin/sysctl/sysctl.c return ( $sensor->{'status'} || $result ); } elsif ( $check eq 'IGNORE' ) { return; } foreach my $code ( 'warn', 'crit' ) { if ( $sensor->{'type'} eq 'fan' || $sensor->{'type'} eq 'fanrpm' || $sensor->{'type'} eq 'volt' || $sensor->{'type'} eq 'volts_dc' || $sensor->{'type'} eq 'amps' || $sensor->{'type'} eq 'watthour' || $sensor->{'type'} eq 'amphour' || $sensor->{'type'} eq 'integer' || $sensor->{'type'} eq 'raw' || $sensor->{'type'} eq 'percent' || $sensor->{'type'} eq 'lux' || $sensor->{'type'} eq 'timedelta' ) { my $data = $sensor->{'data'}; $data =~ s/[^\d\.]//gxms; if ( !length $data ) { warn "INVALID DATA ($sensor->{'data'}) for '$sensor->{'id'}'\n"; next; } if ( defined $check->{ $code . ".low" } || defined $check->{ $code . ".high" } ) { if ( defined $check->{ $code . ".low" } ) { my $c = $check->{ $code . ".low" }; $c =~ s/[^\d\.]//gxms; if ( !length $c ) { warn "INVALID CHECK (" . $check->{ $code . ".low" } . ") for '$sensor->{'id'}:$code.low'\n"; next; } $result = $errors{$code} if ( $c >= $data ); } if ( defined $check->{ $code . ".high" } ) { my $c = $check->{ $code . ".high" }; $c =~ s/[^\d\.]//gxms; if ( !length $c ) { warn "INVALID CHECK (" . $check->{ $code . ".high" } . ") for '$sensor->{'id'}:$code.high'\n"; next; } $result = $errors{$code} if ( $c <= $data ); } $result = 'OK' if $result eq 'UNKNOWN'; } elsif ( @{ $check->{$code} } ) { my $matched = 0; foreach ( @{ $check->{$code} } ) { my $c = $_; $c =~ s/[^\d\.]//gxms; if ( !length $c ) { warn "INVALID CHECK (" . $_ . ") for '$sensor->{'id'}:$code'\n"; next; } if ( $c eq $data ) { $matched = 1; last; } } if ($matched) { $result = 'OK' if $result eq 'UNKNOWN'; } else { $result = $errors{$code}; } } } elsif ( $sensor->{'type'} eq 'temp' ) { my ( $degC, $degF ) = split /\//, $sensor->{'data'}; $degC =~ s/[^\d\.]//gxms; $degF ||= $degC * 9 / 5 + 32; $degF =~ s/[^\d\.]//gxms; if ( defined $check->{ $code . ".low" } || defined $check->{ $code . ".high" } ) { if ( defined $check->{ $code . ".low" } ) { my $c = $check->{ $code . ".low" }; my $data = $degC; $data = $degF if ( $c =~ /F/ixms ); if ( !length $data ) { warn "INVALID DATA (" . $sensor->{'data'} . ") for '$sensor->{'id'}'\n"; next; } $c =~ s/[^\d\.]//gxms; if ( !length $c ) { warn "INVALID CHECK (" . $check->{ $code . ".low" } . ") for '$sensor->{'id'}':$code.low\n"; next; } $result = $errors{$code} if ( $c >= $data ); } if ( defined $check->{ $code . ".high" } ) { my $c = $check->{ $code . ".high" }; my $data = $degC; $data = $degF if ( $c =~ /F/ixms ); if ( !length $data ) { warn "INVALID DATA (" . $sensor->{'data'} . ") for '$sensor->{'id'}'\n"; next; } $c =~ s/[^\d\.]//gxms; if ( !length $c ) { warn "INVALID CHECK (" . $check->{ $code . ".high" } . ") for '$sensor->{'id'}:$code.high'\n"; next; } $result = $errors{$code} if ( $c <= $data ); } $result = 'OK' if $result eq 'UNKNOWN'; } elsif ( @{ $check->{$code} } ) { my $matched = 0; foreach ( @{ $check->{$code} } ) { my $c = $_; my $data = $degC; $data = $degF if ( $c =~ /F/ixms ); if ( !length $data ) { warn "INVALID DATA (" . $sensor->{'data'} . ") for '$sensor->{'id'}'\n"; next; } $c =~ s/[^\d\.]//gxms; if ( !length $c ) { warn "INVALID CHECK (" . $_ . ") for '$sensor->{'id'}':$code\n"; next; } if ( $c eq $data ) { $matched = 1; last; } } if ($matched) { $result = 'OK' if $result eq 'UNKNOWN'; } else { $result = $errors{$code}; } } } elsif ($sensor->{'type'} eq 'drive' || $sensor->{'type'} eq 'indicator' ) { $sensor->{'data'} =~ s/^drive\s+//; if ( @{ $check->{$code} } ) { my $matched = 0; foreach ( @{ $check->{$code} } ) { if ( $_ eq $sensor->{'data'} ) { $matched = 1; last; } } if ($matched) { $result = 'OK' if $result eq 'UNKNOWN'; } else { $result = $errors{$code}; } } } else { warn 'Unknown Sensor Type: ', $sensor->{'id'}, '=', $sensor->{'type'}, "\n"; } } return $result; } sub print_help { print <<"EOL"; $PROGNAME - monitors sysctl hw.sensors on OpenBSD $PROGNAME [-i] [-f []|(-s [-w limit] [-c limit])] Usage: -i, --ignore-status Don't automatically check the status of sensors that report it. -f, --filename=FILE FILE to load checks from (defaults to /etc/sensorsd.conf) -s, --sensor=ID ID of a single sensor. "-s kate0.temp0" means hw.sensors.kate0.temp0 Overrides --filename. -w, --warning=RANGE or single ENTRY Exit with WARNING status if outside of RANGE or if != ENTRY -c, --critical=RANGE or single ENTRY Exit with CRITICAL status if outside of RANGE or if != ENTRY FILE is in the same format as sensorsd.conf(5) plus some additional entries. These additional entries in the file are ignored by sensorsd(8) this means you can use the same config file for $PROGNAME as well as sensorsd(8). $PROGNAME understands the following entries: low, high, crit, warn, crit.low, crit.high, warn.low, warn.high, ignore, status An ENTRY depends on the type. The descriptions in sensorsd.conf(5) can be used when appropriate, or you can use the following: fanrpm, volts_dc, amps, watthour, amphour, integer (raw), percent, lux or timedelta - Anything that includes digits. Both the value of the check and the value of the sensor response that are not either a digit or period are stripped and then the two resultant values are compared. temp - Can be as above, but if the entry has an F in it, it compares farenheit, otherwise it uses celcius. indicator or drive - does a case sensitive match of each entry in the comma separated list and if it does not match any of the entries, it sets the status. The entries 'crit' or 'warn' (or the -c or -w on the command line) may be a RANGE or a comma separated list of acceptable values. The comma separated list of values contains a list of things that will NOT cause the status. This is possibly counterintuitive, but you are more likely to know good values than bad values. A RANGE is a low ENTRY and a high ENTRY separated by a colon (:). It can also be low: or :high with the other side left blank to only make the single check. An entry marked "ignore" will cause that sensor to be skipped. Generally used with state checking of all sensors to ignore sensors you don't care about or that report incorrectly. If you are using --ignore-status, you can still check the status of individual sensors with a status entry. EOL print_revision( $PROGNAME, '$Revision: 1.32 $' ); print $License; } sub print_revision { my ( $prog, $rev ) = @_; $rev =~ s/^\D+([\d\.]+)\D+$/v$1/xms; print "$prog $rev\n"; }