chiark / gitweb /
Add hooks for mounting and unmounting backup devices
authorRichard Kettlewell <rjk@terraraq.org.uk>
Mon, 19 Nov 2012 21:20:30 +0000 (21:20 +0000)
committerRichard Kettlewell <rjk@terraraq.org.uk>
Mon, 19 Nov 2012 21:20:30 +0000 (21:20 +0000)
15 files changed:
doc/CHANGES.html
doc/rsbackup.1
src/Conf.cc
src/Conf.h
src/DeviceAccess.cc [new file with mode: 0644]
src/DeviceAccess.h [new file with mode: 0644]
src/MakeBackup.cc
src/Makefile.am
src/Store.cc
src/rsbackup.cc
tests/backup
tests/hook
tests/prune
tests/retire-volume
tests/setup.sh

index 92401dd..8564b9a 100644 (file)
     
     <ul>
 
+      <li>The new <code>pre-access-hook</code> and
+      <code>post-access-hook</code> options support running “hook”
+      scripts before and after any access to backup storage
+      devices.</li>
+
       <li>The new <code>pre-backup-hook</code> and
       <code>post-backup-hook</code> options support running “hook”
       scripts before and after a backup.  Although these can be used
index 752150f..fe7cf11 100644 (file)
@@ -250,11 +250,25 @@ Include another file as part of the configuration.
 If \fIPATH\fR is a directory then the files within it are included
 (excluding dotfiles and backup files).
 .TP
+.B pre\-access\-hook \fICOMMAND\fR...
+A command to execute before anything that accesses any backup devices
+(i.e. backup and prune operations).
+This is executed only once per invocation of \fBrsbackup\fR and if it
+fails (i.e. exits nonzero) then \fBrsbackup\fR terminates immediately.
+See \fBHOOKS\fR below.
+.TP
+.B post\-access\-hook \fICOMMAND\fR...
+A command to execute after all backup and prune operations.
+This is executed only once per invocation of \fBrsbackup\fR.
+A backup is still considered to have succeeded even if the post-access
+hook fails (i.e. exits nonzero).
+See \fBHOOKS\fR below.
+.TP
 .B pre\-backup\-hook \fICOMMAND\fR...
 A command to execute before starting a backup.
 If this hook fails (i.e. exits nonzero) then the backup is not made
 and the post-backup hook will not be run.
-See \fBHooks\fR below.
+See \fBHOOKS\fR below.
 .IP
 This hook can override the source path for the backup by writing a new
 source path to standard output.
@@ -263,7 +277,7 @@ source path to standard output.
 A command to execute after finishing a backup, or after it failed.
 A backup is still considered to have succeeded even if the post-backup
 hook fails (exits nonzero).
-See \fBHooks\fR below.
+See \fBHOOKS\fR below.
 .TP
 .B rsync\-timeout \fISECONDS
 How long to wait before concluding rsync has hung.
@@ -372,14 +386,36 @@ stanza, and apply to just that volume:
 .br
 \fBdevices\fR
 .RE
-.SS Hooks
-A hook is a command executed just before or just after a backup is
-made.
+.SH HOOKS
+A hook is a command executed by \fBrsbackup\fR just before or just
+after some action.
 The command is passed directly to \fBexecvp\fR(3); to use a shell
 command, therefore, either wrap it in a script or invoke the shell
 with the \fB-c\fR option.
+.SS "Access Hooks"
+Access hooks are executed (once) before doing anything that will
+access backup devices (even just to read them).
+.PP
+The following environment variables are set when a backup hook is executed:
+.TP
+.B RSBACKUP_DEVICES
+A space-separated list of known device names.
+.TP
+.B RSBACKUP_HOOK
+The name of the hook (i.e. \fBpre-access-hook\fR, etc).
+This allows a single hook script to serve as the implementation for
+multiple hooks.
+.TP
+.B RSBACKUP_ACT
+Set to \fBfalse\fR in \fB\-\-dry\-run\fR mode and \fBtrue\fR
+otherwise.
+.PP
+Access hooks \fIare\fR executed in \fB\-\-dry\-run\fR mode.
+.SS "Backup Hooks"
+Backup hooks are executed just before or just after a backup is
+made.
 .PP
-The following environment variables are set when a hook is executed:
+The following environment variables are set when a backup hook is executed:
 .TP
 .B RSBACKUP_DEVICE
 The target device name for the backup.
@@ -421,10 +457,12 @@ The name of the volume.
 .B RSBACKUP_VOLUME_PATH
 The path to the volume.
 .PP
