Annotation of nagios/check_hw_sensors/check_hw_sensors, Revision 1.37
1.14 andrew 1: #!/usr/bin/perl -T
1.37 ! andrew 2: # $RedRiver: check_hw_sensors,v 1.36 2009/11/10 23:26:13 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.34 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.37 ! andrew 72: print_revision( $PROGNAME, '$Revision: 1.36 $ ' );
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.37 ! andrew 133: $CHECKS{$SENSOR}{'warn'} = $WARNING if $WARNING;
! 134: $CHECKS{$SENSOR}{'crit'} = $CRITICAL if $CRITICAL;
1.26 andrew 135: }
1.33 andrew 136: elsif ( defined $FILENAME ) {
137: %CHECKS = read_file($FILENAME);
138: }
139:
140: my @SENSORS = read_sensors($CHECK_SENSOR);
141: my %STATES = check_sensors( \@SENSORS, \%CHECKS,
142: { IGNORE_STATUS => $IGNORE_STATUS } );
143:
144: my $have_results = 0;
145: $state = 'OK';
1.37 ! andrew 146: foreach
! 147: my $error ( reverse sort { $ERRORS{$a} <=> $ERRORS{$b} } keys %ERRORS )
! 148: {
1.33 andrew 149: if ( exists $STATES{$error} ) {
150: $have_results++;
151: $state = $error if $ERRORS{$state} < $ERRORS{$error};
152:
153: if ($NAGIOS_OUTPUT) {
1.36 andrew 154: print $error . ' (' . scalar( @{ $STATES{$error} } ) . ')';
1.33 andrew 155: if ( $error ne 'OK' ) {
156: print '<br>';
1.37 ! andrew 157: print map { s/\s+//xms; " - $_<br>" } @{ $STATES{$error} };
1.33 andrew 158: }
159: }
160: else {
1.36 andrew 161: print $error . ' (' . scalar( @{ $STATES{$error} } ) . "):\n";
1.33 andrew 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: }
1.37 ! andrew 327: elsif ( $sensor->{status} && !$O->{IGNORE_STATUS} ) {
1.33 andrew 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.36 andrew 401: if ( defined $check->{ $code . '.low' }
402: || defined $check->{ $code . '.high' } )
1.26 andrew 403: {
1.36 andrew 404: if (( defined $check->{ $code . '.low' }
405: && $check->{ $code . '.low' } >= $data
1.33 andrew 406: )
1.36 andrew 407: || ( defined $check->{ $code . '.high' }
408: && $check->{ $code . '.high' } <= $data )
1.33 andrew 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 ) {
1.36 andrew 421: warn "INVALID CHECK ($_) for '$code'\n";
1.33 andrew 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.35 andrew 492: sensorsd(8). This means you can use the same config file for $PROGNAME
1.26 andrew 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.37 ! andrew 532: print_revision( $PROGNAME, '$Revision: 1.36 $' );
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>