#!/usr/bin/perl -w

use strict;
use warnings;
use Getopt::Long qw(:config no_ignore_case);
use JSON;
use Storable qw(dclone);

our $VERSION = "1.6";
our $VERBOSITY = 0;

use constant {
	STATE_OK => 0,
	STATE_WARNING => 1,
	STATE_CRITICAL => 2,
	STATE_UNKNOWN => 3,
};

our @hdrmap_a = ('ID#','ATTRIBUTE_NAME','FLAG','VALUE','WORST','THRESH',
	'TYPE','UPDATED','WHEN_FAILED','RAW_VALUE');

our %hdrmap_nvme = (
	'Critical Warning' => 0,
	'Temperature' => 1,
	'Available Spare' => 2,
	'Available Spare Threshold' => 3,
	'Percentage Used' => 4,
	'Data Units Read' => 5,
	'Data Units Written' => 6,
	'Host Read Commands' => 7,
	'Host Write Commands' => 8,
	'Controller Busy Time' => 9,
	'Power Cycles' => 10,
	'Power On Hours' => 11,
	'Unsafe Shutdowns' => 12,
	'Media and Data Integrity Errors' => 13,
	'Error Information Log Entries' => 14,
	'Warning  Comp. Temperature Time' => 15,
	'Critical Comp. Temperature Time' => 16
);

our %hdrmap_sas = (
    'Elements in grown defect list' => 0,
    'read' => [
        'Read errors corrected by ECC (fast)',
        'Read errors corrected by ECC (delayed)',
        'Read errors corrected by ECC (reread)',
        'Total read errors corrected',
        'Total read correction algorithm invocations',
        'Total read gigabytes processed',
        'Total uncorrected read errors'
    ],
    'write' => [
        'Write errors corrected by ECC (fast)',
        'Write errors corrected by ECC (delayed)',
        'Write errors corrected by ECC (rewrite)',
        'Total write errors corrected',
        'Total write correction algorithm invocations',
        'Total write gigabytes processed',
        'Total uncorrected write errors'
    ]
);

sub getVersionString{
	return "check_smart_values version $VERSION 20190208
Copyright (C) 2019 Thomas-Krenn.AG (written by Georg Schönberger)
Current updates available via git repository https://github.com/thomas-krenn/\n";
}

sub getUsageString{
	return "Usage:
sudo check_smart_values -dbj <smartdb json file> -d <device path> [-d <device path>]
[-r <regex pattern to find devices>] [-ucfgj <user config json file>]
[-p <path to smartctl>] [-nosudo] [-cu] [-ap] [-s]
[-O <extra options>][ -v|-vv|-vvv] [-h] [-V]\n";
}