-The error output from hooks is written to the same logfile as the output
+The error output from backup hooks is written to the same logfile as the output
 from \fBrsync\fR.
 .PP
-Hooks are not executed in \fB\-\-dry\-run\fR mode.
+Backup hooks are currently not executed in \fB\-\-dry\-run\fR mode but
+note that this will be changed in the future and an \fBRSBACKUP_ACT\fR
+variable introduced, as for access hooks.
 .SH "BACKUP LIFECYCLE"
 .SS "Adding A New Host"
 To add a new host create a \fBhost\fR entry for it in the configuration file.
index a4a24e1..2fa9e6d 100644 (file)
@@ -86,6 +86,10 @@ void Conf::readOneFile(const std::string &path) {
         if(bits.size() != 2)
           throw SyntaxError("wrong number of arguments to 'sendmail'");
         sendmail = bits[1];
+      } else if(bits[0] == "pre-access-hook") {
+        preAccess.assign(bits.begin() + 1, bits.end());
+      } else if(bits[0] == "post-access-hook") {
+        postAccess.assign(bits.begin() + 1, bits.end());
       } else if(bits[0] == "ssh-timeout") {
         if(bits.size() != 2)
           throw SyntaxError("wrong number of arguments to 'ssh-timeout'");
index fd5c7e1..a9e07b5 100644 (file)
@@ -162,6 +162,12 @@ public:
   /** @brief Path to @c sendmail */
   std::string sendmail;
 
+  /** @brief Pre-access hook */
+  std::vector<std::string> preAccess;
+
+  /** @brief Post-access hook */
+  std::vector<std::string> postAccess;
+
   /** @brief Read the master configuration file */
   void read();
 
diff --git a/src/DeviceAccess.cc b/src/DeviceAccess.cc
new file mode 100644 (file)
index 0000000..9530dc2
--- /dev/null
@@ -0,0 +1,68 @@
+// Copyright © 2012 Richard Kettlewell.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#include <config.h>
+#include "DeviceAccess.h"
+#include "Conf.h"
+#include "Subprocess.h"
+#include "Errors.h"
+#include "Command.h"
+#include "IO.h"
+
+static bool devicesReady;
+static std::vector<IO *> filesToClose;
+
+static void runDeviceAccessHook(const std::vector<std::string> &cmd,
+                                const std::string &name) {
+  if(cmd.size() == 0)
+    return;
+  Subprocess sp(cmd);
+  sp.setenv("RSBACKUP_HOOK", name);
+  sp.setenv("RSBACKUP_ACT", command.act ? "true" : "false");
+  std::string devices;
+  for(devices_type::const_iterator it = config.devices.begin();
+      it != config.devices.end();
+      ++it) {
+    if(devices.size() != 0)
+      devices += " ";
+    devices += it->first;
+  }
+  sp.setenv("RSBACKUP_DEVICES", devices);
+  if(command.verbose)
+    sp.report();
+  int rc = sp.runAndWait(false);
+  if(rc)
+    throw SubprocessFailed(name, rc);
+}
+
+void preDeviceAccess() {
+  if(!devicesReady) {
+    runDeviceAccessHook(config.preAccess, "pre-access-hook");
+    devicesReady = true;
+  }
+}
+
+void closeOnUnmount(IO *f) {
+  filesToClose.push_back(f);
+}
+
+void postDeviceAccess() {
+  if(devicesReady) {
+    for(size_t n = 0; n < filesToClose.size(); ++n)
+      delete filesToClose[n];
+    filesToClose.clear();
+    runDeviceAccessHook(config.postAccess, "post-access-hook");
+    devicesReady = false;
+  }
+}
diff --git a/src/DeviceAccess.h b/src/DeviceAccess.h
new file mode 100644 (file)
index 0000000..d83f19e
--- /dev/null
@@ -0,0 +1,33 @@
+//-*-C++-*-
+// Copyright © 2012 Richard Kettlewell.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#ifndef DEVICEACCESS_H
+#define DEVICEACCESS_H
+/** @file DeviceAccess.h
+ * @brief Device access functions
+ */
+
+class IO;
+
+/** @brief Run pre-access hook */
+void preDeviceAccess();
+
+/** @brief Close a file before running the post-access hook */
+void closeOnUnmount(IO *f);
+
+/** @brief Run post-access hook */
+void postDeviceAccess();
+
+#endif /* DEVICEACCESS_H */
index 2ac3591..50ad8d2 100644 (file)
@@ -172,6 +172,7 @@ void MakeBackup::hookEnvironment(Subprocess &sp) {
   sp.setenv("RSBACKUP_STORE", device->store->path);
   sp.setenv("RSBACKUP_VOLUME", volume->name);
   sp.setenv("RSBACKUP_VOLUME_PATH", volume->path);
+  sp.setenv("RSBACKUP_ACT", command.act ? "true" : "false");
   sp.setTimeout(volume->hookTimeout);
 }
 
index 126dd85..5e0d4bf 100644 (file)
@@ -1,4 +1,4 @@
-# Copyright © 2011 Richard Kettlewell.
+# Copyright © 2011, 2012 Richard Kettlewell.
 #
 # This program is free software: you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
 bin_PROGRAMS=rsbackup
 noinst_PROGRAMS=datetest subprocesstest unicodetest
 rsbackup_SOURCES=Backup.cc BulkRemove.cc Check.cc Command.cc Conf.cc   \
-Date.cc Device.cc Directory.cc Document.cc Email.cc Errors.cc          \
-FileLock.cc Host.cc HTML.cc IO.cc MakeBackup.cc MakeDirectory.cc       \
-Progress.cc Prune.cc Regexp.cc Report.cc Retire.cc RetireDevices.cc    \
-RetireVolumes.cc rsbackup.cc Store.cc stylesheet.cc Subprocess.cc      \
-Text.cc Unicode.cc Volume.cc Command.h Conf.h Date.h Defaults.h                \
-Document.h Email.h Errors.h FileLock.h IO.h Regexp.h Retire.h          \
-rsbackup.h Store.h Subprocess.h Utils.h
+Date.cc DeviceAccess.cc Device.cc Directory.cc Document.cc Email.cc    \
+Errors.cc FileLock.cc Host.cc HTML.cc IO.cc MakeBackup.cc              \
+MakeDirectory.cc Progress.cc Prune.cc Regexp.cc Report.cc Retire.cc    \
+RetireDevices.cc RetireVolumes.cc rsbackup.cc Store.cc stylesheet.cc   \
+Subprocess.cc Text.cc Unicode.cc Volume.cc Command.h Conf.h Date.h     \
+Defaults.h DeviceAccess.h Document.h Email.h Errors.h FileLock.h IO.h  \
+Regexp.h Retire.h rsbackup.h Store.h Subprocess.h Utils.h
 datetest_SOURCES=Date.cc datetest.cc
 subprocesstest_SOURCES=subprocesstest.cc Subprocess.cc Errors.cc IO.cc
 unicodetest_SOURCES=unicodetest.cc Unicode.cc
index e2f0979..eabf0ca 100644 (file)
@@ -18,6 +18,7 @@
 #include "Errors.h"
 #include "IO.h"
 #include "Utils.h"
+#include "DeviceAccess.h"
 #include <cerrno>
 
 // Identify the device on this store, if any
@@ -30,6 +31,8 @@ void Store::identify() {
       return;                     // already identified
     if(stat(path.c_str(), &sb) < 0)
       throw BadStore("store '" + path + "' does not exist");
+    // Make sure backup devices are mounted
+    preDeviceAccess();
     // Read the device name
     f = new IO();
     f->open(path + PATH_SEP + "device-id", "r");
@@ -59,6 +62,10 @@ void Store::identify() {
     }
     device = foundDevice;
     device->store = this;
+    // On success, leave a file open on the store to stop it being unmounted
+    // while we it's a potential destination for backups; but close it before
+    // unmounting.
+    closeOnUnmount(f);
   } catch(IOError &e) {
     if(f)
       delete f;
@@ -68,6 +75,4 @@ void Store::identify() {
     else
       throw BadStore(e.what());
   }
-  // On succes, leave a file open on the store to stop it being unmounted while
-  // we it's a potential destination for backups.
 }
index b536aff..ae3c7fc 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright © 2011 Richard Kettlewell.
+// Copyright © 2011, 2012 Richard Kettlewell.
 //
 // This program is free software: you can redistribute it and/or modify
 // it under the terms of the GNU General Public License as published by
@@ -23,6 +23,7 @@
 #include "IO.h"
 #include "FileLock.h"
 #include "Subprocess.h"
+#include "DeviceAccess.h"
 #include <cstdio>
 #include <cstdlib>
 #include <cerrno>
@@ -88,6 +89,9 @@ int main(int argc, char **argv) {
     if(command.prune)
       prunePruneLogs();
 
+    // Run post-access hook
+    postDeviceAccess();
+
     // Generate report
     if(command.html || command.text || command.email) {
       config.readState();
index 55a8b78..88f504a 100755 (executable)
@@ -20,16 +20,28 @@ setup
 
 echo "| --dry-run should do nothing"
 RUN=dryrun RSBACKUP_TODAY=1980-01-01 s ${RSBACKUP} --dry-run --backup host1:volume1
-absent dryrun-pre.ran
-absent dryrun-post.ran
+exists dryrun-dev-pre.ran
+exists dryrun-dev-post.ran
+absent dryrun-dev-pre.acted
+absent dryrun-dev-post.acted
+#exists dryrun-pre.ran
+#exists dryrun-post.ran
+absent dryrun-pre.acted
+absent dryrun-post.acted
 absent store1/host1/volume1/1980-01-01
 absent store1/host1/volume2
 absent store2/host1
 
 echo "| Create backup for one volume"
 RUN=volume1 RSBACKUP_TODAY=1980-01-01 s ${RSBACKUP} --backup host1:volume1
-exists volume1-pre.ran
-exists volume1-post.ran
+exists volume1-dev-pre.ran
+exists volume1-dev-post.ran
+exists volume1-dev-pre.ran
+exists volume1-dev-post.ran
+exists volume1-pre.acted
+exists volume1-post.acted
+exists volume1-pre.acted
+exists volume1-post.acted
 compare volume1 store1/host1/volume1/1980-01-01
 absent store1/host1/volume2
 absent store1/host1/volume3
@@ -39,8 +51,8 @@ s ${RSBACKUP} -T -
 
 echo "| Create backup for one host"
 RUN=host1 RSBACKUP_TODAY=1980-01-02 s ${RSBACKUP} --backup host1
-exists host1-pre.ran
-exists host1-post.ran
+exists host1-pre.acted
+exists host1-post.acted
 compare volume1 store1/host1/volume1/1980-01-02
 compare volume2 store1/host1/volume2/1980-01-02
 absent store1/host1/volume3
@@ -49,8 +61,8 @@ s ${RSBACKUP} -T -
 
 echo "| Create backup for everything"
 RUN=all RSBACKUP_TODAY=1980-01-03 s ${RSBACKUP} --backup 
-exists all-pre.ran
-exists all-post.ran
+exists all-pre.acted
+exists all-post.acted
 compare volume1 store1/host1/volume1/1980-01-03
 compare volume2 store1/host1/volume2/1980-01-03
 absent store1/host1/volume3
index f52f5d3..c4a5b89 100755 (executable)
@@ -26,7 +26,7 @@ pre-backup-hook )
     exit 1
   fi
   touch hookdata
-  [ -z "${RUN}" ] || touch ${RUN}-pre.ran
+  what=pre
   ;;
 post-backup-hook )
   if [ ! -e hookdata ]; then
@@ -34,6 +34,33 @@ post-backup-hook )
     exit 1
   fi
   rm -f hookdata
