#!/bin/bash
###### ZPool & SMART status report with FreeNAS config backup
### Original script by joeschmuck, modified by Bidelu0hm, then by melp (me)
### At a minimum, enter email address in user-definable parameter section. Feel free to edit other user parameters as needed.
### If you find any errors, feel free to contact me on the FreeNAS forums (username melp) or email me at jason at jro dot io.
### Version: v1.3 TrueNAS Scale (Jeff Alperin 12-6-21)
### Version v1.4b TrueNAS/FreeNAS (joeschmuck 01/07/2022), v1.4c-beta2 (joeschmuck 02/09/2022)
### Version: v1.4c
### Changelog:
# v1.4c:
# - Added sorting drives alphabetically vice the default the OS reports them.
# - Added RED warning in Device for any single failure in the summary (deviceRedFlag switch controlled).
# - Added some additional SSD definitions.
#
# v1.4b:
# - Added SMART test reamining percentage if Last Test has a SMART Test is in progress.
# - Fix for empty SMART fields, typically for unsupported SSD's.
# - Added IGNORE SMART Drive so you can ignore specific drives that may cause you wierd readings.
# --- Updated so blank SSD table header is removed when you ignore all the drives (just crazy talk).
# v1.4a:
# - Fixed report errors for if a SCRUB is in progress, now shows estimated completion time.
# - Fixed report error for a Canceled SCRUB.
# - Fixed FreeBSD/Linux use for SCRUB report (minor oversight).
# v1.4:
# - Run on CRON JOB using /path/multi_report_v1.4.sh
# - Fixed for automatic running between FreeBSD and Linux Debian (aka SCALE) as of this date.
# - All SMART Devices will report.
# - Added conditional Subject Line (Good/Critical/Warning).
# - Added Automatic SSD Support.
# --- Some updates may need to be made to fit some of SSD's. Code in the area of about line 530 will
# --- need to be adjusted to add new attributes for the desired SSD's fields.
# - UDMA_CRC_ERROR Override because once a drive encounters this type of error, it cannot be cleared
# --- so you can offset it now vice having an alarm condition for old UDMA_CRC_Errors.
# - Added listing NON-SMART Supported Drives. Use only if useful to you, some drives will
# --- still output some relevant data, many will not.
# v1.3:
# - Added scrub duration column
# - Fixed for FreeNAS 11.1 (thanks reven!)
# - Fixed fields parsed out of zpool status
# - Buffered zpool status to reduce calls to script
# v1.2:
# - Added switch for power-on time format
# - Slimmed down table columns
# - Fixed some shellcheck errors & other misc stuff
# - Added .tar.gz to backup file attached to email
# - (Still coming) Better SSD SMART support
# v1.1:
# - Config backup now attached to report email
# - Added option to turn off config backup
# - Added option to save backup configs in a specified directory
# - Power-on hours in SMART summary table now listed as YY-MM-DD-HH
# - Changed filename of config backup to exclude timestamp (just uses datestamp now)
# - Config backup and checksum files now zipped (was just .tar before; now .tar.gz)
# - Fixed degrees symbol in SMART table (rendered weird for a lot of people); replaced with a *
# - Added switch to enable or disable SSDs in SMART table (SSD reporting still needs work)
# - Added most recent Extended & Short SMART tests in drive details section (only listed one before, whichever was more recent)
# - Reformatted user-definable parameters section
# - Added more general comments to code
# v1.0:
# - Initial release
###### User-definable Parameters
### Email Address
email="youremailaddress.com"
from="TrueNAS@address.com"
### Drive Ignore List
# Use this section to list any drives to ignore checking. This is useful for ignoring USB Flash Drives.
# I configured this for up to 8 drives, if someone needs more, it's an easy minor code change.
#
# Format: IGNORESN1="DRIVE_SERIAL_NUMBER"
IGNORESN1="DRIVE_SERIAL_NUMBER"
IGNORESN2="DRIVE_SERIAL_NUMBER"
IGNORESN3="DRIVE_SERIAL_NUMBER"
IGNORESN4="DRIVE_SERIAL_NUMBER"
IGNORESN5="DRIVE_SERIAL_NUMBER"
IGNORESN6="DRIVE_SERIAL_NUMBER"
IGNORESN7="DRIVE_SERIAL_NUMBER"
IGNORESN8="DRIVE_SERIAL_NUMBER"
### Drive UDMA_CRC_Error_Count Flags
# List each drive by serial number and include the current UDMA_CRC_Error_Count value.
# If the count exceeds the count in the table below then a warning message will be generated.
# On the Status Report the UDMA CRC Errors block will be YELLOW with a value of "0" for an overridden value, for your indication.
#
# Format: UCRC1SN = Drive Serial Number reported by SMART, UCRC1CNT = Current UDMA Error Count you want to subtract from this drive.
# You may add up to 8 drives with the current code. More drives could be added with a simple modification of the program.
# Do not list your drives with a value of "0" or each one will be Yellow. Yikes!
UCRC1SN="WD-WMC4N2578099"
UCRC1CNT=1
UCRC2SN="2011E2940536"
UCRC2CNT=19
UCRC3SN="2011E2940534"
UCRC3CNT=18
UCRC4SN="YOUR_SERIAL_NUMBER_HERE"
UCRC4CNT=1
UCRC5SN="YOUR_SERIAL_NUMBER_HERE"
UCRC5CNT=1
UCRC6SN="YOUR_SERIAL_NUMBER_HERE"
UCRC6CNT=1
UCRC7SN="YOUR_SERIAL_NUMBER_HERE"
UCRC7CNT=1
UCRC8SN="YOUR_SERIAL_NUMBER_HERE"
UCRC8CNT=1
### zpool status summary table settings
usedWarn=90 # Pool used percentage for CRITICAL color to be used
scrubAgeWarn=37 # Maximum age (in days) of last pool scrub before CRITICAL color will be used (30 + 7 days for day of week)
### SMART status summary table settings
includeSSD="true" # true = SSD Automatic Detection and Reporting, false = Disable SSD Automatic Detection and Reporting.
reportnonSMART="true" # Will force even non-SMART devices to be reported, "false" = normal operation to ignore non-SMART devices.
tempWarn=42 # Drive temp (in C) at which WARNING color will be used
tempCrit=47 # Drive temp (in C) at which CRITICAL color will be used
sectorsCrit=1 # Number of sectors per drive with errors before CRITICAL color will be used
testAgeWarn=2 # Maximum age (in days) of last SMART test before CRITICAL color will be used
powerTimeFormat="ymdh" # Format for power-on hours string, valid options are "ymdh", "ymd", "ym", or "y" (year month day hour)
deviceRedFlag="true" # Set "true" to have the Device Column indicate RED for ANY issue. Default is disabled.
### FreeNAS config backup settings
configBackup="false" # Change to "true" to save config backup (which renders next two options operational); "false" to keep disable config backups
saveBackup="false" # Change to "false" to delete FreeNAS config backup after mail is sent; "true" to keep it in dir below
backupLocation="/tmp/" # Directory in which to save FreeNAS config backups
###### Auto-generated Parameters
programver="Multi-Report v1.4c-beta2 (9 Feb 2022)"
softver=$(uname -s)
host=$(hostname -s)
logfile="/tmp/smart_report_body.tmp"
logfile_header="/tmp/smart_report_header.tmp"
logfile_warning="/tmp/smart_report_warning_flag.tmp"
logfile_critical="/tmp/smart_report_critical_flag.tmp"
boundary="gc0p4Jq0M2Yt08jU534c0p"
### Global table colors
okColor="#c9ffcc" # Hex code for color to use in SMART Status column if drives pass (default is light green, #c9ffcc)
warnColor="#ffd6d6" # Hex code for WARN color (default is light red, #ffd6d6)
critColor="#ff0000" # Hex code for CRITICAL color (default is bright red, #ff0000)
altColor="#f4f4f4" # Table background alternates row colors between white and this color (default is light gray, #f4f4f4)
ovrdColor="#ffff66" # Hex code for Override Yellow
blueColor="#87ceeb" # Hex code for Sky Blue, used for the SCRUB In Progress background
### GET non-SSD listing - MUST support SMART
# smartdrives= All Drives that are SMART
# smartdrivesSSD = SSD all SMART
# nonsmartdrives = Any drive not supporting SMART
# Get Hard Drive listing - MUST support SMART
# variable smartdrives
if [ $softver != "Linux" ]; then
smartdrives=$(for drive in $(sysctl -n kern.disks); do
if [ "$(smartctl -i /dev/"${drive}" | grep "SMART support is: Enabled")" ] && ! [ "$(smartctl -i /dev/"${drive}" | grep "Solid State Device")" ]; then
printf "%s " "${drive}"
fi
done | awk '{for (i=NF; i!=0 ; i--) print $i }' | tr ' ' '\n' | sort | tr '\n' ' ')
else
smartdrives=$(for drive in $(fdisk -l | grep "Disk /dev/sd" | cut -c 11-13 | tr '\n' ' '); do
if [ "$(smartctl -i /dev/"${drive}" | grep "SMART support is: Enabled")" ] && ! [ "$(smartctl -i /dev/"${drive}" | grep "Solid State Device")" ]; then
printf "%s " "${drive}"
fi
done | awk '{for (i=NF; i!=0 ; i--) print $i }' | tr ' ' '\n' | sort | tr '\n' ' ')
fi
# Get SSD listing - MUST suport SMART
# variable smartdrivesSSD
if [ $softver != "Linux" ]; then
smartdrivesSSD=$(for drive in $(sysctl -n kern.disks); do
if [ "$(smartctl -i /dev/"${drive}" | grep "SMART support is: Enabled")" ] && [ "$(smartctl -i /dev/"${drive}" | grep "Solid State Device")" ]; then
targument="$(smartctl -i /dev/"${drive}" | grep "Serial Number:" | awk '{print $3}')";
case $targument in
${IGNORESN1}|${IGNORESN2}|${IGNORESN3}|${IGNORESN4}|${IGNORESN5}|${IGNORESN6}|${IGNORESN7}|${IGNORESN8});;
*)
printf "%s " "${drive}";;
esac
fi
done | awk '{for (i=NF; i!=0 ; i--) print $i }' | tr ' ' '\n' | sort | tr '\n' ' ')
else
smartdrivesSSD=$(for drive in $(fdisk -l | grep "Disk /dev/sd" | cut -c 11-13 | tr '\n' ' '); do
if [ "$(smartctl -i /dev/"${drive}" | grep "SMART support is: Enabled")" ] && [ "$(smartctl -i /dev/"${drive}" | grep "Solid State Device")" ]; then
targument="$(smartctl -i /dev/"${drive}" | grep "Serial Number:" | awk '{print $3}')";
case $targument in
${IGNORESN1}|${IGNORESN2}|${IGNORESN3}|${IGNORESN4}|${IGNORESN5}|${IGNORESN6}|${IGNORESN7}|${IGNORESN8});;
*)
printf "%s " "${drive}";;
esac
fi
done | awk '{for (i=NF; i!=0 ; i--) print $i }' | tr ' ' '\n' | sort | tr '\n' ' ')
fi
### Get the non-SSD listing - MUST support SMART
# variable nonsmartdrives
if [ $softver != "Linux" ]; then
nonsmartdrives=$(for drive in $(sysctl -n kern.disks); do
if [ ! "$(smartctl -i /dev/"${drive}" | grep "SMART support is: Enabled")" ]; then
targument="$(smartctl -i /dev/"${drive}" | grep "Serial Number:" | awk '{print $3}')";
case $targument in
${IGNORESN1}|${IGNORESN2}|${IGNORESN3}|${IGNORESN4}|${IGNORESN5}|${IGNORESN6}|${IGNORESN7}|${IGNORESN8});;
*)
printf "%s " "${drive}";;
esac
fi
done | awk '{for (i=NF; i!=0 ; i--) print $i }' | tr ' ' '\n' | sort | tr '\n' ' ')
else
nonsmartdrives=$(for drive in $(fdisk -l | grep "Disk /dev/sd" | cut -c 11-13 | tr '\n' ' '); do
if [ ! "$(smartctl -i /dev/"${drive}" | grep "SMART support is: Enabled")" ]; then
targument="$(smartctl -i /dev/"${drive}" | grep "Serial Number:" | awk '{print $3}')";
case $targument in
${IGNORESN1}|${IGNORESN2}|${IGNORESN3}|${IGNORESN4}|${IGNORESN5}|${IGNORESN6}|${IGNORESN7}|${IGNORESN8});;
*)
printf "%s " "${drive}";;
esac
fi
done | awk '{for (i=NF; i!=0 ; i--) print $i }' | tr ' ' '\n' | sort | tr '\n' ' ')
fi
### DONE Getting List of Drives
pools=$(zpool list -H -o name)
###### Email pre-formatting
### Set some of the email headers before conditional headers
(
echo "MIME-Version: 1.0"
echo "Content-Type: multipart/mixed; boundary=${boundary}"
) > "$logfile"
###### Config backup (if enabled)
if [ "$configBackup" == "true" ]; then
# Set up file names, etc for later
tarfile="/tmp/config_backup.tar.gz"
filename="$(date "+FreeNAS_Config_%Y-%m-%d")"
### Test config integrity
if ! [ "$(sqlite3 /data/freenas-v1.db "pragma integrity_check;")" == "ok" ]; then
# Config integrity check failed, set MIME content type to html and print warning
(
echo "--${boundary}"
echo "Content-Type: text/html"
echo "Automatic backup of FreeNAS configuration has failed! The configuration file is corrupted!"
echo "You should correct this problem as soon as possible!"
echo "
"
) >> "$logfile"
else
# Config integrity check passed; copy config db, generate checksums, make .tar.gz archive
cp /data/freenas-v1.db "/tmp/${filename}.db"
md5 "/tmp/${filename}.db" > /tmp/config_backup.md5
sha256 "/tmp/${filename}.db" > /tmp/config_backup.sha256
(
cd "/tmp/" || exit;
tar -czf "${tarfile}" "./${filename}.db" ./config_backup.md5 ./config_backup.sha256;
)
(
# Write MIME section header for file attachment (encoded with base64)
echo "--${boundary}"
echo "Content-Type: application/tar+gzip"
echo "Content-Transfer-Encoding: base64"
echo "Content-Disposition: attachment; filename=${filename}.tar.gz"
base64 "$tarfile"
# Write MIME section header for html content to come below
echo "--${boundary}"
echo "Content-Type: text/html"
) >> "$logfile"
# If logfile saving is enabled, copy .tar.gz file to specified location before it (and everything else) is removed below
if [ "$saveBackup" == "true" ]; then
cp "${tarfile}" "${backupLocation}/${filename}.tar.gz"
fi
rm "/tmp/${filename}.db"
rm /tmp/config_backup.md5
rm /tmp/config_backup.sha256
rm "${tarfile}"
fi
else
# Config backup enabled; set up for html-type content
(
echo "--${boundary}"
echo "Content-Type: text/html"
) >> "$logfile"
fi
###### Report Summary Section (html tables)
### zpool status summary table
(
# Write HTML table headers to log file; HTML in an email requires 100% in-line styling (no CSS or