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