-  [ -z "${RUN}" ] || touch ${RUN}-post.ran
+  what=post
+  ;;
+pre-access-hook )
+  if [ -e devhookdata ]; then
+    echo >&2 "ERROR: devhookdata exists"
+    exit 1
+  fi
+  touch devhookdata
+  what=dev-pre
+  ;;
+post-access-hook )
+  if [ ! -e devhookdata ]; then
+    echo >&2 "ERROR: devhookdata does not exist"
+    exit 1
+  fi
+  rm -f devhookdata
+  what=dev-post
+  ;;
+* )
+  echo >&2 "ERROR: unknown hook $RSBACKUP_HOOK"
+  exit 1
   ;;
 esac
+
+if [ ! -z "${RUN}" ]; then
+  touch ${RUN}-${what}.ran
+  if ${RSBACKUP_ACT}; then
+    touch ${RUN}-${what}.acted
+  fi
+fi
index 75125fc..4d459ec 100755 (executable)
@@ -29,6 +29,10 @@ echo "| Null prune"
 RUN=null RSBACKUP_TODAY=1980-02-01 s ${RSBACKUP} --prune
 absent null-pre.ran
 absent null-pos.ran
+absent null-dev-pre.ran
+absent null-dev-post.ran
+absent null-dev-pre.acted
+absent null-dev-post.acted
 compare volume1 store1/host1/volume1/1980-01-01
 compare volume2 store1/host1/volume2/1980-01-01
 
@@ -43,6 +47,10 @@ echo "| --dry-run should do nothing"
 RUN=dryrun RSBACKUP_TODAY=1980-02-01 s ${RSBACKUP} --prune --dry-run
 absent dryrun-pre.ran
 absent dryrun-post.ran
+exists dryrun-dev-pre.ran
+exists dryrun-dev-post.ran
+absent dryrun-dev-pre.acted
+absent dryrun-dev-post.acted
 compare volume1 store1/host1/volume1/1980-01-01
 compare volume2 store1/host1/volume2/1980-01-01
 compare volume1 store1/host1/volume1/1980-01-02
@@ -52,6 +60,10 @@ echo "| Prune affecting volume1"
 RUN=prune1 RSBACKUP_TODAY=1980-02-01 s ${RSBACKUP} --prune
 absent prune1-pre.ran
 absent prune1-post.ran
+exists prune1-dev-pre.ran
+exists prune1-dev-post.ran
+exists prune1-dev-pre.acted
+exists prune1-dev-post.acted
 absent store1/host1/volume1/1980-01-01
 compare volume2 store1/host1/volume2/1980-01-01
 compare volume1 store1/host1/volume1/1980-01-02
@@ -74,8 +86,12 @@ compare volume1 store1/host1/volume1/1980-01-03
 compare volume2 store1/host1/volume2/1980-01-03
 
 echo "| Prune affecting unselected volume"
