#!/usr/bin/env perl
#Copyright (c) 2024, 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.

    */5 * * * * /etc/snmp/extends/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/extends/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.


Switches:

-c <config>   The config file to use.
-u            Update
-p            Pretty print the JSON.
-Z            GZip+Base64 compress the results.

-g            Guess at the config and print it to STDOUT
-C            Enable manual checking for guess and cciss.
-S            Set useSN to 0 when using -g
-t <test>     Run the specified smart self test on all the devices.
-U            When calling cciss_vol_status, call it with -u.
-G <modes>    Guess modes to use. This is a comma seperated list.
              Default :: scan-open,cciss-vol-status

Guess Modes:

- scan :: Use "--scan" with smartctl. "scan-open" will take presidence.

- scan-open :: Call smartctl with "--scan-open".

- cciss-vol-status :: Freebsd/Linux specific and if it sees /dev/sg0(on Linux) or
    /dev/ciss0(on FreebSD) it will attempt to find drives via cciss-vol-status,
    and then optionally checking for disks via smrtctl if -C is given. Should be noted
    though that -C will not find drives that are currently missing/failed. If -U is given,
    cciss_vol_status will be called with -u.

=cut

##
## You should not need to touch anything below here.
##
use warnings;
use strict;
use Getopt::Std;
use JSON;
use MIME::Base64;
use IO::Compress::Gzip qw(gzip $GzipError);

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.3.2\n";
}

sub main::HELP_MESSAGE {
	&VERSION_MESSAGE;
	print "\n" . "-u   Update '" . $cache . "'\n" . '-g            Guess at the config and print it to STDOUT
-c <config>   The config file to use.
-p            Pretty print the JSON.
-Z            GZip+Base64 compress the results.
-C            Enable manual checking for guess and cciss.
-S            Set useSN to 0 when using -g
-t <test>     Run the specified smart self test on all the devices.
-U            When calling cciss_vol_status, call it with -u.
-G <modes>    Guess modes to use. This is a comma seperated list.
              Default :: scan-open,cciss-vol-status


Scan Modes:

- scan :: Use "--scan" with smartctl. "scan-open" will take presidence.

- scan-open :: Call smartctl with "--scan-open".

- cciss-vol-status :: Freebsd/Linux specific and if it sees /dev/sg0(on Linux) or
    /dev/ciss0(on FreebSD) it will attempt to find drives via cciss-vol-status,
    and then optionally checking for disks via smrtctl if -C is given. Should be noted
    though that -C will not find drives that are currently missing/failed. If -U is given,
    cciss_vol_status will be called with -u.
';

} ## end sub main::HELP_MESSAGE

#gets the options
my %opts = ();
getopts( 'ugc:pZhvCSGt:U', \%opts );

if ( $opts{h} ) {
	&HELP_MESSAGE;
	exit;
}
if ( $opts{v} ) {
	&VERSION_MESSAGE;
	exit;
}

#
# figure out what scan modes to use if -g specified
#
my $scan_modes = {
	'scan-open'        => 0,
	'scan'             => 0,
	'cciss_vol_status' => 0,
};
if ( $opts{g} ) {
	if ( !defined( $opts{G} ) ) {
		$opts{G} = 'scan-open,cciss_vol_status';
	}
	$opts{G} =~ s/[\ \t]//g;
	my @scan_modes_split = split( /,/, $opts{G} );
	foreach my $mode (@scan_modes_split) {
		if ( !defined $scan_modes->{$mode} ) {
			die( '"' . $mode . '" is not a recognized scan mode' );
		}
		$scan_modes->{$mode} = 1;
	}
} ## end if ( $opts{g} )

# configure JSON for later usage
# only need to do this if actually running as in -g is not specified
my $json;
if ( !$opts{g} ) {

	$json = JSON->new->allow_nonref->canonical(1);
	if ( $opts{p} ) {
		$json->pretty;
	}
}

