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