sub getHelpString{
	return "
  [-p|--path <path to smartctl]
        Specify the path at which the smartctl binary can be found. Per
        default /usr/sbin/smartctl is taken.
  [-d|--device <path to device being checked>]
        Specify the device being monitored. If a regex pattern should be
        used to find devices don't use '-d' but '-r' with a given device
        pattern (cf. '-r' help text below).
        If multiple devices should be checked with '-d' use the option
        multiple times:
            -d /dev/sda -d /dev/sdb
        For devices behind LSI RAID controllers the megaraid number must be
        used.
        There are different ways to specify the device, the correct one must be
        tested with the corresponding RAID controller in use, e.g.:
            -d megaraid6,/dev/sda
            -d megaraid6,/dev/sda -O 'sat+'
        Use storcli to find out the corresponding device numbers.
        For devices behind Adaptec RAID controllers on Linux specify
        '/dev/sg<X>' where <X> is the number for your device. Use e.g. sg_scan
        to find the device.
        You must also use '-O sat' or '-O scsi' according to the device
        interface. This are extra options only necessary for '/dev/sg<X>'
        devices.
        As an alternative for Adaptec RAID controllers on Windows or Linux
        you can specify 'aacraid,H,L,ID' where <H> is the Host number and <L>
        is the LUN.
        The <ID> can be found by executing 'arcconf getconfig <CONTROLLER-ID>'.
        Sample output for <CONTROLLER-ID>=1:
        '...Reported Channel,Device(T:L): 0,4(4:0)...'
        Configuration would be '-d aacraid,0,0,4'
        For devices behind hpsa/cciss based RAID controller, you'll mostly find in
        HP Hardware, you can use 'cciss,<N>_/dev/sg<X>' for hpsa or
        'cciss,<N>_/dev/cciss/c<X>d<N>' for cciss where <N> is the number
        of the device and <X> the RAID controller. You'll find a sg<X>
        for every Logical Drive.
        For devices behind 3ware RAID controllers, the 3ware device ID must be
        used with the corresponding tw device, e.g.:
        -d 3ware,8,/dev/twa0
        For devices behind Areca RAID controllers, the Areca device IDs must be
        used with the corresponding areca device, e.g.:
        -d areca1/1,/dev/sg0
        For NVMe devices just use '/dev/nvme[a-z0-9]+'. As attributes with NVMe
        are not model specific the generic NVMe entry in the smartdb JSON file
        is used.
  [-r|--regex]
        If no devices are specified by '-d' use a regular expression to find
        devices. The regular expression string is passed to the find command
        which searches in '/dev' for the regex in combination with the find
				types f (files), l (links) and b (block devices).
        Note that find always returns the full path with '-regex' therefore
        found matches always will start with '/dev' as this is the base search
        path. The used regex type by find is 'egrep'.
        Examples:
            -r '.*sdc.*'
            -r \'/dev/sd[c,d]\$\'
            -r \'/dev/sd[a-z]\$\'
            -r \'/dev/.*INTEL.*\'
  [-dbj|--dbjson <path to smartdb JSON file>]
        Specify the path at which the JSON smart db can be found. The JSON file
        defines which parameter (VALUE or RAW_VALUE) must be taken for a
        sensor. In order to interpret a sensor it is necessary to know which
        value to take. As this mapping can be different for device models a
        database is needed for a device.
        Attributes for NVMe devives are not model specific, therefore only one
        generic NMVe entry is present in the smart db. If the device is set up
        as 'nvme[a-z0-9]+' then NVMe specific parsing of attributes
        is done. The generic entry is necessary to enable default thresholds and
        performance values for NVMe devices. Moreover now with the generic entry
        users can still override the smart db entry with 'ucfgj'. This is way
        better than having hardcoded parsing of NVMe attributes in the plugin.
  [-ucfgj|--ucfgjson <path to user config JSON file>]
        Specify the path at which the JSON user config file can be found.
        The user config can be used to override thresholds and performance values
        in the base config. This can be useful if the thresholds for a specific
        device must be changed (e.g. showing up a non-critical error). If a value
        is defined in the user config it overrides its corresponding value in
        the base config. The override is only taken for the specific value, for
        the other ones the base config is still valid.
        Attention: If megaraid devices are used, the path must be set as
        '/dev/megaraidID'. This is necessary for the internal naming convention
        the plugin uses.
        Attention: If 3ware devices are used, the path must be set as
        '/dev/3wareID'. This is necessary for the internal naming convention
        the plugin uses.
        Attention: If Areca devices are used, the path must be set as
        '/dev/areca_ID_ID'. this is necessary for the internal naming convention
        the plugin uses.
  [-cu|--critical_on_unknown]
        Show critical if the device was not found in database.
  [-nosudo]
        Disable the usage of sudo for smartctl. This is handy if a system is
        used where sudo is not available.
  [-ap|--allperfs]
        Get performance data for all monitor-able smart attributes and don't
        limit performance data to the 'Perfs' section in the smartdb.json.
  [-s|--ssdonly]
        Check SSDs only, even if more devices are given. Checks if the smartctl
        output contains 'Solid State Device' in the method 'checkWhichDevice'.
        If yes, the device is skipped even if it is found in the smartdb.json.
  [-O|--options <extra options>]
        Currently the extra options 'sat' or 'scsi' are possible for Adaptec
        controllers. For megaraid devices 'sat+' is allowed.
        The options are only used if a device of type sg<X> or
        megaraid<X>,/dev/sdX is given.
  [-v|-vv|-vvv]
       be verbose
         (no -v) .. single line output
         -v ....... single line output with additional details for warnings
         -vv ...... multi line output, also with additional details for warnings
         -vvv ..... normal output, then debugging output
  [-h|--help]
       show this help
  [-V|--version]
       show version information\n";
}

sub readUCfgJSON{
	my $uCfgJSON = shift;
	my $file_content = do { open my $fh, '<', $uCfgJSON; local $/; <$fh> };
	return(decode_json($file_content));
}

sub readDbJSON{
	my $dbJSON = shift;
	my $file_content = do { open my $fh, '<', $dbJSON; local $/; <$fh> };
	return(decode_json($file_content));
}

sub parseMegaraidRegex{
	my $devicepattern = shift;
	my @megadevs;
	if($devicepattern =~ /^megaraid\[([0-9]+)\-([0-9]+)\],(\/dev\/[a-zA-Z]+)$/){
		for(my $id = $1; $id <= $2; $id++) {
			my $megadev = "megaraid" . $id . "," . $3;
			push @megadevs, $megadev;
		}
	}
	return @megadevs;
}

sub genAttributeName{
	my $device = shift;
	my $attributeName= shift;

	# Parse device for prepending attribute name with device
	my $currDevice;
	if($device =~ m/(nvme[a-z0-9])+$/){
		$currDevice = $1;
	}
	elsif($device =~ m/^\/dev\/(?:disk\/by-(?:id|label)\/)?([A-Za-z0-9_\-,\.\:]+)$/){
		$currDevice = $1;
	}
	$attributeName = $currDevice.'_'.$attributeName;
	# Replace characters not valid in sensor names
	$attributeName =~ s/\s/_/g;
	$attributeName =~ s/[\(\)]//g;
	return $attributeName;
}

