<html><head><meta name="color-scheme" content="light dark"></head><body><pre style="word-wrap: break-word; white-space: pre-wrap;">=head1 smart-status-lib.pl

Functions for getting SMART status

=cut

BEGIN { push(@INC, ".."); };
use WebminCore;
&amp;init_config();
=head2 get_smart_version()

Returns the version number of the SMART tools on this system

=cut
sub get_smart_version
{
if (!defined($smartctl_version_cache)) {
	local $out = &amp;backquote_command(
			"$config{'smartctl'} --version 2&gt;&amp;1 &lt;/dev/null");
	if ($out =~ /smartmontools release\s+(\S+)/i) {
		$smartctl_version_cache = $1;
		}
	}
return $smartctl_version_cache;
}

=head2 list_smart_disks_partitions

Returns a sorted list of disks that can support SMART.

=cut
sub list_smart_disks_partitions
{
if (&amp;foreign_check("fdisk")) {
	return &amp;list_smart_disks_partitions_fdisk();
	}
elsif (&amp;foreign_check("bsdfdisk")) {
	return &amp;list_smart_disks_partitions_bsdfdisk();
	}
elsif (&amp;foreign_check("mount")) {
	return &amp;list_smart_disks_partitions_fstab();
	}
return ( );
}

=head2 list_smart_disks_partitions_fdisk

Returns a sorted list of disks that can support SMART, using the Linux fdisk
module. May include faked-up 3ware devices.

=cut
sub list_smart_disks_partitions_fdisk
{
&amp;foreign_require("fdisk");
local @rv;
my $twcount = 0;
foreach my $d (sort { $a-&gt;{'device'} cmp $b-&gt;{'device'} }
		    &amp;fdisk::list_disks_partitions()) {
	if (($d-&gt;{'type'} eq 'scsi' || $d-&gt;{'type'} eq 'raid') &amp;&amp;
	    $d-&gt;{'model'} =~ /3ware|amcc|9750/i) {
		# A 3ware hardware RAID device.

		# First find the controllers.
		local @ctrls = &amp;list_3ware_controllers();

		# For each controller, find all the units (u0, u1, etc..)
		local @units;
		foreach my $c (@ctrls) {
			push(@units, &amp;list_3ware_subdisks($c));
			}

		# Assume that /dev/sdX maps to units in order
		my $i = 0;
		foreach my $sd (@{$units[$twcount]-&gt;[2]}) {
			my $c = $units[$twcount]-&gt;[1];
			my $cidx = &amp;indexof($c, @ctrls);
			my $dev = "/dev/twa".$cidx;
			if (!-r $dev) {
				$dev = "/dev/twe".$cidx;
				}
			if (!-r $dev) {
				$dev = "/dev/twl".$cidx;
				}
			push(@rv, { 'device' =&gt; $dev,
				    'prefix' =&gt; $dev,
				    'desc' =&gt; '3ware physical disk unit '.
				      $units[$twcount]-&gt;[0].' number '.$sd,
				    'type' =&gt; 'scsi',
				    'subtype' =&gt; '3ware',
				    'subdisk' =&gt; substr($sd, 1),
				    'id' =&gt; $d-&gt;{'id'},
				  });
			$i++;
			}
		$twcount++;
		}
	elsif (($d-&gt;{'type'} eq 'scsi' || $d-&gt;{'type'} eq 'raid') &amp;&amp;
	       $d-&gt;{'model'} =~ /LSI/i &amp;&amp; $d-&gt;{'model'} !~ /9750/) {
		# A LSI megaraid device.
		local @units = &amp;list_megaraid_subdisks(0);

		foreach my $i (@units) {
			push(@rv, { 'device' =&gt; $d-&gt;{'device'},
				    'prefix' =&gt; $d-&gt;{'device'},
				    'desc' =&gt; 'LSI Array '.$i-&gt;[1].' physical disk ID '.$i-&gt;[0],
				    'type' =&gt; 'scsi',
				    'subtype' =&gt; 'sat+megaraid',
				    'subdisk' =&gt; $i-&gt;[0],
				    'id' =&gt; $d-&gt;{'id'},
				  });
			}
		}
	elsif ($d-&gt;{'device'} =~ /^\/dev\/cciss\/(.*)$/) {
		# HP Smart Array .. add underlying disks
		my $count = &amp;count_subdisks($d, "cciss");
		for(my $i=0; $i&lt;$count; $i++) {
			push(@rv, { 'device' =&gt; $d-&gt;{'device'},
				    'prefix' =&gt; $d-&gt;{'device'},
				    'desc' =&gt; 'HP Smart Array physical disk '.$i,
				    'type' =&gt; 'scsi',
				    'subtype' =&gt; 'cciss',
				    'subdisk' =&gt; $i,
				    'id' =&gt; $d-&gt;{'id'},
				  });
			}
		}
	elsif ($d-&gt;{'type'} eq 'scsi' || $d-&gt;{'type'} eq 'ide') {
		# Some other disk
		push(@rv, $d);
		}
	}
return sort { $a-&gt;{'device'} cmp $b-&gt;{'device'} ||
	      $a-&gt;{'subdisk'} &lt;=&gt; $b-&gt;{'subdisk'} } @rv;
}

