tools - Easier incremental backups for hammer-backup.sh.
[dragonfly.git] / tools / tools / hammer-backup / hammer-backup.sh
1 #! /bin/sh
2 #
3 # Copyright (c) 2014 The DragonFly Project.  All rights reserved.
4 #
5 # This code is derived from software contributed to The DragonFly Project
6 # by Antonio Huete <tuxillo@quantumachine.net>
7 #
8 # Redistribution and use in source and binary forms, with or without
9 # modification, are permitted provided that the following conditions
10 # are met:
11 #
12 # 1. Redistributions of source code must retain the above copyright
13 #    notice, this list of conditions and the following disclaimer.
14 # 2. Redistributions in binary form must reproduce the above copyright
15 #    notice, this list of conditions and the following disclaimer in
16 #    the documentation and/or other materials provided with the
17 #    distribution.
18 # 3. Neither the name of The DragonFly Project nor the names of its
19 #    contributors may be used to endorse or promote products derived
20 #    from this software without specific, prior written permission.
21 #
22 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
23 # ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
24 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
25 # FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE
26 # COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
27 # INCIDENTAL, SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING,
28 # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
29 # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
30 # AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
31 # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
32 # OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
33 # SUCH DAMAGE.
34 #
35 # set -x
36 #
37 # hammer-backup
38 #
39 # This script operats HAMMER PFSes and dumps its contents for backup
40 # purposes.
41 #
42 # It uses mirror-read directive (see 'man 8 hammer') to perform a
43 # dump to stdout that is redirected to a file with or without
44 # compression.
45 #
46 # It can take two types of backup:
47 #
48 #   a) Full: Where ALL the data of the PFS is sent to a file.
49 #   b) Inremental: It requires a previous full backup.
50 #
51 # Additionally to the backup data itself, it creates a .bkp file
52 # which contains metadata relative to the full and/or incremental
53 # backups.
54 #
55 # The format is the following
56 #
57 #   filename,rsv01,rsv02,backup type,shared uuid,last TID,md5 hash
58 #
59 #   filename   : Backup data file file.
60 #   rsv01,rsv02: Reserved fields
61 #   backup type: 1 or 2 (full or incremental, respectively)
62 #   shared uuid: PFS shared UUID for mirror ops
63 #   last TID   : Last transaction ID. Next incr. backup starting TID
64 #   md5 hash   : For restoring purposes
65 #
66 # Example:
67 #   $ head -1 20140305222026_pfs_t1_full.xz.bkp
68 #   20140305222026_pfs_t1_full.xz.bkp,,,f,e8decfc5-a4ab-11e3-942b-f56d04d293e0,000000011b36be30,05482d26644bd1e76e69d83002e08258
69 #
70
71 initialization()
72 {
73     VERSION="0.2"
74     SCRIPTNAME=${0##*/}
75
76     dryrun=0      # Dry-run
77     backup_type=0         # Type of backup
78     incr_full_file="" # Full backup file for the incremental
79     input_file=""         # Full backup filename
80     output_file=""        # Output data file
81     metadata_file=""  # Output metadata fiole
82     pfs_path=""   # PFS path to be backed up
83     backup_dir=""         # Target directory for backups
84     compress=0    # Compress output file?
85     comp_rate=6   # Compression rate
86     verbose=0     # Verbosity on/off
87     list_opt=0    # List backups
88     find_last=0   # Find last full backup
89     timestamp=$(date +'%Y%m%d%H%M%S')
90 }
91
92 info()
93 {
94     [ ${verbose} -eq 1 ] && echo "INFO: $1"
95 }
96
97 #
98 # err exitval message
99 #     Display an error and exit
100 #
101 err()
102 {
103     exitval=$1
104     shift
105
106     echo 1>&2 "$0: ERROR: $*"
107     exit $exitval
108 }
109
110 usage()
111 {
112     echo "Usage: ${SCRIPTNAME} [-h] [-l] [-v] [-i <full-backup-file>]" \
113         "[-f] [-c <compress-rate>] -d [<backup-dir>] <path-to-PFS>"
114     exit 1
115 }
116
117 check_pfs()
118 {
119     info "Validating PFS ${pfs}"
120
121     # Backup directory must exist
122     if [ -z "${pfs_path}" ]; then
123         usage
124     fi
125
126     # Make sure we are working on a HAMMER PFS
127     hammer pfs-status ${pfs_path} > /dev/null 2>&1
128     if [ $? -ne 0 ]; then
129         err 2 "${pfs} is not a HAMMER PFS"
130     fi
131 }
132
133 get_endtid()
134 {
135     local logfile=$1
136
137     awk -F "[=: ]" -vRS="\r" '{
138         if ($4 == "tids") {
139                 print $6;
140                 exit
141         }
142     }' ${logfile}
143 }
144
145 get_uuid()
146 {
147     # Get the shared UUID for the PFS
148    hammer pfs-status ${pfs_path} | awk -F'[ =]+' '
149         $2 == "shared-uuid" {
150                 print $3;
151                 exit;
152         }'
153 }
154
155 file2date()
156 {
157     local filename=""
158     local filedate=""
159
160     # Extract the date
161     filename=$(basename $1)
162     filedate=$(echo ${filename} | cut -d "_" -f1)
163
164     date -j -f '%Y%m%d%H%M%S' ${filedate} +"%B %d, %Y %H:%M:%S %Z"
165 }
166
167
168 update_mdata()
169 {
170     local filename=$(basename ${output_file})
171     local uuid=$(get_uuid)
172     local endtid=$1
173     local md5sum=$(md5 -q ${output_file})
174
175     # XXX - Sanity checks missing?!!
176     printf "%s,,,%d,%s,%s,%s\n" $filename $backup_type $uuid $endtid $md5sum \
177         >> ${metadata_file}
178 }
179
180 do_backup()
181 {
182     local tmplog=$1
183     local compress_opts=""
184     local begtid=$2
185
186     # Calculate the compression options
187     if [ ${compress} -eq 1 ]; then
188         compress_opts=" | xz -c -${comp_rate}"
189         output_file="${output_file}.xz"
190     fi
191
192     # Generate the datafile according to the options specified
193     cmd="hammer -y -v mirror-read ${pfs_path} ${begtid} 2> ${tmplog} \
194         ${compress_opts} > ${output_file}"
195
196     info "Launching: ${cmd}"
197     if [ ${dryrun} -eq 0 ]; then
198         # Sync to disk before mirror-read
199         hammer synctid ${pfs_path} > /dev/null 2>&1
200         eval ${cmd}
201         if [ $? -eq 0 ]; then
202             info "Backup completed."
203         else
204             rm -f ${output_file}
205             rm -f ${tmplog}
206             err 1 "Failed to created backup data file!"
207         fi
208     fi
209 }
210
211 full_backup()
212 {
213     local tmplog=$(mktemp)
214     local filename=""
215     local endtid=""
216
217     # Full backup (no param specified)
218     info "Initiating full backup"
219     do_backup ${tmplog}
220
221     # Generate the metadata file itself
222     metadata_file="${output_file}.bkp"
223     endtid=$(get_endtid ${tmplog})
224
225     update_mdata ${endtid}
226
227     # Cleanup
228     rm ${tmplog}
229 }
230
231 check_metadata()
232 {
233     local line=""
234     local f1=""
235     local f2=""
236
237     if [ ! -r ${metadata_file} ]; then
238         err 1 "Could not find ${metadata_file}"
239     fi
240
241     f1=$(basename ${metadata_file})
242     f2=$(head -1 ${metadata_file} | cut -d "," -f1)
243
244     if [ "${f1}" != "${f2}.bkp" ]; then
245         err 2 "Bad metadata file ${metadata_file}"
246     fi
247 }
248
249 incr_backup()
250 {
251     local tmplog=$(mktemp)
252     local endtid=""
253     local line=""
254     local srcuuid=""
255     local tgtuuid=""
256     local latest=""
257     local pattern=""
258
259     # XXX
260     # Find latest metadata backup file if needed.
261     # Right now the timestamp in the filename will
262     # let them be sorted by ls. But this could actually
263     # change
264     if [ ${find_last} -eq 1 ]; then
265         pattern=$(echo ${pfs_path} | tr "/" "_").xz.bkp
266         latest=$(ls -1 ${backup_dir}/*${pattern} | tail -1)
267         incr_full_file=${latest}
268     fi
269
270     # Make sure the file exists and it can be read
271     if [ ! -r ${incr_full_file} ]; then
272         err 1 "Specified file ${incr_full_file} does not exist."
273     fi
274     metadata_file=${incr_full_file}
275
276     # Verify we were passed a real metadata file
277     check_metadata
278
279     # The first backup of the metadata file must be a full one
280     line=$(head -1 ${incr_full_file})
281     btype=$(echo ${line} | cut -d ',' -f4)
282     if [ ${btype} -ne 1 ]; then
283         err 1 "No full backup in ${incr_full_file}. Cannot do incremental ones."
284     fi
285
286     # Read metadata info for the last backup performed
287     line=$(tail -1 ${incr_full_file})
288     srcuuid=$(echo $line| cut -d ',' -f 5)
289     endtid=$(echo $line| cut -d ',' -f 6)
290
291     # Verify shared uuid are the same
292     tgtuuid=$(get_uuid)
293     if [ "${srcuuid}" != "${tgtuuid}" ]; then
294         err 255 "Shared UUIDs do not match! ${srcuuid} -> ${tgtuuid}"
295     fi
296
297     # Do an incremental backup
298     info "Initiating incremental backup"
299     do_backup ${tmplog} 0x${endtid}
300
301     # Store the metadata in the full backup file
302     endtid=$(get_endtid ${tmplog})
303     update_mdata ${endtid}
304
305     # Cleanup
306     rm ${tmplog}
307 }
308
309 list_backups()
310 {
311     local nofiles=1
312
313     for bkp in ${backup_dir}/*.bkp
314     do
315         # Skip files that don't exist
316         if [ ! -f ${bkp} ]; then
317             continue
318         fi
319         # Show incremental backups related to the full backup above
320         awk -F "," '{
321                 if ($4 == 1) {
322                         printf("full: ");
323                 }
324                 if ($4 == 2) {
325                         printf("\tincr: ");
326                 }
327         printf("%s endtid: 0x%s md5: %s\n", $1, $6, $7);
328         }' ${bkp}
329         nofiles=0
330     done
331
332     if [ ${nofiles} -eq 1 ]; then
333         err 255 "No backup files found in ${backup_dir}"
334     fi
335
336     exit 0
337 }
338 # -------------------------------------------------------------
339
340 # Setup some vars
341 initialization
342
343 # Only can be run by root
344 if [  $(id -u) -ne 0 ]; then
345     err 255 "Only root can run this script."
346 fi
347
348 # Checks hammer program
349 if [ ! -x /sbin/hammer ]; then
350     err 1 'Could not find find hammer(8) program.'
351 fi
352
353 info "hammer-backup version ${VERSION}"
354
355 # Handle options
356 while getopts d:i:c:fvhnl op
357 do
358     case $op in
359         d)
360             backup_dir=$OPTARG
361             info "Backup directory is ${backup_dir}."
362             ;;
363         f)
364             if [ ${backup_type} -eq 2 ]; then
365                 err 1 "-f and -i are mutually exclusive."
366             fi
367
368             info "Full backup."
369             backup_type=1
370             ;;
371         i)
372             if [ ${backup_type} -eq 2 ]; then
373                 err 1 "-f and -i are mutually exclusive."
374             fi
375
376             info "Incremental backup."
377             backup_type=2
378             if [ "${OPTARG}" == "auto" ]; then
379                 find_last=1
380             else
381                 incr_full_file=$OPTARG
382             fi
383             ;;
384         c)
385             compress=1
386
387             case "$OPTARG" in
388                 [1-9])
389                     comp_rate=$OPTARG
390                     ;;
391                 *)
392                     err 1 "Bad compression level specified."
393                     ;;
394             esac
395
396             info "XZ compression level ${comp_rate}."
397             ;;
398         n)
399             info "Dry-run execution."
400             dryrun=1
401             ;;
402         l)
403             list_opt=1
404             ;;
405         v)
406             verbose=1
407             ;;
408         h)
409             usage
410             ;;
411         *)
412             usage
413             ;;
414     esac
415 done
416
417 shift $(($OPTIND - 1))
418
419 #
420 # If list option is selected
421 pfs_path="$1"
422
423 # Backup directory must exist
424 if [ -z "${backup_dir}" ]; then
425     usage
426 elif [ ! -d "${backup_dir}" ]; then
427     err 1 "Backup directory does not exist!"
428 fi
429
430 # Output file format is YYYYmmddHHMMSS
431 tmp=$(echo ${pfs_path} | tr '/' '_')
432 output_file="${backup_dir}/${timestamp}${tmp}"
433
434 # List backups if needed
435 if [ ${list_opt} == 1 ]; then
436     info "Listing backups in ${backup_dir}"
437     list_backups
438 fi
439
440 # Only work on a HAMMER fs
441 check_pfs
442
443 # Actually launch the backup itself
444 if [ ${backup_type} -eq 1 ]; then
445     full_backup
446 elif [ ${backup_type} -eq 2 ]; then
447     incr_full_file=${backup_dir}/${incr_full_file}
448     incr_backup
449 else
450     err 255 "Impossible backup type."
451 fi