#!/usr/bin/perl -w use strict; use warnings; use Config::Simple '-strict'; use Getopt::Long; use File::Which; use File::Basename; use Zabbix::Agent::Addons::Disks; use Zabbix::Agent::Addons::UPS; # Output file my $output = undef; # When a threshold can be automatically detected, # you may want to be notified before it's reached, so you can # set a margin which will be substracted from the real threshold my $temp_margin = '20'; my $temp_hd_margin = '10'; my $pwr_margin = '200'; my $pwr_rel_margin = '20'; # This value will be substracted from the higher threshold to define the low one # so you can have hysteresis to prevent flip-flop my $temp_hyst = '10'; my $temp_hd_hyst = '5'; my $temp_ups_hyst = '5'; my $pwr_hyst = '200'; my $pwr_rel_hyst = '10'; # Default threshold if not detected my $def_temp_thres_high = '50'; my $def_hd_temp_thres_high = '50'; my $def_ups_temp_thres_high = '40'; my $def_fan_thres_high = '1000'; my $def_fan_thres_low = '700'; my $def_pwr_thres_high = '1000'; my $def_pwr_rel_thres_high = '80'; GetOptions( "output=s" => \$output, "temp-margin=i" => \$temp_margin, "temp-hd-margin=i" => \$temp_hd_margin, "pwr-margin=i" => \$pwr_margin, "pwr-rel-margin=i" => \$pwr_rel_margin, "temp-hyst=i" => \$temp_hyst, "temp-hd-hyst=i" => \$temp_hd_hyst, "temp-ups-hyst=i" => \$temp_ups_hyst, "pwr-hyst=i" => \$pwr_hyst, "pwr-rel-hys=i" => \$pwr_rel_hyst ); sub usage(){ print<<"_EOF"; Usage: $0 --output=/etc/zabbix/sensors.ini _EOF } unless ($output){ usage(); exit 1; } # Path my $ipmitool = which('ipmitool'); my $smartctl = which('smartctl'); my $lmsensor = which('sensors'); my $upsc = which('upsc'); my $cfg = new Config::Simple(syntax => 'ini'); my $sensors = {}; # Try to detect IPMI sensors if ($ipmitool && -x $ipmitool){ # First check for temperature sensors my @lines = qx($ipmitool sdr type Temperature); if ($? == 0){ SENSOR: foreach my $l (@lines){ chomp $l; # Looks like # Inlet Temp | 04h | ok | 7.1 | 25 degrees C if ($l !~ m/^(\w+[\s\w\/\-]+?\w+)\s*\|.*\|\s*([\w\.\s]+)\s*\|.*\|\s*([\-\w\.\s]+)$/){ next SENSOR; } my $name = $1; my $sensor = {}; my @details = qx($ipmitool sdr get '$name'); if ($? != 0){ print "Couldn't get detail for sensor $name\n"; next SENSOR; } my $val = undef; foreach my $d (@details){ chomp $d; if ($d =~ m/^\s*Sensor\sReading\s*:\s*(\-?\w+)/){ $val = $1; print "Sensor $name has value: $val\n"; if ($val !~ m/^\-?\d+$/){ print "Skipping sensor $name, couldn't parse its value: $val\n"; next SENSOR; } } elsif ($d =~ m/^\s*Upper\scritical\s*:\s*(\-?\d+(\.\d+))/){ $sensor->{threshold_high} = $1-$temp_margin; } elsif ($d =~ m/^\s*Upper\snon\-critical\s*:\s*(\-?\d+(\.\d+))/){ $sensor->{threshold_low} = $1-$temp_margin; } } # Another loop to check for Normal max if Upper critical wasn't found if (!$sensor->{threshold_high}){ foreach my $d (@details){ chomp $d; if ($d =~ m/^\s*Normal\sMaximum\s*:\s*(\-?\d+(\.\d+))/){ $sensor->{threshold_high} = $1-$temp_margin; } } } next SENSOR unless $val; $sensor->{threshold_low} ||= ($sensor->{threshold_high}) ? $sensor->{threshold_high}-$temp_hyst : $def_temp_thres_high-$temp_hyst; $sensor->{threshold_high} ||= $def_temp_thres_high; $sensor->{threshold_high} =~ s/\.0+$//; $sensor->{threshold_low} =~ s/\.0+$//; $sensor->{description} = $name; $sensor->{type} = 'temp'; $sensor->{unit} = '°C'; $sensor->{cmd} = "$ipmitool sdr get '$name' | grep 'Sensor Reading' | awk '{print \$4}' | head -1"; my $id = sensor_name($name); $sensors->{$id} = $sensor; print "Found a temperature sensor using IPMI: $name\n"; } } # Now check for Fan, nearly the same as Temp, but # * We try to detect the unit # * threshold handling is not the same @lines = qx($ipmitool sdr type Fan); if ($? == 0){ SENSOR: foreach my $l (@lines){ chomp $l; if ($l !~ m/^(\w+[\s\w]+?\w+)\s*\|.*\|\s*([\w\.\s]+)\s*\|.*\|\s*([\-\w\.\s]+)$/){ next SENSOR; } my $name = $1; my $value = $3; my $sensor = {}; my @details = qx($ipmitool sdr get '$name'); if ($? != 0){ print "Couldn't get detail for sensor $name\n"; next SENSOR; } my $val = undef; foreach my $d (@details){ chomp $d; if ($d =~ m/^\s*Sensor\sReading\s*:\s*(\w+)/){ $val = $1; if ($val !~ m/^\d+$/){ print "Skipping sensor $name, couldn't parse its value: $val\n"; next SENSOR; } } elsif ($d =~ m/^\s*Lower\scritical\s*:\s*(\d+(\.\d+))/){ $sensor->{threshold_low} = $1-$temp_margin; } elsif ($d =~ m/^\s*Lower\snon\-critical\s*:\s*(\d+(\.\d+))/){ $sensor->{threshold_high} = $1-$temp_margin; } } next SENSOR unless $val; $sensor->{threshold_high} ||= $def_fan_thres_high; $sensor->{threshold_low} ||= $def_fan_thres_high-$temp_hyst; $sensor->{threshold_high} =~ s/\.0+$//; $sensor->{threshold_low} =~ s/\.0+$//; $sensor->{description} = $name; $sensor->{type} = 'fan'; $sensor->{unit} = ($value =~ m/percent|%/ || $val < 100) ? '%' : 'rpm'; $sensor->{cmd} = "$ipmitool sdr get '$name' | grep 'Sensor Reading' | awk '{print \$4}' | head -1"; my $id = sensor_name($name); $sensors->{$id} = $sensor; print "Found a fan sensor using IPMI: $name\n"; } } # Now look for power information @lines = qx($ipmitool sdr type 'Current'); if ($? == 0){ SENSOR: foreach my $l (@lines){ chomp $l; if ($l !~ m/^(\w+[\s\w]+?\w+(\s%)?)\s*\|.*\|\s*([\w\.\s]+)\s*\|.*\|\s*([\-\w\.\s]+)$/){ print "Skiping line $l\n"; next SENSOR; } my $name = $1; my $value = $4; my $sensor = {}; if ($name =~ m/(Power)|(Pwr)|(Consumption)|(PS\d+\sCurr\sOut)/i || $value =~ m/W(att)?/i){ my @details = qx($ipmitool sdr get '$name'); if ($? != 0){ print "Couldn't get detail for sensor $name\n"; next SENSOR; } my $val = undef; my $unit = ($name =~ m/%/) ? '%' : 'Watt'; foreach my $d (@details){ chomp $d; if ($d =~ m/^\s*Sensor\sReading\s*:\s*(\w+)/){ $val = $1; if ($val !~ m/^\d+$/){ print "Skipping sensor $name, couldn't parse its value: $val\n"; next SENSOR; } } elsif ($d =~ m/^\s*Upper\scritical\s*:\s*(\d+(\.\d+)?)/){ $sensor->{threshold_high} = ($unit eq '%') ? $1-$pwr_rel_margin : $1-$pwr_margin; } elsif ($d =~ m/^\s*Upper\snon\-critical\s*:\s*(\d+(\.\d+)?)/){ $sensor->{threshold_low} = ($unit eq '%') ? $1-$pwr_rel_margin : $1-$pwr_margin; } } next SENSOR unless $val; $sensor->{threshold_high} ||= ($unit eq '%') ? $def_pwr_rel_thres_high : $def_pwr_thres_high; $sensor->{threshold_low} ||= ($unit eq '%') ? $def_pwr_rel_thres_high-$pwr_rel_hyst : $def_pwr_thres_high-$pwr_hyst; $sensor->{threshold_high} =~ s/\.0+$//; $sensor->{threshold_low} =~ s/\.0+$//; $sensor->{description} = $name; $sensor->{type} = 'power'; $sensor->{unit} = ($name =~ m/%/) ? '%' : 'Watt'; $sensor->{cmd} = "$ipmitool sdr get '$name' | grep 'Sensor Reading' | awk '{print \$4}' | head -1"; my $id = sensor_name($name); $sensors->{$id} = $sensor; print "Found a power sensor using IPMI: $name\n"; } } } } # Try to detect lm_sensors, using the sensors command if ($lmsensor && -x $lmsensor){ my @lines = qx($lmsensor); if ($? == 0){ SENSOR: foreach my $l (@lines){ chomp $l; # Looks like # temp1: +27.8°C (crit = +119.0°C) # or # Core 0: +36.0°C (high = +80.0°C, crit = +100.0°C) if ($l !~ m/^(\w+[\s\w]+?):\s*\+?(\d+)(\.\d+)?°C\s*(.*)$/){ next SENSOR; } my $name = $1; my $val = $2; my $thr = $4; my $sensor = {}; if ($val !~ m/^\-?\d+$/){ print "Skipping sensor $name, couldn't parse its value: $val\n"; next SENSOR; } if ($name =~ m/^Core\s+\d+/){ print "Skipping individual core sensor $name\n"; next SENSOR; } if ($thr =~ m/high\s+=\s+\+(\d+(\.\d+)?)/){ $sensor->{threshold_high} = $1; } elsif ($thr =~ m/^crit\s+=\s+\+(\d+(\.\d+)?)/){ $sensor->{threshold_high} = $1 - $temp_margin; } next SENSOR unless $val; $sensor->{threshold_low} ||= ($sensor->{threshold_high}) ? $sensor->{threshold_high}-$temp_hyst : $def_temp_thres_high-$temp_hyst; $sensor->{threshold_high} ||= $def_temp_thres_high; $sensor->{threshold_high} =~ s/\.0+$//; $sensor->{threshold_low} =~ s/\.0+$//; $sensor->{description} = $name; $sensor->{type} = 'temp'; $sensor->{unit} = '°C'; $sensor->{cmd} = "$lmsensor | grep '$name:' | cut -d+ -f2 | cut -d. -f1 | head -1"; my $id = sensor_name($name); $sensors->{$id} = $sensor; print "Found a temperature sensor using lm_sensors: $name\n"; } } } # Now, try to detect smart capable HDD if ($smartctl && -x $smartctl){ foreach my $block (Zabbix::Agent::Addons::Disks::list_smart_hdd({ skip_remouvable => 1 })){ my @lines = qx($smartctl -A /dev/$block); next if ($? != 0); foreach my $l (@lines){ if ($l =~ /(Temperature_Celsius|Airflow_Temperature_Cel)/){ $sensors->{$block} = { description => "$block temperature", threshold_low => $def_hd_temp_thres_high-$temp_hd_hyst, threshold_high => $def_hd_temp_thres_high, type => 'temp', unit => '°C', cmd => "$smartctl -A /dev/$block | grep $1 | awk '{print \$10}'" }; print "Found a temperature sensor using smartctl: $block\n"; last; } # Format found on some NVMe SSD elsif ($l =~ /Temperature:\s+(\d+(\.\d+)?)\sCelsius/){ $sensors->{$block} = { description => "$block temperature", threshold_low => $def_hd_temp_thres_high-$temp_hd_hyst, threshold_high => $def_hd_temp_thres_high, type => 'temp', unit => '°C', cmd => "$smartctl -A /dev/$block | grep Temperature: | awk '{ print \$2 }'" }; print "Found a temperature sensor using smartctl: $block\n"; last; } } } # Some LSI based hardware RAID controller can report HDD temp if (-e '/dev/megaraid_sas_ioctl_node'){ # Only check for the firsts 26 drives foreach my $i (0..25){ my @res = qx($smartctl -d megaraid,$i -A /dev/sda); next if ($? != 0); foreach my $l (@res){ if ($l =~ m/Drive\sTrip\sTemperature:\s+(\d+)\s/){ $sensors->{'sda-' . $i} = { description => "Temperature for disk No $i on sda", type => 'temp', threshold_high => $1-$temp_hd_margin, threshold_low => $1-$temp_hd_margin-$temp_hd_hyst, unit => '°C', cmd => "$smartctl -A -d megaraid,$i /dev/sda | grep 'Current Drive Temperature' | awk '{print \$4}'" }; print "Found a temperature sensor using smartctl (megaraid): sda-$i\n"; last; } elsif ($l =~ /(Temperature_Celsius|Airflow_Temperature_Cel)/){ $sensors->{'sda-' . $i} = { description => "Temperature for disk No $i on sda", threshold_low => $def_hd_temp_thres_high-$temp_hd_hyst, threshold_high => $def_hd_temp_thres_high, type => 'temp', unit => '°C', cmd => "$smartctl -A -d megaraid,$i /dev/sda | grep $1 | awk '{print \$10}'" }; print "Found a temperature sensor using smartctl (megaraid): sda-$i\n"; last; } } } } } # Now check UPS if ($upsc && -x $upsc){ foreach my $ups (Zabbix::Agent::Addons::UPS::list_ups()){ my @lines = qx($upsc $ups); next if ($? != 0); foreach my $l (@lines){ if ($l =~ m/^ups\.temperature:\s+(\d+(\.\d+)?)/){ $sensors->{'ups_' . lc $ups . '_temp'} = { description => "ups temperature for $ups", type => 'temp', threshold_high => $def_ups_temp_thres_high, threshold_low => $def_ups_temp_thres_high-$temp_ups_hyst, unit => '°C', cmd => "$upsc $ups ups.temperature" }; print "Found a temperature sensor for ups $ups\n"; last; } elsif ($l =~ m/^ups\.load:\s+(\d+(\.\d+)?)/){ $sensors->{'ups_' . lc $ups . '_load'} = { description => "ups load for $ups", type => 'power', threshold_high => $def_pwr_rel_thres_high, threshold_low => $def_pwr_rel_thres_high-$pwr_rel_hyst, unit => '%', cmd => "$upsc $ups ups.load" }; } } } } # TODO: add support for lm sensors, but its ouput is harder to parse foreach my $s (sort keys %$sensors){ $cfg->set_block($s, $sensors->{$s}); } $cfg->write($output); # Take a sensor description and return a suitable string as sensor ID sub sensor_name{ my $desc = shift; my $id = lc $desc; $id =~ s/[^\w]/_/g; $id =~ s/%/percent/g; $id =~ s/_rpm$//; return $id; }