tools - Improve hammer-backup.sh a bit
[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.1-beta"
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     timestamp=$(date +'%Y%m%d%H%M%S')
88     list_opt=0
89 }
90
91 info()
92 {
93     [ ${verbose} -eq 1 ] && echo "INFO: $1"
94 }
95
96 #
97 # err exitval message
98 #     Display an error and exit
99 #
100 err()
101 {
102     exitval=$1
103     shift
104
105     echo 1>&2 "$0: ERROR: $*"
106     exit $exitval
107 }
108
109 usage()
110 {
111     echo "Usage: ${SCRIPTNAME} [-h] [-l] [-v] [-i <full-backup-file>]" \
112         "[-f] [-c <compress-rate>] -d [<backup-dir>] <path-to-PFS>"
113     exit 1
114 }
115
116 check_pfs()
117 {
118     info "Validating PFS ${pfs}"
119
120     # Backup directory must exist
121     if [ -z "${pfs_path}" ]; then
122         usage
123     fi
124
125     # Make sure we are working on a HAMMER PFS
126     hammer pfs-status ${pfs_path} > /dev/null 2>&1
127     if [ $? -ne 0 ]; then
128         err 2 "${pfs} is not a HAMMER PFS"
129     fi
130 }
131
132 get_endtid()
133 {
134     local logfile=$1
135
136     awk -F "[=: ]" -vRS="\r" '{
137         if ($4 == "tids") {
138                 print $6;
139                 exit
140         }
141     }' ${logfile}
142 }
143
144 get_uuid()
145 {
146     # Get the shared UUID for the PFS
147    hammer pfs-status ${pfs_path} | awk -F'[ =]+' '
148         $2 == "shared-uuid" {
149                 print $3;
150                 exit;
151         }'
152 }
153
154 file2date()
155 {
156     local filename=""
157     local filedate=""
158
159     # Extract the date
160     filename=$(basename $1)
161     filedate=$(echo ${filename} | cut -d "_" -f1)
162
163     date -j -f '%Y%m%d%H%M%S' ${filedate} +"%B %d, %Y %H:%M:%S %Z"
164 }
165
166
167 update_mdata()
168 {
169     local filename=$(basename ${output_file})
170     local uuid=$(get_uuid)
171     local endtid=$1
172     local md5sum=$(md5 -q ${output_file})
173
174     # XXX - Sanity checks missing?!!
175     printf "%s,,,%d,%s,%s,%s\n" $filename $backup_type $uuid $endtid $md5sum \
176         >> ${metadata_file}
177 }
178
179 do_backup()
180 {
181     local tmplog=$1
182     local compress_opts=""
183     local begtid=$2
184
185     # Calculate the compression options
186     if [ ${compress} -eq 1 ]; then
187         compress_opts=" | xz -c -${comp_rate}"
188         output_file="${output_file}.xz"
189     fi
190
191     # Generate the datafile according to the options specified
192     cmd="hammer -y -v mirror-read ${pfs_path} ${begtid} 2> ${tmplog} \
193         ${compress_opts} > ${output_file}"
194
195     info "Launching: ${cmd}"
196     if [ ${dryrun} -eq 0 ]; then
197         # Sync to disk before mirror-read
198         hammer synctid ${pfs_path} > /dev/null 2>&1
199         eval ${cmd}
200         if [ $? -eq 0 ]; then
201             info "Backup completed."
202         else
203             rm -f ${output_file}
204             rm -f ${tmplog}
205             err 1 "Failed to created backup data file!"
206         fi
207     fi
208 }
209
210 full_backup()
211 {
212     local tmplog=$(mktemp)
213     local filename=""
214     local endtid=""
215
216     # Full backup (no param specified)
217     info "Initiating full backup"
218     do_backup ${tmplog}
219
220     # Generate the metadata file itself
221     metadata_file="${output_file}.bkp"
222     endtid=$(get_endtid ${tmplog})
223
224     update_mdata ${endtid}
225
226     # Cleanup
227     rm ${tmplog}
228 }
229
230 check_metadata()
231 {
232     local line=""
233     local f1=""
234     local f2=""
235
236     if [ ! -r ${metadata_file} ]; then
237         err 1 "Could not find ${metadata_file}"
238     fi
239
240     f1=$(basename ${metadata_file})
241     f2=$(head -1 ${metadata_file} | cut -d "," -f1)
242
243     if [ "${f1}" != "${f2}.bkp" ]; then
244         err 2 "Bad metadata file ${metadata_file}"
245     fi
246 }
247
248 incr_backup()
249 {
250     local tmplog=$(mktemp)
251     local endtid=""
252     local line=""
253     local srcuuid=""
254     local tgtuuid=""
255
256     # Make sure the file exists and it can be read
257     if [ ! -r ${incr_full_file} ]; then
258         err 1 "Specified file ${incr_full_file} does not exist."
259     fi
260     metadata_file=${incr_full_file}
261
262     # Verify we were passed a real metadata file
263     check_metadata
264
265     # The first backup of the metadata file must be a full one
266     line=$(head -1 ${incr_full_file})
267     btype=$(echo ${line} | cut -d ',' -f4)
268     if [ ${btype} -ne 1 ]; then
269         err 1 "No full backup in ${incr_full_file}. Cannot do incremental ones."
270     fi
271
272     # Read metadata info for the last backup performed
273     line=$(tail -1 ${incr_full_file})
274     srcuuid=$(echo $line| cut -d ',' -f 5)
275     endtid=$(echo $line| cut -d ',' -f 6)
276
277     # Verify shared uuid are the same
278     tgtuuid=$(get_uuid)
279     if [ "${srcuuid}" != "${tgtuuid}" ]; then
280         err 255 "Shared UUIDs do not match! ${srcuuid} -> ${tgtuuid}"
281     fi
282
283     # Do an incremental backup
284     info "Initiating incremental backup"
285     do_backup ${tmplog} 0x${endtid}
286
287     # Store the metadata in the full backup file
288     endtid=$(get_endtid ${tmplog})
289     update_mdata ${endtid}
290
291     # Cleanup
292     rm ${tmplog}
293 }
294
295 list_backups()
296 {
297     local nofiles=1
298
299     for bkp in ${backup_dir}/*.bkp
300     do
301         # Skip files that don't exist
302         if [ ! -f ${bkp} ]; then
303             continue
304         fi
305         # Show incremental backups related to the full backup above
306         awk -F "," '{
307                 if ($4 == 1) {
308                         printf("full: ");
309                 }
310                 if ($4 == 2) {
311                         printf("\tincr: ");
312                 }
313         printf("%s endtid: 0x%s md5: %s\n", $1, $6, $7);
314         }' ${bkp}
315         nofiles=0
316     done
317
318     if [ ${nofiles} -eq 1 ]; then
319         err 255 "No backup files found in ${backup_dir}"
320     fi
321
322     exit 0
323 }
324 # -------------------------------------------------------------
325
326 # Setup some vars
327 initialization
328
329 # Only can be run by root
330 if [  $(id -u) -ne 0 ]; then
331     err 255 "Only root can run this script."
332 fi
333
334 # Checks hammer program
335 if [ ! -x /sbin/hammer ]; then
336     err 1 'Could not find find hammer(8) program.'
337 fi
338
339 info "hammer-backup version ${VERSION}"
340
341 # Handle options
342 while getopts d:i:c:fvhnl op
343 do
344     case $op in
345         d)
346             backup_dir=$OPTARG
347             info "Backup directory is ${backup_dir}."
348             ;;
349         f)
350             if [ ${backup_type} -eq 2 ]; then
351                 err 1 "-f and -i are mutually exclusive."
352             fi
353
354             info "Full backup."
355             backup_type=1
356             ;;
357         i)
358             if [ ${backup_type} -eq 2 ]; then
359                 err 1 "-f and -i are mutually exclusive."
360             fi
361
362             info "Incremental backup."
363             backup_type=2
364             incr_full_file=$OPTARG
365             ;;
366         c)
367             compress=1
368
369             case "$OPTARG" in
370                 [1-9])
371                     comp_rate=$OPTARG
372                     ;;
373                 *)
374                     err 1 "Bad compression level specified."
375                     ;;
376             esac
377
378             info "XZ compression level ${comp_rate}."
379             ;;
380         n)
381             info "Dry-run execution."
382             dryrun=1
383             ;;
384         l)
385             list_opt=1
386             ;;
387         v)
388             verbose=1
389             ;;
390         h)
391             usage
392             ;;
393         *)
394             usage
395             ;;
396     esac
397 done
398
399 shift $(($OPTIND - 1))
400
401 #
402 # If list option is selected
403 pfs_path="$1"
404
405 # Backup directory must exist
406 if [ -z "${backup_dir}" ]; then
407     usage
408 elif [ ! -d "${backup_dir}" ]; then
409     err 1 "Backup directory does not exist!"
410 fi
411
412 # Output file format is YYYYmmddHHMMSS
413 tmp=$(echo ${pfs_path} | tr '/' '_')
414 output_file="${backup_dir}/${timestamp}${tmp}"
415
416 # List backups if needed
417 if [ ${list_opt} == 1 ]; then
418     info "Listing backups in ${backup_dir}"
419     list_backups
420 fi
421
422 # Only work on a HAMMER fs
423 check_pfs
424
425 # Actually launch the backup itself
426 if [ ${backup_type} -eq 1 ]; then
427     full_backup
428 elif [ ${backup_type} -eq 2 ]; then
429     incr_full_file=${backup_dir}/${incr_full_file}
430     incr_backup
431 else
432     err 255 "Impossible backup type."
433 fi