]> arthur.barton.de Git - backup-script.git/blob - bin/backup-script
Read /etc/backup-script.conf as configureation file, too
[backup-script.git] / bin / backup-script
1 #!/bin/bash
2 #
3 # backup-script system for cloning systems using rsync
4 # Copyright (c)2008-2014 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 CONF_D="/etc/backup-script.d"
15 PIDFILE="/var/run/$NAME.pid"
16
17 DRYRUN=0
18 VERBOSE=0
19
20 export LC_ALL=C
21
22 declare -i count_all=0
23 declare -i count_started=0
24 declare -i count_ok=0
25 declare -i count_ok_vanished=0
26
27 destinations=""
28
29 # Default settings, can be overwritten in backup-script.conf:
30 pre_exec=""
31 post_exec=""
32 default_target=""
33 default_user="root"
34 default_ssh_args_add=""
35 default_rsync_args_add=""
36 default_exclude_args_add=""
37 default_compress=1
38 default_ping=1
39 default_local=0
40 default_generations=0
41
42 Usage() {
43         echo "Usage: $NAME [<options>] [<system> [<system> [...]]]"
44         echo
45         echo "  -p, --progress    Show progress, see rsync(1)."
46         echo "  -n, --dry-run     Test run only, don't copy any data."
47         echo
48         echo "When no <system> is given, all defined systems are used."
49         echo
50         exit 1
51 }
52
53 CleanUp() {
54         if [ -n "$post_exec" ]; then
55                 echo "Executing \"$post_exec\" ..."
56                 sh -c $post_exec
57                 if [ $? -ne 0 ]; then
58                         echo "Warning: post-exec command failed!"
59                 fi
60                 echo
61         fi
62         rm -f "$PIDFILE"
63 }
64
65 GotSignal() {
66         echo
67         echo "--> Got break signal, cleaning up & aborting ..."
68         echo
69         CleanUp
70         echo -n "Aborted: "; date
71         echo
72         exit 9
73 }
74
75 while [ $# -gt 0 ]; do
76         case "$1" in
77           "-n"|"--dry-run")
78                 DRYRUN=1; shift
79                 ;;
80           "-p"|"--progress")
81                 VERBOSE=1; shift
82                 ;;
83           "-"*)
84                 Usage
85                 ;;
86           *)
87                 break
88         esac
89 done
90
91 if [ $# -ge 1 ]; then
92         for s in $@; do
93                 if [ ! -r "${CONF_D}/$s" ]; then
94                         echo "$NAME: Can' read \"${CONF_D}/$s\"!"
95                         exit 1
96                 fi
97                 sys="$sys ${CONF_D}/$s"
98         done
99 else
100         sys=${CONF_D}/*
101 fi
102
103 trap GotSignal SIGINT
104
105 echo -n "Started: "; date
106
107 for conf in "/etc/backup-script.conf" "${CONF_D}/backup-script.conf"; do
108         if [ -r "$conf" ]; then
109                 echo "Reading configuration: \"$conf\" ..."
110                 source "$conf"
111         fi
112 done
113 echo
114
115 # check and create PID file
116 if [ -e "$PIDFILE" ]; then
117         echo "Lockfile \"$PIDFILE\" already exists."
118         echo "Is an other instance still running?"
119         echo
120         echo -n "Aborted: "; date
121         echo
122         exit 3
123 fi
124 touch "$PIDFILE" 2>/dev/null
125 if [ $? -ne 0 ]; then
126         echo "Warning: can't create PID file \"$PIDFILE\"!"
127         echo
128 else
129         echo "$$" >>"$PIDFILE"
130 fi
131
132 if [ -n "$pre_exec" ]; then
133         echo "Executing \"$pre_exec\" ..."
134         sh -c $pre_exec
135         if [ $? -ne 0 ]; then
136                 echo "Error: pre-exec command failed!"; echo
137                 CleanUp
138                 echo "Aborting backup."; echo
139                 exit 2
140         fi
141         sleep 2
142         echo
143 fi
144
145 for f in $sys; do
146         [ -r "$f" -a -f "$f" ] || continue
147
148         fname=`basename $f`
149         case "$fname" in
150                 "backup-script.conf"|*.sh)
151                         continue
152                         ;;
153         esac
154
155         # Set global defaults
156         system="$fname"
157         user="$default_user"
158         target="$default_target"
159         ssh_args_add="$default_ssh_args_add"
160         rsync_args_add="$default_rsync_args_add"
161         exclude_args_add="$default_exclude_args_add"
162         compress="$default_compress"
163         ping="$default_ping"
164         local="$default_local"
165         generations="$default_generations"
166
167         # Read in system configuration file
168         source "$f"
169
170         # Validate configuration
171         [ "$system" = "localhost" -o "$system" = "127.0.0.1" ] && local=1
172
173         [ "$system" = "$fname" ] \
174                 && systxt="\"$system\"" \
175                 || systxt="\"$fname\" [\"$system\"]"
176         [ "$local" -eq 0 ] \
177                 && echo "Working on $systxt ..." \
178                 || echo "Working on $sytxts (local system) ..."
179
180         count_all=$count_all+1
181
182         # Check target directory
183         if [ -z "$target" ]; then
184                 echo "No target directory specified for \"$system\"!? Skipped!"
185                 echo; continue
186         fi
187         if [ ! -d "$target" ]; then
188                 echo "Target \"$target\" is not a directory!? \"$system\" skipped!"
189                 echo; continue
190         fi
191
192         sys_target="$target/$fname"
193         if [ "$DRYRUN" -eq 0 ]; then
194                 mkdir -p "$sys_target" >/dev/null 2>&1
195                 if [ $? -ne 0 ]; then
196                         echo "Can't create \"$sys_target\"!? \"$system\" skipped!"
197                         echo continue
198                 fi
199         fi
200
201         if [ "$local" -eq 0 -a "$ping" -ne 0 ]; then
202                 # Check if system is alive
203                 ping -c 1 "$system" >/dev/null 2>&1
204                 if [ $? -ne 0 ]; then
205                         echo "Host \"$system\" seems not to be alive!? Skipped."
206                         echo; continue
207                 fi
208                 echo "OK, host \"$system\" seems to be alive."
209         fi
210
211         if [ $generations -gt 0 ]; then
212                 # Make sure no old backup is stored in system directory
213                 if [ -e "$sys_target/.stamp" ]; then
214                         # There seems to be a genearation-less backup in the
215                         # target directory!
216                         echo "Target directory \"$sys_target\" seems to be unclean!? \"$system\" skipped!"
217                         echo; continue
218                 fi
219
220                 # Search directory of last generation, if any
221                 last="`ls -1 "$sys_target" 2>/dev/null | sort -r | head -n1`"
222                 if [ -n "$last" ]; then
223                         last="$sys_target/$last"
224                         if [ ! -d "$last" ]; then
225                                 echo "Last snapshot \"$last\" seems not to be a directory!? \"$system\" skipped!"
226                                 echo; continue
227                         fi
228                 fi
229                 sys_target="$sys_target/`date +%Y%m%d-%H%M%S`"
230
231                 if [ -n "$last" -a ! -e "$last/.stamp" ]; then
232                         # Old backup directory without "stamp file", continue
233                         echo "Found incomplete snapshot in \"$last\", reusing and renaming it ..."
234                         mv "$last" "$sys_target" >/dev/null 2>&1
235                         if [ $? -ne 0 ]; then
236                                 echo "Failed to rename last snapshot \"$last\" to \"$sys_target\"!? \"$system\" skipped!"
237                                 echo; continue
238                         fi
239                 elif [ -n "$last" ]; then
240                         # Old backup directory found, create new snapshot
241                         echo "Found last snapshot in \"$last\"."
242                         if [ "$DRYRUN" -eq 0 ]; then
243                                 btrfs subvolume snapshot \
244                                   "$last" "$sys_target" >/dev/null 2>&1; r=$?
245                                 if [ $r -ne 0 ]; then
246                                         echo "Can't create btrfs snapshot \"$sys_target\" of \"$last\", code $r!? \"$system\" skipped!"
247                                         echo; continue
248                                 fi
249                                 echo "Created new snapshot in \"$sys_target\"."
250                         else
251                                 echo " *** Trial run, not creating new snapshot in \"$sys_target\"!"
252                         fi
253                 else
254                         # No old backup found, create new subvolume
255                         if [ "$DRYRUN" -eq 0 ]; then
256                                 btrfs subvolume create \
257                                   "$sys_target" >/dev/null 2>&1; r=$?
258                                 if [ $r -ne 0 ]; then
259                                         echo "Can't create btrfs subvolume \"$sys_target\", code $r!? \"$system\" skipped!"
260                                         echo; continue
261                                 fi
262                                 echo "Created new subvolume in \"$sys_target\"."
263                         else
264                                 echo " *** Trial run, not creating new subvolume \"$sys_target\"!"
265                         fi
266                 fi
267         fi
268
269         ssh_cmd="ssh"
270         [ -n "$ssh_args_add" ] && ssh_cmd="$ssh_cmd $ssh_args_add"
271
272         cmd="rsync --archive"
273         [ "$compress" -ne 0 ] && cmd="$cmd --compress"
274         cmd="$cmd --rsh=\"$ssh_cmd\" --delete --delete-excluded --sparse"
275         [ "$VERBOSE" -gt 0 ] && cmd="$cmd --progress"
276         cmd="$cmd --exclude=/dev --exclude=/proc --exclude=/sys"
277         cmd="$cmd --exclude=/run --exclude=/tmp --exclude=/var/tmp"
278         cmd="$cmd --exclude=/media --exclude=/mnt --exclude=/net"
279         cmd="$cmd --exclude=/var/cache/apt --exclude=/var/log"
280         [ -n "$exclude_args_add" ] && cmd="$cmd $exclude_args_add"
281         [ -n "$rsync_args_add" ] && cmd="$cmd $rsync_args_add"
282
283         [ "$local" -eq 0 ] \
284                 && cmd="$cmd ${user}@${system}:/ $sys_target/" \
285                 || cmd="$cmd / $sys_target/"
286
287         echo "Backing up to \"$sys_target\" ..."
288         echo -n "Start date: "; date
289         echo "$cmd"
290         count_started=$count_started+1
291         ok=0
292         
293         if [ "$DRYRUN" -eq 0 ]; then
294                 rm -f "$sys_target/.stamp"
295                 $SHELL -c "$cmd"; ret=$?
296                 echo "code=$ret" >"$sys_target/.stamp"
297         else
298                 echo " *** Trial run, not executing synchronization command!"
299                 ret=0
300         fi
301
302         if [ $ret -eq 20 ]; then
303                 echo "Backup of \"$system\" interrupted. Aborting ..."
304                 CleanUp
305                 exit 1
306         fi
307
308         echo -n "End date: "; date
309         if [ $ret -eq 0 -o $ret -eq 24 ]; then
310                 [ $ret -eq 24 ] && count_ok_vanished=$count_ok_vanished+1
311
312                 echo "System \"$system\" completed with status $ret, OK."
313                 [ "$DRYRUN" -gt 0 ] || count_ok=$count_ok+1
314                 ok=1
315         else
316                 echo "System \"$system\" completed with ERRORS, code $ret!"
317         fi
318
319         # Clean up old generations
320         if [ $generations -gt 0 ]; then
321                 sys_target="$target/$fname"
322                 to_delete=`ls -1t "$sys_target" 2>/dev/null | tail -n+$generations | sort`
323                 if [ -n "$to_delete" -a $ok -eq 1 ]; then
324                         [ "$DRYRUN" -eq 0 ] \
325                                 && echo "Deleting old backup generations:" \
326                                 || echo " *** Trial run, not deleting old generations:"
327                         for delete in $to_delete; do
328                                 dir="$sys_target/$delete"
329                                 if [ ! -e "$dir/.stamp" ]; then
330                                         echo "Not deleting \"$dir\", not a backup directory!?"
331                                         continue
332                                 fi
333                                 last=`stat "$dir/.stamp" 2>/dev/null | grep "^Modify: " \
334                                  | cut -d':' -f2- | cut -d. -f1`
335                                 echo "Removing backup from" $last "..."
336                                 if [ "$DRYRUN" -eq 0 ]; then
337                                         btrfs subvolume delete \
338                                          "$dir" >/dev/null 2>&1
339                                         [ $? -eq 0 ] || \
340                                          echo "Failed to delete \"$dir\"!"
341                                 fi
342                         done
343                         echo -n "Clean up finished: "; date
344                 elif [ -n "$to_delete" ]; then
345                         echo "There have been errors, not cleaning up old generations!"
346                 else
347                         echo "Nothing to clean up."
348                 fi
349         fi
350
351         destinations="$destinations $target"
352         echo
353 done
354
355 sync
356
357 paths=$( echo $destinations | sed -e 's/ /\n/g' | sort | uniq )
358 if [ "$DRYRUN" -eq 0 -a -n "$paths" ]; then
359         df -h $paths
360         echo
361 fi
362
363 CleanUp
364
365 echo -n "Done: "; date
366 echo
367 [ $count_all -eq 1 ] && s="" || s="s"
368 echo " - $count_all job$s defined,"
369 [ $count_started -eq 1 ] && s="" || s="s"
370 echo " - $count_started job$s started,"
371 echo " - $count_ok done without errors."
372 echo
373
374 if [ $count_started -ne $count_ok ]; then
375         echo "----->  THERE HAVE BEEN ERRORS!  <-----"
376         echo
377 fi
378
379 # -eof-