]> git.alrj.org Git - brioche.git/blob - brioche
Replace lftp's call to 'rmdir' by calls to 'rm -rf'.
[brioche.git] / brioche
1 #! /bin/bash
2
3 # Brioche Backup
4 # Full and Incremental backup script with tar and LVM snapshots.
5 #
6 # Copyright (C) 2008, 2009 Amand Tihon <amand.tihon@alrj.org>
7 #
8 # This program is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation, either version 3 of the License, or
11 # (at your option) any later version.
12 #
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 # GNU General Public License for more details.
17 #
18 # You should have received a copy of the GNU General Public License
19 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
20 #
21 # Note: This script relies on GNU tar specific options,
22 # do not attempt to use it as-is with other tar implementations.
23
24
25 # Mandatory
26 CONFIG_FILE="/etc/brioche.conf"
27
28
29 #######################################################################
30 #  Default config values. Do not change, edit $CONFIG_FILE
31 #######################################################################
32
33 BACKUPTAB="/etc/briochetab"
34 MAILTO="root"
35 REPODIR="/backup"
36 TAR_OPTS="--one-file-system -S "
37 COMPRESS="gz"
38 COMPRESS_OPT="--gzip"
39 SNAPSHOT_MOUNTPOINT="/mnt/backup-snapshot"
40 SNAPSHOT_NAME="backup-snap"
41 SNAPSHOT_SIZE="5G"
42 USAGE_WARN="80"
43
44 USE_FTP="no"
45 FTP_HOST="ftpback.example.com"
46 FTP_DIR="/"
47 FTP_KEEP="4"
48
49 # Ensure that we have a minimal PATH
50 PATH=/sbin:/bin:/usr/sbin:/usr/bin
51 FINAL_STATUS="SUCCESS"
52 SUMMARY="/tmp/backup.sumary"
53
54
55 #######################################################################
56 #  Helpers
57 #######################################################################
58
59 trap 'interrupted ctrl-C' INT
60 trap 'interrupted KILL' TERM
61
62 NOW()
63 {
64   echo -n `date "+%Y-%m-%d %H:%M:%S"`
65 }
66
67 # Log a line with timestamp.
68 log()
69 {
70   echo "[`NOW`] $@"
71 }
72
73 # The summary that will be sent by email
74 summary()
75 {
76   log $@
77   echo $@ >> $SUMMARY
78 }
79
80 # Set final status to CRITICAL
81 set_critical()
82 {
83   FINAL_STATUS="CRITICAL"
84 }
85
86 set_error()
87 {
88   if [ "$FINAL_STATUS" != "CRITICAL" ]; then
89     FINAL_STATUS="ERROR"
90   fi
91 }
92
93 set_warning()
94 {
95   if [ "$FINAL_STATUS" == "SUCCESS" ]; then
96     FINAL_STATUS="WARNING"
97   fi
98 }
99
100 finish()
101 {
102   # Check free space
103   local usage=`df -hP "${REPODIR}" | tail -n 1 | tr -s [:space:] | cut -d" " -f5 | tr -d '%'`
104   log "${REPODIR} usage after backup: ${usage}%."
105
106   if [ "$usage" -ge "$USAGE_WARN" ]; then
107     set_warning
108     summary "Warning : Filesystem ${REPODIR} is ${usage}% full."
109   fi
110
111   summary "Backup procedure ended on `NOW`"
112
113   SUBJECT="Backup report for `hostname` (${FINAL_STATUS})."
114   mail -s "${SUBJECT}" "${MAILTO}" <  "$SUMMARY"
115
116   rm -f "$SUMMARY"
117   exit
118 }
119
120 interrupted()
121 {
122   FINAL_STATUS="INTERRUPTED"
123   summary "Backup procedure interrupted by user ($1)."
124   summary "Take care, an LVM snapshot may still be present !"
125   finish
126 }
127
128 #######################################################################
129 #  Snapshots
130 #######################################################################
131
132 # Make a snapshot of a logical volume.
133 # Usage: make_snapshot vg lv
134 make_snapshot()
135 {
136   log "Creating a snapshot volume of /dev/$1/$2"
137   lvcreate --snapshot -L ${SNAPSHOT_SIZE} -n ${SNAPSHOT_NAME} /dev/$1/$2
138   RETVAL=$?
139   if [ "$RETVAL" != "0" ]; then
140     log "Error ${RETVAL}: Unable to create a snapshot of /dev/$1/$2"
141     return 1
142   fi
143 }
144
145 # Mount the snapshot volume.
146 # On error, tries to remove the snapshot.
147 # Usage: mount_snapshot vg
148 mount_snapshot()
149 {
150   log "Mounting the snapshot volume ${SNAPSHOT_NAME}."
151   if [ ! -d "${SNAPSHOT_MOUNTPOINT}" ]; then
152     log "Creating mountpoint ${SNAPSHOT_MOUNTPOINT}."
153     mkdir -p "${SNAPSHOT_MOUNTPOINT}"
154   fi
155
156   mount /dev/$1/${SNAPSHOT_NAME} ${SNAPSHOT_MOUNTPOINT}
157   RETVAL=$?
158   if [ "$RETVAL" != "0" ]; then
159     log "Error ${RETVAL}: Unable to mount /dev/$1/${SNAPSHOT_NAME} on ${SNAPSHOT_MOUNTPOINT}"
160     remove_snapshot $1 || return 100
161     return 1
162   fi
163 }
164
165 # Remove a previously created snapshot. It must be unmounted.
166 # Usage: remove_snapshot vg
167 remove_snapshot()
168 {
169   log "Removing the snapshot volume /dev/$1/${SNAPSHOT_NAME}."
170   lvremove -f /dev/$1/${SNAPSHOT_NAME}
171   RETVAL=$?
172   if [ "$RETVAL" != "0" ]; then
173     log "Error ${RETVAL}: Unable to remove the snapshot volume /dev/$1/${SNAPSHOT_NAME}"
174     return 100
175   fi
176 }
177
178 # Unmount the previously mounted snapshot.
179 # Usage: unmount_snapshot vg
180 unmount_snapshot()
181 {
182   log "Unmounting the snapshot volume /dev/$1/${SNAPSHOT_NAME}."
183   umount /dev/$1/${SNAPSHOT_NAME}
184   RETVAL=$?
185   if [ "$RETVAL" != "0" ]; then
186     log "Error ${RETVAL}: Unable to unmount the snapshot volume /dev/$1/${SNAPSHOT_NAME}"
187     return 100
188   fi
189 }
190
191 #######################################################################
192 #  Main backup functions
193 #######################################################################
194
195 # Make a full backup of a directory, with snar file for subsequent incremental
196 # Usage: make_full_backup source_dir hostname volumename
197 # Returns 0 on success, 1 on error
198 make_full_backup()
199 {
200   log "Making full backup of $2 - ${3}."
201
202   local destdir="${REPODIR}/$2"
203   local today=`date "+%Y%m%d"`
204   local destfile="${destdir}/${3}.full.${today}.tar.${COMPRESS}"
205   local destsnar="${destdir}/${3}.full.snar"
206
207   # Move previous run to the "undo" directory
208   if [ ! -d "${destdir}/undo" ]; then
209     log "Creating undo/ directory."
210     mkdir -p "${destdir}/undo"
211   fi
212   log "Moving old run into undo/ directory."
213   mv ${destdir}/${3}.* ${destdir}/undo
214
215   # Do the actual backup. Destination file name and snar are like
216   # /backup/valeron/root.full.20090105.tar.bz2
217   # /backup/valeron/root.full.snar
218   log "Running tar..."
219   tar -cf ${destfile} ${TAR_OPTS} ${COMPRESS_OPT} -g ${destsnar} $1
220
221   if [ "$?" = "0" ]; then
222     log "Removing undo/ directory."
223     rm -rf "${destdir}/undo"
224   else
225     log "Error $?: Could not archive $2 - $3"
226     return 1
227   fi
228   return 0
229 }
230
231
232 # Make an incremental backup of a directory, from full's snar file
233 # Usage: make_incr_backup source_dir hostname volumename
234 # Returns 0 on success, 1 on error, 2 if no previous full is found.
235 make_incr_backup()
236 {
237   log "Making incremental backup of $2 - ${3}."
238
239   local destdir="${REPODIR}/$2"
240   local today=`date "+%Y%m%d"`
241   local destfile="${destdir}/${3}.incr.${today}.tar.${COMPRESS}"
242   local destsnar="${destdir}/${3}.incr.${today}.snar"
243   local fullsnar="${destdir}/${3}.full.snar"
244
245   # Test existence of full backup
246   if [ ! -e $fullsnar ]; then
247     log "Could not find catalog ${fullsnar}."
248     return 2
249   fi
250
251   # Prepare the copy of the snar file
252   cp "$fullsnar" "$destsnar"
253
254   # Do the actual backup. Destination file name and snar are like
255   # /backup/valeron/root.incr.20090105.tar.bz2
256   # /backup/valeron/root.incr.20090105.snar
257   log "Running tar..."
258   tar -cf ${destfile} ${TAR_OPTS} ${COMPRESS_OPT} -g ${destsnar} $1
259
260   if [ ! "$?" = "0" ]; then
261     log "Error $?: Could not archive $2 - $3"
262     return 1
263   fi
264   return 0
265 }
266
267
268 #######################################################################
269 #  FTP functions
270 #######################################################################
271
272 # Push everything in the directory given in $1 to the FTP server, under
273 # /FTP_DIR/$2/latest
274 ftp_push()
275 {
276   log "Mirror $1 on FTP (${FTP_HOST})."
277   local source="${1}"
278   local target="${FTP_DIR}/${2}/latest"
279   local command="mkdir -p ${target}; cd ${target}"
280   command="${command}; mirror --reverse --only-newer --verbose ${source}"
281
282   lftp -e "${command}; exit" ${FTP_HOST}
283 }
284
285 # Rotate old files on FTP
286 # Usage: ftp_rotate group
287 ftp_rotate()
288 {
289   log "Rotating backups of $1 on FTP (${FTP_HOST})."
290   local lastrun="run-${FTP_KEEP}"
291   local target="${FTP_DIR}/${1}"
292   local commands="mkdir -p ${target}; cd ${target}"
293
294   # Build commands
295   # Remove oldest run
296   if [ "$FTP_KEEP" != "0" ]; then
297     commands="$commands; rm -rf ${lastrun}"
298
299     # Move everything back
300     for run in `seq $FTP_KEEP -1 2`; do
301       local newer=$run
302       let "newer -= 1"
303       commands="$commands; mv run-$newer run-$run"
304     done
305     # Move "old latest" to run-1
306     commands="$commands; mv latest run-1"
307   else
308     commands="$commands; rm -rf latest"
309   fi
310
311   # Create "new latest" directory
312   commands="$commands; mkdir latest; exit"
313
314   # Run the commands on the FTP server
315   lftp -e "$commands" $FTP_HOST
316 }
317 #######################################################################
318 #  Start here
319 #######################################################################
320
321 # Truncate summary file, start header blurb
322 echo "" > $SUMMARY
323 summary "Backup procedure started on `NOW`"
324
325 if [ -r "${CONFIG_FILE}" ]
326 then
327   source "${CONFIG_FILE}"
328 else
329   summary "Error: Unable to read configuration file ${CONFIG_FILE}. Aborting."
330   set_critical
331   finish
332 fi
333
334 if [ ! -r "${BACKUPTAB}" ]
335 then
336   summary "Error: Unable to read ${BACKUPTAB}. Aborting."
337   set_critical
338   finish
339 fi
340
341 # TODO: Something cleaner, here...
342 DO_FULL_BACKUP="no"
343 case "$1" in
344   --full|-f)
345     DO_FULL_BACKUP="yes"
346     ;;
347   --help|-h)
348     echo "Usage: ${0} [-f]"
349     exit 0
350     ;;
351 esac
352
353 # Discover which COMPRESS_OPT to use, from COMPRESS
354 case "$COMPRESS" in
355   gz)
356     COMPRESS_OPT="--gzip"
357     ;;
358   bz2)
359     COMPRESS_OPT="--bzip2"
360     ;;
361   lzma)
362     COMPRESS_OPT="--lzma"
363     ;;
364   none)
365     COMPRESS_OPT=""
366     COMPRESS=""
367     ;;
368   *)
369     summary "Unknown compression method: ${COMPRESS}. Falling back to gzip."
370     COMPRESS="gz"
371     COMPRESS_OPT="--gzip"
372     ;;
373 esac
374
375 #######################################################################
376 #  Parse backuptab file, call backup functions for each line
377 #######################################################################
378
379 # Rotate FTP groups on full
380 if [ "$USE_FTP" = "yes" -a "$DO_FULL_BACKUP" = "yes" ]; then
381   for group in `grep -v -E '^[[:space:]]*(#.*)?$' $BACKUPTAB | awk '{print $3}' | sort -u`
382   do
383     ftp_rotate $group
384   done
385 fi
386
387 # Ignore empty and commented lines
388 grep -v -E '^[[:space:]]*(#.*)?$' $BACKUPTAB | tr -s [:space:]| while read line
389 do
390   # split line in fields
391   device=`echo $line|cut -d" " -f1`
392   dosnap=`echo $line|cut -d" " -f2`
393   group=`echo $line|cut -d" " -f3`
394   volume=`echo $line|cut -d" " -f4`
395
396   # Make and mount snapshot if needed.
397   if [ "$dosnap" = "yes" ]; then
398     # Split the device to find the VG and LV
399     vg=`echo $device|cut -d"/" -f3`
400     lv=`echo $device|cut -d"/" -f4`
401     make_snapshot $vg $lv
402     if [ "$?" != "0" ]; then
403       summary "Could not take a snapshot of $device"
404       set_error
405       continue
406       # Next one
407     fi
408
409     mount_snapshot $vg
410     RETVAL="$?"
411     if [ "$RETVAL" != "0" ]; then
412       summary "Could not mount the snapshot of $device"
413       set_error
414       if [ "$RETVAL" = "100" ]; then
415         summary "Could not remove the snapshot !"
416         set_critical
417         finish
418       fi
419       continue
420       # Next one
421     fi
422
423     BACKUP_SOURCE="${SNAPSHOT_MOUNTPOINT}"
424   else
425     BACKUP_SOURCE="$device"
426   fi
427
428   # Make the backup
429   if [ "$DO_FULL_BACKUP" = "no" ]; then
430     make_incr_backup ${BACKUP_SOURCE} $group $volume
431     RETVAL=$?
432     if [ "$RETVAL" = "0" ]; then
433       summary "INCREMENTAL backup of $device done on `NOW`."
434     elif [ "$RETVAL" = "1" ]; then
435       summary "Error during incremental backup of $device"
436       set_error
437     elif [ "$RETVAL" = "2" ]; then
438       summary "Can't do an incremental backup without a full one being present."
439       summary "Switching to full backup for $device"
440       make_full_backup ${BACKUP_SOURCE} $group $volume
441       if [ "$?" = "0" ]; then
442         summary "FULL backup of $device done on `NOW`."
443       else
444         summary "Error during full backup of $device"
445         set_error
446       fi
447     else
448       summary "Unknown error during incremental backup of $device"
449       set_error
450     fi
451   else # Do a full backup
452     make_full_backup ${BACKUP_SOURCE} $group $volume
453     if [ "$?" = "0" ]; then
454       summary "FULL backup of $device done on `NOW`."
455     else
456       summary "Error during full backup of $device"
457       set_error
458     fi
459   fi
460
461   # Time to unmount and destroy the snapshot, if needed.
462   # Any error here is critical, since the snapshot and the mountpoint are
463   # the same for each device...
464   if [ "$dosnap" = "yes" ]; then
465     unmount_snapshot $vg
466     if [ "$?" != "0" ]; then
467       summary "Could not unmount snapshot !"
468       set_critical
469       finish
470     fi
471
472     remove_snapshot $vg
473     if [ "$?" != "0" ]; then
474       summary "Could not destroy the snapshot !"
475       set_critical
476       finish
477     fi
478   fi
479
480 done
481
482 # Push everything on the FTP
483 if [ "$USE_FTP" = "yes" ]; then
484   summary ""
485   for group in `grep -v -E '^[[:space:]]*(#.*)?$' $BACKUPTAB | awk '{print $3}' | sort -u`
486   do
487     ftp_push "$REPODIR/$group" $group
488     summary "Mirrored ${group} to ${FTP_HOST}."
489   done
490 fi
491
492
493 finish
494
495