freenas_disk_info.pl

11th February 2016 at 2:52pm
CodeSnippets FreeBSD FreeNAS Perl

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);
}