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