=head2 list_megaraid_subdisks(adapter)

Returns a list, each element of which is a unit, controller and list of subdisks

=cut
sub list_megaraid_subdisks
{
local ($adap) = @_;
local $out = &amp;backquote_command("megacli -pdlist -a$adap");
return () if ($?);
my @rv;
foreach my $l (split(/\r?\n/, $out)) {
	if ($l =~ /^Device\sId:\s(\d+)$/) {
		push(@rv, [ $1, $adap, [ ] ]);
		}
	}
return @rv;
}

=head2 list_3ware_subdisks(controller)

Returns a list, each element of which is a unit, controller and list of subdisks

=cut
sub list_3ware_subdisks
{
local ($ctrl) = @_;
local $out = &amp;backquote_command("tw_cli info $ctrl 2&gt;/dev/null");
return () if ($?);
my @rv;
foreach my $l (split(/\r?\n/, $out)) {
	if ($l =~ /^(u\d+)\s/) {
		push(@rv, [ $1, $ctrl, [ ] ]);
		}
	elsif ($l =~ /^(p\d+)\s+(\S+)\s+(\S+)/ &amp;&amp;
	       $2 ne 'NOT-PRESENT') {
		my ($u) = grep { $_-&gt;[0] eq $3 } @rv;
		if ($u) {
			push(@{$u-&gt;[2]}, $1);
			}
		}
	}
return @rv;
}

=head2 list_3ware_controllers()

Returns a list of 3ware controllers, each of which is just a string like c0

=cut
sub list_3ware_controllers
{
local $out = &amp;backquote_command("tw_cli show 2&gt;/dev/null");
return () if ($?);
my @rv;
foreach my $l (split(/\r?\n/, $out)) {
	if ($l =~ /^(c\d+)\s/) {
		push(@rv, $1);
		}
	}
return @rv;
}

=head2 count_subdisks(&amp;drive, type, [device])

Returns the number of sub-disks for a hardware RAID device, by calling
smartctl on them until failure.

=cut
sub count_subdisks
{
local ($d, $type, $device) = @_;
$device ||= $d-&gt;{'device'};
local $count = 0;
while(1) {
	local $cmd = "$config{'smartctl'} -d $type,$count ".quotemeta($device);
	&amp;execute_command($cmd);
	last if ($?);
	$count++;
	}
return $count;
}

=head2 list_smart_disks_partitions_fstab

Returns a list of disks on which we can use SMART, taken from /etc/fstab.

=cut
sub list_smart_disks_partitions_fstab
{
&amp;foreign_require("mount");
my @rv;
foreach my $m (&amp;mount::list_mounted(1)) {
	if ($m-&gt;[1] =~ /^(\/dev\/(da|ad|ada)([0-9]+))/ &amp;&amp;
	    $m-&gt;[2] ne 'cd9660') {
		# FreeBSD-style disk name
		push(@rv, { 'device' =&gt; $1,
			    'desc' =&gt; ($2 eq 'ad' ? 'IDE' : 'SCSI').
				      ' disk '.$3 });
		}
	elsif ($m-&gt;[1] =~ /^(\/dev\/disk\d+)/ &amp;&amp;
	       ($m-&gt;[2] eq 'ufs' || $m-&gt;[2] eq 'hfs')) {
		# MacOS disk name
		push(@rv, { 'device' =&gt; $1,
			    'desc' =&gt; $1 });
		}
	elsif ($m-&gt;[1] =~ /^(\/dev\/([hs])d([a-z]))/ &amp;&amp;
	       $m-&gt;[2] ne 'iso9660') {
		# Linux disk name
		push(@rv, { 'device' =&gt; $1,
			    'desc' =&gt; ($2 eq 'h' ? 'IDE' : 'SCSI').
				      ' disk '.uc($3) });
		}
	}
my %done;
@rv = grep { !$done{$_-&gt;{'device'}}++ } @rv;
return @rv;
}

=head2 list_smart_disks_partitions_bsdfdisk

Returns a sorted list of disks that can support SMART, using the FreeBSD
fdisk module

