File: //usr/local/bin/zabbix/discover_lsi_raid.pl
#!/usr/bin/perl
# Low level discovery of LSI RAID devices.
#
# There are multiple CLI tools that each work with some LSI RAID models and
# chipsets. The LLD macro for the adapter is used to also pass the info about
# which CLI command to use with the item prototypes. Not all macros are used or
# returned for all types of hardware, depending on the limitations of the CLI
# tool used.
#
# adapter, megacli = {#ADP_MEGA}
# adapter, mpt-status = {#ADP_MPT}
# adapter, sas2ircu = {#ADP_SAS2}
# bbu = {#BBU}
# enclosure = {#ENCLOSURE}
# virtual drive = {#VIRTDRIVE}
# physical drive = {#SLOT}
#
# LSI RAID devices are all either MegaRAID with PCI connections or devices
# using SCSI. MegaRAID devices use megacli (the 'mega' name is a hint).
# Non-MegaRAID devices use one of two other CLIs depending on the chipset
# series: mpt-status for chipsets under 2000, and sas2ircu for all the more
# recent ones.
#
# References used:
# https://www.denniskanbier.nl/blog/monitoring/monitoring-disk-io-using-zabbix/
# https://zabbix.org/wiki/Docs/howto/Nested_LLD
use strict;
use warnings;
use Getopt::Long;
my $debug = 0;
sub exit_empty {
print '{ "data": [] }';
exit;
}
# Default action is to do any of the CLIs that are applicable to the hardware.
my $return_only = "ANY";
GetOptions(
"cli=s" => \$return_only,
);
unless (($return_only eq "ANY")
|| ($return_only eq 'megacli')
|| ($return_only eq 'mpt-status')
|| ($return_only eq 'sas2ircu')) {
warn "Unexpected CLI name passed in, bailing.";
exit_empty();
}
# Look for LSI devices. Expected output examples:
# 02:00.0 RAID bus controller: Broadcom / LSI MegaRAID SAS 2108 [Liberator] (rev 05)
# 02:00.0 SCSI storage controller: Broadcom / LSI SAS1068E PCI-Express Fusion-MPT SAS (rev 08)
# 02:00.0 SCSI storage controller: LSI Logic / Symbios Logic SAS1068E PCI-Express Fusion-MPT SAS (rev 08)
# 03:00.0 RAID bus controller: LSI Logic / Symbios Logic MegaRAID SAS 2108 [Liberator] (rev 05)
my $found_devices= `/usr/bin/lspci | /bin/grep "LSI " | /bin/grep -v PATA`;
# Bail if nothing found
exit_empty() unless $found_devices;
# Start making the json struture to return
my $json = qq({ "data": [\n);
# Determine which CLI(s) can be used to get component info, based on the
# device(s) found. If there happen to be two devices that both use the same
# CLI, we're going to only use the CLI once and let it report the duplicates
# since that's easier.
my $cli; # track which CLIs to use in here
for my $device (split /\n/, $found_devices) {
if ($device =~ /megaraid/i) {
$cli->{"megacli"} = 1;
} else {
# Pull out the chipset integer from the device info.
# Strip off everything before the token with the chipset info
$device =~ s/^.*Logic //;
# Remove any SAS prefix
$device =~ s/^SAS ?//;
# Capture the chipset number and discard the rest.
$device =~ s/^(\d+)\D+.*$/$1/;
if ($device >= 2000) {
$cli->{"sas2ircu"} = 1;
} else {
$cli->{"mpt-status"} = 1;
}
}
}
# Use the appropriate CLI tool(s) to build the json rows for the discovered
# components.
if ($cli->{"megacli"} &&
($return_only eq "megacli" || $return_only eq "ANY")) {
print "Using CLI megacli\n" if $debug;
megacli();
}
if ($cli->{"mpt-status"} &&
($return_only eq "mpt-status" || $return_only eq "ANY")) {
print "Using CLI mpt-status\n" if $debug;
mpt_status();
}
if ($cli->{"sas2ircu"} &&
($return_only eq "sas2ircu" || $return_only eq "ANY")) {
print "Using CLI sas2ircu\n" if $debug;
sas2ircu();
}
# trim the last ',' off, json doesn't like trailing commas
$json =~ s/,\n$/\n/;
# close up shop
$json .= qq(]}\n);
print $json;
exit;
sub megacli {
# Find the adapter(s)
my $adapter_count = `/usr/sbin/megacli -AdpCount -NoLog| /bin/grep "Controller Count" | /usr/bin/awk '{print \$NF}'`;
chomp $adapter_count;
$adapter_count =~ s/\.//; # strip off trailing '.' from number
print "There's $adapter_count adapters here.\n" if $debug;
for (my $adapter_num = 0; $adapter_num < $adapter_count; $adapter_num++) {
print "Adapter $adapter_num up now.\n" if $debug;
$json .= qq( { "{#ADP_MEGA}":"$adapter_num" },\n);
my $adapter_info_cmd = "/usr/sbin/megacli -AdpAllInfo -a$adapter_num -NoLog";
# This Adapter may have a BBU
my $bbu_are_you_there = `$adapter_info_cmd | /bin/grep -A10 "HW Configuration" | /bin/grep "^BBU " | /usr/bin/awk '{print \$NF}'`;
chomp $bbu_are_you_there;
if ($bbu_are_you_there eq 'Present') {
print "\tBBU is present.\n" if $debug;
# We're not going to add BBU as a discovered item just yet. There's
# more BBU logic down where physical drives are found.
}
# This Adapter may have Virtual Drives, numbered 0..N
my $virtual_drive_count = `$adapter_info_cmd | /bin/grep -A10 "Device Present" | /bin/grep "Virtual Drives" | /usr/bin/awk '{print \$NF}'`;
chomp $virtual_drive_count;
print "\tVirtual drives on this adapter: $virtual_drive_count\n" if $debug;
for (my $virt_drive_num = 0; $virt_drive_num < $virtual_drive_count; $virt_drive_num++) {
$json .= qq( { "{#ADP_MEGA}":"$adapter_num", "{#VIRTDRIVE}":"$virt_drive_num"},\n);
}
# Each Adapter can have one or more Enclosures
my $enclosure_info_cmd = "/usr/sbin/megacli -EncInfo -a$adapter_num -NoLog";
my $enclosure_info = `$enclosure_info_cmd`;
my @lines = split /\n/, $enclosure_info;
# Find the number of enclosures
while ($lines[0] !~ m/Number of enclosures on adapter $adapter_num/) {
shift @lines;
if (scalar @lines == 0) {
# Fake a result of zero
$lines[0] = "Number of enclosures on adapter 0 -- 0";
last;
}
}
(my $enclosure_count = $lines[0]) =~ s/.*-- //;
print "\tFound $enclosure_count enclosures " if $debug;
# Find the IDs of each enclosure
my @enclosure_ids;
for my $line (@lines) {
next unless $line =~ /Device ID/;
$line =~ s/.*: //;
push @enclosure_ids, $line;
}
if ($debug) {
for my $id (@enclosure_ids) {
print " >$id< ";
}
print "\n";
}
# Doublecheck we found the right number of enclosure IDs
if ((scalar @enclosure_ids) != $enclosure_count) {
printf "Found %s enclosure IDS for %s enclosures, WTH??\n",
scalar @enclosure_ids,
$enclosure_count
if $debug;
}
for my $ID (@enclosure_ids) {
$json .= qq( { "{#ADP_MEGA}":"$adapter_num", "{#ENCLOSURE}":"$ID"},\n);
}
# Each Adapter&Enclosure has multiple Physical Drives
for my $enclosure_num (@enclosure_ids) {
# Grab info on which slots have drives.
my $physical_drive_slot_list = `/usr/sbin/megacli -PDList -a0 -NoLog|/bin/grep "Slot Number"|/usr/bin/awk '{print \$NF}'`;
my @slots = split /\n/, $physical_drive_slot_list;
print "\tAdapter $adapter_num enclosure $enclosure_num has drives in slots: " if $debug;
my %drive_types;
for my $slot (@slots) {
print "$slot " if $debug;
$json .= qq( { "{#ADP_MEGA}":"$adapter_num", "{#ENCLOSURE}":"$enclosure_num", "{#SLOT}":"$slot"},\n);
# Check what type of drive this is (ex SSD, spinning platter, etc)
my $drive_type = `sudo /usr/sbin/megacli -PDInfo -PhysDrv [$enclosure_num:$slot] -a$adapter_num -NoLog|/bin/grep "Media Type"|cut -d " " -f 3-`;
chomp $drive_type;
$drive_types{$drive_type} = 1;
}
print "\n";
if ($bbu_are_you_there eq 'Present') {
# Determine if the BBU is actually needed. If all the drives
# are SSD, they have their own batteries, so the BBU on the
# RAID card is unnecessary and doesn't need monitoring.
delete $drive_types{"Solid State Device"};
if (keys %drive_types > 0) {
# There's only one BBU, it doesn't have an id or anything, so passing 0
# is just a placeholder
$json .= qq( { "{#ADP_MEGA}":"$adapter_num", "{#BBU}":"0"},\n);
if ($debug) {
print "\tBBU is needed! Found non-SSD drive type(s) ";
for my $type (keys %drive_types) {
print "\"$type\" ";
}
print "\n";
}
}
}
}
}
}
sub mpt_status {
my $component_data = `/usr/sbin/mpt-status`;
for my $component (split /\n/, $component_data) {
my @component = split / /, $component;
# Get the 'adapter', really the SCSI ID #. Usually 0.
my $adapter = $component[0];
$adapter =~ s/^ioc//;
if ($component[1] eq 'vol_id') {
print "Found vol_id $component[2].\n" if $debug;
$json .= qq( { "{#ADP_MPT}":"$adapter", "{#VIRTDRIVE}":"$component[2]" },\n);
} elsif ($component[1] eq 'phy') {
print "Found physical drive $component[2].\n" if $debug;
$json .= qq( { "{#ADP_MPT}":"$adapter", "{#SLOT}":"$component[2]" },\n);
}
}
}
sub sas2ircu {
# Get the adapter info. The output is a nice looking table, designed for
# human eyes, awkward to work with programatically.
my $adapter_list = `/usr/sbin/sas2ircu list | /bin/grep -A10 'Index'`;
my @adapter_lines = split /\n/, $adapter_list;
# Toss the first two lines, they hold the column headers.
shift @adapter_lines;
shift @adapter_lines;
my @adapter_ids;
for my $line (@adapter_lines) {
# Skip the final output line, its not an adapter.
last if $line =~ /Completed Successfully/;
# Grab just the index id number and save it.
$line =~ s/^\s+(\d+)\s+.*$/$1/;
push @adapter_ids, $line;
}
for my $adapter (@adapter_ids) {
print "Adapter $adapter up now.\n" if $debug;
# This CLI does not seem to report any status of the adapter itself,
# but add it to the json as a unique item anyway.
$json .= qq( { "{#ADP_SAS2}":"$adapter" },\n);
# Grab *all* the info for all the devices on this adapter. Unlike
# MegaCLI, there's no command line options or flags that can be used to
# request a subset of that info, its all or nothing with sas2ircu. For
# that reason, we're not bothering with any kind of grep as part of
# this command. We'll do all our own parsing here in perl, examining
# each line in turn.
my $device_info = `/usr/sbin/sas2ircu $adapter display`;
my @device_lines = split /\n/, $device_info;
# Virtual Drive info is listed first, as 'IR Volume'
# Work our way down to that section.
while (scalar @device_lines > 0) {
my $line = shift @device_lines;
if ($line eq "IR Volume information") {
shift @device_lines; # the '---' line
last;
}
}
# Look for volume numbers, or the section divider.
while (scalar @device_lines > 0) {
my $line = shift @device_lines;
# Look out for the bottom of the virtual drive section.
last if $line =~ /---/;
if ($line =~ /IR volume/) {
# Pull out the number and add it to the json.
$line =~ s/^.* (\d+)$/$1/;
print "\tVirtual Drive $line\n" if $debug;
$json .= qq( { "{#ADP_SAS2}":"$adapter", "{#VIRTDRIVE}":"$line"},\n);
}
}
# Physical Drive Info
# Work our way down to that section.
while (scalar @device_lines > 0) {
my $line = shift @device_lines;
if ($line eq "Physical device information") {
shift @device_lines; # the '---' line
last;
}
}
# Look for the enclosure & slot #s for each physical drive.
while (scalar @device_lines > 0) {
my $line = shift @device_lines;
# Look out for the bottom of the physical drive section.
last if $line =~ /---/;
if ($line =~ /Device is a Hard disk/) {
# Get the location of the drive, the enclosure & slot #s.
my $enclosure = shift @device_lines;
$enclosure =~ s/^.*: (\d+)$/$1/;
my $slot = shift @device_lines;
$slot =~ s/^.*: (\d+)$/$1/;
print "\tPhysical drive found in slot $slot in enclosure $enclosure\n" if $debug;
$json .= qq( { "{#ADP_SAS2}":"$adapter", "{#ENCLOSURE}":"$enclosure", "{#SLOT}":"$slot"},\n);
}
}
# Enclosure Info
# This CLI does not seem to report any status of the enclosures,
# but add it to the json as a unique item anyway.
# It is odd to be doing the enclosure as a separate device after doing
# the physical drives (which are in an enclosure) but that's the order
# the output is in so that's the order we process it.
# Work our way down to that section.
while (scalar @device_lines > 0) {
my $line = shift @device_lines;
if ($line eq "Enclosure information") {
shift @device_lines; # the '---' line
last;
}
}
# Look for the enclosure #s.
while (scalar @device_lines > 0) {
my $line = shift @device_lines;
# Look out for the bottom of the enclosures section.
last if $line =~ /---/;
# Yes there is really no space there, unlike how it is for the
# physical drives. I guess that diference is intentional.
if ($line =~ /Enclosure#/) {
$line =~ s/^.*: (\d+)$/$1/;
$json .= qq( { "{#ADP_SAS2}":"$adapter", "{#ENCLOSURE}":"$line"},\n);
}
}
}
}