HEX
Server: Apache
System: Linux pdx1-shared-a1-38 6.6.104-grsec-jammy+ #3 SMP Tue Sep 16 00:28:11 UTC 2025 x86_64
User: mmickelson (3396398)
PHP: 8.1.31
Disabled: NONE
Upload Files
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);
			}
		}
	}
}