#!/bin/bash # ====================================================================== # # IML BACKUP STORAGE HELPER # # ---------------------------------------------------------------------- # # SYNTAX: # storage_helper.sh PARAMETERS # # PARAMETERS: # # register [hostname] # init backup for hostname; backup client must interrupt/ repeat # return code is <> 0 # # unregister [hostname] [statuscode] # send info that a backup was done to delete the connection flag # for the given host. # # status # show current connections # # usage # show diskusage infos # # ---------------------------------------------------------------------- # 2019-11-01 hahn write more clear log messages # 2021-06-xx hahn added setactive + setinactive # 2021-06-16 hahn added backupstatus # 2021-06-18 hahn store backup status in status files ... do not grep frpom logs # 2021-06-21 hahn use single status file; added backup size # 2021-01-22 hahn show inactive backups in status page; colored lines by status # 2021-11-12 hahn backup status: add handling for backup exitcode # 2022-07-06 hahn backup status: show clear message on not started/ deleted backups # 2024-03-22 hahn backup status: check age of restic backups when not using ssh # ====================================================================== # ---------------------------------------------------------------------- # CONFIG # ---------------------------------------------------------------------- . `dirname $0`/inc_config.sh # set in setBackupclient sBackupClient=dummy # value is set in _showConnectionCount iConnections=0 # max age of last backup in h iMaxbackupAge=36 # ---------------------------------------------------------------------- # FUNCTIONS # ---------------------------------------------------------------------- # ------------------------------------------------------------ # color text # ------------------------------------------------------------ function color(){ sColorcode="" case $1 in "reset") sColorcode="0" ;; "head") sColorcode="33" # yellow ;; "cmd") sColorcode="94" # light blue ;; "input") sColorcode="92" # green ;; "ok") sColorcode="92" # green ;; "error") sColorcode="91" # red ;; "warn") sColorcode="33" # yellow ;; "active") sColorcode="94" # light blue ;; "disabled") sColorcode="90" # gray ;; esac if [ ! -z ${sColorcode} ]; then echo -ne "\e[${sColorcode}m" fi } function _checkDir(){ typeset -i iCount mydir=$1 if [ -d $mydir ]; then iCount=`find $mydir -type f -cmin -720 | wc -l ` if [ $iCount -eq 0 ]; then color error fi echo -n "$iCount " echo -n "`du -hs $mydir #| awk ' { print $1 } '` " if [ $iCount -eq 0 ]; then echo -n "<<< WARNING: No changed files detected !!! rc=1" color reset iErrors=$iErrors+1 fi echo else echo SKIP: $mydir is not a directory fi } # ------------------------------------------------------------ # logging # ------------------------------------------------------------ # add a line into logfile # param string message (date will be added as prefix) function _addLog(){ # echo `date` $* >>${sLogDir}${sLogfile} echo `date "+%Y-%m-%d %H:%M:%S"` $* >>${sLogfile} } # get content of all logfiles function _getAllLogs(){ ls $sLogdir/*.gz >/dev/null 2>&1 && ( zcat $sLogdir/*.gz ; cat $sLogfile ) } # get a list of all servers in the log function _getLoggedServers(){ _getAllLogs | cut -f 2 -d '[' | cut -f 1 -d ']' | sort -u } # get timestamp of last backup start of a given server # global string sBackupClient FQDN of current server function _getLastBackupstart(){ #test -r "${sStatusDir}/${sBackupClient}_start" && stat -c %Y "${sStatusDir}/${sBackupClient}_start" #echo $tsBackupStart #local _srv=$1 #_getAllLogs | sort | grep "\[$_srv\]" | grep ACCEPTED | tail -1 | cut -f -2 -d " " local tsBackupStart= test -r "$sStatusDir/${sBackupClient}" && . "$sStatusDir/${sBackupClient}" echo $tsBackupStart } # get timestamp of last backup end of a given server # global string sBackupClient FQDN of current server function _getLastBackupend(){ # test -r "${sStatusDir}/${sBackupClient}_end" && stat -c %Y "${sStatusDir}/${sBackupClient}_end" # local _srv=$1 # _getAllLogs | sort | grep "\[$_srv\]" | grep REMOVED | tail -1 | cut -f -2 -d " " local tsBackupEnd= test -r "$sStatusDir/${sBackupClient}" && . "$sStatusDir/${sBackupClient}" echo $tsBackupEnd } # get status code of last backup from status file # global string sBackupClient FQDN of current server function _getLastBackupstatus(){ if [ -r "${sStatusDir}/${sBackupClient}" ]; then local backupRc= . "${sStatusDir}/${sBackupClient}" echo $backupRc else echo "-2" fi } # get size of backup data from status file # global string sBackupClient FQDN of current server function _getBackupsize(){ local size= test -r "$sStatusDir/${sBackupClient}" && . "$sStatusDir/${sBackupClient}" test -z "$size" && echo "[ no metadata: not started yet or deleted ]" echo $size } # check if a backup for current server is running # it returns 0 for no and 1 for yes # global string sBackupClient FQDN of current server function _isRunning(){ sTouchfile=`getConnFilename $sBackupClient` test -f "$sTouchfile" && echo 1 test -f "$sTouchfile" || echo 0 } # check if a backup for current server is inactive # it returns 0 for no and 1 for yes # global string sBackupClient FQDN of current server # global string sBackupBasedir backup directory function _isInactive(){ ls ${sBackupBasedir}/*/$sBackupClient/inactive.txt >/dev/null 2>/dev/null if [ $? -eq 0 ]; then echo 1 else echo 0 fi } # get age of a file in sec # param string filename to test # function _getFileAge(){ echo $(($(date +%s) - $(date +%s -r "$1"))) } # ------------------------------------------------------------ # slot handling # ------------------------------------------------------------ # show current connction count function _showConnectionCount(){ iConnections=`getConnectionCount` sMax=$iMaxConnections if [ $iMaxConnections -eq 0 ]; then sMax="0 [unlimited]" fi echo STATUS: $iConnections existing connection\(s\). Allowed maximum is $sMax. } # helper: get filename for backup client connection function getConnFilename(){ # sBackupClient=$1 echo $sConncectionDir/$sBackupClient } # allow a slot for a new connection # see PUBLIC_register function addConnection(){ sTouchfile=`getConnFilename $sBackupClient` if [ -f "$sTouchfile" ]; then echo INFO: server [$sBackupClient] has a slot already _addLog "IGNORE request for a slot - [$sBackupClient] has a slot already" else touch "$sTouchfile" echo "client='$sBackupClient'" > "$sTouchfile" echo "# $( date """+%Y-%m-%d %H:%M:%S""" )" >> "$sTouchfile" echo "tsBackupStart=$( date +%s )" >> "$sTouchfile" _addLog "REQUEST a slot for server [$sBackupClient]" fi } # remove slot of a connection # see PUBLIC_unregister # param int backup status function freeConnection(){ local _status=$1 test -z "$1" && _status=-1 sTouchfile=`getConnFilename $sBackupClient` if [ -f "$sTouchfile" ]; then echo "# $( date """+%Y-%m-%d %H:%M:%S""" )" >> "$sTouchfile" echo "tsBackupEnd=$( date +%s )" >> "$sTouchfile" echo "backupRc=$_status" >> "$sTouchfile" echo "size='$( du -hs ${sBackupBasedir}/*/$sBackupClient 2>/dev/null )'" >> "$sTouchfile" mv "$sTouchfile" "$sStatusDir/${sBackupClient}" else echo INFO: server [$sBackupClient] had no reserved slot _addLog "IGNORE freeing the connection - [$sBackupClient] had no reserved slot" fi } # count of conection slots function getConnectionCount(){ # who | grep "^${sBackupUser}" | wc -l find $sConncectionDir -type f | wc -l } # helper function: set internal var for hostname of backup client function setBackupclient(){ sBackupClient=`echo $1 | sed s"#[^a-zA-Z\.0-9\-]#_#g"` if [ -z $sBackupClient ]; then echo ERROR: no backup client was given exit 1 fi # read last backup status typeset -i tsBackupStart=0 typeset -i tsBackupEnd=0 typeset -i backupRc=-2 # test -r "$sStatusDir/${sBackupClient}" && . "$sStatusDir/${sBackupClient}" } # list repositories ... used in setactive/ setinactive function _listRepodirs(){ for mydir in $( ls -1d ${sBackupBasedir}/*/* | grep -vE '(_active|somethinglsetodisable)' ) do echo -n "$mydir " test -f "$mydir/inactive.txt" && ( color error ; echo -n "(inactive)" ) test -f "$mydir/inactive.txt" || ( color ok ; echo -n "(active)" ) color reset echo done } # ---------------------------------------------------------------------- # PUBLIC FUNCTIONS # ---------------------------------------------------------------------- function PUBLIC_backupstatus(){ local ierrors; typeset -i ierrors=0 local tbl # _showConnectionCount echo if ! _getLoggedServers | grep -qc . ; then iKeepBackups=180 local iAgeH; typeset -i iAgeH local iAgeD; typeset -i iAgeD local sAge local mydir local bIsInactive; typeset -i bIsInactive local iCounter; typeset -i iCounter=0 echo "Age of Restic backup repositories" for myresticsnapshotdir in $( ls -1trd ${sBackupBasedir}/*/*/snapshots ) do iCounter=$iCounter+1 mydir=$( dirname "${myresticsnapshotdir}") sBackupClient=$( basename ${mydir} ) iAgeH=$( _getFileAge "${myresticsnapshotdir}" )/60/60 iAgeD=iAgeH/24 bIsInactive=$( _isInactive ) sMore= sStatus="$(color ok)." test $bIsInactive -eq 1 && sStatus="$(color disabled)D" sFile=${mydir}/inactive.txt if [ "$bIsInactive" -eq "1" ]; then typeset -i iAge=`_getFileAge "$sFile"` typeset -i iDays=$iKeepBackups-$iAge/60/60/24 sMore=" - INFO: keeping it $iKeepBackups d .. $iDays d left." if [ "$iDays" -lt 0 ]; then sMore=" - DELETE: overdued $iDays d." sStatus="$(color error)E" ierrors=$ierrors+1 fi elif [ $iAgeH -ge 24 ]; then sStatus="$(color error)E" ierrors=$ierrors+1 fi sAge="$iAgeH h" if [ $iAgeH -gt 96 ]; then sAge="$iAgeD d" fi printf "%2s %7s %s\n" "$sStatus" "$sAge" "${mydir}$sMore" done color reset echo echo "Legend" echo ". OK | D disabled | E error" echo echo "total : $iCounter servers" echo "errors: $ierrors" echo else tbl="%s %-45s | %-20s | %-20s | %8s | %3s | %7s | %s \n" echo "This table shows the time and duration [s] of the last backup for each server." echo printf "$tbl" " " "server" "start" "end" "duration" "rc" "age [h]" "size" echo "-----------------------------------------------------------------------------------------------------------------------------" for myserver in $( _getLoggedServers ) do # get data setBackupclient $myserver typeset -i local istart="$( _getLastBackupstart )" typeset -i local iend="$( _getLastBackupend )" typeset -i local bIsRunning=$( _isRunning ) typeset -i local bIsInactive=$( _isInactive ) typeset -i local iLastStatus=$( _getLastBackupstatus ) local size=$( _getBackupsize ) local tstart=$( date +"%Y-%m-%d %H:%M:%S" -d @${istart} ) local tend=$( date +"%Y-%m-%d %H:%M:%S" -d @${iend} ) typeset -i local iduration=${iend}-${istart} typeset -i local iage=$( date +%s )-${iend} typeset -i local iagehours=$iage/60/60 local sduration=$iduration test $iduration -lt 0 && sduration="RUNNING" sStatusRun="$(color ok)." test $iend -eq 0 && sStatusRun="$(color warn)?" test $bIsRunning -eq 1 && sStatusRun="$(color active)R" test $bIsInactive -eq 1 && sStatusRun="$(color disabled)D" # check values if [ $iLastStatus -gt 0 ]; then ierrors=$ierrors+1 sStatusRun="$(color error)E" else if [ $iend -gt 0 -a $iagehours -gt $iMaxbackupAge -a $bIsInactive -eq 0 ]; then ierrors=$ierrors+1 sStatusRun="$(color error)E" fi fi if [ $iend -eq 0 ]; then tstart="-" tend="-" sduration="" iagehours="" fi # output printf "$tbl" "$sStatusRun" "$myserver" "${tstart}" "${tend}" "${sduration}" "$iLastStatus" "${iagehours}" "$size" done color reset echo echo "Legend" echo ". OK | ? not started | R running | D disabled | E error" echo echo "total : $( _getLoggedServers | wc -l ) servers" echo "errors: $ierrors" echo fi echo "rc=$ierrors" exit $ierrors } function PUBLIC_setactive(){ echo --- list of inactive backup repositories _listRepodirs | grep '\(inactive\)' echo echo -n "Repo to activate again >" read inputdir if [ -f "$inputdir/inactive.txt" ]; then cat "$inputdir/inactive.txt" echo echo Removing $inputdir/inactive.txt rm -f "$inputdir/inactive.txt" echo If it was a mistake run $( basename $0 ) setinactive else echo SKIP [$inputdir] fi echo } function PUBLIC_setinactive(){ echo --- list of backup repositories _listRepodirs echo echo -n "Repo to deactivate >" read inputdir if [ -d "$inputdir" -a ! -f "$inputdir/inactive.txt" ]; then echo -n "Optional short message >" read mymessage echo "$( date ) | $mymessage" > "$inputdir/inactive.txt" ls -l "$inputdir/inactive.txt" else echo SKIP [$inputdir] fi echo } # return backup directory for a client function PUBLIC_getBackupdir(){ setBackupclient $1 echo $sBackupBasedir/$sBackupClient exit 0 } # register a hostname to start backups # param string FQDN function PUBLIC_register(){ setBackupclient $1 echo "REGISTER $1" echo addConnection # iConnections=`getConnectionCount` _showConnectionCount >/dev/null if [ $iConnections -le $iMaxConnections -o $iMaxConnections -eq 0 ]; then sStatus="OK" iExit=0 _addLog "ACCEPTED [$sBackupClient] got a slot." else sStatus="FAILED" iExit=1 freeConnection _addLog "REJECTED maximum of $iMaxConnections connections exceeded - request of server [$sBackupClient] failed." fi _showConnectionCount echo $sStatus - rc=$iExit exit $iExit } # unregister a backup slot # param string FQDN # param int optional: return code of the backup run function PUBLIC_unregister(){ typeset -i local _status=$2 setBackupclient $1 echo "UNREGISTER $1" echo freeConnection $_status _addLog "REMOVED slot for [$sBackupClient]" _showConnectionCount exit 0 } # show used slots function PUBLIC_status(){ _showConnectionCount echo if [ $iConnections -gt 0 ]; then find $sConncectionDir -type f -exec ls -l {} \; | nl echo fi exit 0 } function PUBLIC_usage(){ typeset -i iErrors=0 # can be "full" sDetail=$1 echo "DISK USAGE:" echo echo "time : `date`" echo "Backups are located in : $sBackupBasedir" echo "dirs : `find ${sBackupBasedir} -type d | wc -l`" echo "files : `find ${sBackupBasedir} -type f | wc -l`" echo "volume size : `df -h ${sBackupBasedir} | tail -1 | awk '{ print $2 }'`" echo "backup size : `du -hs ${sBackupBasedir} | cut -f 1 `" echo "free : `df -h ${sBackupBasedir} | tail -1 | awk '{ print $4 }'`" echo echo sizes: # ls -l "${sBackupBasedir}" | grep "^d" | fgrep -v "`basename $sConncectionDir`" du -hs ${sBackupBasedir}* echo echo "changed files last 12 hours and used diskspace:" echo _checkDir ${sBackupBasedir} echo if [ ! -z ${sDetail} ]; then sFilter=${sDetail} if [ "${sDetail}" = "full" ]; then sFilter= fi echo "... and by server and backup sets:" echo for myserver in `ls -1 ${sBackupBasedir} | fgrep -v "_active" | grep "$sFilter"` do color head echo ----- $myserver color reset echo _checkDir ${sBackupBasedir}/$myserver echo for subdir in `ls -1 ${sBackupBasedir}/$myserver` do _checkDir ${sBackupBasedir}/$myserver/$subdir done echo done fi # --- show result echo -n "Status: " if [ $iErrors -eq 0 ]; then echo "OK" else color error echo "$iErrors Errors detected." fi color reset echo rc=$iErrors exit $iErrors } # ---------------------------------------------------------------------- # MAIN # ---------------------------------------------------------------------- if [ ! -d "$sConncectionDir" ]; then mkdir -p "$sConncectionDir" || exit 1 fi if [ ! -d "$sStatusDir" ]; then mkdir -p "$sStatusDir" || exit 1 fi color head echo echo "---------- :: STORAGE :: `hostname` :: ----------" echo color reset sFunction="PUBLIC_$1" fgrep "${sFunction}()" $0 >/dev/null if [ $? -ne 0 ]; then echo SYNTAX: echo "`basename $0` [function]" echo echo " backupstatus" echo " show all servers and their backup times" echo echo " register [hostname]" echo " add a slot for a backup client" echo echo " unregister [hostname] [statuscode]" echo " remove slot for a backup client." echo " The statuscode is an integer of the return code." echo echo " setactive" echo " Reactivate an inctave backup repo." echo echo " setinactive" echo " Mark a backup repo as inactive." echo " This prevents errors of missing backup data" echo " if a host is obsolete and was shut down." echo echo " status" echo " show current reserved slots" echo echo " usage [full|[filter to hostname]] - DEPRECATED" echo " show used diskspace" echo " you can set a string to show additionally details if these hosts" echo " if you add \"full\" then details of all hosts will be shown" echo #echo SCAN... all functions are: #grep "function\ PUBLIC_" $0 | cut -f 2 -d "_" | cut -f 1 -d "(" else shift 1 $sFunction $* fi # ----------------------------------------------------------------------