sub findDevicesByRegex{
	my $devicepattern = shift;
	my @founddevices;
	if($devicepattern =~ /^megaraid\[([0-9]+)\-([0-9]+)\],(\/dev\/[a-zA-Z]+)$/){
		@founddevices = parseMegaraidRegex($devicepattern);
	}
	else{
		my $findcmd;
		chomp($findcmd = `which find`);
		if(! -x $findcmd){
			print "Error: cannot find find executable.\n";
			exit(STATE_UNKNOWN);
		}
		# Since older versions of find don't support multiple types, loop over each type separately
		my @findtypes_a = ('f','b','l','c');
		foreach my $type (@findtypes_a){
			my @founddevices_tmp;
			@founddevices_tmp = `$findcmd /dev -regextype egrep -regex $devicepattern -type $type`;
			if(($? >> 8) != 0){
				print "Error: find command with regex returned an error.\n";
				print "The used command was:\n";
				print "$findcmd /dev -regextype egrep -regex $devicepattern -type $type\n";
				exit(STATE_UNKNOWN);
			}
			else{
				push @founddevices, @founddevices_tmp;
			}
		}
	}
	chomp(@founddevices);
	my @devices_without_parts;
	foreach my $device (@founddevices){
		if($device =~ /\-part[0-9]+$/){
			next;
		}
		else{
			push @devices_without_parts, $device;
		}
	}
	return @devices_without_parts;
}

