]> git.alrj.org Git - brioche.git/blob - brioche
2ccf1c3608769a6dc5a1b8a536a3cca7cd36850a
[brioche.git] / brioche
1 #! /bin/bash
2
3 # Brioche Backup
4 # Full and Incremental backup script with tar and LVM snapshots.
5 #
6 # Copyright Amand Tihon, 2008, 2009 <amand.tihon@alrj.org>
7 # Licensed under GNU GPL version 2.0.
8 # Inspired by
9 # http://blog.kagesenshi.org/2008/01/automated-tar-and-dump-incremental.html
10 #
11 # Note: This script relies on GNU tar specific options,
12 # do not attempt to use it as-is with other tar implementations.
13 #
14 #
15 # Example of use, from crontab
16 # # Daily incremental backup
17 # 30  3  *  *  1-6  /usr/local/bin/brioche > /var/log/backup.`date "+%a"`.log 2>&1
18 # # Weekly full backup on Sunday
19 # 30  3  *  *  0  /usr/local/bin/brioche -f > /var/log/backup.`date "+%a"`.log 2>&1
20
21
22 # Mandatory
23 CONFIG_FILE="/etc/brioche.conf"
24
25
26 #######################################################################
27 #  Default config values. Do not change, edit $CONFIG_FILE
28 #######################################################################
29
30 BACKUPTAB="/etc/briochetab"
31 MAILTO="root"
32 REPODIR="/backup"
33 TAR_OPTS="--one-file-system -S "
34 COMPRESS="gz"
35 COMPRESS_OPT="--gzip"
36 SNAPSHOT_MOUNTPOINT="/mnt/backup-snapshot"
37 SNAPSHOT_NAME="backup-snap"
38 SNAPSHOT_SIZE="5G"
39 USAGE_WARN="80"
40
41 # TODO: Implement the storage on a distant FTP
42 USE_FTP="no"
43 #FTP_HOST="ftpback.example.com"
44 #FTP_USER="username"
45 #FTP_PASS="password"
46 #FTP_KEEP="4"
47
48 # Ensure that we have a minimal PATH
49 PATH=/sbin:/bin:/usr/sbin:/usr/bin
50 FINAL_STATUS="SUCCESS"
51 SUMMARY="/tmp/backup.sumary"
52
53
54 #######################################################################
55 #  Helpers
56 #######################################################################
57
58 trap 'interrupted ctrl-C' INT
59 trap 'interrupted KILL' TERM
60
61 NOW()
62 {
63   echo -n `date "+%Y-%m-%d %H:%M:%S"`
64 }
65
66 # Log a line with timestamp.
67 log()
68 {
69   echo "[`NOW`] $@"
70 }
71
72 # The summary that will be sent by email
73 summary()
74 {
75   log $@
76   echo $@ >> $SUMMARY
77 }
78
79 finish()
80 {
81   # Check free space
82   local usage=`df -hP "${REPODIR}" | tail -n 1 | tr -s [:space:] | cut -d" " -f5 | tr -d '%'`
83   log "${REPODIR} usage after backup: ${usage}%."
84
85   if [ "$usage" -ge "$USAGE_WARN" ]; then
86     if [ "$FINAL_STATUS" = "SUCCESS" ]; then
87       FINAL_STATUS="WARNING"
88     fi
89     summary "Warning : Filesystem ${REPODIR} is ${usage}% full."
90   fi
91
92   summary "Backup procedure ended on `NOW`"
93
94   SUBJECT="Backup report for `hostname` (${FINAL_STATUS})."
95   mail -s "${SUBJECT}" "${MAILTO}" <  "$SUMMARY"
96
97   rm -f "$SUMMARY"
98   exit
99 }
100
101 interrupted()
102 {
103   FINAL_STATUS="INTERRUPTED"
104   summary "Backup procedure interrupted by user ($1)."
105   summary "Take care, an LVM snapshot may still be present !"
106   finish
107 }
108
109 #######################################################################
110 #  Snapshots
111 #######################################################################
112
113 # Make a snapshot of a logical volume.
114 # Usage: make_snapshot vg lv
115 make_snapshot()
116 {
117   log "Creating a snapshot volume of /dev/$1/$2"
118   lvcreate --snapshot -L ${SNAPSHOT_SIZE} -n ${SNAPSHOT_NAME} /dev/$1/$2
119   RETVAL=$?
120   if [ "$RETVAL" != "0" ]; then
121     log "Error ${RETVAL}: Unable to create a snapshot of /dev/$1/$2"
122     return 1
123   fi
124 }
125
126 # Mount the snapshot volume.
127 # On error, tries to remove the snapshot.
128 # Usage: mount_snapshot vg
129 mount_snapshot()
130 {
131   log "Mounting the snapshot volume ${SNAPSHOT_NAME}."
132   if [ ! -d "${SNAPSHOT_MOUNTPOINT}" ]; then
133     log "Creating mountpoint ${SNAPSHOT_MOUNTPOINT}."
134     mkdir -p "${SNAPSHOT_MOUNTPOINT}"
135   fi
136
137   mount /dev/$1/${SNAPSHOT_NAME} ${SNAPSHOT_MOUNTPOINT}
138   RETVAL=$?
139   if [ "$RETVAL" != "0" ]; then
140     log "Error ${RETVAL}: Unable to mount /dev/$1/${SNAPSHOT_NAME} on ${SNAPSHOT_MOUNTPOINT}"
141     remove_snapshot $1 || return 100
142     return 1
143   fi
144 }
145
146 # Remove a previously created snapshot. It must be unmounted.
147 # Usage: remove_snapshot vg
148 remove_snapshot()
149 {
150   log "Removing the snapshot volume /dev/$1/${SNAPSHOT_NAME}."
151   lvremove -f /dev/$1/${SNAPSHOT_NAME}
152   RETVAL=$?
153   if [ "$RETVAL" != "0" ]; then
154     log "Error ${RETVAL}: Unable to remove the snapshot volume /dev/$1/${SNAPSHOT_NAME}"
155     return 100
156   fi
157 }
158
159 # Unmount the previously mounted snapshot.
160 # Usage: unmount_snapshot vg
161 unmount_snapshot()
162 {
163   log "Unmounting the snapshot volume /dev/$1/${SNAPSHOT_NAME}."
164   umount /dev/$1/${SNAPSHOT_NAME}
165   RETVAL=$?
166   if [ "$RETVAL" != "0" ]; then
167     log "Error ${RETVAL}: Unable to unmount the snapshot volume /dev/$1/${SNAPSHOT_NAME}"
168     return 100
169   fi
170 }
171
172 #######################################################################
173 #  Main backup functions
174 #######################################################################
175
176 # Make a full backup of a directory, with snar file for subsequent incremental
177 # Usage: make_full_backup source_dir hostname volumename
178 # Returns 0 on success, 1 on error
179 make_full_backup()
180 {
181   log "Making full backup of $2 - ${3}."
182
183   local destdir="${REPODIR}/$2"
184   local today=`date "+%Y%m%d"`
185   local destfile="${destdir}/${3}.full.${today}.tar.${COMPRESS}"
186   local destsnar="${destdir}/${3}.full.snar"
187
188   # Move previous run to the "undo" directory
189   if [ ! -d "${destdir}/undo" ]; then
190     log "Creating undo/ directory."
191     mkdir -p "${destdir}/undo"
192   fi
193   log "Moving old run into undo/ directory."
194   mv ${destdir}/${3}.* ${destdir}/undo
195
196   # Do the actual backup. Destination file name and snar are like
197   # /backup/valeron/root.full.20090105.tar.bz2
198   # /backup/valeron/root.full.snar
199   log "Running tar..."
200   tar -cf ${destfile} ${TAR_OPTS} ${COMPRESS_OPT} -g ${destsnar} $1
201
202   if [ "$?" = "0" ]; then
203     log "Removing undo/ directory."
204     rm -rf "${destdir}/undo"
205   else
206     log "Error $?: Could not archive $2 - $3"
207     return 1
208   fi
209   return 0
210 }
211
212
213 # Make an incremental backup of a directory, from full's snar file
214 # Usage: make_incr_backup source_dir hostname volumename
215 # Returns 0 on success, 1 on error, 2 if no previous full is found.
216 make_incr_backup()
217 {
218   log "Making incremental backup of $2 - ${3}."
219
220   local destdir="${REPODIR}/$2"
221   local today=`date "+%Y%m%d"`
222   local destfile="${destdir}/${3}.incr.${today}.tar.${COMPRESS}"
223   local destsnar="${destdir}/${3}.incr.${today}.snar"
224   local fullsnar="${destdir}/${3}.full.snar"
225
226   # Test existence of full backup
227   if [ ! -e $fullsnar ]; then
228     log "Could not find catalog ${fullsnar}."
229     return 2
230   fi
231
232   # Prepare the copy of the snar file
233   cp $fullsnar $destsnar
234
235   # Do the actual backup. Destination file name and snar are like
236   # /backup/valeron/root.incr.20090105.tar.bz2
237   # /backup/valeron/root.incr.20090105.snar
238   log "Running tar..."
239   tar -caf ${destfile} ${TAR_OPTS} ${COMPRESS_OPT} -g ${destsnar} $1
240
241   if [ ! "$?" = "0" ]; then
242     log "Error $?: Could not archive $hostname - $volumename"
243     return 1
244   fi
245   return 0
246 }
247
248
249
250 #######################################################################
251 #  Start here
252 #######################################################################
253
254 # Truncate summary file, start header blurb
255 echo "" > $SUMMARY
256 summary "Backup procedure started on `NOW`"
257
258 if [ -r "${CONFIG_FILE}" ]
259 then
260   source "${CONFIG_FILE}"
261 else
262   summary "Error: Unable to read configuration file ${CONFIG_FILE}. Aborting."
263   FINAL_STATUS="CRITICAL"
264   finish
265 fi
266
267 if [ ! -r "${BACKUPTAB}" ]
268 then
269   summary "Error: Unable to read ${BACKUPTAB}. Aborting."
270   FINAL_STATUS="CRITICAL"
271   finish
272 fi
273
274 # TODO: Something cleaner, here...
275 DO_FULL_BACKUP="no"
276 case "$1" in
277   --full|-f)
278     DO_FULL_BACKUP="yes"
279     ;;
280   --help|-h)
281     echo "Usage: ${0} [-f]"
282     exit 0
283     ;;
284 esac
285
286 # Discover which COMPRESS_OPT to use, from COMPRESS
287 case "$COMPRESS" in
288   gz)
289     COMPRESS_OPT="--gzip"
290     ;;
291   bz2)
292     COMPRESS_OPT="--bzip2"
293     ;;
294   lzma)
295     COMPRESS_OPT="--lzma"
296     ;;
297   *)
298     summary "Unknown compression method: ${COMPRESS}. Falling back to gzip."
299     COMPRESS="gz"
300     COMPRESS_OPT="--gzip"
301     ;;
302 esac
303
304 #######################################################################
305 #  Parse backuptab file, call backup functions for each line
306 #######################################################################
307
308 # Ignore empty and commented lines
309 grep -v -E '^[[:space:]]*(#.*)?$' $BACKUPTAB | tr -s [:space:]| while read line
310 do
311   # split line in fields
312   device=`echo $line|cut -d" " -f1`
313   dosnap=`echo $line|cut -d" " -f2`
314   group=`echo $line|cut -d" " -f3`
315   volume=`echo $line|cut -d" " -f4`
316
317   # Make and mount snapshot if needed.
318   if [ "$dosnap" = "yes" ]; then
319     # Split the device to find the VG and LV
320     vg=`echo $device|cut -d"/" -f3`
321     lv=`echo $device|cut -d"/" -f4`
322     make_snapshot $vg $lv
323     if [ "$?" != "0" ]; then
324       summary "Could not take a snapshot of $device"
325       FINAL_STATUS="ERROR"
326       continue
327       # Next one
328     fi
329
330     mount_snapshot $vg
331     RETVAL="$?"
332     if [ "$RETVAL" != "0" ]; then
333       summary "Could not mount the snapshot of $device"
334       FINAL_STATUS="ERROR"
335       if [ "$RETVAL" = "100" ]; then
336         summary "Could not remove the snapshot !"
337         FINAL_STATUS="CRITICAL"
338         finish
339       fi
340       continue
341       # Next one
342     fi
343
344     BACKUP_SOURCE="${SNAPSHOT_MOUNTPOINT}"
345   else
346     BACKUP_SOURCE="$device"
347   fi
348
349   # Make the backup
350   if [ "$DO_FULL_BACKUP" = "no" ]; then
351     make_incr_backup ${BACKUP_SOURCE} $group $volume
352     RETVAL=$?
353     if [ "$RETVAL" = "0" ]; then
354       summary "INCREMENTAL backup of $device done on `NOW`."
355     elif [ "$RETVAL" = "1" ]; then
356       summary "Error during incremental backup of $device"
357       FINAL_STATUS="ERROR"
358     elif [ "$RETVAL" = "2" ]; then
359       summary "Can't do an incremental backup without a full one being present."
360       summary "Switching to full backup for $device"
361       make_full_backup ${BACKUP_SOURCE} $group $volume
362       if [ "$?" = "0" ]; then
363         summary "FULL backup of $device done on `NOW`."
364       else
365         summary "Error during full backup of $device"
366         FINAL_STATUS="ERROR"
367       fi
368     else
369       summary "Unknown error during incremental backup of $device"
370       FINAL_STATUS="ERROR"
371     fi
372   else # Do a full backup
373     make_full_backup ${BACKUP_SOURCE} $group $volume
374     if [ "$?" = "0" ]; then
375       summary "FULL backup of $device done on `NOW`."
376     else
377       summary "Error during full backup of $device"
378       FINAL_STATUS="ERROR"
379     fi
380   fi
381
382   # Time to unmount and destroy the snapshot, if needed.
383   # Any error here is critical, since the snapshot and the mountpoint are
384   # the same for each device...
385   if [ "$dosnap" = "yes" ]; then
386     unmount_snapshot $vg
387     if [ "$?" != "0" ]; then
388       summary "Could not unmount snapshot !"
389       FINAL_STATUS="CRITICAL"
390       finish
391     fi
392
393     remove_snapshot $vg
394     if [ "$?" != "0" ]; then
395       summary "Could not destroy the snapshot !"
396       FINAL_STATUS="CRITICAL"
397       finish
398     fi
399   fi
400 done
401
402 finish
403
404 ## Example Backup description table
405 ##
406 ## Can handle logical volumes, with snapshots, or plain mountpoints.
407 ## Priority is not used yet.
408 #
409 ## Partition or LV        Snapshot     Host name       Volume name     Priority
410 ## ----------------------------------------------------------------------------
411 #/                        no           cottman         root            1
412 #/dev/vg00/kadarin-root   yes          kadarin         root            1
413 #/dev/vg00/valeron-root   yes          valeron         root            1
414 #/dev/vg00/syrtis-root    yes          syrtis          root            1
415 #/dev/vg00/syrtis-home    yes          syrtis          home            1
416 #/dev/vg00/syrtis-usr     yes          syrtis          usr             1
417 #/dev/vg00/syrtis-var     yes          syrtis          var             1
418 #/dev/vg00/syrtis-srv     yes          syrtis          srv             1
419
420