3 # backup-script system for cloning systems using rsync
4 # Copyright (c)2008-2019 Alexander Barton, alex@barton.de
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.
20 # Default settings, can be overwritten in backup-script.conf:
21 [ -d "/usr/local/etc/backup-script.d" ] \
22 && conf_d="/usr/local/etc/backup-script.d" \
23 || conf_d="/etc/backup-script.d"
25 default_backup_type="rsync"
26 default_files="running-config"
28 default_target="/var/backups"
33 # Search configuration file (last one is used as default!)
35 "/usr/local/etc/backup-script.conf" \
36 "/etc/backup-script.conf" \
37 "${conf_d}/backup-script.conf" \
38 "/usr/local/etc/backup-script.conf" \
40 if [ -r "$conf" ]; then
41 # shellcheck source=/dev/null
48 echo "Usage: $NAME [-q|--quiet] [-v|--verbose] [<job> [<job> [...]]]"
49 echo " $NAME <-d|--dirs> <dir1> <dir2>"
51 echo " -d, --dirs Compare two backup directories (not jobs)."
52 echo " -q, --quiet Quite mode, only list jobs with changes or errors."
53 echo " -v, --verbose Verbose mode, show all checks that are run."
55 echo "When no <job> is given, all defined jobs are checked."
61 echo "Differences in $*:"
67 while read -r line; do
83 if [[ "$dir_name" == "/" ]]; then
84 exclude="$exclude"'| \.stamp$| dev$| etc$| proc$| root$| run$| sys$| tmp$'
85 exclude="$exclude"'| data$| net$| srv$'
86 exclude="$exclude"'| [[:alnum:]_-]+\.log(\.[[:alnum:]]+|)$'
87 exclude="$exclude"'| (aquota.user|aquota.group)$'
90 # shellcheck disable=SC2012
91 find "$base_dir$dir_name". -maxdepth 1 -printf '%M %10u:%-10g %t %12s %f\n' 2>/dev/null \
92 | LC_ALL=C sort -k 9 | grep -Ev "($exclude)"
95 ListFilesRecursive() {
100 cd "$base_dir" || return 1
101 find ".$dir_name" -type f -o -type l | cut -d'/' -f2-
108 # Set global defaults
109 local backup_type="$default_backup_type"
110 local files="$default_files"
111 local generations="$default_generations"
113 local system="$fname"
114 local target="$default_target"
116 # Read in system configuration file
117 # shellcheck source=/dev/null
120 target="$target/$(basename "$f")"
122 [[ -d "$target" ]] || return 0
125 [[ "$system" == "$fname" ]] \
126 && systxt="\"$system\"" \
127 || systxt="\"$fname\" [\"$system\"]"
128 [[ "$local" -eq 0 ]] \
129 && echo "Checking $systxt ..." \
130 || echo "Checking $systxt (local system) ..."
132 # Check if job is disabled
133 if [[ "$backup_type" == "disabled" ]]; then
134 echo "Job is DISABLED and will be skipped."
138 if [ $generations -lt 1 ]; then
139 echo "No generations configured, nothing to compare, skipping system!"
143 local latest_d="$target/latest"
144 if [[ ! -d "$latest_d" || ! -r "$latest_d/.stamp" ]]; then
145 echo "Failed to access latest backup generation in \"$latest_d\", skipping system!"
148 echo "Found latest generation in \"$latest_d\"."
151 # shellcheck source=/dev/null
152 source "$latest_d/.stamp"
154 if [[ $code -ne 0 ]]; then
155 echo "Warning: Last backup generation had errors, code $code!"
158 # Search previous generation without errors
160 # shellcheck disable=SC2045
161 for d in $(ls -1dt "$target/"[0-9]*-[0-9]* 2>/dev/null); do
162 [[ -d "$d" && -r "$d/.stamp" ]] || return 0
165 # shellcheck source=/dev/null
168 if [[ $code -eq 0 || $code -eq 24 ]]; then
173 if [[ -z "$previous_d" || ! -d "$previous_d" || ! -r "$previous_d/.stamp" ]]; then
174 echo "Failed to find previous successfull backup generation, skipping system!"
177 echo "Comparing with generation in $previous_d ..."
179 DiffGenerations "$backup_type" "$previous_d" "$latest_d" "$files"
187 local backup_type="$1"
194 if [[ "$backup_type" == "rsync" ]]; then
203 /boot/grub/grub.cfg \
207 /etc/debian_version \
219 /etc/network/interfaces \
227 /etc/ssh/sshd_config \
232 [[ -r "${gen1_d}${file}" ]] || continue
234 [[ $VERBOSE -ne 0 ]] && echo "Checking \"$file\" ..."
235 if ! diff -U 3 "${gen1_d}${file}" "${gen2_d}${file}" >"$tmp_diff"; then
236 BeginDiff "\"$file\""
237 tail -n +3 "$tmp_diff" | PipeDiff
254 [[ ! -d "${gen1_d}${dir}" ]] && continue
255 [[ ! -d "${gen2_d}${dir}" ]] && continue
257 # Make sure that this is a system root; comparing other
258 # root folders results in misleading output ...
259 [[ "$dir" == "/" && ! -d "${gen1_d}${dir}/etc" ]] && continue
261 [[ $VERBOSE -ne 0 ]] && echo "Checking \"$dir\" ..."
262 ListDirectory "${gen1_d}" "${dir}" >"$tmp_1"
263 ListDirectory "${gen2_d}" "${dir}" >"$tmp_2"
264 if ! diff -U 0 "$tmp_1" "$tmp_2" >"$tmp_diff"; then
265 BeginDiff "\"$dir\" directory"
266 tail -n +3 "$tmp_diff" | grep -Ev '^@@ ' | PipeDiff
273 /etc/systemd/network/ \
274 /etc/systemd/system/ \
276 /lib/systemd/network/ \
277 /lib/systemd/system/ \
279 /run/systemd/system/ \
280 /usr/lib/systemd/network/ \
281 /usr/lib/systemd/system/ \
282 /usr/lib/systemd/user/ \
284 [[ ! -d "${gen1_d}${dir}" ]] && continue
285 [[ ! -d "${gen2_d}${dir}" ]] && continue
287 # Make sure that this is a system root; comparing other
288 # root folders results in misleading output ...
289 [[ "$dir" == "/" && ! -d "${gen1_d}${dir}/etc" ]] && continue
291 [[ $VERBOSE -ne 0 ]] && echo "Checking systemd hierarchy \"$dir\" ..."
292 ListFilesRecursive "${gen1_d}" "${dir}" >"$tmp_1"
293 ListFilesRecursive "${gen2_d}" "${dir}" >"$tmp_2"
294 if ! diff -U 0 "$tmp_1" "$tmp_2" >"$tmp_diff"; then
295 BeginDiff "\"$dir\" directory"
296 tail -n +3 "$tmp_diff" | grep -Ev '^@@ ' | PipeDiff
302 if [[ -d "${gen1_d}/var/lib/dpkg/info" && -d "${gen2_d}/var/lib/dpkg/info" ]]; then
303 [[ $VERBOSE -ne 0 ]] && echo "Checking list of installed packages ..."
304 chroot "${gen1_d}" dpkg --get-selections >"$tmp_1" || return 2
305 chroot "${gen2_d}" dpkg --get-selections >"$tmp_2" || return 2
306 if ! diff -U 0 "$tmp_1" "$tmp_2" >"$tmp_diff"; then
307 BeginDiff "list of installed packages"
308 tail -n +3 "$tmp_diff" | grep -v '^@@ ' | PipeDiff
313 elif [[ "$backup_type" == "scp" ]]; then
315 file=$(basename "$files")
316 [[ $VERBOSE -ne 0 ]] && echo "Checking \"$file\" ..."
317 if ! diff -U 3 "${gen1_d}/${file}" "${gen2_d}/${file}" >"$tmp_diff"; then
318 BeginDiff "\"$file\""
319 tail -n +3 "$tmp_diff" | PipeDiff
324 echo "Backup type \"$backup_type\" undefined, \"$system\" skipped!"
332 tmp_1=$(mktemp "/tmp/$NAME.XXXXXX") || exit 1
333 tmp_2=$(mktemp "/tmp/$NAME.XXXXXX") || exit 1
334 tmp_diff=$(mktemp "/tmp/$NAME.XXXXXX") || exit 1
335 tmp_out=$(mktemp "/tmp/$NAME.XXXXXX") || exit 1
339 rm -f "$tmp_1" "$tmp_2" "$tmp_diff" "$tmp_out"
342 while [[ $# -gt 0 ]]; do
346 [[ $# -eq 2 ]] || Usage
348 DiffGenerations "$default_backup_type" "$1" "$2" "$default_files"
367 if [[ $# -ge 1 ]]; then
369 if [ ! -r "${conf_d}/$s" ]; then
370 echo "$NAME: Can' read \"${conf_d}/$s\"!"
373 sys+=("${conf_d}/$s")
380 for f in "${sys[@]}"; do
381 [[ -r "$f" && -f "$f" ]] || continue
383 fname=$(basename "$f")
385 "backup-script.conf"|*.sh)
390 HandleSystem "$fname" >"$tmp_out" 2>&1; result=$?
391 [[ $QUIET -eq 0 || $result -ne 0 ]] && cat "$tmp_out"