check-bkp-status.in: Tweak auto-collapse of linked log sections.
[rsync-backup] / rsync-backup.in
index 292c6eb..a9145d6 100644 (file)
@@ -27,12 +27,7 @@ set -e
 
 thishost=$(hostname -s)
 quis=${0##*/}
-
-VERSION=@VERSION@
-mntbkpdir=@mntbkpdir@
-logdir=@logdir@
-fshashdir=@fshashdir@
-conf=@sysconfdir@/rsync-backup.conf
+. @pkgdatadir@/lib.sh
 
 verbose=:
 dryrun=nil
@@ -78,6 +73,15 @@ maybe () {
   esac
 }
 
+copy () {
+  prefix=$1
+  ## Copy lines from stdin to stdout, adding PREFIX.
+
+  while IFS= read -r line; do
+    printf "%s %s\n" "$prefix" "$line"
+  done
+}
+
 run () {
   tag=$1 cmd=$2; shift 2
   ## Run CMD, logging its output in a pleasing manner.
@@ -94,8 +98,8 @@ run () {
        { { { ( set +e
                "$cmd" "$@" 3>&- 4>&- 5>&- 9>&-
                echo $? >&5; ) |
-             while IFS= read line; do echo "| $line"; done >&4; } 2>&1 |
-           while IFS= read line; do echo "* $line"; done >&4; } 4>&1 |
+             copy "|" >&4; } 2>&1 |
+           copy "*" >&4; } 4>&1 |
          cat >&9; } 5>&1 </dev/null
       )
       case $rc in
