Skip to content

Instantly share code, notes, and snippets.

@erkr
Created May 15, 2022 12:58
Show Gist options
  • Save erkr/843b9c7c2b6fa511c09a5773029c32e0 to your computer and use it in GitHub Desktop.
Save erkr/843b9c7c2b6fa511c09a5773029c32e0 to your computer and use it in GitHub Desktop.
TrueNAS Auto shutdown script
#!/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="yourmail@gmail.com"
# 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