#
#
# 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";
	}

	my $drive_lines = '';

	#
	#
	# scan-open and scan guess mode handling
	#
	#
	if ( $scan_modes->{'scan-open'} || $scan_modes->{'scan'} ) {
		# used for checking if a disk has been found more than once
		my %found_disks_names;
		my @argumentsA;

		# use scan-open if it is set, overriding scan if it is also set
		my $mode = 'scan';
		if ( $scan_modes->{'scan-open'} ) {
			$mode = 'scan-open';
		}

		#have smartctl scan and see if it finds anythings not get found
		my $scan_output  = `$smartctl --$mode`;
		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 );

		} ## end foreach my $arguments (@scan_outputA)

		# second pass, putting the lines together
		my %current_disk;
		foreach my $arguments (@argumentsA) {
			my $not_virt = 1;

			# check to see if we have a virtual device
			my @virt_check = split( /\n/, `smartctl -i $arguments 2> /dev/null` );
			foreach my $virt_check_line (@virt_check) {
				if ( $virt_check_line =~ /(?i)Product\:.*LOGICAL VOLUME/ ) {
					$not_virt = 0;
				}
			}

			my $name = $arguments;
			$name =~ s/ .*//;
			$name =~ s/\/dev\///;

			# only add it if not a virtual RAID drive
			# HP RAID virtual disks will show up with very basical but totally useless smart data
			if ($not_virt) {
				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";
				}
			} ## end if ($not_virt)

		} ## end foreach my $arguments (@argumentsA)
	} ## end if ( $scan_modes->{'scan-open'} || $scan_modes...)

	#
	#
	# scan mode handler for cciss_vol_status
	# /dev/sg* devices for cciss on Linux
	# /dev/ccis* devices for cciss on FreeBSD
	#
	#
	if ( $scan_modes->{'cciss_vol_status'} && ( $^O eq 'linux' || $^O eq 'freebsd' ) ) {
		my $cciss;
		if ( $^O eq 'freebsd' ) {
			$cciss = 'ciss';
		} elsif ( $^O eq 'linux' ) {
			$cciss = 'sg';
		}

		my $uarg = '';
		if ( $opts{U} ) {
			$uarg = '-u';
		}

		# generate the initial device path that will be checked
		my $sg_int = 0;
		my $device = '/dev/' . $cciss . $sg_int;

		my $sg_process = 1;
		if ( -e $device ) {
			my $output = `which cciss_vol_status 2> /dev/null`;
			if ( $? != 0 && !$opts{C} ) {
				$sg_process = 0;
				$drive_lines
					= $drive_lines
					. "# -C not given, but "
					. $device
					. " exists and cciss_vol_status is not present\n"
					. "# in path or 'ccis_vol_status -V "
					. $device
					. "' is failing\n";
			} ## end if ( $? != 0 && !$opts{C} )
		} ## end if ( -e $device )
		my $seen_lines   = {};
		my $ignore_lines = {};
		while ( -e $device && $sg_process ) {
			my $output = `cciss_vol_status -V $uarg $device 2> /dev/null`;
			if ( $? != 0 && $output eq '' && !$opts{C} ) {
				# just empty here as we just want to skip it if it fails and there is no C
				# warning is above
			} elsif ( $? != 0 && $output eq '' && $opts{C} ) {
				my $drive_count = 0;
				my $continue    = 1;
				while ($continue) {
					my $output = `$smartctl -i $device -d cciss,$drive_count 2> /dev/null`;
					if ( $? != 0 ) {
						$continue = 0;
					} else {
						my $add_it = 0;
						my $id;
						while ( $output =~ /(?i)Serial Number:(.*)/g ) {
							$id = $1;
							$id =~ s/^\s+|\s+$//g;
						}
						if ( defined($id) && !defined( $seen_lines->{$id} ) ) {
							$add_it = 1;
							$seen_lines->{$id} = 1;
						}
						if ( $continue && $add_it ) {
							$drive_lines
								= $drive_lines
								. $cciss . '0-'
								. $drive_count . ' '
								. $device
								. ' -d cciss,'
								. $drive_count . "\n";
						}
					} ## end else [ if ( $? != 0 ) ]
					$drive_count++;
				} ## end while ($continue)
			} else {
				my $drive_count = 0;
				# count the connector lines, this will make sure failed are founded as well
				my $seen_conectors = {};
				while ( $output =~ /(connector +\d+[IA]\ +box +\d+\ +bay +\d+.*)/g ) {
					my $cciss_drive_line = $1;
					my $connector        = $cciss_drive_line;
					$connector =~ s/(.*\ bay +\d+).*/$1/;
					if (   !defined( $seen_lines->{$cciss_drive_line} )
						&& !defined( $seen_conectors->{$connector} )
						&& !defined( $ignore_lines->{$cciss_drive_line} ) )
					{
						$seen_lines->{$cciss_drive_line} = 1;
						$seen_conectors->{$connector}    = 1;
						$drive_count++;
					} else {
						# going to be a connector we've already seen
						# which will happen when it is processing replacement drives
						# so save this as a device to ignore
						$ignore_lines->{$cciss_drive_line} = 1;
					}
				} ## end while ( $output =~ /(connector +\d+[IA]\ +box +\d+\ +bay +\d+.*)/g)
				my $drive_int = 0;
				while ( $drive_int < $drive_count ) {
					$drive_lines
						= $drive_lines
						. $cciss
						. $sg_int . '-'
						. $drive_int . ' '
						. $device
						. ' -d cciss,'
						. $drive_int . "\n";

					$drive_int++;
				} ## end while ( $drive_int < $drive_count )
			} ## end else [ if ( $? != 0 && $output eq '' && !$opts{C})]

			$sg_int++;
			$device = '/dev/' . $cciss . $sg_int;
		} ## end while ( -e $device && $sg_process )
	} ## end if ( $scan_modes->{'cciss_vol_status'} && ...)

	my $useSN = 1;
	if ( $opts{S} ) {
		$useSN = 0;
	}

	print '# scan_modes='
		. $opts{G}
		. "\nuseSN="
		. $useSN . "\n"
		. 'smartctl='
		. $smartctl . "\n"
		. $cache
		. $drive_lines;

	exit 0;
} ## end if ( defined( $opts{g} ) )

