Created
May 15, 2022 12:58
-
-
Save erkr/843b9c7c2b6fa511c09a5773029c32e0 to your computer and use it in GitHub Desktop.
TrueNAS Auto shutdown script
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
# AutoShutdown.sh (c)2022, MIT license, by Eric Kreuwels | |
# USAGE: AutoShutdown [test|echo] | |
# | |
# Shutdown TrueNas systems when idle for a defined timeout period (default 1800 seconds), | |
# Active during a configurable monitoring timeframe (default between 01:00:00 to 06:30:00). | |
# Run "AutoShutdown test" to just evaluate the idle checks | |
# Run "AutoShutdown echo" to echo to stdout instead of the log file | |
# For normal operation add a "post init script" in truenas: bash /<path>/AutoShutdown.sh& | |
# Idle is defined as: | |
# No DISKIO on the configurable set of "pools" | |
# - make sure the system-Data and syslog is not configured on one of these pools | |
# - If you make pools array empty, this check is skipped | |
# No Scrub processes (any pool) | |
# No Resilver processes (any pool) | |
# No Smart self tests on a set of configurable "disks" | |
# - If you make disks array empty, this check is skipped | |
# No external TCP connections | |
# No active PLEX streams | |
# - This check is skipped unless the plex token is configured | |
# By default, only the relevant state changes are logged to LOGFILE | |
# - a new logfile is created at startup of the script | |
# - a history of 5 is preserved to allow a review of previueos session | |
# started and shutdown initiated events posted to syslog | |
# | |
NAME=`basename $0 .sh` | |
if [ $(/usr/bin/id -u) -ne 0 ]; then | |
echo "Run $NAME as root. Abort" >&2 | |
exit 1 | |
fi | |
if [ "$1" = "test" ] || [ "$1" = "echo" ]; then | |
LOGFILE="/dev/stdout" # for interactive debug/evaluation in a shell | |
PIDFILE="/dev/null" | |
else | |
LOGFILE="/var/log/$NAME.log" # normal execution in the background | |
PIDFILE="/var/run/$NAME.pid" | |
# keep some logging history | |
for i in {4..1} | |
do | |
if [ -f "/var/log/$NAME.log.$i" ]; then | |
mv "/var/log/$NAME.log.$i" "/var/log/$NAME.log.$(($i+1))" | |
fi | |
done | |
if [ -f $LOGFILE ]; then | |
mv $LOGFILE "/var/log/$NAME.log.1" | |
fi | |
fi | |
# Time definitions to shutdown when idle. | |
# monitoring is enabled from strStart to strStop. | |
strStart="01:00:00" # 24h format hh:mm:ss | |
#strStop="01:00:00" # If strStart=strStop monitoring is enabled always | |
strStop="06:30:00" # 24h format hh:mm:ss | |
idleTimeout=1800 # idle time in seconds before shutting down | |
minLoopDelay=20 # minimal delay for the loop | |
# SMART Self test parameters. | |
disks=() # Just make it empty to disable this test | |
# disks=("/dev/ada0" "/dev/ada1" "/dev/ada2" "/dev/ada3" "/dev/ada4" "/dev/ada5") | |
# Disk IO parameters | |
# pools=("myPool1" "myPool2" "myPool3") # Your ZFS pool(s) to check Disk IO; | |
pools=() # just make it empty to skip this test | |
interval=10 # The time interval for which a pool is tested for Disk IO activity | |
# list of TCP connections to be ignored (keep the first one, needed to filter closed connections): | |
# TcpExcludes=("^\? *\? *\? *\?" "192.168.xxx.yyy:3493") | |
TcpExcludes=("^\? *\? *\? *\?") | |
# Plex plugin active connections | |
plexHOST="192.168.xxx.yyy" | |
plexTOKEN="" # make this one empty to disable this check | |
# define to get mail notifications | |
notify=0 #0 to disable mail notifications, 1 enable | |
mailAddress="[email protected]" | |
# don't edit: | |
nrDisks=${#disks[@]} # number of disks to verify for running SMART Selftests | |
nrPools=${#pools[@]} # number of pools to verify on Disk IO | |
status=("Idle" "Active") | |
# the Check functions | |
timeWindow=-1 # boolean used by checkTime() (1=Not in time frame, 0=if in time frame, -1=not defined yet) | |
checkTime() | |
{ | |
strNow=$( /bin/date +"%H:%M:%S" ) | |
if [ "$1" = "echo" ]; then | |
echo "== Check of $strNow is within the monitoring range $strStart -- $strStop" >> $LOGFILE | |
fi | |
result=1 | |
if [ $strStart == $strStop ]; then | |
# time windows disabled: full 24H true | |
result=0 | |
elif [ $strStart \> $strStop ]; then | |
# time window including midnight | |
if [ $strStart \< $strNow ] || [ $strStop \> $strNow ] ; then | |
result=0 # Now is in time window | |
fi | |
else | |
# a normal time window | |
if [ $strStart \< $strNow ] && [ $strStop \> $strNow ] ; then | |
result=0 # Now is in time window | |
fi | |
fi | |
# report only if in time window state changed | |
if [ $timeWindow -ne $result ] || [ "$1" = "echo" ]; then | |
if [ $result -eq 1 ]; then | |
echo `date`": Outside monitoring timeframe ($strStart - $strStop)" >> $LOGFILE | |
timeWindow=1 | |
else | |
if [ $strStart == $strStop ]; then | |
echo `date`": Monitoring enabled for the full 24h" >> $LOGFILE | |
else | |
echo `date`": Within monitoring timeframe ($strStart - $strStop)" >> $LOGFILE | |
fi | |
timeWindow=0 | |
fi | |
fi | |
return $timeWindow | |
} | |
NrDiskIO=-1 # Initial number active disks; -1 not defined | |
CheckDisksIO() | |
{ | |
if [ $nrPools = "0" ]; then | |
return 0 | |
fi | |
if [ "$1" = "echo" ]; then | |
echo "== Check DiskIO (takes $interval seconds per pool):" >> $LOGFILE | |
fi | |
NrNewIO=0 | |
for p in "${pools[@]}" | |
do | |
diskIOinfo=$( zpool iostat $p $interval 2 ) | |
NewDiskIO=$( echo "$diskIOinfo" | tail +5 | egrep -c "0 *0 *0 *0" ) | |
if [ $NewDiskIO != "1" ]; then | |
NrNewIO=$(( $NrNewIO+1 )) | |
fi | |
if [ "$1" = "echo" ]; then | |
echo "== DiskIO on $p: ${status[$NewDiskIO != "1"]}" >> $LOGFILE | |
echo "$diskIOinfo" >> $LOGFILE | |
fi | |
done | |
if [ "$NrDiskIO" != "$NrNewIO" ] || [ "$1" = "echo" ]; then # Report changes only | |
echo `date`": DiskIO is ${status[$NrNewIO != "0"]}. $NrNewIO pool(s) with DiskIO" >> $LOGFILE | |
fi | |
NrDiskIO=$NrNewIO | |
return $((NrDiskIO)) # 0 is idle | |
} | |
NoScrubbing=-1 # Number active scrubbing jobs; -1 not defined | |
CheckScrub() | |
{ | |
if [ "$1" = "echo" ]; then | |
echo "== Check for scrubbing tasks:" >> $LOGFILE | |
fi | |
NoTasks=$( zpool status | egrep -c "scrub in progress" ) | |
if [ $NoScrubbing != $NoTasks ] || [ "$1" = "echo" ]; then # Report changes only | |
echo `date`": Scrubbing task(s) ${status[$NoTasks != "0"]}. $NoTasks Scrubbing task(s)" >> $LOGFILE | |
fi | |
NoScrubbing=$NoTasks | |
return $((NoScrubbing)) # 0 is idle | |
} | |
NoResilver=-1 # Number active scrubbing jobs; -1 not defined | |
CheckResilver() | |
{ | |
if [ "$1" = "echo" ]; then | |
echo "== Check for Resilver tasks:" >> $LOGFILE | |
fi | |
NoTasks=$( zpool status | egrep -c "resilver in progress" ) | |
if [ $NoResilver != $NoTasks ] || [ "$1" = "echo" ]; then # Report changes only | |
echo `date`": Resilver task(s) ${status[$NoTasks != "0"]}. $NoTasks Resilver task(s)" >> $LOGFILE | |
fi | |
NoResilver=$NoTasks | |
return $((NoResilver)) # 0 is idle | |
} | |
NrSmartTests=-1 # Initial number; -1 not defined | |
CheckSmartTests() | |
{ | |
if [ $nrDisks = "0" ]; then | |
return 0 | |
fi | |
if [ "$1" = "echo" ]; then | |
echo "== Check for active SMART self-tests:" >> $LOGFILE | |
fi | |
NrNewTests=0 | |
for d in "${disks[@]}" | |
do | |
NewSmartTest=$( smartctl -c $d | egrep -c "Self-test routine in progress" ) | |
if [ $NewSmartTest != "0" ]; then | |
NrNewTests=$(( $NrNewTests+1 )) | |
fi | |
if [ "$1" = "echo" ]; then | |
echo "== SMART self-test on $d: ${status[$NewSmartTest != "0"]}" >> $LOGFILE | |
fi | |
done | |
if [ $NrSmartTests != $NrNewTests ] || [ "$1" = "echo" ]; then # Report changes only | |
echo `date`": Smart self-test is ${status[$NrNewTests != "0"]}. $NrNewTests SMART self-test" >> $LOGFILE | |
fi | |
NrSmartTests=$NrNewTests | |
return $((NrSmartTests)) # 0 is idle | |
} | |
NrTcpConnections=-1 # Number active TCP connections; -1 not defined | |
CheckTCP() | |
{ | |
if [ "$1" = "echo" ]; then | |
echo "== Check for Active TCP connections:" >> $LOGFILE | |
fi | |
TcpConnections=$( /usr/bin/sockstat -cLP tcp ) | |
#echo "$TcpConnections" | |
# filter the excluded connections | |
for n in "${TcpExcludes[@]}" | |
do | |
FilteredConnections=$( echo "$TcpConnections" | grep -v "$n" ) | |
TcpConnections=$FilteredConnections | |
#echo "Filtered by $n:" | |
#echo "$TcpConnections" | |
done | |
# count the remaining connections | |
NrNewConnections=$( echo "$TcpConnections" | /usr/bin/tail -n +2 | wc -l | sed 's/^[[:blank:]]*//;s/[[:blank:]]*$//' ) | |
if [ $NrNewConnections != $NrTcpConnections ] || [ "$1" = "echo" ]; then # Report changes only | |
echo `date`": Network is ${status[$NrNewConnections != "0"]}. $NrNewConnections external TCP connections" >> $LOGFILE | |
if [ $NrNewConnections != "0" ]; then | |
echo "$TcpConnections" >> $LOGFILE | |
fi | |
fi | |
NrTcpConnections=$NrNewConnections | |
return $((NrTcpConnections)) # 0 is idle | |
} | |
NoPlexConnections=-1 # Number active plex streams; -1 not defined | |
CheckPlex() | |
{ | |
if [ "$plexTOKEN" = "" ]; then # optional Plex check | |
return 0 | |
fi | |
if [ "$1" = "echo" ]; then | |
echo "== Check Plex plugin for active streams:" >> $LOGFILE | |
fi | |
ping -c1 -t1 $plexHOST > /dev/null | |
if [ $? -eq 0 ]; then # Plex server responds | |
NewConnections=$( curl --silent http://$plexHOST:32400/status/sessions -H "X-Plex-Token: $plexTOKEN"| xmllint --xpath 'string(//MediaContainer/@size)' - ) | |
if [ $NewConnections != $NoPlexConnections ] || [ "$1" = "echo" ]; then # Report changes only | |
echo `date`": Plex is ${status[$NewConnections != "0"]}. $NewConnections active streams" >> $LOGFILE | |
fi | |
else | |
NewConnections=0 | |
if [ $NewConnections != $NoPlexConnections ] || [ "$1" = "echo" ]; then # Report changes only | |
echo `date`": Seems Plex plugin is NOT running." >> $LOGFILE | |
fi | |
fi | |
NoPlexConnections=$NewConnections | |
return $((NoPlexConnections)) # 0 is idle | |
} | |
tmReported=0 | |
reportIdle() # report once per 2 minutes, input param: tmIdle | |
{ | |
tmNow=$( date "+%s" ) | |
if [ $(( tmNow-tmReported )) -gt 120 ]; then | |
echo `date`": System idle for $1 seconds" >> $LOGFILE | |
tmReported=$tmNow | |
fi | |
} | |
# Check if we should just once run all tests (test option): | |
if [ "$1" = "test" ] ; then | |
echo "=== Test run to show all idle conditions ===" | |
checkTime "echo" | |
echo "checkTime: ${status[$? == 0]}" # note: == in stead of != for checktime | |
CheckDisksIO "echo" | |
echo "CheckDisksIO: ${status[$? != 0]}" | |
CheckScrub "echo" | |
echo "CheckScrub: ${status[$? != 0]}" | |
CheckResilver "echo" | |
echo "CheckResilver: ${status[$? != 0]}" | |
CheckSmartTests "echo" | |
echo "CheckSmartTests: ${status[$? != 0]}" | |
CheckTCP "echo" | |
echo "CheckTCP: ${status[$? != 0]}" | |
CheckPlex "echo" | |
echo "CheckPlex: ${status[$? != 0]}" | |
echo "=== Test Finished ===" | |
exit 0 | |
fi | |
# Point reached to start monitoring loop (run script as root in the background ) | |
# Create a new log and pid file every time the script is executed | |
echo $$ > $PIDFILE | |
echo `date`": $NAME Log started, PID=$$" > $LOGFILE | |
logger -p user.notice -t $NAME "Idle system monitoring started, PID=$$" | |
# globals used by loop | |
tmIdleStart=$( date "+%s" ) # start time since measured idle | |
while [ true ]; do | |
# check if the current time falls within the monitoring time frame | |
if ! checkTime $1; then # Not in monitoring time frame | |
sleep $minLoopDelay | |
tmIdleStart=$( date "+%s" ) | |
NrDiskIO=-1 | |
NoScrubbing=-1 | |
NoResilver=-1 | |
NrSmartTests=-1 | |
NrTcpConnections=-1 | |
NoPlexConnections=-1 | |
else | |
tmBeginLoop=$( date "+%s" ) | |
# perform idle checks | |
if ! CheckTCP $1; then | |
tmIdleStart=$tmBeginLoop # returned Not Idle | |
elif ! CheckPlex $1; then | |
tmIdleStart=$tmBeginLoop | |
elif ! CheckScrub $1; then | |
tmIdleStart=$tmBeginLoop | |
elif ! CheckResilver $1; then | |
tmIdleStart=$tmBeginLoop | |
elif ! CheckSmartTests $1; then | |
tmIdleStart=$tmBeginLoop | |
elif ! CheckDisksIO $1; then | |
tmIdleStart=$tmBeginLoop | |
else # is Idle | |
reportIdle $(($( date "+%s" )-tmIdleStart)) | |
fi | |
# check idle timeout conditions | |
tmEndLoop=$( date "+%s" ) | |
# check how long the system is idle | |
tmIsIdle=$(( tmEndLoop-tmIdleStart )) | |
if [ $tmIsIdle -gt $idleTimeout ]; then # time to shutdown the system | |
echo `date`": Initiate ShutDown (system was $tmIsIdle seconds idle)" >> $LOGFILE | |
logger -p user.notice -t $NAME "Initiate ShutDown (system $tmIsIdle seconds idle)" | |
if [ $# -eq 0 ] && [ $notify -eq 1 ]; then | |
cat $LOGFILE | mail -v -s "$NAME logfile" $mailAddress | |
fi | |
sleep 1 | |
/sbin/shutdown -p +5sec | |
exit 0 | |
fi | |
# Some Sleep time if needed (CheckDisksIO delays loop already) | |
tmDiff=$((tmEndLoop-tmBeginLoop)) | |
if [ $tmDiff -lt $minLoopDelay ]; then # add some sleep | |
sleep $((minLoopDelay - tmDiff)) | |
fi | |
fi | |
done | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment