3 # Copyright (c) 2014 The DragonFly Project. All rights reserved.
5 # This code is derived from software contributed to The DragonFly Project
6 # by Antonio Huete <tuxillo@quantumachine.net>
8 # Redistribution and use in source and binary forms, with or without
9 # modification, are permitted provided that the following conditions
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
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.
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
39 # This script operats HAMMER PFSes and dumps its contents for backup
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
46 # It can take two types of backup:
48 # a) Full: Where ALL the data of the PFS is sent to a file.
49 # b) Inremental: It requires a previous full backup.
51 # Additionally to the backup data itself, it creates a .bkp file
52 # which contains metadata relative to the full and/or incremental
55 # The format is the following
57 # filename,rsv01,rsv02,backup type,shared uuid,last TID,md5 hash
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
67 # $ head -1 20140305222026_pfs_t1_full.xz.bkp
68 # 20140305222026_pfs_t1_full.xz.bkp,,,f,e8decfc5-a4ab-11e3-942b-f56d04d293e0,000000011b36be30,05482d26644bd1e76e69d83002e08258
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')
94 [ ${verbose} -eq 1 ] && echo "INFO: $1"
99 # Display an error and exit
106 echo 1>&2 "$0: ERROR: $*"
112 echo "Usage: ${SCRIPTNAME} [-h] [-l] [-v] [-i <full-backup-file>]" \
113 "[-f] [-c <compress-rate>] -d [<backup-dir>] <path-to-PFS>"
119 info "Validating PFS ${pfs}"
121 # Backup directory must exist
122 if [ -z "${pfs_path}" ]; then
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"
137 awk -F "[=: ]" -vRS="\r" '{
147 # Get the shared UUID for the PFS
148 hammer pfs-status ${pfs_path} | awk -F'[ =]+' '
149 $2 == "shared-uuid" {
161 filename=$(basename $1)
162 filedate=$(echo ${filename} | cut -d "_" -f1)
164 date -j -f '%Y%m%d%H%M%S' ${filedate} +"%B %d, %Y %H:%M:%S %Z"
170 local filename=$(basename ${output_file})
171 local uuid=$(get_uuid)
173 local md5sum=$(md5 -q ${output_file})
175 # XXX - Sanity checks missing?!!
176 printf "%s,,,%d,%s,%s,%s\n" $filename $backup_type $uuid $endtid $md5sum \
183 local compress_opts=""
186 # Calculate the compression options
187 if [ ${compress} -eq 1 ]; then
188 compress_opts=" | xz -c -${comp_rate}"
189 output_file="${output_file}.xz"
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}"
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
201 if [ $? -eq 0 ]; then
202 info "Backup completed."
206 err 1 "Failed to created backup data file!"
213 local tmplog=$(mktemp)
217 # Full backup (no param specified)
218 info "Initiating full backup"
221 # Generate the metadata file itself
222 metadata_file="${output_file}.bkp"
223 endtid=$(get_endtid ${tmplog})
225 update_mdata ${endtid}
237 if [ ! -r ${metadata_file} ]; then
238 err 1 "Could not find ${metadata_file}"
241 f1=$(basename ${metadata_file})
242 f2=$(head -1 ${metadata_file} | cut -d "," -f1)
244 if [ "${f1}" != "${f2}.bkp" ]; then
245 err 2 "Bad metadata file ${metadata_file}"
251 local tmplog=$(mktemp)
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
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}
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."
274 metadata_file=${incr_full_file}
276 # Verify we were passed a real metadata file
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."
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)
291 # Verify shared uuid are the same
293 if [ "${srcuuid}" != "${tgtuuid}" ]; then
294 err 255 "Shared UUIDs do not match! ${srcuuid} -> ${tgtuuid}"
297 # Do an incremental backup
298 info "Initiating incremental backup"
299 do_backup ${tmplog} 0x${endtid}
301 # Store the metadata in the full backup file
302 endtid=$(get_endtid ${tmplog})
303 update_mdata ${endtid}
313 for bkp in ${backup_dir}/*.bkp
315 # Skip files that don't exist
316 if [ ! -f ${bkp} ]; then
319 # Show incremental backups related to the full backup above
327 printf("%s endtid: 0x%s md5: %s\n", $1, $6, $7);
332 if [ ${nofiles} -eq 1 ]; then
333 err 255 "No backup files found in ${backup_dir}"
338 # -------------------------------------------------------------
343 # Only can be run by root
344 if [ $(id -u) -ne 0 ]; then
345 err 255 "Only root can run this script."
348 # Checks hammer program
349 if [ ! -x /sbin/hammer ]; then
350 err 1 'Could not find find hammer(8) program.'
353 info "hammer-backup version ${VERSION}"
356 while getopts d:i:c:fvhnl op
361 info "Backup directory is ${backup_dir}."
364 if [ ${backup_type} -eq 2 ]; then
365 err 1 "-f and -i are mutually exclusive."
372 if [ ${backup_type} -eq 2 ]; then
373 err 1 "-f and -i are mutually exclusive."
376 info "Incremental backup."
378 if [ "${OPTARG}" == "auto" ]; then
381 incr_full_file=$OPTARG
392 err 1 "Bad compression level specified."
396 info "XZ compression level ${comp_rate}."
399 info "Dry-run execution."
417 shift $(($OPTIND - 1))
420 # If list option is selected
423 # Backup directory must exist
424 if [ -z "${backup_dir}" ]; then
426 elif [ ! -d "${backup_dir}" ]; then
427 err 1 "Backup directory does not exist!"
430 # Output file format is YYYYmmddHHMMSS
431 tmp=$(echo ${pfs_path} | tr '/' '_')
432 output_file="${backup_dir}/${timestamp}${tmp}"
434 # List backups if needed
435 if [ ${list_opt} == 1 ]; then
436 info "Listing backups in ${backup_dir}"
440 # Only work on a HAMMER fs
443 # Actually launch the backup itself
444 if [ ${backup_type} -eq 1 ]; then
446 elif [ ${backup_type} -eq 2 ]; then
447 incr_full_file=${backup_dir}/${incr_full_file}
450 err 255 "Impossible backup type."