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