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