I wrote this script to output a CSV dump of all the useful information about my disks on my FreeNAS server. I see no reason why this wouldn't work just as well on any FreeBSD based host though.
It gathers information from the smartctl
, gpart
and camcontrol
commands, all of which require superuser permissions to access the necessary underlaying block devices. Consequently, you will need to run this script as root or via sudo
.
I first posted a link to this script in the Linus Tech Tips Storage Rankinks forum post.
#!/usr/local/bin/perl -wT
use strict;
use warnings;
use English qw(-no_match_vars);
use Data::Dumper qw(Dumper);
%ENV = ();
use constant SMARTCTL_BIN => '/usr/local/sbin/smartctl';
use constant GPART_BIN => '/sbin/gpart';
use constant CAMCONTROL_BIN => '/sbin/camcontrol';
use constant SMARTCTL_UNKNOWN => 0;
use constant SMARTCTL_INFO => 1;
use constant GPART_UNKNOWN => 0;
use constant GPART_HEADER => 1;
use constant GPART_PROVIDERS => 2;
use constant GPART_CONSUMERS => 3;
die "Please run with superuser priviledges.\n"
unless $EFFECTIVE_USER_ID == 0;
my %dev;
my $adaptor;
my $host_bus;
open(my $fh, '-|', +CAMCONTROL_BIN, 'devlist', '-v')
|| die "Unable to open file handle for '@{[+CAMCONTROL_BIN]} ".
"devlist -v' command; $!";
while (local $_ = <$fh>) {
if (m/^(?:[a-z0-9]+) on ([a-z0-9]+) bus (\d+):\s*$/i) {
$adaptor = $1;
$host_bus = $2;
}
elsif (my ($device_model, $bus, $target, $lun, $device)
= $_ =~ m/^
<(.+?)>\s+
at\s+(\S+)\s+
target\s+(\d+)\s+
lun\s+(\d+)\s+
\((.+?)\)\s*
$/xi) {
if (my ($disk_dev) = $device =~ m/\b((?:ada|da|ad|aacd|mlxd|
mlyd|amrd|idad|twed)[0-9]+)\b/ix) {
my ($pass_dev) = $device =~ m/(pass\d+)/;
$dev{$disk_dev} //= {
dev_path => "/dev/$disk_dev",
pass_dev => "/dev/$pass_dev",
adaptor => $adaptor,
host_bus => $host_bus,
device_model => $device_model,
scsi_bus => $bus,
scsi_target => $target,
scsi_lun => $lun,
};
}
}
}
close($fh) || warn "Unable to close file handle for '@{[+CAMCONTROL_BIN]}".
" devlist -v' command; $!";
open($fh, '-|', +SMARTCTL_BIN, '--scan')
|| die "Unable to open file handle for '@{[+SMARTCTL_BIN]} --scan' ".
"command; $!";
while (local $_ = <$fh>) {
if (m,^(/dev/(\S+))(?:\s+-d\s+(\S+)\s+)?,) {
$dev{$2} //= {};
$dev{$2}->{dev_path} //= $1;
$dev{$2}->{interface} //= $3;
}
}
close($fh) || warn "Unable to close file handle for '@{[+SMARTCTL_BIN]} ".
"--scan' command; $!";
open($fh, '-|', +GPART_BIN, 'list')
|| die "Unable to open file handle for '@{[+GPART_BIN]} ".
"list' command; $!";
my %geom;
my $gpart_section = +GPART_UNKNOWN;
while (local $_ = <$fh>) {
if (/^\s*$/ && defined($geom{geom_name})) {
$dev{$geom{geom_name}}->{gpart} = { %geom };
$gpart_section = +GPART_UNKNOWN;
%geom = ();
next;
}
elsif (/^Geom name:\s*(\S+)\s*/) {
$gpart_section = +GPART_HEADER;
%geom = ( geom_name => $1 );
next;
}
elsif (/^Providers:\s*/) {
$gpart_section = +GPART_PROVIDERS;
$geom{providers} = [];
}
elsif (/^Consumers:\s*/) {
$gpart_section = +GPART_CONSUMERS;
$geom{consumers} = [];
}
if ($gpart_section == +GPART_HEADER &&
/^([a-z][a-z ]+):\s*(.+)\s*/i) {
$geom{key_prep($1)} = $2;
}
elsif ($gpart_section == +GPART_PROVIDERS &&
/^([0-9]{1,3}\.\s*)?\s+([a-z]+):\s+(.+)\s*$/i) {
push @{$geom{providers}}, {} if defined($1);
my $i = $#{$geom{providers}};
$geom{providers}->[$i]->{key_prep($2)} = $3;
}
elsif ($gpart_section == +GPART_CONSUMERS &&
/^([0-9]{1,3}\.\s*)?\s+([a-z]+):\s+(.+)\s*$/i) {
push @{$geom{consumers}}, {} if defined($1);
my $i = $#{$geom{consumers}};
$geom{consumers}->[$i]->{key_prep($2)} = $3;
}
}
close($fh) || warn "Unable to close file handle for '@{[+GPART_BIN]} ".
"list' command; $!";
while (my ($name, $dev) = each %dev) {
if (exists $dev->{gpart}->{providers}) {
for my $provider (@{$dev->{gpart}->{providers}}) {
if (lc($provider->{type}) eq 'freebsd-zfs') {
$dev->{zfs_gptid}->{$provider->{rawuuid}}
= $provider->{name};
}
}
}
open($fh, '-|', +SMARTCTL_BIN, '-i', $dev->{dev_path})
|| die "Unable to open file handle for '@{[+SMARTCTL_BIN]} ".
"-i $dev->{dev_path}' command; $!";
my $smartctl_section = +SMARTCTL_UNKNOWN;
while (local $_ = <$fh>) {
if (/=== START OF INFORMATION SECTION ===/) {
$smartctl_section = +SMARTCTL_INFO;
next;
}
elsif ($smartctl_section == +SMARTCTL_INFO &&
m,^([^:]+):\s*(.+)\s*$,) {
my ($key, $value) = (key_prep($1), $2);
$dev{$name}->{$key} = $value;
if ($key eq 'user_capacity' && $value =~
m/\[([0-9]+(?:\.[0-9]+)? [MGTP]B)\]/) {
$dev{$name}->{capacity} = $1;
}
}
}
close($fh) || warn "Unable to close file handle for ".
"'@{[+SMARTCTL_BIN]} -i $dev->{dev_path}' command; $!";
}
my @cols = qw(dev_path pass_dev host_bus adaptor
scsi_bus scsi_target scsi_lun lu_wwn_device_id
serial_number interface capacity
model_family device_model form_factor firmware_version
rotation_rate sata_version ata_version smart_support
sector_sizes);
printf(qq/"%s"\n/, join('","',@cols,'zfs_gptid'));
while (my ($name, $dev) = each %dev) {
printf("\"%s\"\n", join('","',
map { defined($_) ? $_ : '' } @{$dev}{@cols},
join(' ',keys(%{$dev->{zfs_gptid}}))
));
}
warn Dumper(\%dev) if grep(/^--?d(?:ebug)?$/i, @ARGV);
exit;
sub key_prep {
my $key = shift;
$key =~ s/\s*is//;
$key =~ s/\s+/_/g;
return lc($key);
}