=cut
sub list_smart_disks_partitions_bsdfdisk
{
&amp;foreign_require("bsdfdisk");
local @rv;
foreach my $d (sort { $a-&gt;{'device'} cmp $b-&gt;{'device'} }
		    &amp;bsdfdisk::list_disks_partitions()) {
	if ($d-&gt;{'type'} eq 'scsi' || $d-&gt;{'type'} eq 'ide') {
		push(@rv, $d);
		}
	}
return sort { $a-&gt;{'device'} cmp $b-&gt;{'device'} } @rv;
}

=head2 get_drive_status(device-name, [&amp;drive])

Returns a hash reference containing the status of some drive

=cut
sub get_drive_status
{
local ($device, $drive) = @_;
if ($device =~ /^(\/dev\/nvme\d+)n\d+$/) {
	# For NVME drives, try the underlying device first
	local $nd = $1;
	local $st = &amp;get_drive_status($nd, $drive);
	return $st if ($st-&gt;{'support'} &amp;&amp; $st-&gt;{'enabled'});
	}
local %rv;
local $qd = quotemeta($device);
local $extra_args = &amp;get_extra_args($device, $drive);
if (&amp;get_smart_version() &gt; 5.0) {
	# Use new command format

	# Check support
	local $out = &amp;backquote_command(
			"$config{'smartctl'} $extra_args  -i $qd 2&gt;&amp;1");
	if ($out =~ /SMART\s+support\s+is:\s+Available/i) {
		$rv{'support'} = 1;
		}
	elsif ($out =~ /Device\s+supports\s+SMART/i) {
		$rv{'support'} = 1;
		}
	else {
		$rv{'support'} = 0;
		}
	if ($out =~ /SMART\s+support\s+is:\s+Enabled/i) {
		$rv{'enabled'} = 1;
		}
	elsif ($out =~ /Device.*is\+Enabled/i) {
		$rv{'enabled'} = 1;
		}
	elsif ($out =~ /Device\s+supports\s+SMART\s+and\s+is\s+Enabled/i) {
		# Added to match output from RHEL5
		$rv{'enabled'} = 1;
		}
	else {
		# Not enabled!
		$rv{'enabled'} = 0;
		}
	if ($device =~ /^\/dev\/nvme/ &amp;&amp;
	    $out =~ /(Model\s+Number|Device\s+Model):/i) {
		# For NVME devices, surprisingly smart support/enabled info is
		# not shown. So assume they work
		$rv{'support'} = 1;
		$rv{'enabled'} = 1;
		}
	if (!$rv{'support'} || !$rv{'enabled'}) {
		# No point checking further!
		return \%rv;
		}

	# Check status
	$out = &amp;backquote_command(
		"$config{'smartctl'} $extra_args -H $qd 2&gt;&amp;1");
	if ($out =~ /test result: FAILED/i) {
		$rv{'check'} = 0;
		}
	else {
		$rv{'check'} = 1;
		}
	}
else {
	# Use old command format

	# Check status
	local $out = &amp;backquote_command(
			"$config{'smartctl'} $extra_args -c $qd 2&gt;&amp;1");
	if ($out =~ /supports S.M.A.R.T./i) {
		$rv{'support'} = 1;
		}
	else {
		$rv{'support'} = 0;
		}
	if ($out =~ /is enabled/i) {
		$rv{'enabled'} = 1;
		}
	else {
		# Not enabled!
		$rv{'enabled'} = 0;
		}
	if (!$rv{'support'} || !$rv{'enabled'}) {
		# No point checking further!
		return \%rv;
		}
	if ($out =~ /Check S.M.A.R.T. Passed/i) {
		$rv{'check'} = 1;
		}
	else {
		$rv{'check'} = 0;
		}
	}

if ($config{'attribs'}) {
	# Fetch other attributes
	local ($lastline, @attribs);
	local $doneknown = 0;
	$rv{'raw'} = "";
	open(OUT, "$config{'smartctl'} $extra_args -a $qd |");
	while(&lt;OUT&gt;) {
		s/\r|\n//g;
		if (/Model\s+Family:\s+(.*)/i) {
			$rv{'family'} = $1;
			}
		elsif (/Device\s+Model:\s+(.*)/i) {
			$rv{'model'} = $1;
			}
		elsif (/Serial\s+Number:\s+(.*)/i) {
			$rv{'serial'} = $1;
			}
		elsif (/User\s+Capacity:\s+(.*)/i) {
			$rv{'capacity'} = $1;
			}
		if (/^\((\s*\d+)\)(.*)\s(0x\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)/) {
			# An old-style vendor attribute
			$doneknown = 1;
			push(@attribs, [ $2, $7 ]);
			}
		elsif (/^\s*(\d+)\s+(\S+)\s+(0x\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)/) {
			# A new-style vendor attribute
			$doneknown = 1;
			push(@attribs, [ $2, $10, undef, $4 ]);
			$attribs[$#attribs]-&gt;[0] =~ s/_/ /g;
			}
		elsif (/^(\S.*\S):\s+\(\s*(\S+)\)\s*(.*)/ &amp;&amp; !$doneknown) {
			# A known attribute
			local $attrib = [ $1, $2, $3 ];
			if ($lastline =~ /^\S/ &amp;&amp; $lastline !~ /:/) {
				$attrib-&gt;[0] = $lastline." ".$attrib-&gt;[0];
				}
			push(@attribs, $attrib);
			}
		elsif (/^\s+(\S.*)/ &amp;&amp; @attribs &amp;&amp; !$doneknown) {
			# Continuation of a known attribute description
			local $cont = $1;
			local $ls = $attribs[$#attribs];
			if ($ls-&gt;[2] =~ /\.\s*$/) {
				$ls-&gt;[2] .= "&lt;br&gt;".$cont;
				}
			else {
				$ls-&gt;[2] .= " ".$cont;
				}
			}
		elsif (/ATA\s+Error\s+Count:\s+(\d+)/i) {
			# An error line!
			$rv{'errors'} = $1;
			}
		$lastline = $_;
		$rv{'raw'} .= $_."\n";
		}
	close(OUT);
	$rv{'attribs'} = \@attribs;
	}
return \%rv;
}

# short_test(device, [&amp;drive])
# Starts a short drive test, and returns 1 for success or 0 for failure, plus
# any output.
sub short_test
{
local ($device, $drive) = @_;
local $qm = quotemeta($device);
local $extra_args = &amp;get_extra_args($device, $drive);
if (&amp;get_smart_version() &gt; 5.0) {
	local $out = &amp;backquote_logged("$config{'smartctl'} $extra_args -t short $qm 2&gt;&amp;1");
	if ($? || $out !~ /testing has begun/i) {
		return (0, $out);
		}
	else {
		return (1, $out);
		}
	}
else {
	local $out = &amp;backquote_logged("$config{'smartctl'} $extra_args -S $qm 2&gt;&amp;1");
	if ($? || $out !~ /test has begun/i) {
		return (0, $out);
		}
	else {
		return (1, $out);
		}
	}
}

# ext_test(device, [&amp;drive])
# Starts an extended drive test, and returns 1 for success or 0 for failure,
# plus any output.
sub ext_test
{
local ($device, $drive) = @_;
local $qm = quotemeta($device);
local $extra_args = &amp;get_extra_args($device, $drive);
if (&amp;get_smart_version() &gt; 5.0) {
	local $out = &amp;backquote_logged("$config{'smartctl'} $extra_args -t long $qm 2&gt;&amp;1");
	if ($? || $out !~ /testing has begun/i) {
		return (0, $out);
		}
	else {
		return (1, $out);
		}
	}
else {
	local $out = &amp;backquote_logged("$config{'smartctl'} $extra_args -X $qm 2&gt;&amp;1");
	if ($? || $out !~ /test has begun/i) {
		return (0, $out);
		}
	else {
		return (1, $out);
		}
	}
}

# data_test(device, [&amp;drive])
# Starts offline data collection, and returns 1 for success or 0 for failure,
# plus any output.
sub data_test
{
local ($device, $drive) = @_;
local $qm = quotemeta($device);
local $extra_args = &amp;get_extra_args($device, $drive);
if (&amp;get_smart_version() &gt; 5.0) {
	local $out = &amp;backquote_logged("$config{'smartctl'} $extra_args -t offline $qm 2&gt;&amp;1");
	if ($? || $out !~ /testing has begun/i) {
		return (0, $out);
		}
	else {
		return (1, $out);
		}
	}
else {
	local $out = &amp;backquote_logged("$config{'smartctl'} $extra_args -O $qm 2&gt;&amp;1");
	if ($? || $out !~ /test has begun/i) {
		return (0, $out);
		}
	else {
		return (1, $out);
		}
	}
}

=head2 get_extra_args(device, [&amp;drive])

Returns extra command-line args to smartctl, needed for some drive type.

=cut
sub get_extra_args
{
local ($device, $drive) = @_;
if (!$drive) {
	($drive) = grep { $_-&gt;{'device'} eq $device }
			&amp;list_smart_disks_partitions();
	}
local $extra_args = $config{'extra'};
if ($drive &amp;&amp; defined($drive-&gt;{'subdisk'})) {
	$extra_args .= " -d $drive-&gt;{'subtype'},$drive-&gt;{'subdisk'}";
	}
elsif ($config{'ata'}) {
	$extra_args .= " -d ata";
	}
return $extra_args;
}

1;

</pre></body></html>