-RSBACKUP_TODAY=1980-01-04 s ${RSBACKUP} --prune host1:volume1
+RUN=prune2 RSBACKUP_TODAY=1980-01-04 s ${RSBACKUP} --prune host1:volume1
 absent store1/host1/volume1/1980-01-01
+absent prune2-dev-pre.ran
+absent prune2-dev-post.ran
+absent prune2-dev-pre.acted
+absent prune2-dev-post.acted
 compare volume2 store1/host1/volume2/1980-01-01
 compare volume1 store1/host1/volume1/1980-01-02
 compare volume2 store1/host1/volume2/1980-01-02
@@ -83,9 +99,13 @@ compare volume1 store1/host1/volume1/1980-01-03
 compare volume2 store1/host1/volume2/1980-01-03
 
 echo "| Prune affecting volume2"
-RSBACKUP_TODAY=1980-01-04 s ${RSBACKUP} --prune host1:volume2
+RUN=prune3 RSBACKUP_TODAY=1980-01-04 s ${RSBACKUP} --prune host1:volume2
 absent store1/host1/volume1/1980-01-01
 absent store1/host1/volume2/1980-01-01
+exists prune3-dev-pre.ran
+exists prune3-dev-post.ran
+exists prune3-dev-pre.acted
+exists prune3-dev-post.acted
 compare volume1 store1/host1/volume1/1980-01-02
 compare volume2 store1/host1/volume2/1980-01-02
 compare volume1 store1/host1/volume1/1980-01-03
index 7141ae4..576026f 100755 (executable)
@@ -36,7 +36,11 @@ sed < config > config.new 's/^ *volume volume2.*//;s/^ *min-backups 2//'
 mv config.new config
 
 echo "| --dry-run should do nothing"
-s ${RSBACKUP} --retire --dry-run host1:volume2
+RUN=dryrun s ${RSBACKUP} --retire --dry-run host1:volume2
+exists dryrun-dev-pre.ran
+exists dryrun-dev-post.ran
+absent dryrun-dev-pre.acted
+absent dryrun-dev-post.acted
 exists logs/1980-01-01-device1-host1-volume1.log
 exists logs/1980-01-01-device1-host1-volume2.log
 exists logs/1980-01-01-device2-host1-volume1.log
@@ -47,7 +51,11 @@ exists store2/host1/volume1/1980-01-01
 exists store2/host1/volume1/1980-01-01
 
 echo "| Retire volume2"
-s ${RSBACKUP} --retire host1:volume2
+RUN=retire s ${RSBACKUP} --verbose --retire host1:volume2
+exists retire-dev-pre.ran
+exists retire-dev-post.ran
+exists retire-dev-pre.acted
+exists retire-dev-post.acted
 exists logs/1980-01-01-device1-host1-volume1.log
 absent logs/1980-01-01-device1-host1-volume2.log
 exists logs/1980-01-01-device2-host1-volume1.log
index 6debeac..d064999 100644 (file)
@@ -32,6 +32,9 @@ setup() {
 
   echo "public" >> config
 
+  echo "pre-access-hook ${srcdir:-.}/hook" >> config
+  echo "post-access-hook ${srcdir:-.}/hook" >> config
+
   mkdir logs
   echo "logs $PWD/logs" >> config
   echo "lock lock" >> config
@@ -75,6 +78,7 @@ cleanup() {
   rm -rf volume1 volume2 volume3
   rm -f diffs
   rm -f *.ran
+  rm -f *.acted
 }
 
 compare() {