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