#!/usr/bin/perl
	
use utf8;
use strict;
use Getopt::Long;
use JSON;
use Data::Dumper;
use File::stat;
use POSIX qw(:fcntl_h);
use File::Temp qw(tempfile tempdir);

my $MegaCLI = new MegaCLI();
my $cache   = "/tmp/.raid-status-megacli-$<";

sub getdata {
	my $fd = POSIX::open($cache);
	my $data = undef;
	if ($fd) {
		my $stat = File::stat::populate(POSIX::fstat($fd));
		if ($stat && time() - $stat->mtime() < 60) {
			# Cache is fresh, just use it
			my $bytes = POSIX::read($fd, my $buffer, $stat->size);
			$data  = from_json($buffer);
		}
		POSIX::close($fd);
	}
	if ( ! defined $data ) {
		# Cache is too old, or missing refresh it (atomically!)
		# We should really use locking here so only one process ever refreshes the data
		# concurrently, but it's not that slow, so just do it the crude way for now...
		my ($fh, $filename) = tempfile(".raid-status-megacli.XXXXXXXX", TMPDIR => 1);
		$data = $MegaCLI->CollectInfo();
		print $fh to_json($data, { utf8=>1, pretty=>1, canonical=>1 });
		close($fh);
		rename($filename,$cache);	
	}
	return $data;
}

my $debug = 0;
GetOptions(
	'd|discover|discovery=s' => \&discovery,
	'q|query'                => \&query,
	'j|json'                 => \&json,
	'h|help'                 => sub { usage(); },
	'debug!'                 => \$debug
);

sub usage {
	my $msg = shift;
	print STDERR "$msg\n" if defined $msg and $msg ne '';
	print "$0 [--debug] ( --discover (controllers|enclosures|logical-drives|physical-drives) | --query Path To Single Value | --json [path [to [key...]]])\n";
	exit(1);
}