sub getSmartctl{
	my $smartctl = shift;
	my @devicesToCheck_a = @{(shift)};
	my $extraOpts = shift;
	#a hash of all devices with their corresponding output
	my %output_h;
	my %command_h;

	foreach my $device (@devicesToCheck_a){
		my @output;
		my $usedCommand;
		# Check if a /dev/disk/by-* device is used
		if($device =~ /^\/dev\/disk\/by\-[a-zA-Z]+\/(.*)$/){
			# smartctl can directly read this device links
			$usedCommand = $smartctl.' -a '.$device;
			if(defined($extraOpts)){
				$usedCommand .= ' -d '.$extraOpts;
			}
			@output = `$usedCommand`;
		}
		# Check if a megaraid device is used
		elsif($device =~ /^(megaraid[0-9]+),(\/dev\/[a-zA-Z]+)$/){
			my $megaDevice = $1;
			my $devicePath = $2;
			# Insert a ',' after 'megaraid'
			substr($megaDevice,8,0,',');
			my $megaDeviceOptions = $megaDevice;
			# Device path and megaraid device are used together to
			# check the attributes with smartctl
			if(defined($extraOpts)){
				if($extraOpts ne 'sat+'){
					print "Error: please use 'sat+' as extra options for megaraid devices!\n";
					print getUsageString();
					exit(STATE_UNKNOWN);
				}
				$megaDeviceOptions = $extraOpts.$megaDevice
			}
			$usedCommand = $smartctl.' -a -d '.$megaDeviceOptions.' '.$devicePath;
			@output = `$usedCommand`;
			# From now on we use only e.g. megaraid6 as device
			$megaDevice =~ s/,//;
			$device = '/dev/'.$megaDevice;
		}
		# Check if an Adaptec sg device is used
		elsif($device =~ /^\/dev\/sg[0-9]+/ && defined($extraOpts)){
			if($extraOpts ne 'sat' && $extraOpts ne 'scsi'){
				print "Error: please use 'sat' or 'scsi' as options for Adaptec sg devices!\n";
				print getUsageString();
				exit(STATE_UNKNOWN);
			}
			# Call smartctl with extra options for Adaptec sg device
			$usedCommand = $smartctl.' -a -d '.$extraOpts.' '.$device;
			@output = `$usedCommand`;
		}
		# Check if Adaptec aacraid format is used
		elsif($device =~ /^(aacraid),([0-9]+),([0-9]+),([0-9]+),(\/dev\/[a-zA-Z]+)$/){
			my $aacDevice = $1 . "," . $2 . "," . $3 . "," .$4;
			my $devicePath = $5;
			$usedCommand = $smartctl.' -a -d '.$aacDevice.' '.$devicePath;
			@output = `$usedCommand`;
			# From now on we use only e.g. aacraid_0_0_4 as device
			$aacDevice =~ s/,/_/g;
			$device = '/dev/'.$aacDevice;
		}
		# Check if a cciss device is used
		elsif($device =~ /^(cciss,[0-9]+)[\s_]+(\/dev\/cciss\/c[0-9]+d[0-9]+|\/dev\/sg[0-9]+)$/){
			my $ccissDevice = $1;
			my $devicePath = $2;
			my @controller = split(/\//,$devicePath);
			$usedCommand = $smartctl.' -a -d '.$ccissDevice.' '.$devicePath;
			@output = `$usedCommand`;
			$ccissDevice =~ s/,/_/g;
			$device = '/dev/'.$ccissDevice."_".$controller[-1];
		}
		# Check if 3ware format is used
		elsif($device =~ /^(3ware,[0-9]+),(\/dev\/tw[a-z0-9]+)$/){
			my $twDevice = $1;
			my $devicePath = $2;
			$usedCommand = $smartctl.' -a -d '.$twDevice.' '.$devicePath;
			@output = `$usedCommand`;
			# From now on we use only e.g. 3ware8 as device
			$twDevice =~ s/,//;
			$device = '/dev/'.$twDevice;
		}
		# Check if Areca format is used
		elsif($device =~ /^(areca),([0-9]+)\/([0-9]+),(\/dev\/sg[0-9]+)$/){
			my $arecaDevice = $1 . "," . $2 . "/" . $3;
			my $devicePath = $4;
			$usedCommand = $smartctl.' -a -d '.$arecaDevice.' '.$devicePath;
			@output = `$usedCommand`;
			# From now on we use only e.g. areca_1_1 as device
			$arecaDevice =~ s/,/_/g;
			$arecaDevice =~ s/\//_/g;
			$device = '/dev/'.$arecaDevice;
		}
		else{
			# If the device is a symlink, simply follow it
			if(-l $device){
				use Cwd qw(abs_path);
				$device = abs_path(readlink $device);
			}
			$usedCommand = $smartctl.' -a '.$device;
			@output = `$usedCommand`;
		}
		if(($? >> 8) & 2){
			print "Error: smartctl returned \"No such device\" for device $device.\n";
			exit(STATE_UNKNOWN);
		}
		$output_h{$device} = \@output;
		$command_h{$device} = $usedCommand;
	}
	return (\%output_h, \%command_h);
}

sub checkWhichDevice{
	my $dbConfig = shift;
	my $uCfgJSON = shift;
	my $ssdonly = shift;
	my %smartctlOut_h = %{(shift)};;
	# Array of found device hashes
	my @devices_a;
	my @devices_missing_a;

	# Fetch the device JSON hash
	my $devices_h = $dbConfig->{'Devices'};

	# If given, fetch the user config for the devices
	my $uCfgDevices_h;
	if(defined($uCfgJSON)){
		$uCfgDevices_h = $uCfgJSON->{'Devices'};
	}

	# Check each device whose smartvalues are present
	foreach my $device (keys %smartctlOut_h){
		my @smartctlOut = @{$smartctlOut_h{$device}};
		if(defined($ssdonly)){
			if($ssdonly & !grep { /Solid State Device/i } @smartctlOut){ next }
		}
		my @model = grep { /Model Family/i || /Device Model/i || /^Product:/i } @smartctlOut;
		my @transport = grep { /^Transport protocol:/i } @smartctlOut;
		# NVMe devices don't require a database entry, use the generic NVMe one
		if (($device =~ /nvme[a-z0-9]+$/) || ($device =~ /\/dev\/disk\/by-id\/nvme-[A-Za-z0-9_\-,\.\:]+$/)) {
			@model = ('Generic NVMe');
		}
		# SAS devices don't require a database entry, use the generic SAS one
		elsif (@transport && $transport[0] =~ /SAS/) {
			@model = ('Generic SAS');
		}
		my $found = 0;# We have not found the device yet
		foreach my $currModel (@model){
			# Check all models in the db
			foreach my $key (keys %$devices_h){
				# Search for the model in the smart DB
				foreach (@{$devices_h->{$key}->{'Device'}}){
					# If the model matches we have found the correct db entry
					if($currModel =~ /$_$/){
						$found=1;
					}
					# For NVMe we don't have a specific model, but we use the smartdb's generic NVMe entry
					if(($currModel =~ /NVMe/) && ($currModel =~ /$_/)){
						$found=1;
					}
					if(($currModel =~ /Generic SAS/) && ($currModel =~ /$_/)){
						$found =1;
					}
					if($found == 1){
						my %foundDevice_h;
						# Clone the values from the JSON smart db, ensure to not work
						# with the same reference every time
						$foundDevice_h{'Path'} = $device;
						$foundDevice_h{'Device'} = dclone $devices_h->{$key}->{'Device'};
						$foundDevice_h{'ID#'} = dclone $devices_h->{$key}->{'ID#'};
						$foundDevice_h{'Threshs'} = dclone $devices_h->{$key}->{'Threshs'};
						$foundDevice_h{'Perfs'} = dclone $devices_h->{$key}->{'Perfs'};
						# Tresholds and perf variables are defined in the user cfg
						if(defined($uCfgDevices_h->{$device})){
							if(defined($uCfgDevices_h->{$device}->{'Threshs'})){
								# A defined user config sensor overwrites the values from the json db
								my %threshs_h = %{$uCfgDevices_h->{$device}->{'Threshs'}};
								foreach my $ID (keys %threshs_h){
									$foundDevice_h{'Threshs'}->{$ID} = $threshs_h{$ID};
								}
							}
							# A defined Perfs array overwrites the json db one
							if(defined($uCfgDevices_h->{$device}->{'Perfs'})){
								$foundDevice_h{'Perfs'} = $uCfgDevices_h->{$device}->{'Perfs'};
							}
						}
						push @devices_a, \%foundDevice_h;
						last;
					}
				}
				# Finish searching for the model in the smart DB
				if($found){last;}
			}
			# Finish the current device models, one model already matched
			if($found){last;}
		}
		# The device was not found in the smartdb
		if(!$found){
			push @devices_missing_a, $device;
		}
	}
	return (\@devices_a, \@devices_missing_a);
}

sub parseSmartctlOut{
	my %smartctlOut_h = %{(shift)};
	# A hash with the parsed smart values for all devices
	my %smartValues_h;

	foreach my $device (keys %smartctlOut_h){
		my @smartctlOut = @{$smartctlOut_h{$device}};
		# Check for smart value lines
		my @smartValues;
		my @splittedLine;
		my @transport = grep { /^Transport protocol:/i } @smartctlOut;
		foreach my $line (@smartctlOut) {
			if($line =~ /\d+\s+[A-Za-z0-9_\/]+\s+0[xX][0-9a-fA-F]+\s+\d+\s+\d+\s+[0-9\-]+\s+\w+/){
				$line =~ s/^\s+|\s+$//g;
				# Split the found line, and map its elements
				# The header map defines the keys for the hash
				@splittedLine = map { s/^\s*//; s/\s*$//; $_; } split(/\s+/,$line);
				my %lineValues_h;
				for(my $i = 0; $i < @hdrmap_a; $i++){
					# Prepend the attribute name with the device name
					if($hdrmap_a[$i] eq 'ATTRIBUTE_NAME'){
						$lineValues_h{$hdrmap_a[$i]} = genAttributeName($device, $splittedLine[$i]);
					}
					else{
						$lineValues_h{$hdrmap_a[$i]} = $splittedLine[$i];
					}
				}
				push @smartValues, \%lineValues_h;
			}
			# Parse smart value lines for NVMe output
			elsif((($device =~ /nvme[a-z0-9]+$/) || ($device =~ /\/dev\/disk\/by-id\/nvme-[A-Za-z0-9_\-,\.\:]+$/)) && $line =~ /([A-Za-z\s]+)\:\s+(.+)$/){
				if(exists $hdrmap_nvme{$1}){
					my %lineValues_h;
					$lineValues_h{'ATTRIBUTE_NAME'} = $1;
					$lineValues_h{'ID#'} = $hdrmap_nvme{$1};
					$lineValues_h{'RAW_VALUE'} = $2;
					# Remove whitespace , . in units read/written and any percentage and celsius suffix
					$lineValues_h{'RAW_VALUE'} =~ s/[\s\%\,\.]|Celsius//g;
					# NVMe's Critical Warning Attribute is hex, convert it to decimal
					if($lineValues_h{'ATTRIBUTE_NAME'} eq "Critical Warning"){
						$lineValues_h{'RAW_VALUE'} = hex($2);
					}
					# Data units are presented as 1000 512bytes, convert it
					if($lineValues_h{'ATTRIBUTE_NAME'} eq "Data Units Read" ||
					$lineValues_h{'ATTRIBUTE_NAME'} eq "Data Units Written"){
						$lineValues_h{'RAW_VALUE'} =~ m/^(\d+)\[?/;
						$lineValues_h{'RAW_VALUE'} = ($1*512000)/(1000**3);
					}
					$lineValues_h{'ATTRIBUTE_NAME'} = genAttributeName($device, $lineValues_h{'ATTRIBUTE_NAME'});
					push @smartValues, \%lineValues_h;
				}
			}
			elsif(@transport && $transport[0] =~ /SAS/ && $line =~ /([A-Za-z\s]+)\:\s+(.+)$/) {
				if(exists $hdrmap_sas{$1}){
					my $idoffset = 1;
					if (ref $hdrmap_sas{$1} eq 'ARRAY') {
						my $op = $1;
						my $values = $2;
						my $n = 0;
						while ($values =~ /([\d,\.]+)/g) {
							my %lineValues_h;
							$lineValues_h{'ATTRIBUTE_NAME'} = $hdrmap_sas{$op}->[$n];
							$lineValues_h{'ID#'} = $n + $idoffset;
							$lineValues_h{'RAW_VALUE'} = $1;
							$lineValues_h{'ATTRIBUTE_NAME'} = genAttributeName($device, $lineValues_h{'ATTRIBUTE_NAME'});
							push @smartValues, \%lineValues_h;
							$n++;
						}
						$idoffset += 7;
					}
					else {
						my %lineValues_h;
						$lineValues_h{'ATTRIBUTE_NAME'} = $1;
						$lineValues_h{'ID#'} = $hdrmap_sas{$1};
						$lineValues_h{'RAW_VALUE'} = $2;
						$lineValues_h{'ATTRIBUTE_NAME'} = genAttributeName($device, $lineValues_h{'ATTRIBUTE_NAME'});
						push @smartValues, \%lineValues_h;
					}
				}
			}
			if($line =~ /(ATA Error Count)\:\s+(\d+)/){
				my %lineValues_h;
				$lineValues_h{'ATTRIBUTE_NAME'} = $1;
				$lineValues_h{'VALUE'} = $2;
				$lineValues_h{'ID#'} = 1024;
				$lineValues_h{'ATTRIBUTE_NAME'} = genAttributeName($device, $lineValues_h{'ATTRIBUTE_NAME'});
				push @smartValues, \%lineValues_h;
			}
			if($line =~ /(SMART overall-health self-assessment test result)\:\s(\w+)/){
				my %lineValues_h;
				$lineValues_h{'ATTRIBUTE_NAME'} = 'Overall_Health_Test_Result';
				$lineValues_h{'ID#'} = 1025;
				if($2 eq 'PASSED'){
					$lineValues_h{'VALUE'} = 0;
				}
				else{
					$lineValues_h{'VALUE'} = 1;
				}
				$lineValues_h{'ATTRIBUTE_NAME'} = genAttributeName($device, $lineValues_h{'ATTRIBUTE_NAME'});
				push @smartValues, \%lineValues_h;
			}
		}
		$smartValues_h{$device} = \@smartValues;
	}
	return \%smartValues_h;
}

sub checkThreshs{
	my $value = shift;
	my $pattern = shift;

	if($pattern =~ /(^[0-9]*$)/){
		if($value < 0 || $value > $1){
			return 0;
		}
	}
	if($pattern =~ /(^[0-9]*)\:$/){
		if($value < $1){
			return 0;
		}
	}
	if($pattern =~ /^\~\:([0-9]*)$/){
		if($value > $1){
			return 0;
		}
	}
	return 1;
}

sub checkSmartctl{
	my %smartValues_h = %{(shift)};
	my @devices_a = @{(shift)};
	# Resulting status level variables
	my @warnings_a;
	my @criticals_a;
	my @statusLevel_a = ("OK");

	foreach my $device (@devices_a){
		# Fetch the configured variables for a device
		my %IDs_h = %{$device->{'ID#'}};
		my %threshs_h = %{$device->{'Threshs'}};
		my @smartValues_a = @{$smartValues_h{$device->{'Path'}}};
		# Check the corresponding smart values
		foreach my $row (@smartValues_a){
			my $ID = $row->{'ID#'};
			if(exists $IDs_h{$ID}{'value'} && exists $threshs_h{$ID}){
				if(defined($threshs_h{$ID}->[2])){
					# Check if a bit pattern exits, then shift it
					$threshs_h{$ID}->[2] =~ /^([l,r])([0-9]+)$/;
					my $shift_dir = $1;
					my $shift_ct = $2;
					if($shift_dir eq 'l'){
						$row->{$IDs_h{$ID}{'value'}} = $row->{$IDs_h{$ID}{'value'}} << $2;
					}
					if($shift_dir eq 'r'){
						$row->{$IDs_h{$ID}{'value'}} = $row->{$IDs_h{$ID}{'value'}} >> $2;
					}
				}
				if(!(checkThreshs($row->{$IDs_h{$ID}{'value'}},$threshs_h{$ID}->[0]))){
					# Don't loose the critical state
					$statusLevel_a[0] = 'Warning' unless $statusLevel_a[0] eq 'Critical';
					push @warnings_a, $row->{'ATTRIBUTE_NAME'};
				}
				if(!(checkThreshs($row->{$IDs_h{$ID}{'value'}},$threshs_h{$ID}->[1]))){
					$statusLevel_a[0] = 'Critical';
					pop @warnings_a;
					push @criticals_a, $row->{'ATTRIBUTE_NAME'};
				}
			}
		}
	}

	push @statusLevel_a, \@warnings_a;
	push @statusLevel_a, \@criticals_a;
	return \@statusLevel_a;
}

sub printMissingDevicesDB{
	my @devices_a = @{(shift)};
	my $missingDevs_str;
	my $found;
	foreach my $device (@devices_a){
		$missingDevs_str .= ', ' if defined($missingDevs_str);
		$missingDevs_str .= $device;
	}
	return $missingDevs_str;
}


sub getStatusString{
	my $level = shift;
	my @statusLevel_a = @{(shift)};
	my %smartValues_h = %{(shift)};
	my @devices_a = @{(shift)};
	# Sensors to check, warn or crit
	my @sensors_a;
	my $status_str = "";

	if($level eq "Warning"){
		@sensors_a = @{$statusLevel_a[1]};
	}
	if($level eq "Critical"){
		@sensors_a = @{$statusLevel_a[2]};
	}

	if($level eq "Warning" || $level eq "Critical"){
		if(@sensors_a){
			# Print which sensors are Warn or Crit
			foreach my $sensor (@sensors_a){
				$status_str .= "[".$sensor." = ".$level;
				if($VERBOSITY){
					# Parse out the used device, use numbers for megaraid devs
					$sensor =~ /^([a-zA-Z0-9]+)/;
					my $devicePath = '/dev/'.$1;
					my %IDs_h;
					my @smartValues_a;
					foreach my $device (@devices_a){
						# Search for the device the sensor belongs to
						if($devicePath eq $device->{'Path'}){
							%IDs_h = %{$device->{'ID#'}};
							@smartValues_a = @{$smartValues_h{$device->{'Path'}}};
						}
					}
					# Append the value for the given sensor
					foreach my $row (@smartValues_a){
						if($row->{'ATTRIBUTE_NAME'} eq $sensor){
							my $ID = $row->{'ID#'};
							if(exists $IDs_h{$ID}{'value'}){
								$status_str .= " (".$row->{$IDs_h{$ID}{'value'}}.")";
							}
						}
					}
				}
				$status_str .= "]";
			}
		}
	}
	return $status_str;
}
sub getPerfString{
	my %smartValues_h = %{(shift)};
	my @devices_a = @{(shift)};
	my $allPerfs = shift;
	my $perf_str;

	foreach my $device (@devices_a){
		# Fetch the configured variables for a device
		my %IDs_h = %{$device->{'ID#'}};
		my %threshs_h = %{$device->{'Threshs'}};
		my @smartValues_a = @{$smartValues_h{$device->{'Path'}}};
		my @perfmap_a = ();
		if(!defined($allPerfs)){
			@perfmap_a = @{$device->{'Perfs'}};
		}
		else{
			@perfmap_a = keys %IDs_h;
		}

		foreach my $perf (@perfmap_a){
			# Search for the sensor and print its value
			foreach my $row (@smartValues_a){
				if($perf eq $row->{'ID#'}){
					my $ID = $row->{'ID#'};
					my $attr_str;
					if(exists $IDs_h{$ID}{'value'}){
						$attr_str = "'".$row->{'ATTRIBUTE_NAME'}."'=".$row->{$IDs_h{$ID}{'value'}};
						if($perf_str){
							$attr_str = " ".$attr_str;
						}
					}
					$perf_str .= $attr_str;
					# If available also print its thresholds
					if(exists $IDs_h{$ID}{'value'} && exists $threshs_h{$ID}){
						$threshs_h{$ID}->[0] =~ /(\d+)/;
						$perf_str .= ';'.$1.';';
						$threshs_h{$ID}->[1] =~ /(\d+)/;
						$perf_str .= $1;
					}
				}
			}
		}
	}
	return $perf_str;
}

sub getVerboseString{
	my %smartValues_h = %{(shift)};
	my @devices_a = @{(shift)};
	my %smartctlOut_h = %{(shift)};
	my %command_h = %{(shift)};
	my $verb_str;

	foreach my $device (@devices_a){
		# Fetch the configured variables for a device
		my %IDs_h = %{$device->{'ID#'}};
		my %threshs_h = %{$device->{'Threshs'}};
		my @perfmap_a = @{$device->{'Perfs'}};
		my @smartValues_a = @{$smartValues_h{$device->{'Path'}}};

		foreach my $row (@smartValues_a){
			my $ID = $row->{'ID#'};
			if(exists $IDs_h{$ID}{'value'}){
				$verb_str .= "\n".$row->{'ATTRIBUTE_NAME'}." = ".$row->{$IDs_h{$ID}{'value'}};
				$verb_str .= " (".$IDs_h{$ID}{'value'}.")";
			}
		}
		$verb_str .= "\n";
		if($VERBOSITY == 3){
			my @smartctlOut = @{$smartctlOut_h{$device->{'Path'}}};
			$verb_str .= "\n========================================";
			$verb_str .= "\nBegin of verbosity level 3 (-vvv) output for device: ".$device->{'Path'}."\n";
			$verb_str .= "\n================= \n";
			$verb_str .= "Used smartctl command:\n";
			$verb_str .= $command_h{$device->{'Path'}};
			$verb_str .= "\n================= \n";
			$verb_str .= "Thresholds:\n";
			foreach my $ID (keys %threshs_h){
				if(defined($threshs_h{$ID}->[2])){
					$verb_str .= $ID.": [".$threshs_h{$ID}->[0].",".$threshs_h{$ID}->[1].",".$threshs_h{$ID}->[2]."]\n";
				}
				else{
					$verb_str .= $ID.": [".$threshs_h{$ID}->[0].",".$threshs_h{$ID}->[1]."]\n";
				}
			}
			$verb_str .= "================= \n";
			$verb_str .= "Performance Value IDs:\n";
			$verb_str .= "[@perfmap_a]\n";
			$verb_str .= "================= \n";
			$verb_str .= "Begin of smartctl output:\n";
			foreach my $line (@smartctlOut){
				$verb_str .= $line;
			}
			$verb_str .= "\nEnd of verbosity level 3 (-vvv) output for device: ".$device->{'Path'}."\n";
			$verb_str .= "======================================== \n";
		}
	}

	return $verb_str;
}

MAIN: {
	my ($smartctl, $dbJSON, $uCfgJSON, $exitCode, $noSudo, $allPerfs, $extraOpts, $criticalOnUnknown, $ssdonly, $findregex);
	my @devicesToCheck_a;
	if ( !(GetOptions(
		'v|verbose' => sub { $VERBOSITY = 1 },
		'vv' => sub { $VERBOSITY = 2 },
		'vvv' => sub { $VERBOSITY = 3 },
		'h|help' => sub {
			print getVersionString();
			print getUsageString();
			print getHelpString();
			exit(STATE_OK);
		;},
		'p|path=s' => \$smartctl,
		'dbj|dbjson=s' => \$dbJSON,
		'd|device=s' => \@devicesToCheck_a,
		'ucfgj|ucfgjson=s' => \$uCfgJSON,
		'cu|critical_on_unknown' => \$criticalOnUnknown,
		'nosudo' => \$noSudo,
		'ap|allperfs' => \$allPerfs,
		'O|options=s' => \$extraOpts,
		's|ssdonly' => \$ssdonly,
		'r|regex=s' => \$findregex,
		'V|version' => sub{
			print getVersionString()."\n";
			exit(STATE_OK);
		},
	)))	{
		print getUsageString();
		exit(STATE_UNKNOWN);
	}
	# Check smartclt tool
	if(!defined($smartctl)){
		$smartctl = "/usr/sbin/smartctl";
	}
	if(! -x $smartctl){
		print "Error: cannot find smartctl executable.\n";
		exit(STATE_UNKNOWN);
	}
	# Check for sudo unless disabled
	if(!defined($noSudo)){
		my $sudo;
		chomp($sudo = `which sudo`);
		if(! -x $sudo){
			print "Error: cannot find sudo executable.\n";
			exit(STATE_UNKNOWN);
		}
		$smartctl = $sudo.' '.$smartctl;
	}
	# The smartdb must be present
	if(!defined($dbJSON)){
		print "Error: smartctl requires a valid smartdb JSON file.\n";
		print getUsageString();
		exit(STATE_UNKNOWN);
	}
	# Devices to check are required
	if(!(@devicesToCheck_a)){
		# If no devices are given check if a regex should be used
		if($findregex){
			@devicesToCheck_a = findDevicesByRegex($findregex);
			if(!(@devicesToCheck_a)){
				print "Error: the find command with the given regex did not return any device(s).\n";
			}
		}
		if(!(@devicesToCheck_a)){
			print "Error: check_smart_values requires a device to check.\n";
			print getUsageString();
			exit(STATE_UNKNOWN);
		}
  }
	# Check if a user config is given
	my $uCfg;
	if(defined($uCfgJSON)){
		$uCfg = readUCfgJSON($uCfgJSON);
	}
	my ( $output_h, $command_h ) = getSmartctl($smartctl,\@devicesToCheck_a,$extraOpts);
	my $dbConfig = readDbJSON($dbJSON);
	my ( $devices_a, $devices_missing_a ) = checkWhichDevice($dbConfig,$uCfg,$ssdonly,$output_h);
	if(@$devices_missing_a){
		print "Error: the following device(s) were not found in the smartdb JSON file: ";
		print printMissingDevicesDB($devices_missing_a)."\n";
		print "Smartctl was called with the following commands:\n";
		foreach my $device (keys %$command_h){
			print $device.': '.$command_h->{$device}."\n";
		}
		if(!defined($criticalOnUnknown)){
			exit(STATE_UNKNOWN);
		}
		else {
			exit(STATE_CRITICAL);
		}
	}

	my $smartValues_h = parseSmartctlOut($output_h);
	my $statusLevel_a = checkSmartctl($smartValues_h,$devices_a);

	$exitCode = STATE_OK;
	if($statusLevel_a->[0] eq "Critical"){
		$exitCode = STATE_CRITICAL;
	}
	if($statusLevel_a->[0] eq "Warning"){
		$exitCode = STATE_WARNING;
	}

	print $statusLevel_a->[0];
	print' (';
	my $dev_str;
	foreach my $devChecked (@$devices_a){
		$dev_str .= ', ' if defined($dev_str);
		$devChecked->{'Path'} =~ m/^\/dev\/(?:disk\/by-(?:id|label)\/)?([A-Za-z0-9_\-,\.\:]+)$/;
		$dev_str .= $1;
	}
	$dev_str .= ') ';
	# If the status string exceeds 36 chars, just use the number of checked devices
	if(length($dev_str) > 36){
		$dev_str = scalar(@$devices_a).' devices) ';
	}
	print $dev_str;
	print getStatusString("Critical",$statusLevel_a,$smartValues_h,$devices_a);
	print getStatusString("Warning",$statusLevel_a,$smartValues_h,$devices_a);
	my $perf_str = getPerfString($smartValues_h,$devices_a,$allPerfs);
	if($perf_str){
		print "|".$perf_str;
	}
	if($VERBOSITY == 2 || $VERBOSITY == 3){
		print getVerboseString($smartValues_h,$devices_a,$output_h,$command_h);
	}
	else{
		print "\n";
	}
	exit ($exitCode);
}
