tools - Fix backup file permissions 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 operates HAMMER PFSes and dumps its contents for backup
40 # purposes.
41 #
42
43 initialization()
44 {
45     VERSION="0.2"
46     SCRIPTNAME=${0##*/}
47
48     dryrun=0      # Dry-run
49     backup_type=0         # Type of backup
50     incr_full_file="" # Full backup file for the incremental
51     input_file=""         # Full backup filename
52     output_file=""        # Output data file
53     metadata_file=""  # Output metadata fiole
54     pfs_path=""   # PFS path to be backed up
55     backup_dir=""         # Target directory for backups
56     compress=0    # Compress output file?
57     comp_rate=6   # Compression rate
58     verbose=0     # Verbosity on/off
59     list_opt=0    # List backups
60     checksum_opt=0 # Perfom a checksum of all backups
61     find_last=0   # Find last full backup
62     timestamp=$(date +'%Y%m%d%H%M%S')
63 }
64
65 info()
66 {
67     [ ${verbose} -eq 1 ] && echo "INFO: $1"
68 }
69
70 #
71 # err exitval message
72 #     Display an error and exit
73 #
74 err()
75 {
76     exitval=$1
77     shift
78
79     echo 1>&2 "$0: ERROR: $*"
80     exit $exitval
81 }
82
83 usage()
84 {
85     exitval=${1:-1}
86     echo "Usage: ${SCRIPTNAME} [-hlvfk] [-i <full-backup-file|auto>]" \
87         "[-c <compress-rate>] -d [<backup-dir>] [pfs path]"
88     exit $exitval
89 }
90
91 check_pfs()
92 {
93     info "Validating PFS ${pfs_path}"
94
95     # Backup directory must exist
96     if [ -z "${pfs_path}" ]; then
97         usage
98     fi
99
100     # Make sure we are working on a HAMMER PFS
101     hammer pfs-status ${pfs_path} > /dev/null 2>&1
102     if [ $? -ne 0 ]; then
103         err 2 "${pfs} is not a HAMMER PFS"
104     fi
105 }
106
107 get_endtid()
108 {
109     local logfile=$1
110
111     awk -F "[=: ]" -vRS="\r" '{
112         if ($4 == "tids") {
113                 print $6;
114                 exit
115         }
116     }' ${logfile}
117 }
118
119 get_uuid()
120 {
121     # Get the shared UUID for the PFS
122    hammer pfs-status ${pfs_path} | awk -F'[ =]+' '
123         $2 == "shared-uuid" {
124                 print $3;
125                 exit;
126         }'
127 }
128
129 file2date()
130 {
131     local filename=""
132     local filedate=""
133
134     # Extract the date
135     filename=$(basename $1)
136     filedate=$(echo ${filename} | cut -d "_" -f1)
137
138     date -j -f '%Y%m%d%H%M%S' ${filedate} +"%B %d, %Y %H:%M:%S %Z"
139 }
140
141
142 update_mdata()
143 {
144     local filename=$(basename ${output_file})
145     local uuid=$(get_uuid)
146     local endtid=$1
147     local md5sum=$(md5 -q ${output_file} 2> /dev/null)
148
149     if [ -z "${endtid}" ]; then
150         rm ${output_file}
151         err 1 "Couldn't update the metadata file! Deleting ${output_file}"
152     fi
153     # XXX - Sanity checks missing?!!
154     if [ ${dryrun} -eq 0 ]; then
155         printf "%s,,,%d,%s,%s,%s\n" ${filename} ${backup_type} ${uuid} \
156             ${endtid} ${md5sum} >> ${metadata_file}
157     fi
158 }
159
160 do_backup()
161 {
162     local tmplog=$1
163     local compress_opts=""
164     local begtid=$2
165
166     # Calculate the compression options
167     if [ ${compress} -eq 1 ]; then
168         compress_opts=" | xz -c -${comp_rate}"
169         output_file="${output_file}.xz"
170     fi
171
172     # Generate the datafile according to the options specified
173     cmd="hammer -y -v mirror-read ${pfs_path} ${begtid} 2> ${tmplog} \
174         ${compress_opts} > ${output_file}"
175
176     info "Launching: ${cmd}"
177     if [ ${dryrun} -eq 0 ]; then
178         # Sync to disk before mirror-read
179         hammer synctid ${pfs_path} > /dev/null 2>&1
180         eval ${cmd}
181         if [ $? -eq 0 ]; then
182             # On completion, make sure only root can access backup files.
183             chmod 600 ${output_file}
184             info "Backup completed."
185         else
186             rm -f ${output_file}
187             rm -f ${tmplog}
188             err 1 "Failed to created backup data file!"
189         fi
190     else
191         info "Dry-run execution."
192     fi
193 }
194
195 full_backup()
196 {
197     local tmplog=$(mktemp)
198     local filename=""
199     local endtid=""
200
201     # Full backup (no param specified)
202     info "Initiating full backup."
203     do_backup ${tmplog}
204
205     # Generate the metadata file itself
206     metadata_file="${output_file}.bkp"
207     endtid=$(get_endtid ${tmplog})
208
209     update_mdata ${endtid}
210
211     # Cleanup
212     rm ${tmplog}
213 }
214
215 check_metadata()
216 {
217     local line=""
218     local f1=""
219     local f2=""
220
221     if [ ! -r ${metadata_file} ]; then
222         err 1 "Could not find ${metadata_file}"
223     fi
224
225     f1=$(basename ${metadata_file})
226     f2=$(head -1 ${metadata_file} | cut -d "," -f1)
227
228     if [ "${f1}" != "${f2}.bkp" ]; then
229         err 2 "Bad metadata file ${metadata_file}"
230     fi
231 }
232
233 detect_latest_backup()
234 {
235     local latest=""
236     local pattern=""
237
238     # XXX
239     # Find latest metadata backup file if needed. Right now the timestamp
240     # in the filename will let them be sorted by ls. But this could actually
241     # change.
242     if [ ${find_last} -eq 1 ]; then
243         pattern=$(echo ${pfs_path} | tr "/" "_").xz.bkp
244         latest=$(ls -1 ${backup_dir}/*${pattern} 2> /dev/null | tail -1)
245         if [ -z "${latest}" ]; then
246             err 1 "Failed to detect the latest full backup file."
247         fi
248         incr_full_file=${latest}
249     fi
250 }
251
252 incr_backup()
253 {
254     local tmplog=$(mktemp)
255     local begtid=""
256     local endtid=""
257     local line=""
258     local srcuuid=""
259     local tgtuuid=""
260     local btype=0
261
262     detect_latest_backup
263
264     # Make sure the file exists and it can be read
265     if [ ! -r ${incr_full_file} ]; then
266         err 1 "Specified file ${incr_full_file} does not exist."
267     fi
268     metadata_file=${incr_full_file}
269
270     # Verify we were passed a real metadata file
271     check_metadata
272
273     # The first backup of the metadata file must be a full one
274     line=$(head -1 ${incr_full_file})
275     btype=$(echo ${line} | cut -d ',' -f4)
276     if [ ${btype} -ne 1 ]; then
277         err 1 "No full backup in ${incr_full_file}. Cannot do incremental ones."
278     fi
279
280     # Read metadata info for the last backup performed
281     line=$(tail -1 ${incr_full_file})
282     srcuuid=$(echo $line| cut -d ',' -f 5)
283     begtid=$(echo $line| cut -d ',' -f 6)
284
285     # Verify shared uuid are the same
286     tgtuuid=$(get_uuid)
287     if [ "${srcuuid}" != "${tgtuuid}" ]; then
288         err 255 "Shared UUIDs do not match! ${srcuuid} -> ${tgtuuid}"
289     fi
290
291     # Do an incremental backup
292     info "Initiating incremental backup."
293     do_backup ${tmplog} 0x${begtid}
294
295     # Store the metadata in the full backup file
296     endtid=$(get_endtid ${tmplog})
297
298     #
299     # Handle the case where the hammer mirror-read command did not retrieve
300     # any data because the PFS was not modified at all. In that case we keep
301     # TID of the previous backup.
302     #
303     if [ -z "${endtid}" ]; then
304         endtid=${begtid}
305     fi
306     update_mdata ${endtid}
307
308     # Cleanup
309     rm ${tmplog}
310 }
311
312 list_backups()
313 {
314     local nofiles=1
315
316     for bkp in ${backup_dir}/*.bkp
317     do
318         # Skip files that don't exist
319         if [ ! -f ${bkp} ]; then
320             continue
321         fi
322         # Show incremental backups related to the full backup above
323         awk -F "," '{
324                 if ($4 == 1) {
325                         printf("full: ");
326                 }
327                 if ($4 == 2) {
328                         printf("\tincr: ");
329                 }
330         printf("%s endtid: 0x%s md5: %s\n", $1, $6, $7);
331         }' ${bkp}
332         nofiles=0
333     done
334
335     if [ ${nofiles} -eq 1 ]; then
336         err 255 "No backup files found in ${backup_dir}"
337     fi
338
339     exit 0
340 }
341
342 checksum_backups()
343 {
344     local nofiles=1
345     local storedck=""
346     local fileck=""
347     local tmp=""
348
349     for bkp in ${backup_dir}/*.bkp
350     do
351         # Skip files that don't exist
352         if [ ! -f ${bkp} ]; then
353             continue
354         fi
355         # Perform a checksum test
356         while read line
357         do
358             tmp=$(echo $line | cut -d "," -f1)
359             fname=${backup_dir}/${tmp}
360             storedck=$(echo $line | cut -d "," -f7)
361             fileck=$(md5 -q ${fname} 2> /dev/null)
362             echo -n "${fname} : "
363             if [ ! -f ${fname} ]; then
364                 echo "MISSING"
365                 continue
366             elif [ "${storedck}" == "${fileck}" ]; then
367                 echo "OK"
368             else
369                 echo "FAILED"
370             fi
371         done < ${bkp}
372         nofiles=0
373     done
374
375     if [ ${nofiles} -eq 1 ]; then
376         err 255 "No backup files found in ${backup_dir}"
377     fi
378
379     exit 0
380 }
381 # -------------------------------------------------------------
382
383 # Setup some vars
384 initialization
385
386 # Only can be run by root
387 if [  $(id -u) -ne 0 ]; then
388     err 255 "Only root can run this script."
389 fi
390
391 # Checks hammer program
392 if [ ! -x /sbin/hammer ]; then
393     err 1 'Could not find find hammer(8) program.'
394 fi
395
396 # Handle options
397 while getopts d:i:c:fvhnlk op
398 do
399     case $op in
400         d)
401             backup_dir=$OPTARG
402             ;;
403         f)
404             if [ ${backup_type} -eq 2 ]; then
405                 err 1 "-f and -i are mutually exclusive."
406             fi
407             backup_type=1
408             ;;
409         i)
410             if [ ${backup_type} -eq 2 ]; then
411                 err 1 "-f and -i are mutually exclusive."
412             fi
413             backup_type=2
414             if [ "${OPTARG}" == "auto" ]; then
415                 find_last=1
416             else
417                 incr_full_file=$OPTARG
418             fi
419             ;;
420         c)
421             compress=1
422
423             case "$OPTARG" in
424                 [1-9])
425                     comp_rate=$OPTARG
426                     ;;
427                 *)
428                     err 1 "Bad compression level specified."
429                     ;;
430             esac
431             ;;
432         k)
433             checksum_opt=1
434             ;;
435         n)
436             dryrun=1
437             ;;
438         l)
439             list_opt=1
440             ;;
441         v)
442             verbose=1
443             ;;
444         h)
445             usage 0
446             ;;
447         *)
448             usage
449             ;;
450     esac
451 done
452
453 shift $(($OPTIND - 1))
454
455 info "hammer-backup version ${VERSION}"
456
457 #
458 # If list option is selected
459 pfs_path="$1"
460
461 # Backup directory must exist
462 if [ -z "${backup_dir}" ]; then
463     usage
464 elif [ ! -d "${backup_dir}" ]; then
465     err 1 "Backup directory does not exist!"
466 fi
467 info "Backup dir is ${backup_dir}"
468
469 # Output file format is YYYYmmddHHMMSS
470 tmp=$(echo ${pfs_path} | tr '/' '_')
471 output_file="${backup_dir}/${timestamp}${tmp}"
472
473 # List backups if needed
474 if [ ${list_opt} -eq 1 ]; then
475     info "Listing backups."
476     list_backups
477 fi
478
479 # Checksum test
480 if [ ${checksum_opt} -eq 1 ]; then
481     info "Checksum test for all backup files."
482     checksum_backups
483 fi
484
485 # Only work on a HAMMER fs
486 check_pfs
487
488 # Actually launch the backup itself
489 if [ ${backup_type} -eq 1 ]; then
490     info "Full backup."
491     full_backup
492 elif [ ${backup_type} -eq 2 ]; then
493     info "Incremental backup."
494     incr_full_file=${backup_dir}/${incr_full_file}
495     incr_backup
496 else
497     err 255 "Impossible backup type."
498 fi