]> arthur.barton.de Git - backup-script.git/blob - bin/backup-script
Enable shell options for safer execution environment
[backup-script.git] / bin / backup-script
1 #!/bin/bash
2 #
3 # backup-script system for cloning systems using rsync
4 # Copyright (c)2008-2017 Alexander Barton <alex@barton.de>
5 #
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
10 # Please read the file COPYING, README and AUTHORS for more information.
11 #
12
13 NAME=$(basename "$0")
14 PIDFILE="/var/run/$NAME.pid"
15
16 DRYRUN=0
17 VERBOSE=0
18 TAG=""
19 PREPOSTEXEC=1
20
21 export LC_ALL=C
22
23 declare -i count_all=0
24 declare -i count_started=0
25 declare -i count_ok=0
26 declare -i count_ok_vanished=0
27 declare -i count_enabled=0
28
29 destinations=""
30
31 # Default settings, can be overwritten in backup-script.conf:
32 [ -d "/usr/local/etc/backup-script.d" ] \
33         && conf_d="/usr/local/etc/backup-script.d" \
34         || conf_d="/etc/backup-script.d"
35 setup_exec=""
36 pre_exec=""
37 post_exec=""
38 default_backup_type="rsync"
39 default_source_root="/"
40 default_files="running-config"
41 default_target="/var/backups"
42 default_user="root"
43 default_ssh_args_add=""
44 default_rsync_args_add=""
45 default_exclude_args_add=""
46 default_exclude_dirs_add=""
47 default_compress=1
48 default_ping=1
49 default_local=0
50 default_generations=0
51 default_io_timeout="1800"
52 default_job_pre_exec=""
53 default_job_post_exec=""
54 default_tags=""
55
56 # Set shell options.
57 shopt -s nullglob
58 set -o pipefail
59
60 Usage() {
61         {
62                 echo "Usage: $NAME [<options>] [<job> [<job> [...]]]"
63                 echo
64                 echo "  -n, --dry-run       Test run only, don't copy any data."
65                 echo "  -p, --progress      Show progress, see rsync(1)."
66                 echo "  -t TAG, --tag TAG   Only run jobs with tag TAG."
67                 echo "  -x, --no-exec       Don't run global pre-/post-exec commands."
68                 echo
69                 echo "When no <job> is given, all defined systems are used."
70                 echo
71                 # shellcheck disable=SC2086
72                 echo -e $config_info
73                 echo
74         } >&2
75         exit 2
76 }
77
78 ErrorMsg () {
79         printf "%s\n" "$@" >&2
80 }
81
82 CleanUp() {
83         if [[ -n "$pre_exec" && $PREPOSTEXEC -ne 0 ]]; then
84                 echo "Executing \"$post_exec\" ..."
85                 if ! sh -c $post_exec; then
86                         ErrorMsg "Warning: post-exec command failed!"
87                 fi
88                 echo
89         fi
90         rm -f "$PIDFILE"
91 }
92
93 GotSignal() {
94         echo
95         ErrorMsg "--> Got signal, cleaning up & aborting ..."
96         echo
97         CleanUp
98         ErrorMsg -n "Aborted: " >&2; date
99         echo
100         sleep 3
101         exit 9
102 }
103
104 ExecJob() {
105         local what="$1"
106         local cmd="$2"
107
108         echo "Running job ${what}-exec command ..."
109         [ "$local" -eq 0 ] \
110                 && cmd="$ssh_cmd ${user}@${system} $cmd"
111         echo -n "Start date (${what}-exec): "; date
112         echo "$cmd"
113         if [ "$DRYRUN" -eq 0 ]; then
114                 $SHELL -c "$cmd"; local ret=$?
115         else
116                 echo " *** Trial run, not executing ${what}-exec command!"
117                 ret=0
118         fi
119         if [ $ret -eq 0 ]; then
120                 echo "The ${what}-exec command completed with status 0, OK."
121         else
122                 ErrorMsg "The ${what}-exec command completed with ERRORS, code $ret!"
123         fi
124         return $ret
125 }
126
127 GetFS() {
128         local dir="$1"
129
130         while [ -n "$dir" ]; do
131                 findmnt -fn -o FSTYPE --raw "$dir" 2>/dev/null; local r=$?
132                 if [ $r -eq 0 ]; then
133                         return 0
134                 elif [ $r -eq 127 ]; then
135                         echo "UNKNOWN"
136                         return 1
137                 fi
138                 dir=$(dirname "$dir") || return 1
139         done
140 }
141
142 CreateSubvolume() {
143         local volume="$1"
144         local fs
145         local dir
146
147         dir=$(dirname "$volume")
148         fs=$(GetFS "$dir")
149         case "$fs" in
150           "btrfs")
151                 btrfs subvolume create "$volume"  >/dev/null || return 1
152                 ;;
153           "zfs")
154                 zfs create "$(echo "$volume" | cut -c2-)" || return 1
155                 ;;
156           *)
157                 ErrorMsg "CreateSubvolume: Incompatible FS type \"$fs\" on \"$dir\"!"
158                 return 9
159         esac
160         return 0
161 }
162
163 CloneSubvolume() {
164         local source="$1"
165         local volume="$2"
166         local snapshot="$3"
167         local dir
168         local fs
169         local link_name
170
171         dir=$(dirname "source")
172         fs=$(GetFS "$source")
173         case "$fs" in
174           "btrfs")
175                 btrfs subvolume snapshot "$source" "$snapshot"  >/dev/null || return 1
176                 ;;
177           "zfs")
178                 zfs snapshot "$snapshot" || return 1
179                 link_name="$(echo "$snapshot" | cut -d@ -f2-)"
180                 ln -s \
181                         "current/.zfs/snapshot/$link_name" \
182                         "$(dirname "$volume")/$link_name"
183                 ;;
184           *)
185                 ErrorMsg "CloneSubvolume: Incompatible FS type \"$fs\" on \"$source\"!"
186                 return 9
187         esac
188         return 0
189 }
190
191 RenameSubvolume() {
192         local source="$1"
193         local target="$2"
194         local fs
195
196         fs=$(GetFS "$source")
197         case "$fs" in
198           "btrfs")
199                 mv "$source" "$target" || return 1
200                 ;;
201           "zfs")
202                 zfs rename \
203                   "$(echo "$source" | cut -c2-)" \
204                   "$(echo "$target" | cut -c2-)" \
205                         || return 1
206                 ;;
207           *)
208                 ErrorMsg "RenameSubvolume: Incompatible FS type \"$fs\" on \"$source\"!"
209                 return 9
210         esac
211         return 0
212 }
213
214 DeleteSubvolume() {
215         local volume="$1"
216         local fs
217         local id
218         local snapshot
219
220         fs=$(GetFS "$volume")
221         case "$fs" in
222           "btrfs")
223                 btrfs subvolume delete "$volume" >/dev/null || return 1
224                 ;;
225           "zfs")
226                 id="$(basename "$volume")"
227                 if [ -h "$volume" ]; then
228                         snapshot="$(dirname "$volume")/current@$id"
229                 else
230                         snapshot="$volume"
231                 fi
232                 zfs destroy -r "$(echo "$snapshot" | cut -c2-)" >/dev/null || return 1
233                 [ -h "$volume" ] && rm "$volume"
234                 ;;
235           *)
236                 ErrorMsg "DeleteSubvolume: Incompatible FS type \"$fs\" on \"$volume\"!"
237                 return 9
238         esac
239         return 0
240 }
241
242 Initialize_Last_SysTarget_Snapshot() {
243         sys_target="$1"
244         unset last
245         unset snapshot
246
247         fs=$(GetFS "$sys_target")
248         case "$fs" in
249           "btrfs")
250                 # Search directory of last generation, if any
251                 # shellcheck disable=SC2012
252                 last=$(ls -1d "$sys_target"/[0-9]* 2>/dev/null | sort -r | head -n1)
253                 if [ -n "$last" ]; then
254                         if [ ! -d "$last" ]; then
255                                 ErrorMsg "Last snapshot \"$last\" seems not to be a directory!? \"$system\" skipped!"
256                                 echo
257                                 return 1
258                         fi
259                 fi
260                 sys_target="$sys_target/$(date +%Y%m%d-%H%M%S)"
261                 snapshot="$sys_target"
262                 ;;
263           "zfs")
264                 # On ZFS, the last generation is always named "current"
265                 if [ -e "$sys_target/current" ]; then
266                         last="$sys_target/current"
267                         if [ "$(uname)" = "Linux" ]; then
268                                 date=$(LC_ALL=C stat "$1" | grep "^Modify: " \
269                                  | cut -d':' -f2- | cut -d. -f1)
270                         else
271                                 date=$(LC_ALL=C stat -f "%Sc" "$1")
272                         fi
273                         date=$(echo "$date" | sed -e's/^ //g' -e 's/[-:]//g' -e 's/ /-/g')
274
275                 else
276                         last=""
277                         date="$(date +%Y%m%d-%H%M%S)"
278                 fi
279                 snapshot="$(echo "$sys_target/current" | cut -c2-)@$date"
280                 sys_target="$sys_target/current"
281                 ;;
282           *)
283                 ErrorMsg "Initialize_Last_SysTarget_Snapshot: Incompatible FS type \"$fs\" on \"$sys_target\"!"
284                 return 1
285         esac
286         return 0
287 }
288
289 # Search configuration file (last one is used as default!)
290 for conf in \
291         "/usr/local/etc/backup-script.conf" \
292         "/etc/backup-script.conf" \
293         "${conf_d}/backup-script.conf" \
294         "/usr/local/etc/backup-script.conf" \
295 ; do
296         [ -r "$conf" ] && break
297 done
298
299 # Read in configuration file
300 config_info="Configuration file is \"$conf\""
301 if [ -r "$conf" ]; then
302         # shellcheck source=/dev/null
303         source "$conf"
304 else
305         config_info="${config_info} (not readable, using defaults)"
306 fi
307 config_info="${config_info},\nusing \"$conf_d\" as configuration directory."
308
309 while [ $# -gt 0 ]; do
310         case "$1" in
311           "-n"|"--dry-run")
312                 DRYRUN=1; shift
313                 ;;
314           "-p"|"--progress")
315                 VERBOSE=1; shift
316                 ;;
317           "-t"|"--tag")
318                 shift; TAG="$1"; shift
319                 [ -n "$TAG" ] || Usage
320                 ;;
321           "-x"|"--no-exec")
322                 PREPOSTEXEC=0; shift
323                 ;;
324           "-"*)
325                 Usage
326                 ;;
327           *)
328                 break
329         esac
330 done
331
332 echo -n "Started: "; date
333 echo -e "$config_info"
334
335 # Check rsync and its protocol version
336 if ! rsync=$(which "rsync" 2>/dev/null); then
337         ErrorMsg "Failed to detect rsync(1)! Is it installed in your \$PATH?"
338         exit 1
339 fi
340 if ! rsync_proto=$($rsync --version 2>/dev/null | head -n 1 | sed 's/.*  protocol version \([0-9]*\)$/\1/'); then
341         ErrorMsg "Failed to detect protocol version of $rsync!"
342         exit 1
343 fi
344 echo "Rsync command is $rsync, protocol version $rsync_proto."
345
346 [[ -n "$TAG" ]] && echo "Running jobs tagged with \"$TAG\"."
347 echo
348
349 if [ $# -ge 1 ]; then
350         for s in "$@"; do
351                 if [ ! -r "${conf_d}/$s" ]; then
352                         ErrorMsg "$NAME: Can' read \"${conf_d}/$s\"!"
353                         exit 3
354                 fi
355                 sys+=("${conf_d}/$s")
356         done
357 else
358         sys=("${conf_d}/"*)
359 fi
360
361 if [[ -n "$setup_exec" && $PREPOSTEXEC -ne 0 ]]; then
362         echo "Executing \"$setup_exec\" ..."
363         if ! sh -c $setup_exec; then
364                 ErrorMsg "Error: setup command failed!"; echo
365                 ErrorMsg "Aborting backup."; echo
366                 exit 5
367         fi
368         sleep 2
369         echo
370 fi
371
372 trap GotSignal SIGINT SIGTERM
373
374 # check and create PID file
375 if [ -e "$PIDFILE" ]; then
376         ErrorMsg "Lockfile \"$PIDFILE\" already exists."
377         ErrorMsg "Is an other instance still running?"
378         echo
379         ErrorMsg -n "Aborted: " >&2; date
380         echo
381         exit 4
382 fi
383 if ! touch "$PIDFILE" 2>/dev/null; then
384         ErrorMsg "Warning: can't create PID file \"$PIDFILE\"!"
385         echo
386 else
387         echo "$$" >>"$PIDFILE"
388 fi
389
390 if [[ -n "$pre_exec" && $PREPOSTEXEC -ne 0 ]]; then
391         echo "Executing \"$pre_exec\" ..."
392         if ! sh -c $pre_exec; then
393                 ErrorMsg "Error: pre-exec command failed!"; echo
394                 CleanUp
395                 ErrorMsg "Aborting backup."; echo
396                 exit 5
397         fi
398         sleep 2
399         echo
400 fi
401
402 for f in "${sys[@]}"; do
403         [[ -r "$f" && -f "$f" ]] || continue
404
405         fname=$(basename "$f")
406         case "$fname" in
407                 "backup-script.conf"|*.sh)
408                         continue
409                         ;;
410         esac
411
412         # Set global defaults
413         system="$fname"
414         backup_type="$default_backup_type"
415         user="$default_user"
416         source_root="$default_source_root"
417         files="$default_files"
418         target="$default_target"
419         ssh_args_add="$default_ssh_args_add"
420         rsync_args_add="$default_rsync_args_add"
421         exclude_args_add="$default_exclude_args_add"
422         exclude_dirs_add="$default_exclude_dirs_add"
423         compress="$default_compress"
424         ping="$default_ping"
425         local="$default_local"
426         generations="$default_generations"
427         job_pre_exec="$default_job_pre_exec"
428         job_post_exec="$default_job_post_exec"
429         tags="$default_tags"
430         io_timeout="$default_io_timeout"
431
432         # Compatibility with backup-pull(1) script: Save global values ...
433         pre_exec_saved="$pre_exec"
434         post_exec_saved="$post_exec"
435
436         # Compatibility with backup-pull(1) script: Set defaults
437         host=""
438         unset source
439         unset pre_exec
440         unset post_exec
441
442         # Read in system configuration file
443         # shellcheck source=/dev/null
444         source "$f"
445
446         # Compatibility with backup-pull(1) script: Fix up configuration
447         [[ "$system" = "$fname" && -n "$host" ]] \
448                 && system="$host"
449         [[ "$source_root" = "$default_source_root" && -n "$source" ]] \
450                 && source_root="$source"
451         [[ -z "$job_pre_exec" && -n "$pre_exec" ]] \
452                 && job_pre_exec="$pre_exec"
453         [[ -z "$job_post_exec" && -n "$post_exec" ]] \
454                 && job_post_exec="$post_exec"
455
456         # Compatibility with backup-pull(1) script: Restore global values ...
457         pre_exec="$pre_exec_saved"
458         post_exec="$post_exec_saved"
459
460         # Validate configuration
461         if [[ "$system" = "localhost" || "$system" = "127.0.0.1" ]]; then
462                 # Local system
463                 local=1
464                 compress=0
465         fi
466
467         # Add "NONE" tag when no tags are given in the config file:
468         [[ -z "$tags" ]] && tags="NONE"
469         # Add "auto-tags":
470         [[ "$local" -eq 1 ]] && tags="$tags,LOCAL"
471         # Check tags
472         if [[ -n "$TAG" && "$TAG" != "ALL" ]]; then
473                 if ! echo "$tags" | grep -E "(^|,)$TAG(,|$)" >/dev/null 2>&1; then
474                         if [ "$DRYRUN" -ne 0 ]; then
475                                 echo "Tags of system \"$system\" don't match \"$TAG\": \"$tags\". Skipped."
476                                 echo
477                         fi
478                         continue
479                 fi
480         fi
481
482         # Make sure "source_root" ends with a slash ("/")
483         case "$source_root" in
484           *"/")
485                 ;;
486           *)
487                 source_root="$source_root/"
488         esac
489
490         # Make sure "target" DOESN'T end with a slash ("/")
491         case "$target" in
492           "*/")
493                 target=$( echo "$target" | sed -e 's/\/$//g' )
494                 ;;
495         esac
496
497         [ "$system" = "$fname" ] \
498                 && systxt="\"$system\"" \
499                 || systxt="\"$fname\" [\"$system\"]"
500         [ "$local" -eq 0 ] \
501                 && echo "Working on $systxt ..." \
502                 || echo "Working on $systxt (local system) ..."
503
504         count_all=$count_all+1
505
506         # Check if job is disabled
507         if [ "$backup_type" = "disabled" ]; then
508                 echo "Job is DISABLED and will be skipped."
509                 echo; continue
510         fi
511
512         count_enabled=$count_enabled+1
513
514         # Check target directory
515         if [ -z "$target" ]; then
516                 ErrorMsg "No target directory specified for \"$system\"!? Skipped!"
517                 echo; continue
518         fi
519         if [ ! -d "$target" ]; then
520                 ErrorMsg "Target \"$target\" is not a directory!? \"$system\" skipped!"
521                 echo; continue
522         fi
523
524         sys_target="$target/$fname"
525         sys_root="$sys_target"
526         if [[ "$DRYRUN" -eq 0 && ! -e "$sys_target" ]]; then
527                 if [ $generations -gt 0 ]; then
528                         CreateSubvolume "$sys_target"; r=$?
529                 else
530                         mkdir -p "$sys_target"; r=$?
531                 fi
532                 if [ $r -ne 0 ]; then
533                         ErrorMsg "Can't create \"$sys_target\"!? \"$system\" skipped!"
534                         echo; continue
535                 fi
536         fi
537
538         if [[ "$local" -eq 0 && "$ping" -ne 0 ]]; then
539                 # Check if system is alive
540                 if ! ping -c 1 "$system" >/dev/null 2>&1; then
541                         ErrorMsg "Host \"$system\" seems not to be alive!? Skipped."
542                         echo; continue
543                 fi
544                 echo "OK, host \"$system\" seems to be alive."
545         fi
546
547         if [ $generations -gt 0 ]; then
548                 # Make sure no old backup is stored in system directory
549                 if [ -e "$sys_target/.stamp" ]; then
550                         # There seems to be a genearation-less backup in the
551                         # target directory!
552                         ErrorMsg "Target directory \"$sys_target\" seems to be unclean!? \"$system\" skipped!"
553                         echo; continue
554                 fi
555
556                 Initialize_Last_SysTarget_Snapshot "$sys_target" || continue
557
558                 if [[ -n "$last" && ! -e "$last/.stamp" ]]; then
559                         # Old backup directory without "stamp file", continue
560                         echo "Found incomplete snapshot in \"$last\", reusing and renaming it ..."
561                         if [ "$DRYRUN" -eq 0 ]; then
562                                 if ! RenameSubvolume "$last" "$sys_target"; then
563                                         ErrorMsg "Failed to rename last snapshot \"$last\" to \"$sys_target\"!? \"$system\" skipped!"
564                                         echo; continue
565                                 fi
566                         else
567                                 echo " *** Trial run, not renaming snapshot \"$last\" to \"$sys_target\"!"
568                         fi
569                 elif [ -n "$last" ]; then
570                         # Old backup directory found, create new snapshot
571                         echo "Found last snapshot in \"$last\"."
572                         if [ "$DRYRUN" -eq 0 ]; then
573                                 CloneSubvolume "$last" "$sys_target" "$snapshot"; r=$?
574                                 if [ $r -ne 0 ]; then
575                                         ErrorMsg "Can't create snapshot \"$snapshot\" of \"$last\", code $r!? \"$system\" skipped!"
576                                         echo; continue
577                                 fi
578                                 echo "Created new snapshot in \"$snapshot\"."
579                         else
580                                 echo " *** Trial run, not creating new snapshot in \"$snapshot\"!"
581                         fi
582                 else
583                         # No old backup found, create new subvolume
584                         if [ "$DRYRUN" -eq 0 ]; then
585                                 CreateSubvolume "$sys_target"; r=$?
586                                 if [ $r -ne 0 ]; then
587                                         ErrorMsg "Can't create subvolume \"$sys_target\", code $r!? \"$system\" skipped!"
588                                         echo; continue
589                                 fi
590                                 echo "Created new subvolume in \"$sys_target\"."
591                         else
592                                 echo " *** Trial run, not creating new subvolume \"$sys_target\"!"
593                         fi
594                 fi
595         fi
596
597         ssh_cmd="ssh"
598         [ -n "$ssh_args_add" ] && ssh_cmd="$ssh_cmd $ssh_args_add"
599
600         # execute job "pre-exec" command, if any
601         if [ -n "$job_pre_exec" ]; then
602                 ExecJob pre "$job_pre_exec" ; ret=$?
603                 if [ $ret -ne 0 ]; then
604                         [ $ret -ne 99 ] && count_started=$count_started+1
605                         ErrorMsg "Pre-exec command failed, \"$system\" skipped!"
606                         echo; continue
607                 fi
608         fi
609
610         # prepare (remote) command ...
611         if [[ "$backup_type" == "rsync" ]]; then
612                 cmd="$rsync --archive --timeout=$io_timeout"
613                 [ "$compress" -ne 0 ] && cmd="$cmd --compress"
614                 [ "$local" -eq 0 ] && cmd="$cmd --rsh=\"$ssh_cmd\""
615                 cmd="$cmd --delete-during --delete-excluded --sparse"
616                 if [ "$VERBOSE" -gt 0 ]; then
617                         [ "$rsync_proto" -ge 31 ] \
618                                 && cmd="$cmd --info=progress2" \
619                                 || cmd="$cmd --progress"
620                 fi
621                 set -f
622                 if [ "$source_root" = "$default_source_root" ]; then
623                         for dir in \
624                                 "/dev/**" \
625                                 "/media/**" \
626                                 "/mnt/**" \
627                                 "/net/**" \
628                                 "/proc/**" \
629                                 "/run/**" \
630                                 "/sys/**" \
631                                 "/tmp/**" \
632                                 "/var/cache/apt/**" \
633                                 "/var/log/**" \
634                                 "/var/tmp/**" \
635                         ; do
636                                 cmd="$cmd --exclude=$dir"
637                         done
638                 fi
639                 [ -n "$exclude_args_add" ] && cmd="$cmd $exclude_args_add"
640                 for dir in $exclude_dirs_add; do
641                         cmd="$cmd --exclude=$dir"
642                 done
643                 [ -n "$rsync_args_add" ] && cmd="$cmd $rsync_args_add"
644                 set +f
645
646                 [ "$local" -eq 0 ] \
647                         && cmd="$cmd ${user}@${system}:$source_root $sys_target/" \
648                         || cmd="$cmd $source_root $sys_target/"
649         elif [[ "$backup_type" == "scp" ]]; then
650                 cmd="scp"
651                 [ "$VERBOSE" -eq 0 ] && cmd="$cmd -q"
652                 for file in $files; do
653                         cmd="$cmd ${user}@${system}:$file $sys_target/"
654                 done
655         else
656                 ErrorMsg "Backup type \"$backup_type\" undefined, \"$system\" skipped!"
657                 echo; continue
658         fi
659
660         echo "Backing up to \"$sys_target\" ..."
661         echo -n "Start date: "; date
662         echo "$cmd"
663         count_started=$count_started+1
664         ok=0
665
666         if [ "$DRYRUN" -eq 0 ]; then
667                 stamp_file="$sys_target/.stamp"
668                 rm -f "$stamp_file"
669
670                 # Execute backup command:
671                 start_t=$(date "+%s")
672                 $SHELL -c "$cmd"; ret=$?
673                 end_t=$(date "+%s")
674
675                 {
676                         echo "code=$ret"
677                         echo "start_t=$start_t"
678                         echo "end_t=$end_t"
679                         echo "cmd='$cmd'"
680                         echo "backup_host='$(hostname -f)'"
681                         echo "backup_user='$(id -un)'"
682                 } >"$stamp_file"
683         else
684                 echo " *** Trial run, not executing save command!"
685                 ret=0
686         fi
687
688         if [ $ret -eq 20 ]; then
689                 ErrorMsg "Backup of \"$system\" interrupted. Aborting ..."
690                 GotSignal
691         fi
692
693         echo -n "End date: "; date
694         if [[ $ret -eq 0 || $ret -eq 24 ]]; then
695                 [ $ret -eq 24 ] && count_ok_vanished=$count_ok_vanished+1
696
697                 echo "System \"$system\" completed with status $ret, OK."
698                 [ "$DRYRUN" -gt 0 ] || count_ok=$count_ok+1
699                 ok=1
700         else
701                 ErrorMsg "System \"$system\" completed with ERRORS, code $ret!"
702         fi
703
704         # execute job "post-exec" command, if any
705         if [ -n "$job_post_exec" ]; then
706                 ExecJob post "$job_post_exec"
707         fi
708
709         if [ $generations -gt 0 ]; then
710                 # Update "latest" symlink
711                 if [ "$DRYRUN" -eq 0 ]; then
712                         rm -f "$sys_root/latest"
713                         ln -s "$sys_target" "$sys_root/latest"
714                 fi
715                 # Clean up old generations
716                 declare -i gen_count=$generations+2
717                 # shellcheck disable=SC2012
718                 to_delete=$(ls -1t "$sys_root" 2>/dev/null | tail -n+$gen_count | sort)
719                 if [[ -n "$to_delete" && $ok -eq 1 ]]; then
720                         [ "$DRYRUN" -eq 0 ] \
721                                 && echo "Deleting old backup generations (keep $generations) ..." \
722                                 || echo " *** Trial run, not deleting old generations:"
723                         for delete in $to_delete; do
724                                 dir="$sys_root/$delete"
725                                 if [ ! -e "$dir/.stamp" ]; then
726                                         ErrorMsg "Not deleting \"$dir\" of \"$system\", not a backup directory!?"
727                                         continue
728                                 fi
729                                 last=$(stat "$dir/.stamp" 2>/dev/null | grep "^Modify: " \
730                                  | cut -d':' -f2- | cut -d. -f1)
731                                 # shellcheck disable=SC2086
732                                 echo "Removing backup from" $last "..."
733                                 if [ "$DRYRUN" -eq 0 ]; then
734                                         DeleteSubvolume "$dir" \
735                                                 || ErrorMsg "Failed to delete \"$dir\" of \"$system\"!"
736                                 fi
737                         done
738                         echo -n "Clean up finished: "; date
739                 elif [ -n "$to_delete" ]; then
740                         ErrorMsg "There have been errors for \"$system\", not cleaning up old generations!"
741                 else
742                         echo "Nothing to clean up (keep up to $generations generations)."
743                 fi
744         fi
745
746         destinations="$destinations $target"
747         echo
748 done
749
750 sync
751
752 if [ "$DRYRUN" -eq 0 ]; then
753         paths=""
754         paths_zfs=""
755         # shellcheck disable=SC2086
756         for dest in $(echo $destinations | sed -e 's/ /\n/g' | sort | uniq); do
757                 fs=$(GetFS "$dest")
758                 case $fs in
759                   "zfs" )
760                         paths_zfs="$paths_zfs $dest"
761                         ;;
762                   *)
763                         paths="$paths $dest"
764                 esac
765         done
766         if [ -n "$paths" ]; then
767                 # shellcheck disable=SC2086
768                 df -h $paths
769                 echo
770         fi
771         if [ -n "$paths_zfs" ]; then
772                 # shellcheck disable=SC2086
773                 zfs list $paths_zfs
774                 echo
775         fi
776 fi
777
778 CleanUp
779
780 echo -n "Done: "; date
781 echo
782 [ $count_all -eq 1 ] && s="" || s="s"
783 [ $count_enabled -eq $count_all ] \
784         && echo " - $count_all job$s defined (all enabled)," \
785         || echo " - $count_all job$s defined ($count_enabled enabled),"
786 [ $count_started -eq 1 ] && s="" || s="s"
787 echo " - $count_started job$s started,"
788 echo " - $count_ok done without errors."
789 echo
790
791 if [ $count_started -ne $count_ok ]; then
792         echo "----->  THERE HAVE BEEN ERRORS!  <-----"
793         echo
794         exit 6
795 elif [ $count_enabled -ne $count_started ]; then
796         exit 7
797 fi
798
799 # -eof-