@@ -124,13 +128,13 @@ hostrun () {
   ## way it will be processed by a shell.
 
   if localp $host; then run "@$host: $tag" sh -c "$cmd"
-  else run "@$host: $tag" ssh $host "$cmd"
+  else run "@$host: $tag" ssh $userat$host "$cmd"
   fi
 }
 
 _hostrun () {
   h=$1 cmd=$2
-  ## Like hostrun, but without the complicated logging, but targetted at a
+  ## Like hostrun, but without the complicated logging, and targetted at a
   ## specific host.
 
   if localp $h; then sh -c "$cmd"
@@ -144,7 +148,81 @@ hostpath () {
   ## current host is local.
 
   if localp $host; then echo $path
-  else echo $host:$path
+  else echo $userat$host:$path
+  fi
+}
+
+defhook () {
+  hook=$1
+  ## Define a hook called HOOK.
+
+  eval hk_$hook=
+}
+
+addhook () {
+  hook=$1 cmd=$2
+  ## Add command CMD to the hook HOOK.
+
+  eval old=\$hk_$hook; new="$old $cmd"
+  eval hk_$hook=\$new
+}
+
+runhook () {
+  hook=$1; shift 1
+  ## Invoke HOOK, passing it the remaining arguments.
+
+  eval cmds=\$hk_$hook
+  for cmd in $cmds; do
+    if ! $cmd "$@"; then return $?; fi
+  done
+}
+
+remove_old_logfiles () {
+  base=$1
+  ## Remove old logfiles with names of the form BASE.DATE#N, so that there
+  ## are at most $MAXLOG of them.
+
+  ## Count up the logfiles.
+  nlog=0
+  for i in "$base".*; do
+    if [ ! -f "$i" ]; then continue; fi
+    nlog=$(( nlog + 1 ))
+  done
+
+  ## If there are too many, go through and delete some early ones.
+  if [ $dryrun = nil ] && [ $nlog -gt $MAXLOG ]; then
+    n=$(( nlog - MAXLOG ))
+    for i in "$base".*; do
+      if [ ! -f "$i" ]; then continue; fi
+      rm -f "$i"
+      n=$(( n - 1 ))
+      if [ $n -eq 0 ]; then break; fi
+    done
+  fi
+}
+
+###--------------------------------------------------------------------------
+### Database operations.
+
+insert_index () {
+  host=$1 fs=$2 date=$3 vol=$4
+
+  if [ -f "$INDEXDB" ]; then
+    sqlite3 "$INDEXDB" <<EOF
+INSERT INTO idx (host, fs, date, vol)
+       VALUES ('$host', '$fs', '$date', '$vol');
+EOF
+  fi
+}
+
+delete_index () {
+  host=$1 fs=$2 date=$3
+
+  if [ -f "$INDEXDB" ]; then
+    sqlite3 "$INDEXDB" <<EOF
+DELETE FROM idx WHERE
+       host = '$host' AND fs = '$fs' AND date = '$date';
+EOF
   fi
 }
 
@@ -194,7 +272,6 @@ unsnap_ro () {
 ## Snapshot using LVM.
 
 SNAPSIZE="-l10%ORIGIN"
-SNAPDIR=@mntbkpdir@/snap
 
 snap_lvm () {
   vg=$1 lv=$2
@@ -274,7 +351,7 @@ do_rfreezefs () {
 
     ## Get the volume host to create the snapshot.
     set +e
-    _hostrun >&2 3>&- $lvhost \
+    _hostrun >&2 3>&- $userat$lvhost \
       "lvcreate --snapshot -n$lv.bkp $SNAPSIZE $vg/$lv"
     snaprc=$?
     set -e
@@ -285,7 +362,7 @@ do_rfreezefs () {
     case $tok in
       "$tok_THAWED") ;;
       *)
-       _hostrun >&2 3>&- $lvhost "lvremove -f $vg/$lv.bkp" || :
+       _hostrun >&2 3>&- $userat$lvhost "lvremove -f $vg/$lv.bkp" || :
        echo >&2 "$quis: unexpected token $tok (rfreezefs $fsdir on $fshost)"
        exit 1
        ;;
@@ -312,7 +389,7 @@ do_rfreezefs () {
   esac
 
   ## Mount the snapshot on the volume host.
-  _hostrun >&2 $lvhost "
+  _hostrun >&2 $userat$lvhost "
        mkdir -p $SNAPDIR/$lv
        mount -oro /dev/$vg/$lv.bkp $SNAPDIR/$lv"
 }
@@ -336,53 +413,6 @@ unsnap_rfreezefs () {
 ###--------------------------------------------------------------------------
 ### Expiry computations.
 
-parsedate () {
-  date=$1
-  ## Parse an ISO8601 DATE, and set YEAR, MONTH, DAY appropriately (and
-  ## without leading zeros).
-
-  ## Extract the components of the date and trim leading zeros (which will
-  ## cause things to be interpreted as octal and fail).
-  year=${date%%-*} rest=${date#*-}; month=${rest%%-*} day=${rest#*-}
-  year=${year#0} month=${month#0} day=${day#0}
-}
-
-julian () {
-  date=$1
-  ## Convert an ISO8601 DATE to a Julian Day Number.
-
-  parsedate $date
-
-  ## The actual calculation: convert a (proleptic) Gregorian calendar date
-  ## into a Julian day number.  This is taken from Wikipedia's page
-  ## http://en.wikipedia.org/wiki/Julian_day#Calculation but the commentary
-  ## is mine.  The epoch is 4713BC-01-01 (proleptic) Julian, or 4714BC-11-24
-  ## proleptic Gregorian.
-
-  ## If the MONTH is January or February then set a = 1, otherwise set a = 0.
-  a=$(( (14 - $month)/12 ))
-
-  ## Compute a year offset relative to 4799BC-03-01.  This puts the leap day
-  ## as the very last day in a year, which is very convenient.  The offset
-  ## here is sufficient to make all y values positive (within the range of
-  ## the JDN calendar), and is a multiple of 400, which is the Gregorian
-  ## cycle length.
-  y=$(( $year + 4800 - $a ))
-
-  ## Compute the offset month number in that year.  These months count from
-  ## zero, not one.
-  m=$(( $month + 12*$a - 3 ))
-
-  ## Now for the main event.  The (153 m + 2)/5 term is a surprising but
-  ## correct trick for obtaining the number of days in the first m months of
-  ## the (shifted) year).  The magic offset 32045 is what you get when you
-  ## plug the proper JDN epoch (year = -4713, month = 11, day = 24) into the
-  ## above machinery.
-  jdn=$(( $day + (153*$m + 2)/5 + 365*$y + $y/4 - $y/100 + $y/400 - 32045 ))
-
-  echo $jdn
-}
-
 expire () {
   ## Read dates on stdin; write to stdout `EXPIRE date' for dates which
   ## should be expired and `RETAIN date' for dates which should be retained.
@@ -480,14 +510,14 @@ EOF
 ###--------------------------------------------------------------------------
 ### Actually taking backups of filesystems.
 
-STOREDIR=@mntbkpdir@/store
 MAXLOG=14
 HASH=sha256
+unset VOLUME
 
 bkprc=0
 
 remote_fshash () {
-  _hostrun $host "
+  _hostrun $userat$host "
        umask 077
        mkdir -p $fshashdir
        cd ${snapmnt#*:}
@@ -528,25 +558,35 @@ expire_backups () {
        echo "delete $date"
        $verbose -n "   expire $date..."
        rm -rf $date $date.*
+       delete_index $host $fs $date
        $verbose " done"
        ;;
     esac
   done
 }
 
+## Backup hooks.
+defhook setup
+defhook precommit
+defhook postcommit
+
 backup_precommit_hook () {
   host=$1 fs=$2 date=$3
-  ## Override this hook in the configuration file for special effects.
+  ## Compatibility: You can override this hook in the configuration file for
+  ## special effects; but it's better to use `addhook precommit'.
 
   :
 }
+addhook precommit backup_precommit_hook
 
 backup_commit_hook () {
   host=$1 fs=$2 date=$3
-  ## Override this hook in the configuration file for special effects.
+  ## Compatibility: You can override this hook in the configuration file for
+  ## special effects; but it's better to use `addhook commit'.
 
   :
 }
+addhook commit backup_commit_hook
 
 do_backup () {
   date=$1 fs=$2 fsarg=$3
@@ -555,6 +595,14 @@ do_backup () {
   set -e
   attempt=0
 
+  ## Run a hook beforehand.
+  set +e; runhook setup $host $fs $date; rc=$?; set -e
+  case $? in
+    0) ;;
+    99) log "BACKUP of $host:$fs SKIPPED by hook"; return 0 ;;
+    *) log "BACKUP of $host:$fs FAILED (hook returns $?)"; return $? ;;
+  esac
+
   ## Report the start of this attempt.
   log "START BACKUP of $host:$fs"
 
@@ -654,10 +702,11 @@ do_backup () {
   ## Commit this backup.
   case $dryrun in
     nil)
-      backup_precommit_hook $host $fs $date
+      runhook precommit $host $fs $date
       mv new $date
       mv new.fshash $date.fshash
-      backup_commit_hook $host $fs $date
+      insert_index $host $fs $date $VOLUME
+      runhook commit $host $fs $date
       mkdir hack
       ln -s $date hack/last
       mv hack/last .
@@ -667,10 +716,14 @@ do_backup () {
   $verbose "   commit"
 
   ## Expire old backups.
+  case "${expire_policy+t},${default_policy+t}" in
+    ,t) expire_policy=$default_policy ;;
+  esac
   case "${expire_policy+t},$dryrun" in
     t,nil) run "expiry for $host:$fs" expire_backups ;;
     t,t) expire_backups ;;
   esac
+  clear_policy=t
 
   ## Report success.
   case $dryrun in
@@ -679,6 +732,46 @@ do_backup () {
   esac
 }
 
+run_backup_cmd () {
+  fs=$1 date=$2 cmd=$3; shift 3
+  ## try_backup FS DATE COMMAND ARGS ...
+  ##
+  ## Run COMMAND ARGS to back up filesystem FS on the current host,
+  ## maintaining a log, and checking whether it worked.  The caller has
+  ## usually worked out the DATE in order to set up the filesystem, and we
+  ## need it to name the log file properly.
+
+  ## Find a name for the log file.  In unusual circumstances, we may have
+  ## deleted old logs from today, so just checking for an unused sequence
+  ## number is insufficient.  Instead, check all of the logfiles for today,
+  ## and use a sequence number that's larger than any of them.
+  case $dryrun in
+    t)
+      log=/dev/null
+      ;;
+    nil)
+      seq=1
+      for i in "$logdir/$host/$fs.$date#"*; do
+       tail=${i##*#}
+       case "$tail" in [!1-9]* | *[!0-9]*) continue ;; esac
+       if [ -f "$i" -a $tail -ge $seq ]; then seq=$(( tail + 1 )); fi
+      done
+      log="$logdir/$host/$fs.$date#$seq"
+      ;;
+  esac
+
+  ## Run the backup command.
+  case $dryrun in nil) mkdir -p $logdir/$host ;; esac
+  if ! "$cmd" "$@" 9>$log 1>&9; then
+    echo >&2
+    echo >&2 "$quis: backup of $host:$fs FAILED!"
+    bkprc=1
+  fi
+
+  ## Clear away any old logfiles.
+  remove_old_logfiles "$logdir/$host/$fs"
+}
+
 backup () {
   ## backup FS[:ARG] ...
   ##
@@ -693,6 +786,12 @@ backup () {
     exit 15
   fi
 
+  ## Read the volume name if we don't have one already.  Again, this allows
+  ## the configuration file to provide a volume name.
+  case "${VOLUME+t}${VOLUME-nil}" in
+    nil) VOLUME=$(cat $METADIR/volume) ;;
+  esac
+
   ## Back up each requested file system in turn.
   for fs in "$@"; do
 
@@ -726,63 +825,40 @@ backup () {
       continue
     fi
 
-    ## Find a name for the log file.  In unusual circumstances, we may have
-    ## deleted old logs from today, so just checking for an unused sequence
-    ## number is insufficient.  Instead, check all of the logfiles for today,
-    ## and use a sequence number that's larger than any of them.
-    case $dryrun in
-      t)
-       log=/dev/null
-       ;;
-      nil)
-       seq=1
-       for i in "$logdir/$host/$fs.$date#"*; do
-         tail=${i##*#}
-         case "$tail" in [!1-9]* | *[!0-9]*) continue ;; esac
-         if [ -f "$i" -a $tail -ge $seq ]; then seq=$(( tail + 1 )); fi
-       done
-       log="$logdir/$host/$fs.$date#$seq"
-       ;;
-    esac
-
     ## Do the backup of this filesystem.
-    case $dryrun in nil) mkdir -p $logdir/$host ;; esac
-    if ! do_backup $date $fs $fsarg 9>$log 1>&9; then
-      echo >&2
-      echo >&2 "$quis: backup of $host:$fs FAILED!"
-      bkprc=1
-    fi
-
-    ## Count up the logfiles.
-    nlog=0
-    for i in "$logdir/$host/$fs".*; do
-      if [ ! -f "$i" ]; then continue; fi
-      nlog=$(( nlog + 1 ))
-    done
-
-    ## If there are too many, go through and delete some early ones.
-    if [ $dryrun = nil ] && [ $nlog -gt $MAXLOG ]; then
-      n=$(( nlog - MAXLOG ))
-      for i in "$logdir/$host/$fs".*; do
-       if [ ! -f "$i" ]; then continue; fi
-       rm -f "$i"
-       n=$(( n - 1 ))
-       if [ $n -eq 0 ]; then break; fi
-      done
-    fi
+    run_backup_cmd $fs $date do_backup $date $fs $fsarg
   done
 }
 
 ###--------------------------------------------------------------------------
 ### Configuration functions.
 
-host () { host=$1; like=; $verbose "host $host"; }
+defhook start
+defhook end
+
+done_first_host_p=nil
+
+host () {
+  host=$1
+  like= userat=
+  case $done_first_host_p in
+    nil) runhook start; done_first_host_p=t ;;
+  esac
+  case "${expire_policy+t},${default_policy+t}" in
+    t,) default_policy=$expire_policy ;;
+  esac
+  unset expire_policy
+  $verbose "host $host"
+}
+
 snaptype () { snap=$1; shift; snapargs="$*"; retry=0; }
 rsyncargs () { rsyncargs="$*"; }
 like () { like="$*"; }
 retry () { retry="$*"; }
+user () { userat="$*@"; }
 
 retain () {
+  case $clear_policy in t) unset expire_policy; clear_policy=nil ;; esac
   expire_policy="${expire_policy+$expire_policy
 }$*"
 }
@@ -791,23 +867,13 @@ retain () {
 ### Read the configuration and we're done.
 
 usage () {
-  echo "usage: $quis [-v] [-c CONF]"
+  echo "usage: $quis [-nv] [-c CONF]"
 }
 
 version () {
   echo "$quis version $VERSION"
 }
 
-config () {
-  echo
-  cat <<EOF
-conf = $conf
-mntbkpdir = $mntbkpdir
-fshashdir = $fshashdir
-logdir = $logdir
-EOF
-}
-
 whine () { echo >&8 "$@"; }
 
 while getopts "hVvc:n" opt; do
@@ -826,6 +892,12 @@ exec 8>&1
 
 . "$conf"
 
+runhook end $bkprc
+case "$bkprc" in
+  0) $verbose "All backups successful" ;;
+  *) $verbose "Backups FAILED" ;;
+esac
+
 ###----- That's all, folks --------------------------------------------------
 
 exit $bkprc