#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++;
} ## end while ( defined( $configA[$configA_int] ) )

#
#
# run the specified self test on all disks if asked
#
#
if ( defined( $opts{t} ) ) {

	# make sure we have something that atleast appears sane for the test name
	my $valid_tesks = {
		'offline'        => 1,
		'short'          => 1,
		'long'           => 1,
		'conveyance'     => 1,
		'afterselect,on' => 1,
	};
	if ( !defined( $valid_tesks->{ $opts{t} } ) && $opts{t} !~ /select,(\d+[\-\+]\d+|next|next\+\d+|redo\+\d+)/ ) {
		print '"' . $opts{t} . "\" does not appear to be a valid test\n";
		exit 1;
	}

	print "Running the SMART $opts{t} on all devices in the config...\n\n";

	foreach my $line (@disks) {
		my $disk;
		my $name;
		if ( $line =~ /\ / ) {
			( $name, $disk ) = split( /\ /, $line, 2 );
		} else {
			$disk = $line;
			$name = $line;
		}
		if ( $disk !~ /\// ) {
			$disk = '/dev/' . $disk;
		}

		print "\n------------------------------------------------------------------\nDoing "
			. $smartctl . ' -t '
			. $opts{t} . ' '
			. $disk
			. "  ...\n\n";
		print `$smartctl -t $opts{t} $disk` . "\n";

	} ## end foreach my $line (@disks)

	exit 0;
} ## end if ( defined( $opts{t} ) )

#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;
	}
} ## end if ( !defined( $opts{u} ) )

