1
0
mirror of https://github.com/librenms/librenms-agent.git synced 2024-05-09 09:54:52 +00:00
Files

462 lines
12 KiB
Perl
Executable File

#!/usr/bin/env perl
#Copyright (c) 2019, Zane C. Bowers-Hadley
#All rights reserved.
#
#Redistribution and use in source and binary forms, with or without modification,
#are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
#THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
#ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
#WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
#IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
#INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
#BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
#DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
#LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
#OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
#THE POSSIBILITY OF SUCH DAMAGE.
=for comment
Add this to snmpd.conf like below.
extend smart /etc/snmp/smart
Then add to root's cron tab, if you have more than a few disks.
*/3 * * * * /etc/snmp/smart -u
You will also need to create the config file, which defaults to the same path as the script,
but with .config appended. So if the script is located at /etc/snmp/smart, the config file
will be /etc/snmp/smart.config. Alternatively you can also specific a config via -c.
Anything starting with a # is comment. The format for variables is $variable=$value. Empty
lines are ignored. Spaces and tabes at either the start or end of a line are ignored. Any
line with out a matched variable or # are treated as a disk.
#This is a comment
cache=/var/cache/smart
smartctl=/usr/local/sbin/smartctl
useSN=0
ada0
da5 /dev/da5 -d sat
twl0,0 /dev/twl0 -d 3ware,0
twl0,1 /dev/twl0 -d 3ware,1
twl0,2 /dev/twl0 -d 3ware,2
The variables are as below.
cache = The path to the cache file to use. Default: /var/cache/smart
smartctl = The path to use for smartctl. Default: /usr/bin/env smartctl
useSN = If set to 1, it will use the disks SN for reporting instead of the device name.
1 is the default. 0 will use the device name.
A disk line is can be as simple as just a disk name under /dev/. Such as in the config above
The line "ada0" would resolve to "/dev/ada0" and would be called with no special argument. If
a line has a space in it, everything before the space is treated as the disk name and is what
used for reporting and everything after that is used as the argument to be passed to smartctl.
If you want to guess at the configuration, call it with -g and it will print out what it thinks
it should be.
=cut
##
## You should not need to touch anything below here.
##
use warnings;
use strict;
use Getopt::Std;
my $cache='/var/cache/smart';
my $smartctl='/usr/bin/env smartctl';
my @disks;
my $useSN=1;
$Getopt::Std::STANDARD_HELP_VERSION = 1;
sub main::VERSION_MESSAGE {
print "SMART SNMP extend 0.1.0\n";
};
sub main::HELP_MESSAGE {
print "\n".
"-u Update '".$cache."'\n".
"-g Guess at the config and print it to STDOUT.\n".
"-c <config> The config file to use.\n";
}
#gets the options
my %opts=();
getopts('ugc:', \%opts);
# guess if asked
if ( defined( $opts{g} ) ){
#get what path to use for smartctl
$smartctl=`which smartctl`;
chomp($smartctl);
if ( $? != 0 ){
warn("'which smartctl' failed with a exit code of $?");
exit 1;
}
#try to touch the default cache location and warn if it can't be done
system('touch '.$cache.'>/dev/null');
if ( $? != 0 ){
$cache='#Could not touch '.$cache. "You will need to manually set it\n".
"cache=?\n";
}else{
system('rm -f '.$cache.'>/dev/null');
$cache='cache='.$cache."\n";
}
# used for checking if a disk has been found more than once
my %found_disks_names;
my @argumentsA;
#have smartctl scan and see if it finds anythings not get found
my $scan_output=`$smartctl --scan-open`;
my @scan_outputA=split(/\n/, $scan_output);
# remove non-SMART devices sometimes returned
@scan_outputA=grep(!/ses[0-9]/, @scan_outputA); # not a disk, but may or may not have SMART attributes
@scan_outputA=grep(!/pass[0-9]/, @scan_outputA); # very likely a duplicate and a disk under another name
@scan_outputA=grep(!/cd[0-9]/, @scan_outputA); # CD drive
if ( $^O eq 'freebsd' ){
@scan_outputA=grep(!/sa[0-9]/, @scan_outputA); # tape drive
@scan_outputA=grep(!/ctl[0-9]/, @scan_outputA); # CAM target layer
}elsif( $^O eq 'linux' ){
@scan_outputA=grep(!/st[0-9]/, @scan_outputA); # SCSI tape drive
@scan_outputA=grep(!/ht[0-9]/, @scan_outputA); # ATA tape drive
}
# make the first pass, figuring out what all we have and trimming comments
foreach my $arguments ( @scan_outputA ){
my $name = $arguments;
$arguments =~ s/ \#.*//; # trim the comment out of the argument
$name =~ s/ .*//;
$name =~ s/\/dev\///;
if (defined( $found_disks_names{$name} )){
$found_disks_names{$name}++;
}else{
$found_disks_names{$name}=0;
}
push( @argumentsA, $arguments );
}
# second pass, putting the lines together
my %current_disk;
my $drive_lines='';
foreach my $arguments ( @argumentsA ){
my $name = $arguments;
$name =~ s/ .*//;
$name =~ s/\/dev\///;
if ( $found_disks_names{$name} == 0 ){
# If no other devices, just name it after the base device.
$drive_lines=$drive_lines.$name." ".$arguments."\n";
}else{
# if more than one, start at zero and increment, apennding comma number to the base device name
if (defined( $current_disk{$name} )){
$current_disk{$name}++;
}else{
$current_disk{$name}=0;
}
$drive_lines=$drive_lines.$name.",".$current_disk{$name}." ".$arguments."\n";
}
}
print "useSN=1\n".'smartctl='.$smartctl."\n".
$cache.
$drive_lines;
exit 0;
}
#get which config file to use
my $config=$0.'.config';
if ( defined( $opts{c} ) ){
$config=$opts{c};
}
#reads the config file, optionally
my $config_file='';
open(my $readfh, "<", $config) or die "Can't open '".$config."'";
read($readfh , $config_file , 1000000);
close($readfh);
#parse the config file and remove comments and empty lines
my @configA=split(/\n/, $config_file);
@configA=grep(!/^$/, @configA);
@configA=grep(!/^\#/, @configA);
@configA=grep(!/^[\s\t]*$/, @configA);
my $configA_int=0;
while ( defined( $configA[$configA_int] ) ){
my $line=$configA[$configA_int];
chomp($line);
$line=~s/^[\t\s]+//;
$line=~s/[\t\s]+$//;
my ( $var, $val )=split(/=/, $line, 2);
my $matched;
if ( $var eq 'cache' ){
$cache=$val;
$matched=1;
}
if ( $var eq 'smartctl' ){
$smartctl=$val;
$matched=1;
}
if ( $var eq 'useSN' ){
$useSN=$val;
$matched=1;
}
if ( !defined( $val ) ){
push(@disks, $line);
}
$configA_int++;
}
#if set to 1, no cache will be written and it will be printed instead
my $noWrite=0;
# if no -u, it means we are being called from snmped
if ( ! defined( $opts{u} ) ){
# if the cache file exists, print it, otherwise assume one is not being used
if ( -f $cache ){
my $old='';
open(my $readfh, "<", $cache) or die "Can't open '".$cache."'";
read($readfh , $old , 1000000);
close($readfh);
print $old;
exit 0;
}else{
$opts{u}=1;
$noWrite=1;
}
}
my $toReturn='';
foreach my $line ( @disks ){
my $disk;
my $name;
if ( $line =~ /\ / ){
($name, $disk)=split(/\ /, $line, 2);
}else{
$disk=$line;
$name=$line;
}
my $output;
if ( $disk !~ /\// ){
$disk = '/dev/'.$disk;
}
$output=`$smartctl -A $disk`;
my %IDs=( '5'=>'null',
'10'=>'null',
'173'=>'null',
'177'=>'null',
'183'=>'null',
'184'=>'null',
'187'=>'null',
'188'=>'null',
'190'=>'null',
'194'=>'null',
'196'=>'null',
'197'=>'null',
'198'=>'null',
'199'=>'null',
'231'=>'null',
'233'=>'null',
'9'=>'null',
);
my @outputA;
if($output =~ /NVMe Log/)
{
# we have an NVMe drive with annoyingly different output
my %mappings=(
'Temperature' => 194,
'Power Cycles' => 12,
'Power On Hours' => 9,
'Percentage Used' => 231,
);
foreach(split(/\n/, $output ))
{
if(/:/)
{
my ($key, $val) = split(/:/);
$val =~ s/^\s+|\s+$|\D+//g;
if(exists($mappings{$key}))
{
if ($mappings{$key} == 231) {
$IDs{$mappings{$key}} = 100-$val;
} else {
$IDs{$mappings{$key}} = $val;
}
}
}
}
}
else
{
@outputA=split( /\n/, $output );
my $outputAint=0;
while ( defined($outputA[$outputAint]) ) {
my $line=$outputA[$outputAint];
$line=~s/^ +//;
$line=~s/ +/ /g;
if ( $line =~ /^[0123456789]+ / ) {
my @lineA=split(/\ /, $line, 10);
my $raw=$lineA[9];
my $normalized=$lineA[3];
my $id=$lineA[0];
# Crucial SSD
# 202, Percent_Lifetime_Remain, same as 231, SSD Life Left
if ( $id == 202 ) {
$IDs{231}=$raw;
}
# single int raw values
if (
( $id == 5 ) ||
( $id == 10 ) ||
( $id == 173 ) ||
( $id == 183 ) ||
( $id == 184 ) ||
( $id == 187 ) ||
( $id == 196 ) ||
( $id == 197 ) ||
( $id == 198 ) ||
( $id == 199 )
) {
my @rawA=split( /\ /, $raw );
$IDs{$id}=$rawA[0];
}
# single int normalized values
if (
( $id == 177 ) ||
( $id == 231 ) ||
( $id == 233 )
) {
$IDs{$id}=int($normalized);
}
# 9, power on hours
if ( $id == 9 ) {
my @runtime=split(/[\ h]/, $raw);
$IDs{$id}=$runtime[0];
}
# 188, Command_Timeout
if ( $id == 188 ) {
my $total=0;
my @rawA=split( /\ /, $raw );
my $rawAint=0;
while ( defined( $rawA[$rawAint] ) ) {
$total=$total+$rawA[$rawAint];
$rawAint++;
}
$IDs{$id}=$total;
}
# 190, airflow temp
# 194, temp
if (
( $id == 190 ) ||
( $id == 194 )
) {
my ( $temp )=split(/\ /, $raw);
$IDs{$id}=$temp;
}
}
# SAS Wrapping
# Section by Cameron Munroe (munroenet[at]gmail.com)
# Elements in Grown Defect List.
# Marking as 5 Reallocated_Sector_Ct
if ($line =~ "Elements in grown defect list:"){
my @lineA=split(/\ /, $line, 10);
my $raw=$lineA[5];
# Reallocated Sector Count ID
$IDs{5}=$raw;
}
# Current Drive Temperature
# Marking as 194 Temperature_Celsius
if ($line =~ "Current Drive Temperature:"){
my @lineA=split(/\ /, $line, 10);
my $raw=$lineA[3];
# Temperature C ID
$IDs{194}=$raw;
}
# End of SAS Wrapper
$outputAint++;
}
}
#get the selftest logs
$output=`$smartctl -l selftest $disk`;
@outputA=split( /\n/, $output );
my $completed=scalar grep(/Completed without error/, @outputA);
my $interrupted=scalar grep(/Interrupted/, @outputA);
my $read_failure=scalar grep(/read failure/, @outputA);
my $unknown_failure=scalar grep(/unknown failure/, @outputA);
my $extended=scalar grep(/Extended/, @outputA);
my $short=scalar grep(/Short/, @outputA);
my $conveyance=scalar grep(/Conveyance/, @outputA);
my $selective=scalar grep(/Selective/, @outputA);
# get the drive serial number, if needed
my $disk_id=$name;
if ( $useSN ){
while (`$smartctl -i $disk` =~ /(?i)Serial Number:(.*)/g) {
$disk_id = $1;
$disk_id =~ s/^\s+|\s+$//g;
}
}
$toReturn=$toReturn.$disk_id.','.$IDs{'5'}.','.$IDs{'10'}.','.$IDs{'173'}.','.$IDs{'177'}.','.$IDs{'183'}.','.$IDs{'184'}.','.$IDs{'187'}.','.$IDs{'188'}
.','.$IDs{'190'} .','.$IDs{'194'}.','.$IDs{'196'}.','.$IDs{'197'}.','.$IDs{'198'}.','.$IDs{'199'}.','.$IDs{'231'}.','.$IDs{'233'}.','.
$completed.','.$interrupted.','.$read_failure.','.$unknown_failure.','.$extended.','.$short.','.$conveyance.','.$selective.','.$IDs{'9'}."\n";
}
if ( ! $noWrite ){
open(my $writefh, ">", $cache) or die "Can't open '".$cache."'";
print $writefh $toReturn;
close($writefh);
}else{
print $toReturn;
}