sub discovery {
	my $type = $_[1];

	my $discovered = [];
	if (lc($type) =~ m/^controllers?$/) {
		my $data = getdata();
		if (exists $data->{Controllers}) {
			foreach my $idx (keys $data->{Controllers}) {
				push($discovered, {
					'{#CONTROLLER_IDX}'    => $idx,
					'{#CONTROLLER_MODEL}'  => $data->{Controllers}->{$idx}->{Versions}->{'Product Name'},
					'{#CONTROLLER_SERIAL}' => $data->{Controllers}->{$idx}->{Versions}->{'Serial No'},
					'{#CONTROLLER_DISKS}'  => $data->{Controllers}->{$idx}->{'Device Present'}->{Disks},
					'{#CONTROLLER_KEY}'    => "Controllers.$idx"
				});
			}
		}	
	} elsif (lc($type) =~ m/^enclosures?$/) {
		my $data = getdata();
                if (exists $data->{Controllers}) {
                        foreach my $idx (keys $data->{Controllers}) {
				foreach my $enc (keys $data->{Controllers}->{$idx}->{Enclosures}) {
				
                                	push($discovered, {
                                        	'{#CONTROLLER_IDX}'    => $idx,
	                                        '{#CONTROLLER_MODEL}'  => $data->{Controllers}->{$idx}->{Versions}->{'Product Name'},
        	                                '{#CONTROLLER_SERIAL}' => $data->{Controllers}->{$idx}->{Versions}->{'Serial No'},
						'{#CONTROLLER_KEY}'    => "Controllers.$idx",
						'{#ENCLOSURE_IDX}'     => $enc,
						'{#ENCLOSURE_ID}'      => $data->{Controllers}->{$idx}->{'Enclosures'}->{$enc}->{'Device ID'},
						'{#ENCLOSURE_MODEL}'   => $data->{Controllers}->{$idx}->{'Enclosures'}->{$enc}->{'Product Identification'},
						'{#ENCLOSURE_VENDOR}'  => $data->{Controllers}->{$idx}->{'Enclosures'}->{$enc}->{'Vendor Identification'},
                	                        '{#ENCLOSURE_DRIVES}'  => $data->{Controllers}->{$idx}->{'Enclosures'}->{$enc}->{'Number of Physical Drives'},
						'{#ENCLOSURE_SLOTS}'   => $data->{Controllers}->{$idx}->{'Enclosures'}->{$enc}->{'Number of Slots'},
						'{#ENCLOSURE_KEY}'     => "Controllers.$idx.Enclosures.$enc"
                        	        });
				}
                        }
                }
	} elsif (lc($type) =~ m/^physical-drives?$/) {
		my $data = getdata();
                if (exists $data->{Controllers}) {
                        foreach my $idx (keys $data->{Controllers}) {
                                foreach my $enc (keys $data->{Controllers}->{$idx}->{Enclosures}) {
					foreach my $slot (keys $data->{Controllers}->{$idx}->{Enclosures}->{$enc}->{Slots}) {

	                                        push($discovered, {
	                                                '{#CONTROLLER_IDX}'    => $idx,
	                                                '{#CONTROLLER_MODEL}'  => $data->{Controllers}->{$idx}->{Versions}->{'Product Name'},
	                                                '{#CONTROLLER_SERIAL}' => $data->{Controllers}->{$idx}->{Versions}->{'Serial No'},
							'{#CONTROLLER_KEY}'    => "Controllers.$idx",
	                                                '{#ENCLOSURE_IDX}'     => $enc,
	                                                '{#ENCLOSURE_ID}'      => $data->{Controllers}->{$idx}->{'Enclosures'}->{$enc}->{'Device ID'},
	                                                '{#ENCLOSURE_MODEL}'   => $data->{Controllers}->{$idx}->{'Enclosures'}->{$enc}->{'Product Identification'},
	                                                '{#ENCLOSURE_VENDOR}'  => $data->{Controllers}->{$idx}->{'Enclosures'}->{$enc}->{'Vendor Identification'},
	                                                '{#ENCLOSURE_DRIVES}'  => $data->{Controllers}->{$idx}->{'Enclosures'}->{$enc}->{'Number of Physical Drives'},
							'{#ENCLOSURE_SLOTS}'   => $data->{Controllers}->{$idx}->{'Enclosures'}->{$enc}->{'Number of Slots'},
							'{#ENCLOSURE_KEY}'     => "Controllers.$idx.Enclosures.$enc",
							'{#ENCLOSURE_SLOT}'    => $slot,
							'{#DRIVE_VENDOR}'      => $data->{Controllers}->{$idx}->{'Enclosures'}->{$enc}->{Slots}->{$slot}->{Vendor},
							'{#DRIVE_MODEL}'       => $data->{Controllers}->{$idx}->{'Enclosures'}->{$enc}->{Slots}->{$slot}->{Product},
							'{#DRIVE_SERIAL}'      => $data->{Controllers}->{$idx}->{'Enclosures'}->{$enc}->{Slots}->{$slot}->{'Serial number'},
							'{#DRIVE_FORMFACTOR}'  => $data->{Controllers}->{$idx}->{'Enclosures'}->{$enc}->{Slots}->{$slot}->{'Form Factor'},
							'{#DRIVE_WWN}'         => $data->{Controllers}->{$idx}->{'Enclosures'}->{$enc}->{Slots}->{$slot}->{WWN},
							'{#DRIVE_SIZE}'        => $data->{Controllers}->{$idx}->{'Enclosures'}->{$enc}->{Slots}->{$slot}->{'Raw Size'},
							'{#DRIVE_RPM}'         => $data->{Controllers}->{$idx}->{'Enclosures'}->{$enc}->{Slots}->{$slot}->{'Rotation Rate'},
							'{#DRIVE_KEY}'         => "Controllers.$idx.Enclosures.$enc.Slots.$slot",
							'{#DRIVE_ID}'          => $data->{Controllers}->{$idx}->{'Enclosures'}->{$enc}->{Slots}->{$slot}->{'Device Id'}
        	                                });
					}
                                }
                        }
                }
	} elsif (lc($type) =~ m/^logical-drives?$/) {
		my $data = getdata();
                if (exists $data->{Controllers}) {
                        foreach my $idx (keys $data->{Controllers}) {
                                foreach my $dev (keys $data->{Controllers}->{$idx}->{'Logical Drives'}) {

                                        push($discovered, {
                                                '{#CONTROLLER_IDX}'    => $idx,
                                                '{#CONTROLLER_MODEL}'  => $data->{Controllers}->{$idx}->{Versions}->{'Product Name'},
                                                '{#CONTROLLER_SERIAL}' => $data->{Controllers}->{$idx}->{Versions}->{'Serial No'},
						'{#CONTROLLER_KEY}'    => "Controllers.$idx",
                                                '{#ARRAY_IDX}'         => $dev,
                                                '{#ARRAY_OSDEV}'       => $data->{Controllers}->{$idx}->{'Logical Drives'}->{$dev}->{'OS Device Path'},
						'{#ARRAY_NAME}'        => $data->{Controllers}->{$idx}->{'Logical Drives'}->{$dev}->{'Name'},
						'{#ARRAY_SIZE}'        => $data->{Controllers}->{$idx}->{'Logical Drives'}->{$dev}->{'Size'},
						'{#ARRAY_TYPE}'        => $data->{Controllers}->{$idx}->{'Logical Drives'}->{$dev}->{'RAID Level'},
						'{#ARRAY_KEY}'         => "Controllers.$idx.Logical Drives.$dev"
						
                                        });
                                }
                        }
                }
	} else {
		usage("Unknown/unsupported discovery type '$type'");
	}
	print to_json($discovered, { utf8 => 1, pretty => 1, canonical => 1});	
}