#
#
# Process each disk
#
#
my $to_return = {
	data        => { disks => {}, exit_nonzero => 0, unhealthy => 0, useSN => $useSN },
	version     => 1,
	error       => 0,
	errorString => '',
};
foreach my $line (@disks) {
	my $disk;
	my $name;
	if ( $line =~ /\ / ) {
		( $name, $disk ) = split( /\ /, $line, 2 );
	} else {
		$disk = $line;
		$name = $line;
	}
	if ( $disk !~ /\// ) {
		$disk = '/dev/' . $disk;
	}

	my $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',
		'232'          => 'null',
		'233'          => 'null',
		'9'            => 'null',
		'disk'         => $disk,
		'serial'       => undef,
		'selftest_log' => undef,
		'health_pass'  => 0,
		max_temp       => 'null',
		exit           => $?,
	);
	$IDs{'disk'} =~ s/^\/dev\///;

	# if polling exited non-zero above, no reason running the rest of the checks
	my $disk_id = $name;
	if ( $IDs{exit} != 0 ) {
		$to_return->{data}{exit_nonzero}++;
	} else {
		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;
						}
					}
				} ## end if (/:/)
			} ## end foreach ( split( /\n/, $output ) )

		} 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
						&& $line =~ /Percent_Lifetime_Remain/ )
					{
						$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];
					} ## end if ( ( $id == 5 ) || ( $id == 10 ) || ( $id...))

					# single int normalized values
					if (   ( $id == 177 )
						|| ( $id == 230 )
						|| ( $id == 231 )
						|| ( $id == 232 )
						|| ( $id == 233 ) )
					{
				 # annoying non-standard disk
				 # WDC WDS500G2B0A
				 # 230 Media_Wearout_Indicator 0x0032   100   100   ---    Old_age   Always       -       0x002e000a002e
				 # 232 Available_Reservd_Space 0x0033   100   100   004    Pre-fail  Always       -       100
				 # 233 NAND_GB_Written_TLC     0x0032   100   100   ---    Old_age   Always       -       9816

						if (   $id == 230
							&& $line =~ /Media_Wearout_Indicator/ )
						{
							$IDs{233} = int($normalized);
						} elsif ( $id == 232
							&& $line =~ /Available_Reservd_Space/ )
						{
							$IDs{232} = int($normalized);
						} else {
							# only set 233 if it has not been set yet
							# if it was set already then the above did it and we don't want
							# to overwrite it
							if ( $id == 233 && $IDs{233} eq "null" ) {
								$IDs{$id} = int($normalized);
							} elsif ( $id != 233 ) {
								$IDs{$id} = int($normalized);
							}
						} ## end else [ if ( $id == 230 && $line =~ /Media_Wearout_Indicator/)]
					} ## end if ( ( $id == 177 ) || ( $id == 230 ) || (...))

					# 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;
					} ## end if ( $id == 188 )

					# 190, airflow temp
					# 194, temp
					if (   ( $id == 190 )
						|| ( $id == 194 ) )
					{
						my ($temp) = split( /\ /, $raw );
						$IDs{$id} = $temp;
					}
				} ## end if ( $line =~ /^[0123456789]+ / )

				# 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++;
			} ## end while ( defined( $outputA[$outputAint] ) )
		} ## end else [ if ( $output =~ /NVMe Log/ ) ]

		#get the selftest logs
		$output  = `$smartctl -l selftest $disk`;
		@outputA = split( /\n/, $output );
		my @completed = grep( /Completed/, @outputA );
		$IDs{'completed'} = scalar @completed;
		my @interrupted = grep( /Interrupted/, @outputA );
		$IDs{'interrupted'} = scalar @interrupted;
		my @read_failure = grep( /read failure/, @outputA );
		$IDs{'read_failure'} = scalar @read_failure;
		my @read_failure2 = grep( /Failed in segment/, @outputA );
		$IDs{'read_failure'} = $IDs{'read_failure'} + scalar @read_failure2;
		my @unknown_failure = grep( /unknown failure/, @outputA );
		$IDs{'unknown_failure'} = scalar @unknown_failure;
		my @extended = grep( /\d.*\ ([Ee]xtended|[Ll]ong).*(?![Dd]uration)/, @outputA );
		$IDs{'extended'} = scalar @extended;
		my @short = grep( /[Ss]hort/, @outputA );
		$IDs{'short'} = scalar @short;
		my @conveyance = grep( /[Cc]onveyance/, @outputA );
		$IDs{'conveyance'} = scalar @conveyance;
		my @selective = grep( /[Ss]elective/, @outputA );
		$IDs{'selective'} = scalar @selective;
		my @offline = grep( /(\d|[Bb]ackground|[Ff]oreground)+\ +[Oo]ffline/, @outputA );
		$IDs{'offline'} = scalar @offline;

		# if we have logs, actually grab the log output
		if (   $IDs{'completed'} > 0
			|| $IDs{'interrupted'} > 0
			|| $IDs{'read_failure'} > 0
			|| $IDs{'extended'} > 0
			|| $IDs{'short'} > 0
			|| $IDs{'conveyance'} > 0
			|| $IDs{'selective'} > 0
			|| $IDs{'offline'} > 0 )
		{
			my @headers = grep( /(Num\ +Test.*LBA| Description .*[Hh]ours)/, @outputA );

			my @log_lines;
			push( @log_lines, @extended, @short, @conveyance, @selective, @offline );
			$IDs{'selftest_log'} = join( "\n", @headers, sort(@log_lines) );
		} ## end if ( $IDs{'completed'} > 0 || $IDs{'interrupted'...})

		# get the drive serial number, if needed
		$disk_id = $name;
		$output  = `$smartctl -i $disk`;
		# generally upper case, HP branded drives seem to report with lower case n
		while ( $output =~ /(?i)Serial Number:(.*)/g ) {
			$IDs{'serial'} = $1;
			$IDs{'serial'} =~ s/^\s+|\s+$//g;
		}
		if ($useSN) {
			$disk_id = $IDs{'serial'};
		}

		while ( $output =~ /(?i)Model Family:(.*)/g ) {
			$IDs{'model_family'} = $1;
			$IDs{'model_family'} =~ s/^\s+|\s+$//g;
		}

		while ( $output =~ /(?i)Device Model:(.*)/g ) {
			$IDs{'device_model'} = $1;
			$IDs{'device_model'} =~ s/^\s+|\s+$//g;
		}

		while ( $output =~ /(?i)Model Number:(.*)/g ) {
			$IDs{'model_number'} = $1;
			$IDs{'model_number'} =~ s/^\s+|\s+$//g;
		}

		while ( $output =~ /(?i)Firmware Version:(.*)/g ) {
			$IDs{'fw_version'} = $1;
			$IDs{'fw_version'} =~ s/^\s+|\s+$//g;
		}

		# mainly HP drives
		while ( $output =~ /(?i)Vendor:(.*)/g ) {
			$IDs{'vendor'} = $1;
			$IDs{'vendor'} =~ s/^\s+|\s+$//g;
		}

		# mainly HP drives
		while ( $output =~ /(?i)Product:(.*)/g ) {
			$IDs{'product'} = $1;
			$IDs{'product'} =~ s/^\s+|\s+$//g;
		}

		# mainly HP drives
		while ( $output =~ /(?i)Revision:(.*)/g ) {
			$IDs{'revision'} = $1;
			$IDs{'revision'} =~ s/^\s+|\s+$//g;
		}

		# figure out what to use for the max temp, if there is one
		if ( $IDs{'190'} =~ /^\d+$/ ) {
			$IDs{max_temp} = $IDs{'190'};
		} elsif ( $IDs{'194'} =~ /^\d+$/ ) {
			$IDs{max_temp} = $IDs{'194'};
		}
		if ( $IDs{'194'} =~ /^\d+$/ && defined( $IDs{max_temp} ) && $IDs{'194'} > $IDs{max_temp} ) {
			$IDs{max_temp} = $IDs{'194'};
		}

		$output = `$smartctl -H $disk`;
		if ( $output =~ /SMART\ overall\-health\ self\-assessment\ test\ result\:\ PASSED/ ) {
			$IDs{'health_pass'} = 1;
		} elsif ( $output =~ /SMART\ Health\ Status\:\ OK/ ) {
			$IDs{'health_pass'} = 1;
		}

		if ( !$IDs{'health_pass'} ) {
			$to_return->{data}{unhealthy}++;
		}
	} ## end else [ if ( $IDs{exit} != 0 ) ]

	# only bother to save this if useSN is not being used
	if ( !$useSN ) {
		$to_return->{data}{disks}{$disk_id} = \%IDs;
	} elsif ( $IDs{exit} == 0 && defined($disk_id) ) {
		$to_return->{data}{disks}{$disk_id} = \%IDs;
	}

	# smartctl will in some cases exit zero when it can't pull data for cciss
	# so if we get a zero exit, but no serial then it means something errored
	# and the device is likely dead
	if ( $IDs{exit} == 0 && !defined( $IDs{serial} ) ) {
		$to_return->{data}{unhealthy}++;
	}
} ## end foreach my $line (@disks)

my $toReturn = $json->encode($to_return);

if ( !$opts{p} ) {
	$toReturn = $toReturn . "\n";
}

if ( $opts{Z} ) {
	my $toReturnCompressed;
	gzip \$toReturn => \$toReturnCompressed;
	my $compressed = encode_base64($toReturnCompressed);
	$compressed =~ s/\n//g;
	$compressed = $compressed . "\n";
	if ( length($compressed) < length($toReturn) ) {
		$toReturn = $compressed;
	}
} ## end if ( $opts{Z} )

if ( !$noWrite ) {
	open( my $writefh, ">", $cache ) or die "Can't open '" . $cache . "'";
	print $writefh $toReturn;
	close($writefh);
} else {
	print $toReturn;
}
