5 ### (c) 2012 Mark Wooding
8 ###----- Licensing notice ---------------------------------------------------
10 ### This file is part of the `rsync-backup' program.
12 ### rsync-backup is free software; you can redistribute it and/or modify
13 ### it under the terms of the GNU General Public License as published by
14 ### the Free Software Foundation; either version 2 of the License, or
15 ### (at your option) any later version.
17 ### rsync-backup is distributed in the hope that it will be useful,
18 ### but WITHOUT ANY WARRANTY; without even the implied warranty of
19 ### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 ### GNU General Public License for more details.
22 ### You should have received a copy of the GNU General Public License
23 ### along with rsync-backup; if not, write to the Free Software Foundation,
24 ### Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
28 thishost=$(hostname -s)
35 conf=@sysconfdir@/rsync-backup.conf
40 ###--------------------------------------------------------------------------
41 ### Utility functions.
46 ## Run rsync(1) in an appropriate manner. Configuration should ovrride
47 ## this or set $RSYNCOPTS if it wants to do something weirder. Arguments
48 ## to this function are passed on to rsync.
51 --archive --hard-links --numeric-ids --del \
56 --filter="dir-merge .rsync-backup" \
66 now=$(date +"%Y-%m-%d %H:%M:%S %z")
73 ## Run CMD, if this isn't a dry run.
76 t) echo >&2 " +++ $*" ;;
82 tag=$1 cmd=$2; shift 2
83 ## Run CMD, logging its output in a pleasing manner.
87 echo >&2 " *** RUN $tag"
88 echo >&2 " +++ $cmd $*"
95 "$cmd" "$@" 3>&- 4>&- 5>&- 9>&-
97 while IFS= read line; do echo "| $line"; done >&4; } 2>&1 |
98 while IFS= read line; do echo "* $line"; done >&4; } 4>&1 |
99 cat >&9; } 5>&1 </dev/null
103 *) log "FAIL $tag (rc = $rc)" ;;
112 ## Answer whether H is a local host.
115 "$thishost") return 0 ;;
122 ## Run CMD on the current host. If the host seems local then run the
123 ## command through a local shell; otherwise run it through ssh(1). Either
124 ## way it will be processed by a shell.
126 if localp $host; then run "@$host: $tag" sh -c "$cmd"
127 else run "@$host: $tag" ssh $host "$cmd"
133 ## Like hostrun, but without the complicated logging, but targetted at a
136 if localp $h; then sh -c "$cmd"
143 ## Output (to stdout) either PATH or HOST:PATH, choosing the former if the
144 ## current host is local.
146 if localp $host; then echo $path
147 else echo $host:$path
151 ###--------------------------------------------------------------------------
152 ### Snapshot handling.
154 ## Snapshot protocol. Each snapshot type has a pair of functions snap_TYPE
155 ## and unsnap_TYPE. Each is given the current snapshot arguments and the
156 ## filesystem name to back up. The snap_TYPE function should create and
157 ## mount the snapshot and output an rsync(1) path to where the filesystem can
158 ## be copied; the unsnap_TYPE function should unmount and tear down the
161 ## Fake snapshot by not doing anything. Use only if you have no choice.
162 snap_live () { hostpath "$2"; }
163 unsnap_live () { :; }
165 ## Fake snapshot by remounting a live filesystem read-only. Useful if the
166 ## underlying storage isn't in LVM.
171 ## Place a marker in the filesystem so we know why it was made readonly.
172 ## (Also this serves to ensure that the filesystem was writable before.)
173 hostrun "snap-ro $mnt" "
174 echo rsync-backup >$mnt/.lock
175 mount -oremount,ro $mnt" || return $?
184 ## Check that the filesystem still has our lock marker.
185 hostrun "unsnap-ro $mnt" "
186 case \$(cat $mnt/.lock) in
188 *) echo unlocked by someone else; exit 31 ;;
190 mount -oremount,rw $mnt
191 rm $mnt/.lock" || return $?
194 ## Snapshot using LVM.
196 SNAPSIZE="-l10%ORIGIN"
197 SNAPDIR=@mntbkpdir@/snap
202 ## Make the snapshot.
203 hostrun "snap-lvm $vg/$lv" "
204 lvcreate --snapshot -n$lv.bkp $SNAPSIZE $vg/$lv
205 mkdir -p $SNAPDIR/$lv
206 mount -oro /dev/$vg/$lv.bkp $SNAPDIR/$lv" || return $?
209 hostpath $SNAPDIR/$lv
215 ## Remove the snapshot. Sometimes LVM doesn't notice that the snapshot is
216 ## no longer in open immdiately, so try several times.
217 hostrun "unsnap-lvm $vg/$lv" "
221 if lvremove -f $vg/$lv.bkp; then rc=0; break; fi
224 exit $rc" || return $?
227 ## Complicated snapshot using LVM, where the volume group and filesystem are
228 ## owned by different machines, so they need to be synchronized during the
232 lvhost=$1 vg=$2 lv=$3 fshost=$4 fsdir=$5
234 ## Engage in the rfreezefs protocol with the filesystem host. This
235 ## involves some hairy plumbing. We want to get exit statuses out of both
238 ssh $fshost rfreezefs $fsdir | {
241 ## Read the codebook from the remote end.
247 TOKEN) eval tok_$2=$3 ;;
248 READY) ready=t; break ;;
250 echo >&2 "$quis: unexpected keyword $1 (rfreezefs to $rhost)"
257 echo >&2 "$quis: unexpected eof (rfreezefs to $rhost)"
262 ## Connect to the filesystem host's TCP port and get it to freeze its
264 exec 3<>/dev/tcp/$fshost/$port
270 echo >&2 "$quis: unexpected token $tok (rfreezefs $fsdir on $fshost)"
275 ## Get the volume host to create the snapshot.
277 _hostrun >&2 3>&- $lvhost \
278 "lvcreate --snapshot -n$lv.bkp $SNAPSIZE $vg/$lv"
282 ## The filesystem can thaw now.
288 _hostrun >&2 3>&- $lvhost "lvremove -f $vg/$lv.bkp" || :
289 echo >&2 "$quis: unexpected token $tok (rfreezefs $fsdir on $fshost)"
298 ## Sift through the wreckage to find out what happened.
299 rc_rfreezefs=${PIPESTATUS[0]} rc_snapshot=${PIPESTATUS[1]}
301 case $rc_rfreezefs:$rc_snapshot in
305 echo >&2 "$quis: EMERGENCY failed to thaw $fsdir on $fshost!"
309 echo >&2 "$quis: failed to snapshot $vg/$lv ($fsdir on $fshost)"
314 ## Mount the snapshot on the volume host.
315 _hostrun >&2 $lvhost "
316 mkdir -p $SNAPDIR/$lv
317 mount -oro /dev/$vg/$lv.bkp $SNAPDIR/$lv"
321 rhost=$1 vg=$2 lv=$3 rfs=$4
324 run "snap-rfreezefs $host:$vg/$lv $rhost:$rfs" \
325 do_rfreezefs $host $vg $lv $rhost $rfs || return $?
326 hostpath $SNAPDIR/$lv
329 unsnap_rfreezefs () {
331 ## Unshapping is the same as for plain LVM.
332 rhost=$1 vg=$2 lv=$3 rfs=$4
336 ###--------------------------------------------------------------------------
337 ### Expiry computations.
341 ## Parse an ISO8601 DATE, and set YEAR, MONTH, DAY appropriately (and
342 ## without leading zeros).
344 ## Extract the components of the date and trim leading zeros (which will
345 ## cause things to be interpreted as octal and fail).
346 year=${date%%-*} rest=${date#*-}; month=${rest%%-*} day=${rest#*-}
347 year=${year#0} month=${month#0} day=${day#0}
352 ## Convert an ISO8601 DATE to a Julian Day Number.
356 ## The actual calculation: convert a (proleptic) Gregorian calendar date
357 ## into a Julian day number. This is taken from Wikipedia's page
358 ## http://en.wikipedia.org/wiki/Julian_day#Calculation but the commentary
359 ## is mine. The epoch is 4713BC-01-01 (proleptic) Julian, or 4714BC-11-24
360 ## proleptic Gregorian.
362 ## If the MONTH is January or February then set a = 1, otherwise set a = 0.
363 a=$(( (14 - $month)/12 ))
365 ## Compute a year offset relative to 4799BC-03-01. This puts the leap day
366 ## as the very last day in a year, which is very convenient. The offset
367 ## here is sufficient to make all y values positive (within the range of
368 ## the JDN calendar), and is a multiple of 400, which is the Gregorian
370 y=$(( $year + 4800 - $a ))
372 ## Compute the offset month number in that year. These months count from
374 m=$(( $month + 12*$a - 3 ))
376 ## Now for the main event. The (153 m + 2)/5 term is a surprising but
377 ## correct trick for obtaining the number of days in the first m months of
378 ## the (shifted) year). The magic offset 32045 is what you get when you
379 ## plug the proper JDN epoch (year = -4713, month = 11, day = 24) into the
381 jdn=$(( $day + (153*$m + 2)/5 + 365*$y + $y/4 - $y/100 + $y/400 - 32045 ))
387 ## Read dates on stdin; write to stdout `EXPIRE date' for dates which
388 ## should be expired and `RETAIN date' for dates which should be retained.
390 ## Get the current date and convert it into useful forms.
391 now=$(date +%Y-%m-%d)
393 now_jdn=$(julian $now) now_year=$year now_month=$month now_day=$day
396 ## Work through each date in the input.
400 ## Convert the date into a useful form.
404 ## Work through the policy list.
405 if [ $jdn -le $now_jdn ]; then
406 while read ival age; do
408 ## Decide whether the policy entry applies to this date.
415 if [ $year -eq $now_year ] ||
416 ([ $year -eq $(( $now_year - 1 )) ] &&
417 [ $month -ge $now_month ])
421 if ([ $month -eq $now_month ] && [ $year -eq $now_year ]) ||
422 ((([ $month -eq $(( $now_month - 1 )) ] &&
423 [ $year -eq $now_year ]) ||
424 ([ $month -eq 12 ] && [ $now_month -eq 1 ] &&
425 [ $year -eq $(( $now_year - 1 )) ])) &&
426 [ $day -ge $now_day ])
430 if [ $jdn -ge $(( $now_jdn - 7 )) ]; then apply=t; fi
433 echo >&2 "$quis: unknown age symbol \`$age'"
437 case $apply in nil) continue ;; esac
439 ## Find the interval marker for this date.
445 ydn=$(julian $year-01-01)
446 wk=$(( ($jdn - $ydn)/7 + 1 ))
456 echo >&2 "$quis: unknown interval symbol \`$ival'"
461 ## See if we've alredy retained something in this interval.
464 *) keep=t kept=$kept$marker: ;;
473 t) echo RETAIN $date ;;
474 *) echo EXPIRE $date ;;
480 ###--------------------------------------------------------------------------
481 ### Actually taking backups of filesystems.
483 STOREDIR=@mntbkpdir@/store
494 echo \"*** $host $fs $date\"; echo
495 rsync -rx --filter='dir-merge .rsync-backup' ./ |
496 fshash -c$fshashdir/$fs.bkp -a -H$HASH -frsync
501 { echo "*** $host $fs $date"; echo
502 fshash -c$STOREDIR/fshash.cache -H$HASH new/
510 case $i in *[!-0-9]*) continue ;; esac
511 case $seen in *:"$i":*) continue ;; esac
516 while read op date; do
519 echo >&2 " --- keep $date"
522 echo >&2 " --- delete $date"
529 $verbose -n " expire $date..."
537 backup_precommit_hook () {
538 host=$1 fs=$2 date=$3
539 ## Override this hook in the configuration file for special effects.
544 backup_commit_hook () {
545 host=$1 fs=$2 date=$3
546 ## Override this hook in the configuration file for special effects.
552 date=$1 fs=$2 fsarg=$3
553 ## Back up FS on the current host.
558 ## Report the start of this attempt.
559 log "START BACKUP of $host:$fs"
561 ## Maybe we need to retry the backup.
564 ## Create and mount the remote snapshot.
567 maybe snap_$snap $fs $fsarg
571 snapmnt=$(snap_$snap $snapargs $fs $fsarg) || return $?
574 $verbose " create snapshot"
576 ## Build the list of hardlink sources.
578 for i in $host $like; do
579 d=$STOREDIR/$i/$fs/last/
580 if [ -d $d ]; then linkdests="$linkdests --link-dest=$d"; fi
583 ## Copy files from the remote snapshot.
586 t) $verbose " running rsync" ;;
587 nil) $verbose -n " running rsync..." ;;
590 run "RSYNC of $host:$fs (snapshot on $snapmnt)" do_rsync \
596 case $dryrun in nil) $verbose " done" ;; esac
598 ## Collect a map of the snapshot for verification purposes.
601 t) $verbose " remote fshash" ;;
602 nil) $verbose -n " remote fshash..." ;;
604 run "@$host: fshash $fs" remote_fshash
607 case $dryrun in nil) $verbose " done" ;; esac
609 ## Remove the snapshot.
610 maybe unsnap_$snap $snapargs $fs $fsarg
611 $verbose " remove snapshot"
613 ## If we failed to copy, then give up.
614 case $rc_rsync:$rc_fshash in
616 0:*) return $rc_fshash ;;
617 *) return $rc_rsync ;;
620 ## Get a matching map of the files received.
621 maybe mkdir -m750 -p $STOREDIR/tmp/
622 localmap=$STOREDIR/tmp/fshash.$host.$fs.$date
624 t) $verbose " local fshash" ;;
625 nil) $verbose -n " local fshash..." ;;
627 run "local fshash $host:$fs" local_fshash || return $?
628 case $dryrun in nil) $verbose " done" ;; esac
630 ## Compare the two maps.
632 run "compare fshash maps for $host:$fs" diff -u new.fshash $localmap
640 if [ $attempt -ge $retry ]; then return $rc; fi
641 $verbose " fshash mismatch; retrying"
642 attempt=$(( $attempt + 1 ))
651 maybe rm -f $localmap
652 $verbose " fshash match"
654 ## Commit this backup.
657 backup_precommit_hook $host $fs $date
659 mv new.fshash $date.fshash
660 backup_commit_hook $host $fs $date
662 ln -s $date hack/last
669 ## Expire old backups.
670 case "${expire_policy+t},$dryrun" in
671 t,nil) run "expiry for $host:$fs" expire_backups ;;
672 t,t) expire_backups ;;
677 t) log "END BACKUP of $host:$fs" ;;
678 nil) log "SUCCESSFUL BACKUP of $host:$fs" ;;
683 ## backup FS[:ARG] ...
685 ## Back up the filesystems on the currently selected host using the
686 ## currently selected snapshot type.
688 ## Make sure that there's a store volume. We must do this here rather than
689 ## in the main body of the script, since the configuration file needs a
690 ## chance to override STOREDIR.
691 if ! [ -r $STOREDIR/.rsync-backup-store ]; then
692 echo >&2 "$quis: no backup volume mounted"
696 ## Back up each requested file system in turn.
699 ## Parse the argument.
701 *:*) fsarg=${fs#*:} fs=${fs%%:*} ;;
704 $verbose " filesystem $fs"
706 ## Move to the store directory and set up somewhere to put this backup.
710 if [ ! -d $host ]; then
712 chown root:root $host
714 if [ ! -d $host/$fs ]; then
715 mkdir -m750 $host/$fs
716 chown root:backup $host/$fs
722 ## Find out if we've already copied this filesystem today.
723 date=$(date +%Y-%m-%d)
724 if [ $dryrun = nil ] && [ -d $date ]; then
725 $verbose " already dumped"
729 ## Find a name for the log file. In unusual circumstances, we may have
730 ## deleted old logs from today, so just checking for an unused sequence
731 ## number is insufficient. Instead, check all of the logfiles for today,
732 ## and use a sequence number that's larger than any of them.
739 for i in "$logdir/$host/$fs.$date#"*; do
741 case "$tail" in [!1-9]* | *[!0-9]*) continue ;; esac
742 if [ -f "$i" -a $tail -ge $seq ]; then seq=$(( tail + 1 )); fi
744 log="$logdir/$host/$fs.$date#$seq"
748 ## Do the backup of this filesystem.
749 case $dryrun in nil) mkdir -p $logdir/$host ;; esac
750 if ! do_backup $date $fs $fsarg 9>$log 1>&9; then
752 echo >&2 "$quis: backup of $host:$fs FAILED!"
756 ## Count up the logfiles.
758 for i in "$logdir/$host/$fs".*; do
759 if [ ! -f "$i" ]; then continue; fi
763 ## If there are too many, go through and delete some early ones.
764 if [ $dryrun = nil ] && [ $nlog -gt $MAXLOG ]; then
765 n=$(( nlog - MAXLOG ))
766 for i in "$logdir/$host/$fs".*; do
767 if [ ! -f "$i" ]; then continue; fi
770 if [ $n -eq 0 ]; then break; fi
776 ###--------------------------------------------------------------------------
777 ### Configuration functions.
779 host () { host=$1; like=; $verbose "host $host"; }
780 snaptype () { snap=$1; shift; snapargs="$*"; retry=0; }
781 rsyncargs () { rsyncargs="$*"; }
782 like () { like="$*"; }
783 retry () { retry="$*"; }
786 expire_policy="${expire_policy+$expire_policy
790 ###--------------------------------------------------------------------------
791 ### Read the configuration and we're done.
794 echo "usage: $quis [-v] [-c CONF]"
798 echo "$quis version $VERSION"
805 mntbkpdir = $mntbkpdir
806 fshashdir = $fshashdir
811 whine () { echo >&8 "$@"; }
813 while getopts "hVvc:n" opt; do
816 V) version; config; exit 0 ;;
823 shift $((OPTIND - 1))
824 case $# in 0) ;; *) usage >&2; exit 1 ;; esac
829 ###----- That's all, folks --------------------------------------------------