sub query { 
	print extract(0) . "\n";
}

sub json {
	print extract(1) . "\n";
}

sub extract {
	my $json = shift;
	my $data = getdata();
	my $p = $data;
	my $result = 'ZBX_NOTSUPPORTED';

	my @keys = split(/\./, join('.', @ARGV));
	my @path = ();

	foreach my $k (@keys) {
		my $lc = { map { lc($_) => $_ } (keys $p) };
		last unless exists $lc->{lc($k)};
		$p = $p->{$lc->{lc($k)}};
		push(@path, $k);
	}

	if (@keys == @path) {
		# We matched all keys, use $p if it's not a REF
		if (ref $p eq '') {
			if ($json) {
				$result = to_json({ Value => $p }, { utf8 => 1, pretty => 1, canonical => 1});
			} else {
				$result = $p;
			}
		} elsif ($json) {
			$result = to_json($p, { utf8 => 1, pretty => 1, canonical => 1});
		} elsif ($debug) {
			print STDERR "Key " . join('->', @path) . " is not a scalar / simple value (try --json ?)\n"
		}	
	} elsif ($debug) {
		my $oklen = length(join('->', @keys[0..$#path]));
		my $msg = "Key '" . $keys[$#path+1] . "' not found at index " . ($#path) . ": ";
		my $indent = (" " x (length($msg) + $oklen + 2));
		$msg .= join('->', @keys) . "\n"
		      . $indent . ('^' x length($keys[$#path+1])) . "\n";
		print STDERR $msg;
	}

	return $result;
}





	
BEGIN {
	{
		package MegaCLI;

		use IPC::Run qw(run);
		use Data::Dumper;
		use Carp;
		use Tie::Hash::Indexed;
		use Hash::Merge::Simple qw(merge);
		use Cwd qw(abs_path);
		use Date::Parse qw(str2time);

		my $MEGACLI = "/opt/MegaRAID/MegaCli/MegaCli64";
	
		sub new {
			my $classname = shift;
			my $self      = {@_};
			$self->{CMD}  = $MEGACLI unless $self->{CMD};
			bless($self, $classname);
			return $self;
		}
	
		# Run a MegaCli command and return the results
		sub runCommand {
			my $self = shift;
		        # The request options
		        my @args = @_;
	
	        	# Run the command
		        run(["sudo", "-n", $self->{CMD}, @args, -NoLog], \undef, \my $stdout, \my $stderr);
	
		        # Normalize the newlines
	        	$stdout =~ s/\r\n/\n/g;
		        $stderr =~ s/\r\n/\n/g;
	
		        # Trim the test;
		        $stdout =~ s/^\s+|\s+$//sg;
		        $stderr =~ s/^\s+|\s+$//sg;
	
		        # Yank the exit code
	        	$stdout =~ s/^\sExit Code: (0x[0-9a-f]+)\z//msi;
		        my $exit = hex($1);
	
		        # Return the results
		        return {
	        	        arguments => \@args,
	                	exitcode => $exit,
		                output => $stdout
	        	};
		}
	
		sub parseBytes {
			my $self = shift;
			my $text = shift;
			my $mult = { 'K' => 1<<10, 'M' => 1<<20, 'G' => 1<<30, 'T' => 1<<40 }; 
			$text =~ s/^(\d+(?:\.\d+)?) ?(K|M|G|T)B( \[0x[0-9a-f]+ Sectors\])?$/int($1*$mult->{$2})/ie;
			return $text;
		}
	
		sub parseTemperature {
			my $self = shift;
	                my $text = shift;
			$text =~ s/^(\d+(?:\.\d+)?)C \(\d+(?:\.\d+)? F\)$/\1/;
			$text =~ s/^(\d+(?:\.\d+)?) +degree Celcius/\1/i;
			return $text;
		}
		
		sub parseDate {
			my $self = shift;
			my $text = shift;
			$text =~ s/^(0?\d|1[0-2])\/([0-2]?\d|3[0-1])\/(\d{2}(?:\d{2})?)\b/sprintf('%04d-%02d-%02d', ($1 > 0 && $2 > 0 && $3 <= 99) ? 2000+$3 : $3, $1, $2)/e;
			$text =~ s/^([012][0-9]|3[01])\/([0[0-9]|1[0-2]), (\d{4})\s*$/sprintf('%04d-%02d-%02d', $3, $1, $2)/e;
			return $text;
		}
	
		sub parseDateTime {
			my $self = shift;
			my $text = shift;
			# Current Time: 23:38:30 10/1, 2019
			$text =~ s/^\s*(\d{1,2}):(\d{1,2}):(\d{1,2}) (\d+)\/(\d+), (\d{4})\s*$/sprintf('%04d-%02d-%02dT%02d:%02d:%02d', $6, $4, $5, $1, $2, $3)/e;
			$text =~ s/^\s*((Sun|Mon|Tue|Wed|Thu|Fri|Sat) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) ([ 012]\d|3[0-2]) \d{2}:\d{2}:\d{2} \d{4})\s*$/str2time($1)/e;
			return $text;
		}
		
		sub parseRPM {
                        my $self = shift;
                        my $text = shift;
			#print " >>> $text\n";
                        $text =~ s/^\s*(\d+)\s*rpm\s*$/\1/i;
			#print "  > $text\n";
                        return $text;
                }
	
		sub parseKeys {
			my $self = shift;
			my $text = shift;
			# Generic key: value parse
			my $keys = { 
				map { 
					s/^\s+|\s+$//g;                                            # Trim whitespace
					s/^(\d+)[ \t]+([0-9a-f]+)*$/Port \1 Address : \2/g;        # Normalise SAS Port addresses (<N> <ADDRESS>)
					s/S\.M\.A\.R\.T/SMART/g;                                   # Strip dots from SMART key(s)
					split(/\s*(?::| = )\s*/, $_, 2)
				}
				split(/\n/, $text)
			};
			# Normalise Sizes to Bytes
			map { $keys->{$_} = $self->parseBytes($keys->{$_}) if $_ =~ m/\bSize|Capacity|Mirror Data\b/i } (keys $keys);
			# Normalise Temperatures to C
			map { $keys->{$_} = $self->parseTemperature($keys->{$_}) if $_ =~ m/\bTemperature\b/i } (keys $keys);
			# Normalise Dates to ISO8601
			map { $keys->{$_} = $self->parseDate($keys->{$_}) if $_ =~ m/\bDate\b/i } (keys $keys);
			map { $keys->{$_} = $self->parseDateTime($keys->{$_}) if $_ =~ m/\bCurrent Time|Next Learn time\b/i } (keys $keys);
			# Normalise rpm to bare number
			map { $keys->{$_} = $self->parseRPM($keys->{$_}) if $_ =~ m/\bRotation(al)? Rate\b/i } (keys $keys);
			return $keys;
		}
	
		sub ControllerCount {
			my $self= shift;
		        my $cmd = $self->runCommand(-adpCount);
		        return $1 if $cmd->{output} =~  m/^Controller Count: (\d+)\./msi;
	        	return undef;
		}
	
		sub ControllerInfo {
			my $self = shift;
			my $cnum = shift // 'ALL'; 
	
			croak "Invalid controller '$cnum', expected integer or ALL " unless $cnum =~ m/^(\d+|ALL)$/i;
	
		        my $cmd = $self->runCommand(-adpAllInfo, "-a${cnum}");
		        my $data = {};
	        	my $adapters = { splice( [ split(/^Adapter #(\d+)$/m, $cmd->{output}) ], 1 ) };
		        foreach my $idx (sort { $a <=> $b } keys $adapters) {
	        	        $data->{$idx} = {};
	                	my $sections = { splice([split(/\n[ \t]+([A-Za-z.: ]+)\n[ \t]+=======+\n/ms, $adapters->{$idx})], 1) };
		                foreach my $name (keys $sections) {
					my $key = $name;
					$key =~ s/Mfg\. Data/Mfg Data/g;
	        	                $data->{$idx}->{$key} = $self->parseKeys($sections->{$name});
	                	}
		        }
	        	return $data;
		}
	
		sub PCIInfo {
			my $self = shift;
			my $text = $self->runCommand(-AdpGetPciInfo, -aALL);
			my $data = {};
			my $adapters = { splice( [ split(/^PCI information for Controller (\d+)[ \t]*\n-+$/m, $text->{output}) ], 1) };
			foreach my $idx (keys $adapters) {
				$data->{$idx} = { 'PCI Info' => $self->parseKeys($adapters->{$idx}) };
				
				$data->{$idx}->{'PCI Info'}->{'Bus Identifier'} = sprintf(
					'0000:%02x:%02x.%x',
					hex($data->{$idx}->{'PCI Info'}->{'Bus Number'}),
					hex($data->{$idx}->{'PCI Info'}->{'Device Number'}),
					hex($data->{$idx}->{'PCI Info'}->{'Function Number'})
				);
			}
			return $data;
		}
	
		sub ControllerSummary {
	                my $self = shift;
	                my $cnum = shift // 'undef';
	
	                croak "Invalid controller '$cnum', expected integer" unless $cnum =~ m/^\d+$/;
	
	                my $cmd = $self->runCommand(-ShowSummary, "-a${cnum}");
			$cmd->{output} =~ s/^System.*?^Hardware$//ms;
			$cmd->{output} =~ s/^Storage\n//msg;
			$cmd->{output} =~ s/[ \t]+\n/\n/mg;
	                my $data = {};
	
			my $sections = ( { splice( [ split(/^\s*([^:]+)\s*\n/m, $cmd->{output}) ], 1 ) } );
			foreach my $name (keys $sections) {
				if ($name =~ m/^Controller$/) {
					my $keys = $self->parseKeys($sections->{$name});
					$keys->{Bus} = $1 if $keys->{'ProductName'} =~ m/\(Bus (\d+), Dev \d+\)/i;
					$keys->{Dev} = $1 if $keys->{'ProductName'} =~ m/\(Bus \d+, Dev (\d+)\)/i;	
					$keys->{Product} = $1 if $keys->{'ProductName'} =~ m/^(.*)\(Bus \d+, Dev \d+\)$/;
					$data->{$name} = $keys;
				} elsif ($name =~ m/^PD$/i) {
					# This probably doesn't work correctly with additional enclosures....
					$data->{'Slots'} = {};
					foreach my $part  (split(/\n\s*\n/s, $sections->{$name})) {
						my $keys = $self->parseKeys($part);
						$keys->{'Backplane'} = $1 if $keys->{'Connector'} =~ m/(Port \d+ - \d+<Internal>):/;
						$keys->{'Slot'     } = $1 if $keys->{'Connector'} =~ m/: Slot (\d+)\s*$/;
						$data->{'Slots'}->{$keys->{'Slot'}} = $keys;
					}
				} elsif ($name =~ m/^Virtual Drives$/i) {
					$data->{'Arrays'} = {};
					foreach my $part  (split(/\n\s*\n/s, $sections->{$name})) {
	                                        my $keys = $self->parseKeys($part);
						$keys->{'Target Id'} = $1 if $keys->{'Virtual drive'} =~ m/\bTarget Id (\d+)\b/i;
						$keys->{'Name'     } = $1 if $keys->{'Virtual drive'} =~ m/\bVD name (.+?)\s*$/i;
						$data->{'Arrays'}->{$keys->{'Target Id'}} = $keys;
	                                }
				} else {
					$data->{$name} = $self->parseKeys($sections->{$name});
				}
			}
			return $data;
		}
	
		sub BBUInfo {
			my $self = shift;
		        my $cmd = $self->runCommand(-AdpBbuCmd, -aALL);
	       		my $data = {};
		       	my $adapters = { splice( [ split(/^BBU status for Adapter: (\d+)$/m, $cmd->{output}) ], 1 ) };
	       		foreach my $idx (keys $adapters) {
	                	my $sections = { 'Status', map { s/^\s+|\s+$//g; $_ } split(/^BBU (.*) for Adapter: \Q$idx\E[ \t]*$/m, $adapters->{$idx}) };
		                $data->{$idx} = {"BBU" => {} };
	        	        foreach my $name (keys $sections) {
					$sections->{$name} =~ s/^\s*(Voltage: \d+ mV)\s*$/Battery \1/mg;
					$sections->{$name} =~ s/^\s*(Temperature: \d+ C)\s*$/Battery \1/mg;
	                	        $data->{$idx}->{BBU}->{$name} = $self->parseKeys($sections->{$name});
					delete $data->{$idx}->{BBU}->{$name}->{'BBU Firmware Status'};
		                }
	        	}
		        return $data;
		}
	
		sub EncInfo {
			my $self = shift;
			my $cmd  = $self->runCommand(-EncInfo, -aALL);
			my $data = {};
			my $adapters = { splice( [ split(/^\s*Number of enclosures on adapter (\d+) -- \d+\s*$/m, $cmd->{output}) ], 1) };
			foreach my $idx (keys $adapters) {
				$data->{$idx} = { Enclosures => {} };
				my $enclosures = { splice( [ split(/^\s*Enclosure (\d+):\s*$/m, $adapters->{$idx}) ], 1 ) };
				foreach my $enc (keys $enclosures) {
					$data->{$idx}->{Enclosures}->{$enc} = $self->parseKeys($enclosures->{$enc});
				}
			}
			return $data;
		}
	
	
		sub PDInfo {
			my $self = shift;
		        my $cmd = $self->runCommand(-PdList, -aALL);
	        	my $data = {};
		        my $adapters = { splice( [ split(/^Adapter #(\d+)[ \t]*$/m, $cmd->{output}) ], 1 ) };
	        	foreach my $idx (keys $adapters) {
		                my $devices = { splice( [ split(/^(?=(Enclosure Device ID: \d+\nSlot Number: \d+)\n)/ms, $adapters->{$idx}) ], 1 ) };
	        	        $devices = { map { my $k=$_; s/^\s+|\s$//g; s/^[^:]+:\s*(\d+)$/\1/mg; s/\n/\//; $_ => $devices->{$k}; } (keys $devices) };
	                	$data->{$idx} = {"EnclosureIds" => {}};
		                foreach my $dev (keys $devices) {
					(my $encid, my $devid) = split('/', $dev);
					$data->{$idx}->{EnclosureIds}->{$encid} = {"Slots" => {}} unless exists $data->{$idx}->{EnclosureIds}->{$encid};
					$data->{$idx}->{EnclosureIds}->{$encid}->{"Slots"}->{$devid} = $self->parseKeys($devices->{$dev});
	                	}
		        }
		        return $data;
		}
	
		sub LDInfo {
			my $self = shift;
		        my $cmd = $self->runCommand(-LDInfo, -LAll, -aALL);
		        my $data = {};
		        my $adapters = { splice( [ split(/^Adapter (\d+) -- Virtual Drive Information:$/m, $cmd->{output}) ], 1) };
		        foreach my $idx (keys $adapters) {
	                	$data->{$idx} = { "Logical Drives" => {} };
				$adapters->{$idx} =~ s/\((Target Id: \d+)\)\s*$/\n\1/mg;
	        	        my $devices = { splice( [ split(/^Virtual Drive: (\d+)\s+/m, $adapters->{$idx}) ], 1) };
		                foreach my $dev (keys $devices) {
	                	        $data->{$idx}->{"Logical Drives"}->{$dev} = $self->parseKeys($devices->{$dev});
	        	        }
		        }
	        	return $data;
		}		
	
		sub CollectInfo {
			my $self = shift;

			# We need MegaCli or we aren't going anywhere with this...
			if (! -x $self->{CMD}) {
				croak "MegaCli not present or not executable ($self->{CMD})";
			}	

			my $data = {
				'Controllers' =>  merge(
					$self->ControllerInfo(),
					$self->PCIInfo(),
					$self->BBUInfo(),
					$self->EncInfo(),
					$self->PDInfo(),
					$self->LDInfo()
				)
			};
	
			# Remap drive slots which are provided by Enclosure Id, to the Enclosure index
			foreach my $idx (keys $data->{Controllers}) {
				foreach my $enc (keys $data->{Controllers}->{$idx}->{Enclosures}) {
					my $id = $data->{Controllers}->{$idx}->{Enclosures}->{$enc}->{'Device ID'};
					if (exists $data->{Controllers}->{$idx}->{EnclosureIds}->{$id}) {
						$data->{Controllers}->{$idx}->{Enclosures}->{$enc}->{Slots} = {} unless exists $data->{Controllers}->{$idx}->{Enclosures}->{$enc}->{Slots};
						$data->{Controllers}->{$idx}->{Enclosures}->{$enc}->{Slots} = merge(
							$data->{Controllers}->{$idx}->{Enclosures}->{$enc}->{Slots},
							$data->{Controllers}->{$idx}->{EnclosureIds}->{$id}->{Slots}
						);
						delete $data->{Controllers}->{$idx}->{EnclosureIds}->{$id};
					}	
				}
				delete $data->{Controllers}->{$idx}->{EnclosureIds};
			}
	
			# Identify the logical device at the OS level + add sane RAID levels
			foreach my $idx (keys $data->{Controllers}) {
				foreach my $dev (keys $data->{Controllers}->{$idx}->{'Logical Drives'}) {
					my $path = sprintf(
						"/dev/disk/by-path/pci-%s-scsi-0:[0-9]:%d:0",
						$data->{Controllers}->{$idx}->{'PCI Info'}->{'Bus Identifier'},
						$data->{Controllers}->{$idx}->{'Logical Drives'}->{$dev}->{'Target Id'}
					);
					$data->{Controllers}->{$idx}->{'Logical Drives'}->{$dev}->{'OS Device Path'} = abs_path([glob($path)]->[0]);

					# TODO: This is probably NOT completely accurate....
					my $level = $data->{Controllers}->{$idx}->{'Logical Drives'}->{$dev}->{'RAID Level'};
					my $depth = $data->{Controllers}->{$idx}->{'Logical Drives'}->{$dev}->{'Span Depth'};
					if ($level =~ /^Primary-(\d), Secondary-(\d), RAID Level Qualifier-(\d)$/i) {
						if ($depth == 0) {
							$level = 'JBOD';
						} elsif ($depth == 1) {
							$level = 'RAID-' . $1;
						} elsif ($depth == 2) {
							$level = 'RAID-' . $1 . $2;
						}
					}
					$data->{Controllers}->{$idx}->{'Logical Drives'}->{$dev}->{'RAID Level'} = $level;
				}
			}
	
			# Collect SMART info from the physical devices
			foreach my $idx (keys $data->{Controllers}) {
				# Grab as OS dev from this controller, it actually doesn't matter which one....
				my $osdev = $data->{Controllers}->{$idx}->{'Logical Drives'}->{ (keys $data->{Controllers}->{$idx}->{'Logical Drives'})[0] }->{'OS Device Path'};
				foreach my $enc (keys $data->{Controllers}->{$idx}->{Enclosures}) {
					foreach my $slot (keys $data->{Controllers}->{$idx}->{Enclosures}->{$enc}->{'Slots'}) {
						my $smart = $self->SMARTInfo($osdev, $data->{Controllers}->{$idx}->{Enclosures}->{$enc}->{'Slots'}->{$slot}->{'Device Id'});
						$data->{Controllers}->{$idx}->{Enclosures}->{$enc}->{'Slots'}->{$slot} = merge(
							$data->{Controllers}->{$idx}->{Enclosures}->{$enc}->{'Slots'}->{$slot},
							$smart
						);
					}
				}
			}
	
	
			return $data;
		}
	
		sub SMARTInfo {
			my $self = shift;
			my $dev  = shift;
			my $id   = shift;
			run(["sudo", "-n", "smartctl", "-d", "megaraid,$id", "-i", $dev], \undef, \my $stdout, \my $stderr);
			$stdout =~ s/^[^:]+\n//mg;
			my $keys = $self->parseKeys($stdout);
			$keys = { map { $_ => $keys->{$_} } grep { m/Vendor|Product|Rotation Rate|Form Factor|Serial number/i } (keys $keys) };
			return $keys;
		}
	
	
	}
}
	
