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 operates HAMMER PFSes and dumps its contents for backup
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')
67 [ ${verbose} -eq 1 ] && echo "INFO: $1"
72 # Display an error and exit
79 echo 1>&2 "$0: ERROR: $*"
86 echo "Usage: ${SCRIPTNAME} [-hlvfk] [-i <full-backup-file|auto>]" \
87 "[-c <compress-rate>] -d [<backup-dir>] [pfs path]"
93 info "Validating PFS ${pfs_path}"
95 # Backup directory must exist
96 if [ -z "${pfs_path}" ]; then
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"
111 awk -F "[=: ]" -vRS="\r" '{
121 # Get the shared UUID for the PFS
122 hammer pfs-status ${pfs_path} | awk -F'[ =]+' '
123 $2 == "shared-uuid" {
135 filename=$(basename $1)
136 filedate=$(echo ${filename} | cut -d "_" -f1)
138 date -j -f '%Y%m%d%H%M%S' ${filedate} +"%B %d, %Y %H:%M:%S %Z"
144 local filename=$(basename ${output_file})
145 local uuid=$(get_uuid)
147 local md5sum=$(md5 -q ${output_file} 2> /dev/null)
149 if [ -z "${endtid}" ]; then
151 err 1 "Couldn't update the metadata file! Deleting ${output_file}"
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}
163 local compress_opts=""
166 # Calculate the compression options
167 if [ ${compress} -eq 1 ]; then
168 compress_opts=" | xz -c -${comp_rate}"
169 output_file="${output_file}.xz"
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}"
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
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."
188 err 1 "Failed to created backup data file!"
191 info "Dry-run execution."
197 local tmplog=$(mktemp)
201 # Full backup (no param specified)
202 info "Initiating full backup."
205 # Generate the metadata file itself
206 metadata_file="${output_file}.bkp"
207 endtid=$(get_endtid ${tmplog})
209 update_mdata ${endtid}
221 if [ ! -r ${metadata_file} ]; then
222 err 1 "Could not find ${metadata_file}"
225 f1=$(basename ${metadata_file})
226 f2=$(head -1 ${metadata_file} | cut -d "," -f1)
228 if [ "${f1}" != "${f2}.bkp" ]; then
229 err 2 "Bad metadata file ${metadata_file}"
233 detect_latest_backup()
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
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."
248 incr_full_file=${latest}
254 local tmplog=$(mktemp)
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."
268 metadata_file=${incr_full_file}
270 # Verify we were passed a real metadata file
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."
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)
285 # Verify shared uuid are the same
287 if [ "${srcuuid}" != "${tgtuuid}" ]; then
288 err 255 "Shared UUIDs do not match! ${srcuuid} -> ${tgtuuid}"
291 # Do an incremental backup
292 info "Initiating incremental backup."
293 do_backup ${tmplog} 0x${begtid}
295 # Store the metadata in the full backup file
296 endtid=$(get_endtid ${tmplog})
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.
303 if [ -z "${endtid}" ]; then
306 update_mdata ${endtid}
316 for bkp in ${backup_dir}/*.bkp
318 # Skip files that don't exist
319 if [ ! -f ${bkp} ]; then
322 # Show incremental backups related to the full backup above
330 printf("%s endtid: 0x%s md5: %s\n", $1, $6, $7);
335 if [ ${nofiles} -eq 1 ]; then
336 err 255 "No backup files found in ${backup_dir}"
349 for bkp in ${backup_dir}/*.bkp
351 # Skip files that don't exist
352 if [ ! -f ${bkp} ]; then
355 # Perform a checksum test
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
366 elif [ "${storedck}" == "${fileck}" ]; then
375 if [ ${nofiles} -eq 1 ]; then
376 err 255 "No backup files found in ${backup_dir}"
381 # -------------------------------------------------------------
386 # Only can be run by root
387 if [ $(id -u) -ne 0 ]; then
388 err 255 "Only root can run this script."
391 # Checks hammer program
392 if [ ! -x /sbin/hammer ]; then
393 err 1 'Could not find find hammer(8) program.'
397 while getopts d:i:c:fvhnlk op
404 if [ ${backup_type} -eq 2 ]; then
405 err 1 "-f and -i are mutually exclusive."
410 if [ ${backup_type} -eq 2 ]; then
411 err 1 "-f and -i are mutually exclusive."
414 if [ "${OPTARG}" == "auto" ]; then
417 incr_full_file=$OPTARG
428 err 1 "Bad compression level specified."
453 shift $(($OPTIND - 1))
455 info "hammer-backup version ${VERSION}"
458 # If list option is selected
461 # Backup directory must exist
462 if [ -z "${backup_dir}" ]; then
464 elif [ ! -d "${backup_dir}" ]; then
465 err 1 "Backup directory does not exist!"
467 info "Backup dir is ${backup_dir}"
469 # Output file format is YYYYmmddHHMMSS
470 tmp=$(echo ${pfs_path} | tr '/' '_')
471 output_file="${backup_dir}/${timestamp}${tmp}"
473 # List backups if needed
474 if [ ${list_opt} -eq 1 ]; then
475 info "Listing backups."
480 if [ ${checksum_opt} -eq 1 ]; then
481 info "Checksum test for all backup files."
485 # Only work on a HAMMER fs
488 # Actually launch the backup itself
489 if [ ${backup_type} -eq 1 ]; then
492 elif [ ${backup_type} -eq 2 ]; then
493 info "Incremental backup."
494 incr_full_file=${backup_dir}/${incr_full_file}
497 err 255 "Impossible backup type."