<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
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.
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.
.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.
.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.
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'");
/** @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();
--- /dev/null
+// 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;
+ }
+}
--- /dev/null
+//-*-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 */
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);
}
-# 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
#include "Errors.h"
#include "IO.h"
#include "Utils.h"
+#include "DeviceAccess.h"
#include <cerrno>
// Identify the device on this store, if any
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");
}
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;
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.
}
-// 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
#include "IO.h"
#include "FileLock.h"
#include "Subprocess.h"
+#include "DeviceAccess.h"
#include <cstdio>
#include <cstdlib>
#include <cerrno>
if(command.prune)
prunePruneLogs();
+ // Run post-access hook
+ postDeviceAccess();
+
// Generate report
if(command.html || command.text || command.email) {
config.readState();
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
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
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
exit 1
fi
touch hookdata
- [ -z "${RUN}" ] || touch ${RUN}-pre.ran
+ what=pre
;;
post-backup-hook )
if [ ! -e hookdata ]; then
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
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
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
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
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
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
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
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
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
rm -rf volume1 volume2 volume3
rm -f diffs
rm -f *.ran
+ rm -f *.acted
}
compare() {