mirror of
				https://github.com/librenms/librenms-agent.git
				synced 2024-05-09 09:54:52 +00:00 
			
		
		
		
	* - added equivalent wear level for nvme ssd - remove touched cache file to avoid no data if config is guessed - take only 1st raw response to avoid taking strings instead of int (eg. adacom devices like supermicro sata dom moule) * fix identation
		
			
				
	
	
		
			455 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Perl
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			455 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 $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 == 177 ) ||
 | |
| 					( $id == 183 ) ||
 | |
| 					( $id == 184 ) ||
 | |
| 					( $id == 187 ) ||
 | |
| 					( $id == 196 ) ||
 | |
| 					( $id == 197 ) ||
 | |
| 					( $id == 198 ) ||
 | |
| 					( $id == 199 ) ||
 | |
| 					( $id == 231 ) ||
 | |
| 					( $id == 233 )
 | |
| 					) {
 | |
| 					my @rawA=split( /\ /, $raw );
 | |
| 					$IDs{$id}=$rawA[0];
 | |
| 				}
 | |
| 
 | |
| 				# 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;
 | |
| }
 |