Annotation of nagios/check_hw_sensors/check_hw_sensors, Revision 1.33
1.14 andrew 1: #!/usr/bin/perl -T
1.33 ! andrew 2: # $RedRiver: check_hw_sensors,v 1.32 2009/11/10 19:46:06 andrew Exp $
1.2 andrew 3: ########################################################################
1.26 andrew 4: # check_hw_sensors *** A nagios check for OpenBSD sysctl hw.sensors
5: #
1.31 andrew 6: # 2006.05.01 #*#*# andrew fresh <andrew@afresh1.com>
1.2 andrew 7: ########################################################################
8: use strict;
9: use warnings;
10:
1.33 ! andrew 11: local %ENV = ();
1.14 andrew 12:
1.33 ! andrew 13: my $NAGIOS_OUTPUT => 1;
1.4 andrew 14:
1.33 ! andrew 15: my $LICENSE = <<'EOL';
1.26 andrew 16: Copyright (c) 2009 Andrew Fresh <andrew@afresh1.com>
17: Permission to use, copy, modify, and distribute this software for any
18: purpose with or without fee is hereby granted, provided that the above
19: copyright notice and this permission notice appear in all copies.
20:
21: THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
22: WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
23: MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
24: ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
25: WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
26: ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
27: OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
28: EOL
29:
1.2 andrew 30: use POSIX;
1.20 andrew 31: use Config;
1.33 ! andrew 32: use lib '/usr/local/libexec/nagios';
1.26 andrew 33: use utils qw($TIMEOUT %ERRORS &support);
1.2 andrew 34:
35: use Getopt::Long;
36: Getopt::Long::Configure('bundling');
37:
1.33 ! andrew 38: my $PROGNAME = 'check_hw_sensors';
1.2 andrew 39:
1.26 andrew 40: my $SYSCTL = '/sbin/sysctl';
41: my $GETCAP = '/usr/bin/getcap';
42: my $BASE = 'hw.sensors';
1.4 andrew 43: my $DEFAULT_CONFIG = '/etc/sensorsd.conf';
1.26 andrew 44: my $OSVer = $Config{'osvers'} || 0;
1.2 andrew 45:
1.26 andrew 46: my $state = 'UNKNOWN'; # tells whether the it is warning, critical, or OK
1.28 andrew 47: my $opt_V;
48: my $opt_h;
1.33 ! andrew 49: my $IGNORE_STATUS;
! 50: my $FILENAME;
! 51: my $SENSOR;
! 52: my $WARNING;
! 53: my $CRITICAL;
1.2 andrew 54:
55: #Option checking
1.33 ! andrew 56: my $getopt_status = GetOptions(
! 57: 'version|V' => \$opt_V,
! 58: 'help|h' => \$opt_h,
! 59: 'ignore-status|i' => \$IGNORE_STATUS,
! 60: 'filename|f:s' => \$FILENAME,
! 61: 'sensor|s=s' => \$SENSOR,
! 62: 'warning|w=s' => \$WARNING,
! 63: 'critical|c=s' => \$CRITICAL,
1.2 andrew 64: );
1.18 andrew 65:
1.33 ! andrew 66: if ( $getopt_status == 0 ) {
1.32 andrew 67: print_help();
68: exit $ERRORS{'OK'};
69: }
70:
71: if ($opt_V) {
1.33 ! andrew 72: print_revision( $PROGNAME, '$Revision: 1.32 $ ' );
1.32 andrew 73: exit $ERRORS{'OK'};
74: }
75:
76: if ($opt_h) {
77: print_help();
78: exit $ERRORS{'OK'};
79: }
80:
1.2 andrew 81: # set the default this way so it only happens if someone typed -f or --filename
1.33 ! andrew 82: $FILENAME = $DEFAULT_CONFIG if ( defined $FILENAME && $FILENAME eq q{} );
1.2 andrew 83:
1.18 andrew 84: # Stuff is output in this file by print_sensor()
85: # http://www.openbsd.org/cgi-bin/cvsweb/src/sbin/sysctl/sysctl.c
1.33 ! andrew 86: my @TYPE_MAP = (
1.26 andrew 87: { type => 'temp',
1.33 ! andrew 88: regex => qr/\sdegC$/xms,
1.26 andrew 89: },
90: { type => 'fanrpm',
1.33 ! andrew 91: regex => qr/\sRPM$/xms,
1.26 andrew 92: },
93: { type => 'volts_dc',
1.33 ! andrew 94: regex => qr/\sV\sDC$/xms,
1.26 andrew 95: },
96: { type => 'amps',
1.33 ! andrew 97: regex => qr/\sA$/xms,
1.26 andrew 98: },
99: { type => 'watthour',
1.33 ! andrew 100: regex => qr/\sWh$/xms,
1.26 andrew 101: },
102: { type => 'amphour',
1.33 ! andrew 103: regex => qr/\sAh$/xms,
1.26 andrew 104: },
105: { type => 'indicator',
1.33 ! andrew 106: regex => qr/^(On|Off)$/xms,
1.26 andrew 107: },
108: { type => 'integer',
1.33 ! andrew 109: regex => qr/\sraw$/xms,
1.26 andrew 110: },
111: { type => 'percent',
1.33 ! andrew 112: regex => qr/\s\%$/xms,
1.26 andrew 113: },
114: { type => 'lux',
1.33 ! andrew 115: regex => qr/\slx$/xms,
1.26 andrew 116: },
117: { type => 'drive',
1.33 ! andrew 118: regex => qr/^drive\s/xms,
1.26 andrew 119: },
120: { type => 'timedelta',
1.33 ! andrew 121: regex => qr/\ssecs$/xms,
1.26 andrew 122: },
123: );
124:
1.33 ! andrew 125: my $CHECK_SENSOR = $BASE;
! 126: my %CHECKS;
! 127: if ( defined $SENSOR ) {
! 128: if ( $SENSOR !~ /^$BASE/xms ) {
! 129: $SENSOR = join q{.}, $BASE, $SENSOR;
1.26 andrew 130: }
1.33 ! andrew 131: $CHECK_SENSOR = $SENSOR;
1.2 andrew 132:
1.33 ! andrew 133: $CHECKS{$SENSOR}{'warn'} = $WARNING;
! 134: $CHECKS{$SENSOR}{'crit'} = $CRITICAL;
1.2 andrew 135:
1.26 andrew 136: }
1.33 ! andrew 137: elsif ( defined $FILENAME ) {
! 138: %CHECKS = read_file($FILENAME);
! 139: }
! 140:
! 141: my @SENSORS = read_sensors($CHECK_SENSOR);
! 142: my %STATES = check_sensors( \@SENSORS, \%CHECKS,
! 143: { IGNORE_STATUS => $IGNORE_STATUS } );
! 144:
! 145: my $have_results = 0;
! 146: $state = 'OK';
! 147: foreach my $error ( sort { $ERRORS{$b} <=> $ERRORS{$a} } keys %ERRORS ) {
! 148: if ( exists $STATES{$error} ) {
! 149: $have_results++;
! 150: $state = $error if $ERRORS{$state} < $ERRORS{$error};
! 151:
! 152: if ($NAGIOS_OUTPUT) {
! 153: print "$error (" . scalar( @{ $STATES{$error} } ) . ")";
! 154: if ( $error ne 'OK' ) {
! 155: s/\s+/ /gxms;
! 156: print '<br>';
! 157: print map {" - $_<br>"} @{ $STATES{$error} };
! 158: }
! 159: }
! 160: else {
! 161: print "$error (" . scalar( @{ $STATES{$error} } ) . "):\n";
! 162: foreach ( @{ $STATES{$error} } ) {
! 163: print " $_\n";
! 164: }
! 165: }
! 166: }
! 167: }
! 168: if ( $have_results == 0 ) {
! 169: print "No results found\n";
1.2 andrew 170: }
1.33 ! andrew 171: exit $ERRORS{$state};
1.2 andrew 172:
1.33 ! andrew 173: sub read_sensors {
! 174: my ($sensor) = @_;
! 175: my @S;
! 176: open my $sysctl, '-|', $SYSCTL, $sensor
! 177: or die "Couldn't open sysctl: $!\n";
! 178: while (<$sysctl>) {
! 179: chomp;
! 180: push @S, parse_sensor($_);
! 181: }
! 182: ## no critic 'die'
! 183: close $sysctl
! 184: or die $!
! 185: ? "Error closing sysctl pipe: $!\n"
! 186: : "Exit status $? from sysctl\n";
! 187:
! 188: return @S;
! 189: }
! 190:
! 191: sub parse_sensor {
! 192: my ($sensor) = @_;
! 193:
! 194: my ( $id, $output ) = split /=/xms, $sensor;
1.32 andrew 195: my @s = split /\./xms, $id;
196: my @o = split /,\s*/xms, $output;
1.26 andrew 197:
198: my ( $type, $source, $descr, $data, $status );
199:
200: $source = $o[0];
201: $descr = $o[1];
202:
203: if ( $OSVer >= 4.1 ) {
204: $data = $o[0];
1.32 andrew 205: if ( $data =~ s/\s+\((.*)\).*$//xms ) {
1.26 andrew 206: $descr = $1;
207: }
208: $status = $o[1];
1.32 andrew 209: ( $source, $type ) = $id =~ /([^\.]+)\.([^\.]+?)\d+$/xms;
1.26 andrew 210: }
211: elsif ( $OSVer >= 4.0 ) {
212: $data = $o[2];
213: $status = $o[3];
1.33 ! andrew 214: foreach my $t (@TYPE_MAP) {
1.32 andrew 215: if ( $data =~ /$t->{'regex'}/xms ) {
1.26 andrew 216: $type = $t->{'type'};
217: last;
218: }
219: }
220: }
221: else {
222: $data = $o[-1];
223: $status = $o[2] if @o == 5;
224: $type = $o[-2];
225: }
226:
227: $type ||= 'unknown';
228:
1.33 ! andrew 229: return {
1.26 andrew 230: id => $id,
231: output => $output,
232: source => $source,
233: description => $descr,
234: status => $status,
235: type => $type,
236: data => $data,
1.33 ! andrew 237: };
1.2 andrew 238: }
239:
1.33 ! andrew 240: sub read_file {
1.26 andrew 241: my $filename = shift;
242: my %contents;
1.2 andrew 243:
1.33 ! andrew 244: die "file '$filename' does not exist.\n" unless -e $filename;
1.26 andrew 245:
246: open my $fh, '-|', $GETCAP, '-a', '-f', $filename
1.33 ! andrew 247: or die "Couldn't open '$GETCAP -a -f $filename': $!\n";
1.26 andrew 248: while (<$fh>) {
249: chomp;
1.33 ! andrew 250: my ( $s, @c ) = split /\:/xms;
! 251: $contents{$s} = parse_line(@c);
1.26 andrew 252: }
1.33 ! andrew 253: ## no critic 'die'
! 254: close $fh
! 255: or die $!
! 256: ? "Error closing getcap pipe: $!\n"
! 257: : "Exit status $? from getcap\n";
1.2 andrew 258:
1.26 andrew 259: return %contents;
1.2 andrew 260: }
261:
1.33 ! andrew 262: sub parse_line {
! 263: my (@c) = @_;
! 264: my %c;
! 265: foreach (@c) {
! 266: my ( $k, $v ) = split /\=/xms;
! 267: if ( lc($k) eq 'ignore' ) { $c{'IGNORE'} = 1; }
! 268: elsif ( lc($k) eq 'status' ) { $c{'STATUS'} = 1; }
! 269: else { $c{$k} = $v; }
! 270: }
! 271: return \%c;
! 272: }
! 273:
1.2 andrew 274: sub parse_check {
1.26 andrew 275: my $check = shift;
1.2 andrew 276:
1.26 andrew 277: return unless $check;
1.32 andrew 278: return 'STATUS' if $check->{'STATUS'};
1.26 andrew 279: return 'IGNORE' if $check->{'IGNORE'};
280:
281: foreach my $code ( 'crit', 'warn' ) {
1.33 ! andrew 282: if ( defined $check->{$code} && $check->{$code} =~ /:/xms ) {
! 283: if ( my ( $low, $high ) = split /:/xms, $check->{$code} ) {
1.26 andrew 284: $check->{ $code . '.low' } = $low if length $low;
285: $check->{ $code . '.high' } = $high if length $high;
286: }
287: delete $check->{$code};
288: }
289:
1.33 ! andrew 290: foreach my $direction ( 'low', 'high' ) {
! 291: my $c = $code . '.' . $direction;
! 292: if ( defined $check->{$direction} ) {
! 293: $check->{$c} ||= $check->{$direction};
1.26 andrew 294: }
1.33 ! andrew 295:
! 296: if ( defined $check->{$c} ) {
! 297: my $old = $check->{$c};
! 298: $check->{$c} =~ s/[^\d\.]//gxms;
! 299: if ( !length $check->{$c} ) {
! 300: warn "INVALID CHECK ($old)\n";
! 301: delete $check->{$c};
! 302: }
! 303: }
! 304: }
! 305:
! 306: if ( defined $check->{$code} ) {
! 307: $check->{$code} = [ split /,\s*/xms, $check->{$code} ];
! 308: }
! 309: else {
! 310: $check->{$code} = [];
1.26 andrew 311: }
312: }
1.2 andrew 313:
1.26 andrew 314: return $check;
1.2 andrew 315: }
316:
1.33 ! andrew 317: sub check_sensors {
! 318: my ( $S, $C, $O ) = @_;
! 319:
! 320: my %states;
! 321: foreach my $sensor ( @{$S} ) {
! 322: my ( $r, $data );
! 323: if ( exists $C->{ $sensor->{id} } ) {
! 324: $r = check_sensor( $sensor, $C->{ $sensor->{id} } );
! 325: $data = $sensor->{id} . '=' . $sensor->{output};
! 326: }
! 327: elsif ( !$O->{ignore_status} && $sensor->{status} ) {
! 328: $r = check_sensor( $sensor, { STATUS => 1 } );
! 329: $data = $sensor->{id} . '=' . $sensor->{output};
! 330: }
! 331: else {
! 332:
! 333: # ignore this sensor, theoretically you could do the check and
! 334: # that would show unknown sensors.
! 335: }
! 336: if ( defined $r ) {
! 337: push @{ $states{$r} }, $data;
! 338: }
! 339: }
! 340:
! 341: return %states;
! 342: }
! 343:
1.2 andrew 344: sub check_sensor {
1.33 ! andrew 345: my ( $sensor, $check ) = @_;
1.26 andrew 346: my $result = 'UNKNOWN';
347:
348: return $result unless ref $sensor eq 'HASH';
349: $check = parse_check($check) if $check;
350:
1.33 ! andrew 351: if ( !$check ) { return $result; }
1.32 andrew 352: elsif ( $check eq 'STATUS' ) {
1.27 andrew 353:
1.33 ! andrew 354: # It looks like returning $sensor->{status} should be safe, from
1.26 andrew 355: # src/sbin/sysctl/sysctl.c
1.32 andrew 356: return ( $sensor->{'status'} || $result );
1.26 andrew 357: }
1.33 ! andrew 358: elsif ( $check eq 'IGNORE' ) { return; }
! 359:
! 360: my $type = $sensor->{'type'};
! 361: if (grep { $type eq $_ }
! 362: qw(
! 363: fan fanrpm
! 364: volt volts_dc
! 365: amps watthour amphour
! 366: integer raw percent
! 367: lux temp timedelta
! 368: )
! 369: )
! 370: {
! 371: $result = check_sensor_numeric( $sensor->{'data'}, $check );
! 372: }
! 373: elsif ( grep { $type eq $_ } qw( drive indicator ) ) {
! 374: my $data = $sensor->{'data'};
! 375: $data =~ s/^drive\s+//xms;
! 376: $result = check_sensor_list( $data, $check );
! 377: }
! 378: else {
! 379: warn 'Unknown Sensor Type: ', $sensor->{'id'}, '=', $type, "\n";
! 380: }
! 381:
! 382: return $result;
! 383: }
! 384:
! 385: sub check_sensor_numeric {
! 386: my ( $data, $check ) = @_;
! 387:
! 388: my $result = 'UNKNOWN';
! 389: my %errors = (
! 390: 'warn' => 'WARNING',
! 391: 'crit' => 'CRITICAL',
! 392: );
! 393:
! 394: $data =~ s/[^\d\.]//gxms;
! 395: if ( !length $data ) {
! 396: warn "INVALID DATA ($data)\n";
! 397: return $result;
1.26 andrew 398: }
399:
400: foreach my $code ( 'warn', 'crit' ) {
1.33 ! andrew 401: if ( defined $check->{ $code . ".low" }
! 402: || defined $check->{ $code . ".high" } )
1.26 andrew 403: {
1.33 ! andrew 404: if (( defined $check->{ $code . ".low" }
! 405: && $check->{ $code . ".low" } >= $data
! 406: )
! 407: || ( defined $check->{ $code . ".high" }
! 408: && $check->{ $code . ".high" } <= $data )
! 409: )
! 410: {
! 411: $result = $errors{$code};
1.26 andrew 412: }
1.33 ! andrew 413: $result = 'OK' if $result eq 'UNKNOWN';
! 414: }
! 415: elsif ( @{ $check->{$code} } ) {
! 416: my $matched = 0;
! 417: NUMERIC_CHECK: foreach ( @{ $check->{$code} } ) {
! 418: my $c = $_;
! 419: $c =~ s/[^\d\.]//gxms;
! 420: if ( !length $c ) {
! 421: warn "INVALID CHECK (" . $_ . ") for '$code'\n";
! 422: next;
1.26 andrew 423: }
424:
1.33 ! andrew 425: if ( $c eq $data ) {
! 426: $matched = 1;
! 427: last NUMERIC_CHECK;
1.26 andrew 428: }
1.33 ! andrew 429: }
! 430: if ($matched) {
1.32 andrew 431: $result = 'OK' if $result eq 'UNKNOWN';
1.26 andrew 432: }
1.33 ! andrew 433: else {
! 434: $result = $errors{$code};
1.26 andrew 435: }
436: }
1.33 ! andrew 437: }
! 438:
! 439: return $result;
! 440: }
1.26 andrew 441:
1.33 ! andrew 442: sub check_sensor_list {
! 443: my ( $data, $check ) = @_;
1.26 andrew 444:
1.33 ! andrew 445: my $result = 'UNKNOWN';
! 446: my %errors = (
! 447: 'warn' => 'WARNING',
! 448: 'crit' => 'CRITICAL',
! 449: );
1.26 andrew 450:
1.33 ! andrew 451: foreach my $code ( 'warn', 'crit' ) {
! 452: if ( @{ $check->{$code} } ) {
! 453: my $matched = 0;
! 454: LIST_CHECK: foreach ( @{ $check->{$code} } ) {
! 455: if ( $_ eq $data ) {
! 456: $matched = 1;
! 457: last LIST_CHECK;
1.26 andrew 458: }
1.33 ! andrew 459: }
! 460: if ($matched) {
1.32 andrew 461: $result = 'OK' if $result eq 'UNKNOWN';
1.26 andrew 462: }
1.33 ! andrew 463: else {
! 464: $result = $errors{$code};
1.26 andrew 465: }
466: }
467: }
1.2 andrew 468:
1.26 andrew 469: return $result;
1.2 andrew 470: }
471:
472: sub print_help {
1.26 andrew 473: print <<"EOL";
474: $PROGNAME - monitors sysctl hw.sensors on OpenBSD
1.30 andrew 475: $PROGNAME [-i] [-f [<FILENAME>]|(-s <hw.sensors id> [-w limit] [-c limit])]
1.2 andrew 476:
477: Usage:
1.15 andrew 478: -i, --ignore-status
1.26 andrew 479: Don't automatically check the status of sensors that report it.
1.12 andrew 480: -f, --filename=FILE
481: FILE to load checks from (defaults to /etc/sensorsd.conf)
482: -s, --sensor=ID
1.26 andrew 483: ID of a single sensor. "-s kate0.temp0" means hw.sensors.kate0.temp0
1.27 andrew 484: Overrides --filename.
1.12 andrew 485: -w, --warning=RANGE or single ENTRY
486: Exit with WARNING status if outside of RANGE or if != ENTRY
1.13 andrew 487: -c, --critical=RANGE or single ENTRY
1.12 andrew 488: Exit with CRITICAL status if outside of RANGE or if != ENTRY
489:
1.13 andrew 490: FILE is in the same format as sensorsd.conf(5) plus some additional
491: entries. These additional entries in the file are ignored by
1.26 andrew 492: sensorsd(8) this means you can use the same config file for $PROGNAME
493: as well as sensorsd(8).
1.12 andrew 494:
495: $PROGNAME understands the following entries:
496:
1.15 andrew 497: low, high, crit, warn, crit.low, crit.high, warn.low, warn.high,
498: ignore, status
1.12 andrew 499:
500: An ENTRY depends on the type. The descriptions in sensorsd.conf(5)
501: can be used when appropriate, or you can use the following:
502:
1.20 andrew 503: fanrpm, volts_dc, amps, watthour, amphour, integer (raw), percent,
1.33 ! andrew 504: lux, temp or timedelta - Anything that includes digits. Both the
! 505: value of the check and the value of the sensor response that are not
! 506: either a digit or period are stripped and then the two resultant
! 507: values are compared.
1.12 andrew 508:
509: indicator or drive - does a case sensitive match of each
510: entry in the comma separated list and if it does not match
1.20 andrew 511: any of the entries, it sets the status.
1.12 andrew 512:
513: The entries 'crit' or 'warn' (or the -c or -w on the command line)
514: may be a RANGE or a comma separated list of acceptable values.
515: The comma separated list of values contains a list of things that
516: will NOT cause the status. This is possibly counterintuitive, but
517: you are more likely to know good values than bad values.
518:
519: A RANGE is a low ENTRY and a high ENTRY separated by a colon (:).
520: It can also be low: or :high with the other side left blank to only
1.26 andrew 521: make the single check.
1.2 andrew 522:
1.15 andrew 523: An entry marked "ignore" will cause that sensor to be skipped.
524: Generally used with state checking of all sensors to ignore sensors you
525: don't care about or that report incorrectly.
526:
527: If you are using --ignore-status, you can still check the status of
528: individual sensors with a status entry.
529:
1.2 andrew 530: EOL
1.15 andrew 531:
1.33 ! andrew 532: print_revision( $PROGNAME, '$Revision: 1.32 $' );
1.26 andrew 533:
1.33 ! andrew 534: print $LICENSE;
! 535:
! 536: return;
1.2 andrew 537: }
1.26 andrew 538:
539: sub print_revision {
1.27 andrew 540: my ( $prog, $rev ) = @_;
1.26 andrew 541: $rev =~ s/^\D+([\d\.]+)\D+$/v$1/xms;
542:
543: print "$prog $rev\n";
1.33 ! andrew 544:
! 545: return;
1.26 andrew 546: }
1.2 andrew 547:
FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>