]> arthur.barton.de Git - backup-script.git/blob - bin/backup-script
Use correct counter when checking which generations to delete
[backup-script.git] / bin / backup-script
1 #!/bin/bash
2 #
3 # backup-script system for cloning systems using rsync
4 # Copyright (c)2008-2015 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
19 export LC_ALL=C
20
21 declare -i count_all=0
22 declare -i count_started=0
23 declare -i count_ok=0
24 declare -i count_ok_vanished=0
25
26 destinations=""
27
28 # Default settings, can be overwritten in backup-script.conf:
29 conf_d="/etc/backup-script.d"
30 pre_exec=""
31 post_exec=""
32 default_source_root="/"
33 default_target=""
34 default_user="root"
35 default_ssh_args_add=""
36 default_rsync_args_add=""
37 default_exclude_args_add=""
38 default_compress=1
39 default_ping=1
40 default_local=0
41 default_generations=0
42 default_job_pre_exec=""
43 default_job_post_exec=""
44
45 Usage() {
46         echo "Usage: $NAME [<options>] [<system> [<system> [...]]]"
47         echo
48         echo "  -p, --progress    Show progress, see rsync(1)."
49         echo "  -n, --dry-run     Test run only, don't copy any data."
50         echo
51         echo "When no <system> is given, all defined systems are used."
52         echo
53         exit 1
54 }
55
56 CleanUp() {
57         if [ -n "$post_exec" ]; then
58                 echo "Executing \"$post_exec\" ..."
59                 sh -c $post_exec
60                 if [ $? -ne 0 ]; then
61                         echo "Warning: post-exec command failed!"
62                 fi
63                 echo
64         fi
65         rm -f "$PIDFILE"
66 }
67
68 GotSignal() {
69         echo
70         echo "--> Got break signal, cleaning up & aborting ..."
71         echo
72         CleanUp
73         echo -n "Aborted: "; date
74         echo
75         exit 9
76 }
77
78 ExecJob() {
79         local what="$1"
80         local cmd="$2"
81
82         echo "Running job ${what}-exec command ..."
83         [ "$local" -eq 0 ] \
84                 && cmd="$ssh_cmd ${user}@${system} $cmd"
85         echo -n "Start date (${what}-exec): "; date
86         echo "$cmd"
87         if [ "$DRYRUN" -eq 0 ]; then
88                 $SHELL -c "$cmd"; local ret=$?
89         else
90                 echo " *** Trial run, not executing ${what}-exec command!"
91                 ret=0
92         fi
93         [ $ret -eq 0 ] \
94                 && echo "The ${what}-exec command completed with status 0, OK." \
95                 || echo "The ${what}-exec command completed with ERRORS, code $ret!"
96         return $ret
97 }
98
99 GetFS() {
100         local dir="$1"
101
102         while [ -n "$dir" ]; do
103                 findmnt -fn -o FSTYPE --raw "$dir" 2>/dev/null; local r=$?
104                 if [ $r -eq 0 ]; then
105                         return 0
106                 elif [ $r -eq 127 ]; then
107                         echo "UNKNOWN"
108                         return 1
109                 fi
110                 dir=$(dirname "$dir") || return 1
111         done
112 }
113
114 CreateSubvolume() {
115         local volume="$1"
116         local fs
117         local dir
118
119         dir=$(dirname "$volume")
120         fs=$(GetFS "$dir")
121         case "$fs" in
122           "btrfs")
123                 btrfs subvolume create "$volume"  >/dev/null || return 1
124                 ;;
125           *)
126                 echo "CreateSubvolume: Incompatible FS type \"$fs\" on \"$dir\"!"
127                 return 9
128         esac
129         return 0
130 }
131
132 CloneSubvolume() {
133         local source="$1"
134         local volume="$2"
135         local dir
136         local fs
137
138         dir=$(dirname "source")
139         fs=$(GetFS "$source")
140         case "$fs" in
141           "btrfs")
142                 btrfs subvolume snapshot "$source" "$volume"  >/dev/null || return 1
143                 ;;
144           *)
145                 echo "CloneSubvolume: Incompatible FS type \"$fs\" on \"$source\"!"
146                 return 9
147         esac
148         return 0
149 }
150
151 RenameSubvolume() {
152         local source="$1"
153         local target="$2"
154         local fs
155
156         fs=$(GetFS "$source")
157         case "$fs" in
158           "btrfs")
159                 mv "$source" "$target" || return 1
160                 ;;
161           *)
162                 echo "RenameSubvolume: Incompatible FS type \"$fs\" on \"$source\"!"
163                 return 9
164         esac
165         return 0
166 }
167
168 DeleteSubvolume() {
169         local volume="$1"
170         local fs
171
172         fs=$(GetFS "$source")
173         case "$fs" in
174           "btrfs")
175                 btrfs subvolume delete "$volume" >/dev/null || return 1
176                 ;;
177           *)
178                 echo "DeleteSubvolume: Incompatible FS type \"$fs\" on \"$volume\"!"
179                 return 9
180         esac
181         return 0
182 }
183
184 while [ $# -gt 0 ]; do
185         case "$1" in
186           "-n"|"--dry-run")
187                 DRYRUN=1; shift
188                 ;;
189           "-p"|"--progress")
190                 VERBOSE=1; shift
191                 ;;
192           "-"*)
193                 Usage
194                 ;;
195           *)
196                 break
197         esac
198 done
199
200 trap GotSignal SIGINT
201
202 echo -n "Started: "; date
203
204 for conf in "/etc/backup-script.conf" "${conf_d}/backup-script.conf"; do
205         if [ -r "$conf" ]; then
206                 echo "Reading configuration: \"$conf\" ..."
207                 source "$conf"
208         fi
209 done
210 echo
211
212 if [ $# -ge 1 ]; then
213         for s in "$@"; do
214                 if [ ! -r "${conf_d}/$s" ]; then
215                         echo "$NAME: Can' read \"${conf_d}/$s\"!"
216                         exit 1
217                 fi
218                 sys="$sys ${conf_d}/$s"
219         done
220 else
221         sys="${conf_d}/"*
222 fi
223
224 # check and create PID file
225 if [ -e "$PIDFILE" ]; then
226         echo "Lockfile \"$PIDFILE\" already exists."
227         echo "Is an other instance still running?"
228         echo
229         echo -n "Aborted: "; date
230         echo
231         exit 3
232 fi
233 touch "$PIDFILE" 2>/dev/null
234 if [ $? -ne 0 ]; then
235         echo "Warning: can't create PID file \"$PIDFILE\"!"
236         echo
237 else
238         echo "$$" >>"$PIDFILE"
239 fi
240
241 if [ -n "$pre_exec" ]; then
242         echo "Executing \"$pre_exec\" ..."
243         sh -c $pre_exec
244         if [ $? -ne 0 ]; then
245                 echo "Error: pre-exec command failed!"; echo
246                 CleanUp
247                 echo "Aborting backup."; echo
248                 exit 2
249         fi
250         sleep 2
251         echo
252 fi
253
254 for f in $sys; do
255         [ -r "$f" -a -f "$f" ] || continue
256
257         fname=$(basename "$f")
258         case "$fname" in
259                 "backup-script.conf"|*.sh)
260                         continue
261                         ;;
262         esac
263
264         # Set global defaults
265         system="$fname"
266         user="$default_user"
267         source_root="$default_source_root"
268         target="$default_target"
269         ssh_args_add="$default_ssh_args_add"
270         rsync_args_add="$default_rsync_args_add"
271         exclude_args_add="$default_exclude_args_add"
272         compress="$default_compress"
273         ping="$default_ping"
274         local="$default_local"
275         generations="$default_generations"
276         job_pre_exec="$default_job_pre_exec"
277         job_post_exec="$default_job_post_exec"
278
279         # Read in system configuration file
280         source "$f"
281
282         # Validate configuration
283         [ "$system" = "localhost" -o "$system" = "127.0.0.1" ] && local=1
284
285         [ "$system" = "$fname" ] \
286                 && systxt="\"$system\"" \
287                 || systxt="\"$fname\" [\"$system\"]"
288         [ "$local" -eq 0 ] \
289                 && echo "Working on $systxt ..." \
290                 || echo "Working on $systxt (local system) ..."
291
292         count_all=$count_all+1
293
294         # Check target directory
295         if [ -z "$target" ]; then
296                 echo "No target directory specified for \"$system\"!? Skipped!"
297                 echo; continue
298         fi
299         if [ ! -d "$target" ]; then
300                 echo "Target \"$target\" is not a directory!? \"$system\" skipped!"
301                 echo; continue
302         fi
303
304         sys_target="$target/$fname"
305         sys_root="$sys_target"
306         if [ "$DRYRUN" -eq 0 ]; then
307                 mkdir -p "$sys_target" >/dev/null 2>&1
308                 if [ $? -ne 0 ]; then
309                         echo "Can't create \"$sys_target\"!? \"$system\" skipped!"
310                         echo; continue
311                 fi
312         fi
313
314         if [ "$local" -eq 0 -a "$ping" -ne 0 ]; then
315                 # Check if system is alive
316                 ping -c 1 "$system" >/dev/null 2>&1
317                 if [ $? -ne 0 ]; then
318                         echo "Host \"$system\" seems not to be alive!? Skipped."
319                         echo; continue
320                 fi
321                 echo "OK, host \"$system\" seems to be alive."
322         fi
323
324         if [ $generations -gt 0 ]; then
325                 # Make sure no old backup is stored in system directory
326                 if [ -e "$sys_target/.stamp" ]; then
327                         # There seems to be a genearation-less backup in the
328                         # target directory!
329                         echo "Target directory \"$sys_target\" seems to be unclean!? \"$system\" skipped!"
330                         echo; continue
331                 fi
332
333                 # Search directory of last generation, if any
334                 last=$(ls -1d "$sys_target"/[0-9]* 2>/dev/null | sort -r | head -n1)
335                 if [ -n "$last" ]; then
336                         if [ ! -d "$last" ]; then
337                                 echo "Last snapshot \"$last\" seems not to be a directory!? \"$system\" skipped!"
338                                 echo; continue
339                         fi
340                 fi
341                 sys_target="$sys_target/$(date +%Y%m%d-%H%M%S)"
342
343                 if [ -n "$last" -a ! -e "$last/.stamp" ]; then
344                         # Old backup directory without "stamp file", continue
345                         echo "Found incomplete snapshot in \"$last\", reusing and renaming it ..."
346                         RenameSubvolume "$last" "$sys_target"
347                         if [ $? -ne 0 ]; then
348                                 echo "Failed to rename last snapshot \"$last\" to \"$sys_target\"!? \"$system\" skipped!"
349                                 echo; continue
350                         fi
351                 elif [ -n "$last" ]; then
352                         # Old backup directory found, create new snapshot
353                         echo "Found last snapshot in \"$last\"."
354                         if [ "$DRYRUN" -eq 0 ]; then
355                                 CloneSubvolume "$last" "$sys_target"; r=$?
356                                 if [ $r -ne 0 ]; then
357                                         echo "Can't create snapshot \"$sys_target\" of \"$last\", code $r!? \"$system\" skipped!"
358                                         echo; continue
359                                 fi
360                                 echo "Created new snapshot in \"$sys_target\"."
361                         else
362                                 echo " *** Trial run, not creating new snapshot in \"$sys_target\"!"
363                         fi
364                 else
365                         # No old backup found, create new subvolume
366                         if [ "$DRYRUN" -eq 0 ]; then
367                                 CreateSubvolume "$sys_target"; r=$?
368                                 if [ $r -ne 0 ]; then
369                                         echo "Can't create subvolume \"$sys_target\", code $r!? \"$system\" skipped!"
370                                         echo; continue
371                                 fi
372                                 echo "Created new subvolume in \"$sys_target\"."
373                         else
374                                 echo " *** Trial run, not creating new subvolume \"$sys_target\"!"
375                         fi
376                 fi
377         fi
378
379         ssh_cmd="ssh"
380         [ -n "$ssh_args_add" ] && ssh_cmd="$ssh_cmd $ssh_args_add"
381
382         # execute job "pre-exec" command, if any
383         if [ -n "$job_pre_exec" ]; then
384                 ExecJob pre "$job_pre_exec" ; ret=$?
385                 if [ $ret -ne 0 ]; then
386                         [ $ret -ne 99 ] && count_started=$count_started+1
387                         echo "Pre-exec command failed, \"$system\" skipped!"
388                         echo; continue
389                 fi
390         fi
391
392         # prepare (remote) command ...
393         cmd="rsync --archive"
394         [ "$compress" -ne 0 ] && cmd="$cmd --compress"
395         [ "$local" -eq 0 ] && cmd="$cmd --rsh=\"$ssh_cmd\""
396         cmd="$cmd --delete --delete-excluded --sparse"
397         [ "$VERBOSE" -gt 0 ] && cmd="$cmd --progress"
398         if [ "$source_root" = "$default_source_root" ]; then
399                 cmd="$cmd --exclude=/dev --exclude=/proc --exclude=/sys"
400                 cmd="$cmd --exclude=/run --exclude=/tmp --exclude=/var/tmp"
401                 cmd="$cmd --exclude=/media --exclude=/mnt --exclude=/net"
402                 cmd="$cmd --exclude=/var/cache/apt --exclude=/var/log"
403         fi
404         [ -n "$exclude_args_add" ] && cmd="$cmd $exclude_args_add"
405         [ -n "$rsync_args_add" ] && cmd="$cmd $rsync_args_add"
406
407         [ "$local" -eq 0 ] \
408                 && cmd="$cmd ${user}@${system}:$source_root $sys_target/" \
409                 || cmd="$cmd $source_root $sys_target/"
410
411         echo "Backing up to \"$sys_target\" ..."
412         echo -n "Start date: "; date
413         echo "$cmd"
414         count_started=$count_started+1
415         ok=0
416
417         if [ "$DRYRUN" -eq 0 ]; then
418                 rm -f "$sys_target/.stamp"
419                 $SHELL -c "$cmd"; ret=$?
420                 echo "code=$ret" >"$sys_target/.stamp"
421         else
422                 echo " *** Trial run, not executing synchronization command!"
423                 ret=0
424         fi
425
426         if [ $ret -eq 20 ]; then
427                 echo "Backup of \"$system\" interrupted. Aborting ..."
428                 CleanUp
429                 exit 1
430         fi
431
432         echo -n "End date: "; date
433         if [ $ret -eq 0 -o $ret -eq 24 ]; then
434                 [ $ret -eq 24 ] && count_ok_vanished=$count_ok_vanished+1
435
436                 echo "System \"$system\" completed with status $ret, OK."
437                 [ "$DRYRUN" -gt 0 ] || count_ok=$count_ok+1
438                 ok=1
439         else
440                 echo "System \"$system\" completed with ERRORS, code $ret!"
441         fi
442
443         # execute job "post-exec" command, if any
444         if [ -n "$job_post_exec" ]; then
445                 ExecJob post "$job_post_exec"
446         fi
447
448         if [ $generations -gt 0 ]; then
449                 # Update "latest" symlink
450                 if [ "$DRYRUN" -eq 0 ]; then
451                         rm -f "$sys_root/latest"
452                         ln -s "$sys_target" "$sys_root/latest"
453                 fi
454                 # Clean up old generations
455                 declare -i gen_count=$generations+2
456                 to_delete=$(ls -1t "$sys_root" 2>/dev/null | tail -n+$gen_count | sort)
457                 if [ -n "$to_delete" -a $ok -eq 1 ]; then
458                         [ "$DRYRUN" -eq 0 ] \
459                                 && echo "Deleting old backup generations (keep $generations) ..." \
460                                 || echo " *** Trial run, not deleting old generations:"
461                         for delete in $to_delete; do
462                                 dir="$sys_root/$delete"
463                                 if [ ! -e "$dir/.stamp" ]; then
464                                         echo "Not deleting \"$dir\", not a backup directory!?"
465                                         continue
466                                 fi
467                                 last=$(stat "$dir/.stamp" 2>/dev/null | grep "^Modify: " \
468                                  | cut -d':' -f2- | cut -d. -f1)
469                                 echo "Removing backup from" $last "..."
470                                 if [ "$DRYRUN" -eq 0 ]; then
471                                         DeleteSubvolume "$dir"
472                                         [ $? -eq 0 ] || \
473                                           echo "Failed to delete \"$dir\"!"
474                                 fi
475                         done
476                         echo -n "Clean up finished: "; date
477                 elif [ -n "$to_delete" ]; then
478                         echo "There have been errors, not cleaning up old generations!"
479                 else
480                         echo "Nothing to clean up."
481                 fi
482         fi
483
484         destinations="$destinations $target"
485         echo
486 done
487
488 sync
489
490 paths=$( echo $destinations | sed -e 's/ /\n/g' | sort | uniq )
491 if [ "$DRYRUN" -eq 0 -a -n "$paths" ]; then
492         df -h $paths
493         echo
494 fi
495
496 CleanUp
497
498 echo -n "Done: "; date
499 echo
500 [ $count_all -eq 1 ] && s="" || s="s"
501 echo " - $count_all job$s defined,"
502 [ $count_started -eq 1 ] && s="" || s="s"
503 echo " - $count_started job$s started,"
504 echo " - $count_ok done without errors."
505 echo
506
507 if [ $count_started -ne $count_ok ]; then
508         echo "----->  THERE HAVE BEEN ERRORS!  <-----"
509         echo
510 fi
511
512 # -eof-