You are not logged in.
OK, figured out how to delay starting a backup after boot up. The backup if run from a service.
Explanation of timer settings:
1) OnBootSec=...: timer will run after boot.
2) OnCalendar=... + Persistent=true: timer will run after boot, if the timer was not active
(for example, machine was shutdown) when it was time to trigger the service.
Delay options:
1) OnBootSec=30min
2) Random delay (30 minutes or less).
OnCalendar=... + Persistent=true: RandomizedDelaySec=30min.
AccuracySec=30min does not delay the timer.
OnCalendar=... + Persistent=false: is also an option.
3) Put a "sleep 30m" in the service (ExecStartPre=/usr/bin/sleep 30m)
or in the script that is called by the service.
Offline
Hi!
No, you can only delay shutdown. Have you ever seen "stop jobs" during shutdown? Services will be killed after a timeout when shutting down.
That's what I meant. I have added 'TimeoutStop=10min' in rsnapshot@.service (based on the time of the first backup, which is probably the longest one since everything is sinced). Hopefully, this will prevent backups to be interrupted if the system is shut down normally.
Instead of playing with timer settings, I prefer to run a bash script 'rsnapshot-sequential' in the background. It seems easier to me to obtain exactly what I want from such a script than from systemd timers. I start it at boot by means of a dedicated service, though.
I have tested the script (not the service) with a short base interval and a single directory to backup, and it seems to work pretty well. I will test it for real in the next few days, and post it then.
Last edited by LuX (2021-07-06 00:18:48)
Offline
You have different use cases for making backups: on a laptop or on machine acting as a server (24/7).
A real backup is on another machine (and also in another location).
I keep the hourly timer (hourly backups) separate from the daily timer (daily, weekly, monthly backups), because the latter involves sending backups over the internet (try to reduce risk of interrupted backups and can take longer than 10 minutes).
- hourly backups: may run on boot (Persistent=true is optional).
- daily, weekly, monthly backups: to not miss creating weekly or monthly backups, needs to be persistent, which means the timer may run on boot.
(Normally everything happens sequentially in a script, so the name 'rsnapshot-sequential' is a bit redundant. Maybe something like rsnapshot.sh or rsnapshot-daily.sh?)
Last edited by willemw (2021-07-08 06:12:25)
Offline
Hi!
As I promised, I'm posting here the pair of bash scripts that I have written in order to manage backups with rsnapshot in a satisfactory way (for me). The goals and the main features are explained in the comments, at the beginning of the main script.
Finally, I do not consider this pair of scripts as extremely valuable, far from it: the regular timers given in the wiki are good enough for most use. But it was fun to write it, and the occasion to learn a bit about rsnapshot and systemd (--user) services.
Thanks for the help I received here (in this discussion and in this other one on systemd).
LuX
==============================================================
Main script rsnapshot-pc:
#!/bin/bash
# This script is intended to perform periodic backups with /usr/bin/rsnapshot,
# whithout being disturbed by the frequent switch on/off of personnal computers
# (so the trailing '-pc' in its name). Its main purpose is to determine the
# right moment to run /usr/bin/rsnasphot, so that the intervals between the
# various backups are just what they must be (e.g. 1 hour for hourly backups,
# 24 hours for daily backups, etc) when **only the time when the computer was
# on** is taken into account.
# Example: The computer is shutdown from 10:05 to 13:55. If the hourly
# snapshots are done each hour at *:00 (by means of cron rules or a systemd
# timers for example), then two backups hourly.<N> and hourly.<N+1> will be
# stored, with probably the same content since they are done at 10:00 and 14:00
# respectively. But if rsnaphot-pc is used instead with 1h interval between
# hourly backups, and if the backup hourly.<N> was done at 10:00, then
# hourly.<N+1> will be done at 14:50 (that is 1h after 10:00, if the shutdown
# interval 10:05-13:55 is discarded).
# The various snapshots levels, the number of them as well as the root
# directory for snapshots, are read from the configuration file rsnapshot.conf
# of rsnapshot. Time intervals between the snapshots of the same level are
# given in the configuration file rsnapshot-pc.conf, as well as other options
# which are explained there.
# In addition, this script prevents an instance of rsnapshop to start before
# the previous one is over. It also tries to prevent, as much as possible, all
# the backups to be messed up if the syncing step of rsnapshot is interrupted
# (this requires the option rsync_first to be set to 1 in rsnapshot.conf).
# It provides its own logs (which can be merged to rsnapshot logs if wanted),
# and sends notifications when it encounters a problem. This last feature
# requires the companion script rsnapshot-pc-notify to be run simoultaneausly,
# in order to display these notifications.
# Both scripts can be started as systemd services, using the commands:
# 'systemctl start rsnapshot-pc.service'
# 'systemctl --user start rsnapshot-pc-notify.service'
# Note that rsnapshot itself shouldn't be called simultaneously (by the user or
# the system, a command or a systemd service) since rsnapshot-pc will call it
# (as a service or not, depending on the line 'service=*' in the configuration
# file).
# Dependencies: rsnapshot
# Optionnal dependencies (for notifications): gxmessage, inotify-tools
# List of files with their default locations:
# /usr/local/bin/rsnapshot-pc
# /usr/local/bin/rsnasphot-pc-notify
# /usr/local/etc/rsnapshot-pc.conf
# /etc/systemd/system/rsnapshot-pc.service
# /etc/systemd/user/rsnapshot-pc-notify.service
# Reminder: Some of the variables used by this programm (snapshot's levels and
# so on, see above) are set in the configuration file of rsnapshot, which is a
# priori located in /etc/rsnapshot.conf (see man rsnapshot to check this).
### Set default values, then read the config file.
# Auxiliary files and paths
rsnapshot_conf=/etc/rsnapshot.conf
rsnapshot_pc_conf=/usr/local/etc/rsnapshot-pc.conf
log=/var/log/rsnapshot-pc.log
notif=/tmp/rsnapshot-pc-msg
# Other defaults (see comments in $rsnapshot_pc_conf file).
boot_delay=5m # Wait 5 minutes after boot before starting backups
declare -a interval
interval=(1) # 1 day = 24 hours between snapshots of the first level
discount_time_off=true # Take into account only the time when the PC is on
service=true # Call rsnapshot as a systemd service
message="/usr/bin/gxmessage -title ${0##*/} -geometry 500x200 -wrap -default okay"
verbosity=1 # Write every rsnapshot command in the log file
log_size_max=1000000 # 1 Mo
too_old=864000 # 864000 seconds = 10 days
# Read $rsnapshot_pc_conf
if [ -e "$rsnapshot_pc_conf" ]
then
. $rsnapshot_pc_conf
fi
# Reset $too_old in case of typo in $rsnapshot_pc_conf.
[[ "$too_old" =~ ^[1-9][0-9]*$ ]] || too_old=864000
### Functions
# The next function calls rsnapshot.
# Its first argument is:
# - the current level for a rotation of snapshots;
# - the string "sync" for a syncing command.
# Its second argument is the number of the line where this function was called.
rsnapshot_cmd()
{
local cmd
local arg=$1
# [ ! "$arg" = "sync" ] && arg="${LEVEL[$arg]}"
if $service
then
cmd="systemctl start rsnapshot@$arg.service"
else
cmd="/usr/bin/rsnapshot -c $rsnapshot_conf $arg"
fi
if $cmd
then
write_log "$cmd"
else
error "rsnapshot" "$2" "$cmd"
fi
}
# Conversion function of time formats, from dhms (see $rsnapshot_pc_conf) to seconds
time_conv()
{
local in=$1
local out=0
[[ ! "$in" =~ [dhms] ]] && in=$in"d"
if [[ "$in" =~ "d" ]]
then
out=$((86400*10#${in%d*}))
in=${in#*d}
[[ -z "$in" ]] && in=0
[[ ! "$in" =~ [hms] ]] && in=$in"h"
fi
if [[ "$in" =~ "h" ]]
then
out=$(($out + 3600*10#${in%h*}))
in=${in#*h}
[[ -z "$in" ]] && in=0
[[ ! "$in" =~ [ms] ]] && in=$in"m"
fi
if [[ "$in" =~ "m" ]]
then
out=$(($out + 60*10#${in%m*}))
in=${in#*m}
[[ -z "$in" ]] && in=0
fi
out=$(($out + 10#${in%s*}))
echo $out
}
# The next function takes two times T0 and T1, and returns the number of
# seconds the computer was off between T0 and T1.
time_off()
{
local begin=$1
local end=$2
local out=0
while read line
do
sleep 1
if [[ "$line" =~ "shutdown " ]]
then
line=${line##*(}
if [[ "$line" =~ "+" ]]
then
out=$((out + 86400*10#${line%+*}))
line=${line#*+}
fi
out=$(($out + 3600*10#${line%:*}))
line=${line#*:}
out=$((out + 60*10#${line%\)*}))
fi
done < <(last -x shutdown --since "$begin" --until "$end")
echo $out
}
# The next function returns the 'relative age' of <level>.0, the youngest
# snapshot in the current level. This 'relative age' is the number of seconds
# between the time stamp of <level>.0 and a certain time T, where:
# - T = present time if the current level is the first one, or;
# - T = the time stamp of the oldest snapshot in the previous level, if this
# level is full, or;
# - T = the time stamp of <level>.0 (hence relative age is 0), otherwise.
# If the option $discount_time_off is true, this relative age is diminished by
# the number of seconds when the computer was off during this interval.
relative_age()
{
local off
local out
if [ "$i" -gt "0" ]
then
# Compare the age of <level>.0 with the age of the oldest snapshot
# in the previous level (if it is full).
if [ -d "$last_prev" ]
then
out=$(($(date -r $last_prev +%s) - $(date -r $dir +%s)))
off=$(time_off "$(date -r $dir +'%F %R')" "$(date -r $last_prev +'%F %R')")
else
out=0
off=0
fi
else
# Compare the age of <level>.0 with current time.
out=$(($(date +%s) - $(date -r $dir +%s)))
off=$(time_off "$(date -r $dir +'%F %R')" "$(date +'%F %R')")
fi
[ "$discount_time_off" ] && out=$(($out-$off))
echo $out
}
# The next function writes messages in the log file.
write_log()
{
# First argument = Message to display.
# Second argument (optionnal) = True if the process is going to exit.
local msg="$1"
local exiting
local ansi="\033[1;32m" # Green bold
[ "$#" -gt "1" ] && exiting=$2 || exiting=false
$exiting && ansi="\033[1;31m" # Red bold
if [ "$verbosity" -gt "0" ]
then
echo -e "[$ansi$(date +'%F %R:%S')\033[0m] $msg" >> $log
if [[ "$verbosity" -gt "1" ]]
then
echo "Content of $rsnapshot_root:" >> $log
ls -al --color=always --time-style=+'%F %R:%S' $rsnapshot_root >> $log 2>&1
fi
$exiting && echo -e "\033[1;31mExiting!\033[0m" >> $log
fi
}
# The next function takes an error message and extra information, prepares with
# this the text of a notification and sends it to write_log and the command in
# $message, which will write it in logs and display it respectively.
alert()
{
# First argument = Line number where the error occured.
# Second argument = Error message to display.
# Third argument = Exit code (= 0 if there is no need to exit).
local msg="Line $1: An error occured.
$2"
local exiting=false
[ "$3" -gt "0" ] && exiting=true
write_log "$msg" "$exiting"
$exiting && msg="$msg
Exiting!"
if $service
then
if [ -n "$notif" ]
then
echo "$msg" > $notif || \
write_log "Unable to write into $notif the last notification above."
fi
else
$message "$msg" &
fi
$exiting && exit $3
}
# The next function converts error codes and extra information to a human
# readable error message.
error()
{
# First argument = Descriptive code of the error.
# Second argument = Number of the line where the error occured.
# Remaining arguments (optionnal) = extra information.
case $1 in
notif) alert $2 "Unable to create notification file '$notif'." 0 ;;
no_dir) alert $2 "Root directory of rsnapshot backups is: $rsnapshot_root .
This directory does not exist and can not be created." 10 ;;
no_write) alert $2 "Root directory of rsnapshot backups is: $rsnapshot_root .
This directory is not writable, sorry." 11 ;;
prev_sync) alert $2 "A $rsnapshot_root/.sync directory already exists.
This probably means that the last previous backup did not finish gracefully.
Trying to recover (finger-crossed). I recommand you to check if everything is correct." 0 ;;
lock) alert $2 "A $rsnapshot_lock file has been detected.
This means either that rsnapshot is still running, or that its last instance was interrupted.
Please check what is going on." 20 ;;
no_rsnapshot_conf) alert $2 "Unable to find $rsnapshot_conf" 30 ;;
no_good_rsnapshot_conf) alert $2 "The values in LEVEL and RETAIN read from $rsnapshot_conf are inconsistent or incorrect.
LEVEL=(${LEVEL[*]})
RETAIN=(${RETAIN[*]})" 31 ;;
too_old) alert $2 "No backup since $3.
Except if the computer was turned off during a long time, this probably means that something goes wrong and snapshots are no longer done.
Trying to continue. I recommend you to check $log (and increase verbosity to 2)." 0 ;;
*) alert $2 "Command '$3' produced an error." 1 ;;
esac
}
### Preparation
# Read from $rsnapshot_conf the various levels (-> LEVEL) and maximal number of
# snapshots in each of them (-> RETAIN), the path to $rsnapshot_root
# (where backups are stored by rsnapshot) and $rsnapshot_lock
# Example: /etc/rsnaphsot contains four 'retain' lines as follows.
# retain hourly 24
# retain daily 7
# retain weekly 4
# Then LEVEL=(hourly daily weekly) and RETAIN=(24 7 4).
declare -a LEVEL
declare -a RETAIN
if [ -e "$rsnapshot_conf" ]
then
i=0
while read line
do
LINE=($line)
if [ "${LINE[0]}" = "snapshot_root" ]
then
rsnapshot_root=${LINE[1]}
elif [ "${LINE[0]}" = "lockfile" ]
then
rsnapshot_lock=${LINE[1]}
else
LEVEL[$i]=${LINE[1]}
RETAIN[$i]=${LINE[2]}
let i++
fi
done < <(cat $rsnapshot_conf | grep -E "^retain|^snapshot_root|^lockfile")
else
error "no_rsnapshot_conf" $LINENO
fi
# Test if something went wrong while feeding LEVEL and RETAIN.
Nlevel=${#LEVEL[*]}
Nretain=${#RETAIN[*]}
CharLevel="^[a-zA-Z0-9_-][a-zA-Z0-9_\\.-]*$"
err=false
if [ "$Nlevel" -lt "1" ] || [ "$Nretain" -lt "1" ] || [ "$Nlevel" -ne "$Nretain" ]
then
err=true
else
for ((i = 0; i < $Nlevel; i++))
do
[[ "${LEVEL[$i]}" =~ $CharLevel ]] || err=true
done
fi
$err && error "no_good_rsnapshot_conf" $LINENO
# Test if the last backup is too old (see rsnapshot-pc.conf).
if [ -e "$rsnapshot_root/${LEVEL[0]}.0" ]
then
if [ "$(($(date +%s) - $(date -r $rsnapshot_root/${LEVEL[0]}.0 +%s)))" -gt "$too_old" ]
then
error "too_old" $LINENO "$(date -r $rsnapshot_root/${LEVEL[0]}.0)"
fi
fi
# Compute the minimal intervals, in seconds, separating two snapshots of the
# same level, and store them in the array DELTA. The first entries of DELTA
# are taken from the 'interval' array of $rsnapshot_pc_conf. If there are more
# levels, their intervals are computed as follows
# Example: LEVEL=(hourly daily weekly monthly), RETAIN=(5 3 4 12) and
# interval=(1h30 2d8).
# DELTA[0]=1*3600+30*60=5400 is 1h30 for snaphsots in the level 'hourly'.
# DELTA[1]=2*86400+8*3600 is 2 days 8 hours for snaphsots in the level 'daily'.
# DELTA[2]=${DELTA[1]}*3 is 7 days for snaphsots in the level 'weekly'.
# DELTA[3]=${DELTA[2]}*4 is 4 weeks for snapshots in the level 'monthly'.
declare -a DELTA
i=0
while [ "$i" -lt "${#interval[*]}" ]
do
DELTA[$i]=`time_conv ${interval[$i]}`
let i++
done
while [ "$i" -lt "${#LEVEL[*]}" ]
do
DELTA[$i]=$((${RETAIN[$i-1]}*${DELTA[$i-1]}))
let i++
done
# Recap
if [ -n "$log" ]
then
echo -e "[\033[1;32m$(date +'%F %R:%S')\033[0m] Starting $0" >> $log
echo -e "\033[0;33mboot_delay\033[0m=$boot_delay" >> $log
echo -e "\033[0;33mrsnapshot_root\033[0m=$rsnapshot_root" >> $log
echo -e "\033[0;33mrsnapshot_lock\033[0m=$rsnapshot_lock" >> $log
echo -e "\033[0;33minterval\033[0m=(${interval[*]})" >> $log
echo -e "\033[0;33mLEVEL\033[0m=(${LEVEL[*]})" >> $log
echo -e "\033[0;33mRETAIN\033[0m=(${RETAIN[*]})" >> $log
echo -e "\033[0;33mDELTA\033[0m=(${DELTA[*]})" >> $log
if [[ "$verbosity" -gt "1" ]]
then
echo "Content of $rsnapshot_root:" >> $log
ls -al --color=always --time-style=+'%F %R:%S' $rsnapshot_root >> $log 2>&1
else
echo "Set verbosity=2 in $rsnapshot_pc_conf if you want the content of $rsnapshot_root to be displayed here." >> $log
fi
fi
# Create notification file (so that rsnapshot-pc-notify can start to watch it).
if [ -n "$notif" ]
then
touch $notif || error "notif" $LINENO
fi
# Move $log when it becomes too big.
log_size=$(wc -c $log)
if [ "${log_size% *}" -gt "$log_size_max" ]
then
/usr/bin/mv $log $log.old
fi
# Sleep until $boot_delay is over.
boot_delay=`time_conv $boot_delay`
uptime="$(cat /proc/uptime)"
uptime=${uptime%%.*}
delay=$(($boot_delay-$uptime))
if [ "$delay" -gt "0" ]
then
echo "Sleeping $delay seconds until the end of boot delay at $(date -d@$(($(date +%s)+$delay)) +%R:%S)." >> $log
sleep $delay
fi
### Initial backup
# Check if the backups directory of rsnapshot is accessible.
if [ ! -d "$rsnapshot_root" ]
then
if mkdir $rsnapshot_root
then
write_log "Creating $rsnapshot_root. Done."
else
error "no_dir" $LINENO
fi
elif [ ! -w "$rsnapshot_root" ]
then
error "no_write" $LINENO
fi
# Check if last backup ended gracefully, or if rsnapshot is still running.
if [ -d "$rsnapshot_root/.sync" ]
then
error "prev_sync" $LINENO
fi
if [ -e "$rsnapshot_lock" ]
then
error "lock" $LINENO
fi
# This step is run only the very first time, or when it is detected that the
# previous run of rsnapshot was interrupted.
if [ ! -d "$rsnapshot_root/${LEVEL[0]}.0" -o -d "$rsnapshot_root/.sync" ]
then
rsnapshot_cmd "sync" $LINENO # Syncing
rsnapshot_cmd "${LEVEL[0]}" $LINENO # Rotating
fi
### Main loop
# Run 'rsnapshot <level>' (or start rsnaphot@<level>.service) for each
# level for which the previous level (if any) is full, and:
# - <level>.0 does not exist, or;
# - <level>.0 is old enough (in the sense that its relative age is greater than
# the time interval between snapshots in this level).
# Then sleep until next snapshot time, and repeat (for ever).
while true
do
i=${#LEVEL[*]}
while [ "$i" -gt "0" ]
do
i=$((i-1))
# Youngest snapshot in the current level.
dir="$rsnapshot_root/${LEVEL[$i]}.0"
# Oldest snapshot in the previous level, if this level is full.
last_prev=""
[ "$i" -gt "0" ] && last_prev="$rsnapshot_root/${LEVEL[$((i-1))]}.$((${RETAIN[$((i-1))]}-1))"
if [ -d "$dir" ]
then # Current level is not empty.
# If <level>.0 is old enough, run:
# - 'rsnapshot sync' if current level is the first one, and;
# - 'rsnapshot <level>' in any case.
delay=$((${DELTA[$i]} - $(relative_age)))
if [ "$delay" -le "0" ]
then
if [ "$i" -eq "0" ]
then
rsnapshot_cmd "sync" $LINENO # Syncing
fi
rsnapshot_cmd "${LEVEL[$i]}" $LINENO # Rotating
fi
else # Current level is empty (hence not the first one)
# If previous level is full, then fill <level>.0 up.
if [ -d "$last_prev" ]
then
rsnapshot_cmd "${LEVEL[$i]}" $LINENO # Rotating
fi
fi
done
# Sleep all the remaining time until the next snapshot.
if [ "$delay" -gt "0" ]
then
write_log "Sleeping $delay seconds until next snapshot at $(date -d@$(($(date +%s)+$delay)) +%R:%S)."
sleep $delay
fi
doneConfiguration file rsnapshot-pc.conf:
### Configuration file of rsnapshot-pc
# Syntax of time format dhms: [<number>d][<number>h][<number>m][<number>s].
# <number> can be any non-negative integer.
# The last unit is optionnal. It is assumed to be the closest one after the
# previous one:
# 1h30 = 1h30m = 90m
# 1d30 = 1d30h = 54h
# 2 = 2d = 48h.
### Uncomment the options below if you want to modify their values.
# Time to wait after boot, before starting rsnapshot.
#boot_delay=5m
# Time intervals between snapshots in the same level.
#interval=(1)
interval=(30m 1h 1 7 30)
# If true, the time elapsed when the computer is off is discounted from the
# intervals of time between consecutive snapshots.
#discount_time_off=true
# If true, rsnapshot will be started as a systemd service (provided these
# services are implemented). Otherwise, it will run as a normal process.
#service=false
service=true
# Command to display notifications (alert strings are added as argument).
#message="/usr/bin/gxmessage -title ${0##*/} -geometry 500x200 -wrap -default okay"
# Log file.
#log=/var/log/rsnapshot-pc.log
# Notification file, to be used with rsnapshot-pc-notify.
# Let notif="" to disable notifications.
#notif=/tmp/rsnapshot-pc-msg
# Maximum size (in bytes) of the log file: when $log reachs this size, it is
# moved to $log.old.
#log_size_max=1000000 # 1 Mo.
# Choose what is written in the log file.
# 0 = Nothing.
# 1 = Every rsnapshots command (the default).
# 2 = 1 + The content of the directory of snapshot after each rsnapshot command.
#verbosity=1
# Config file to be used by rsnapshot (not recommanded).
# Warning: If rsnapshot is called as a service, this option will be ignored and
# rsnapshot will use its default config file /etc/rsnapshot.conf (except, of
# course, if the rsnapshot@service is modified accordingly).
# Thus it is recommanded, instead of using this option, to backup the default
# config file of rsnapshot and modify it if necessary.
#rsnapshot_conf=/etc/rsnapshot.confCompanion script rsnapshot-pc-notify, for watching and displaying notifications from rsnapshot-pc in the user X session:
#!/bin/bash
# Defaults of rsnapshot-pc
notif=/tmp/rsnapshot-pc-msg
message="/usr/bin/gxmessage -title ${0##*/} -geometry 500x200 -wrap -default okay"
# Config of rsnashot-pc overwrites defaults
. /usr/local/etc/rsnapshot-pc.conf
if [ -n "$notif" ]
then
if [ -s "$notif" ]
then
$message "$(cat $notif)"
fi
while inotifywait -e modify $notif
do
$message "$(cat $notif)"
done
fiSystemd service rsnapshot-pc.service (for the system):
[Unit]
Description=rsnapshot-pc backup
[Service]
Type=simple
Nice=19
IOSchedulingClass=idle
ExecStart=/usr/local/bin/rsnapshot-pc
[Install]
WantedBy=multi-user.targetNote: I don't understand the Nice= and IOS*= lines above, since I simply copied them from the service for rsnapshot given in the wiki.
Systemd service rsnapshot-pc-notify.service (for the user):
[Unit]
Description= Watch and display notifications of the rsnapshot-pc service
[Service]
Type= simple
ExecStart= /usr/local/bin/rsnapshot-pc-notify
ExecStartPre= /usr/bin/sleep 1
[Install]
WantedBy=graphical.targetNote: The ExecStartPre= line above is intended to solve a race condition problem (explanations in this post).
Last edited by LuX (2021-12-23 10:34:15)
Offline
Running the script through shellcheck (shellcheck-bin) will give you some hints and warnings (unused variable t and function relative_age is seen as a variable).
Offline
Unused variable t is a reminiscence of my tests. Removed.
I don't understand the problem with relative_age seen as a variable: it is not so in my script. I probably confused two versions. Fixed.
Offline