Merge branch 'master' into preserve-recordsize

This commit is contained in:
Christoph Klaffl 2020-11-03 17:50:50 +01:00
commit bdd0dfb733
No known key found for this signature in database
GPG Key ID: FC1C525C2A47CC28
8 changed files with 414 additions and 195 deletions

View File

@ -8,6 +8,7 @@
- [Debian/Ubuntu](#debianubuntu)
- [CentOS](#centos)
- [FreeBSD](#freebsd)
- [Alpine Linux / busybox](#alpine-Linux-busybox-based-distributions)
- [Other OSes](#other-oses)
- [Configuration](#configuration)
- [Sanoid](#sanoid)
@ -21,7 +22,7 @@ Install prerequisite software:
```bash
apt install debhelper libcapture-tiny-perl libconfig-inifiles-perl pv lzop mbuffer
apt install debhelper libcapture-tiny-perl libconfig-inifiles-perl pv lzop mbuffer build-essential
```
@ -31,6 +32,8 @@ Clone this repo, build the debian package and install it (alternatively you can
# Download the repo as root to avoid changing permissions later
sudo git clone https://github.com/jimsalterjrs/sanoid.git
cd sanoid
# checkout latest stable release or stay on master for bleeding edge stuff (but expect bugs!)
git checkout $(git tag | grep "^v" | tail -n 1)
ln -s packages/debian .
dpkg-buildpackage -uc -us
apt install ../sanoid_*_all.deb
@ -52,6 +55,11 @@ Install prerequisite software:
sudo yum install -y epel-release git
# Install the packages that Sanoid depends on:
sudo yum install -y perl-Config-IniFiles perl-Data-Dumper perl-Capture-Tiny lzop mbuffer mhash pv
# if the perl dependencies can't be found in the configured repositories you can install them from CPAN manually:
sudo dnf install perl-CPAN perl-CPAN
cpan # answer the questions and past the following lines
# install Capture::Tiny
# install Config::IniFiles
```
Clone this repo, then put the executables and config files into the appropriate directories:
@ -60,6 +68,8 @@ Clone this repo, then put the executables and config files into the appropriate
# Download the repo as root to avoid changing permissions later
sudo git clone https://github.com/jimsalterjrs/sanoid.git
cd sanoid
# checkout latest stable release or stay on master for bleeding edge stuff (but expect bugs!)
git checkout $(git tag | grep "^v" | tail -n 1)
# Install the executables
sudo cp sanoid syncoid findoid sleepymutex /usr/local/sbin
# Create the config directory
@ -154,6 +164,13 @@ pkg install p5-Config-Inifiles p5-Capture-Tiny pv mbuffer lzop
* See note about mbuffer and other things in FREEBSD.readme
## Alpine Linux / busybox based distributions
The busybox implementation of ps is lacking needed arguments so a proper ps program needs to be installed.
For Alpine Linux this can be done with:
`apk --no-cache add procps`
## Other OSes
**Sanoid** depends on the Perl module Config::IniFiles and will not operate without it. Config::IniFiles may be installed from CPAN, though the project strongly recommends using your distribution's repositories instead.
@ -167,6 +184,17 @@ pkg install p5-Config-Inifiles p5-Capture-Tiny pv mbuffer lzop
3. Create the config directory `/etc/sanoid` and put `sanoid.defaults.conf` in there, and create `sanoid.conf` in it too
4. Create a cron job or a systemd timer that runs `sanoid --cron` once per minute
## cron
If you use cron there is the need to ensure that only one instance of sanoid is run at any time (or else there will be funny error messages about missing snapshots, ...). It's also good practice to separate the snapshot taking and pruning so the later won't block the former in case of long running pruning operations. Following is the recommend setup for a standard install:
```
*/15 * * * * root flock -n /var/run/sanoid/cron-take.lock -c "TZ=UTC sanoid --take-snapshots"
*/15 * * * * root flock -n /var/run/sanoid/cron-prune.lock -c "sanoid --prune-snapshots"
```
Adapt the timer interval to the lowest configured snapshot interval.
# Configuration
**Sanoid** won't do anything useful unless you tell it how to handle your ZFS datasets in `/etc/sanoid/sanoid.conf`.

112
README.md
View File

@ -4,7 +4,7 @@
<p align="center"><a href="https://youtu.be/ZgowLNBsu00" target="_blank"><img src="http://www.openoid.net/sanoid_video_launcher.png" alt="sanoid rollback demo" title="sanoid rollback demo"></a><br clear="all"><sup>(Real time demo: rolling back a full-scale cryptomalware infection in seconds!)</sup></p>
More prosaically, you can use Sanoid to create, automatically thin, and monitor snapshots and pool health from a single eminently human-readable TOML config file at /etc/sanoid/sanoid.conf. (Sanoid also requires a "defaults" file located at /etc/sanoid/sanoid.defaults.conf, which is not user-editable.) A typical Sanoid system would have a single cron job:
More prosaically, you can use Sanoid to create, automatically thin, and monitor snapshots and pool health from a single eminently human-readable TOML config file at /etc/sanoid/sanoid.conf. (Sanoid also requires a "defaults" file located at /etc/sanoid/sanoid.defaults.conf, which is not user-editable.) A typical Sanoid system would have a single cron job but see INSTALL.md fore more details:
```
* * * * * TZ=UTC /usr/local/bin/sanoid --cron
```
@ -39,6 +39,8 @@ And its /etc/sanoid/sanoid.conf might look something like this:
Which would be enough to tell sanoid to take and keep 36 hourly snapshots, 30 dailies, 3 monthlies, and no yearlies for all datasets under data/images (but not data/images itself, since process_children_only is set). Except in the case of data/images/win7, which follows the same template (since it's a child of data/images) but only keeps 4 hourlies for whatever reason.
**Note**: Be aware that if you don't specify some interval options the defaults will be used (from /etc/sanoid/sanoid.defaults.conf)
##### Sanoid Command Line Options
+ --cron
@ -49,6 +51,14 @@ Which would be enough to tell sanoid to take and keep 36 hourly snapshots, 30 da
Specify a location for the config file named sanoid.conf. Defaults to /etc/sanoid
+ --cache-dir
Specify a directory to store the zfs snapshot cache. Defaults to /var/cache/sanoid
+ --run-dir
Specify a directory for temporary files such as lock files. Defaults to /var/run/sanoid
+ --take-snapshots
This will process your sanoid.conf file, create snapshots, but it will NOT purge expired ones. (Note that snapshots taken are atomic in an individual dataset context, <i>not</i> a global context - snapshots of pool/dataset1 and pool/dataset2 will each be internally consistent and atomic, but one may be a few filesystem transactions "newer" than the other.)
@ -101,6 +111,100 @@ Which would be enough to tell sanoid to take and keep 36 hourly snapshots, 30 da
Show help message.
### Sanoid script hooks
There are three script types which can optionally be executed at various stages in the lifecycle of a snapshot:
#### `pre_snapshot_script`
Will be executed before the snapshot(s) of a single dataset are taken. The following environment variables are passed:
| Env vars | Description |
| ----------------- | ----------- |
| `SANOID_SCRIPT` | The type of script being executed, one of `pre`, `post`, or `prune`. Allows for one script to be used for multiple tasks |
| `SANOID_TARGET` | **DEPRECATED** The dataset about to be snapshot (only the first dataset will be provided) |
| `SANOID_TARGETS` | Comma separated list of all datasets to be snapshoted (currently only a single dataset, multiple datasets will be possible later with atomic groups) |
| `SANOID_SNAPNAME` | **DEPRECATED** The name of the snapshot that will be taken (only the first name will be provided, does not include the dataset name) |
| `SANOID_SNAPNAMES` | Comma separated list of all snapshot names that will be taken (does not include the dataset name) |
| `SANOID_TYPES` | Comma separated list of all snapshot types to be taken (yearly, monthly, weekly, daily, hourly, frequently) |
If the script returns a non-zero exit code, the snapshot(s) will not be taken unless `no_inconsistent_snapshot` is false.
#### `post_snapshot_script`
Will be executed when:
- The pre-snapshot script succeeded or
- The pre-snapshot script failed and `force_post_snapshot_script` is true.
| Env vars | Description |
| -------------------- | ----------- |
| `SANOID_SCRIPT` | as above |
| `SANOID_TARGET` | **DEPRECATED** as above |
| `SANOID_TARGETS` | as above |
| `SANOID_SNAPNAME` | **DEPRECATED** as above |
| `SANOID_SNAPNAMES` | as above |
| `SANOID_TYPES` | as above |
| `SANOID_PRE_FAILURE` | This will indicate if the pre-snapshot script failed |
#### `pruning_script`
Will be executed after a snapshot is successfully deleted. The following environment variables will be passed:
| Env vars | Description |
| ----------------- | ----------- |
| `SANOID_SCRIPT` | as above |
| `SANOID_TARGET` | as above |
| `SANOID_SNAPNAME` | as above |
#### example
**sanoid.conf**:
```
...
[sanoid-test-0]
use_template = production
recursive = yes
pre_snapshot_script = /tmp/debug.sh
post_snapshot_script = /tmp/debug.sh
pruning_script = /tmp/debug.sh
...
```
**verbose sanoid output**:
```
...
executing pre_snapshot_script '/tmp/debug.sh' on dataset 'sanoid-test-0'
taking snapshot sanoid-test-0@autosnap_2020-02-12_14:49:33_yearly
taking snapshot sanoid-test-0@autosnap_2020-02-12_14:49:33_monthly
taking snapshot sanoid-test-0@autosnap_2020-02-12_14:49:33_daily
taking snapshot sanoid-test-0@autosnap_2020-02-12_14:49:33_hourly
executing post_snapshot_script '/tmp/debug.sh' on dataset 'sanoid-test-0'
...
```
**pre script env variables**:
```
SANOID_SCRIPT=pre
SANOID_TARGET=sanoid-test-0/b/bb
SANOID_TARGETS=sanoid-test-0/b/bb
SANOID_SNAPNAME=autosnap_2020-02-12_14:49:32_yearly
SANOID_SNAPNAMES=autosnap_2020-02-12_14:49:32_yearly,autosnap_2020-02-12_14:49:32_monthly,autosnap_2020-02-12_14:49:32_daily,autosnap_2020-02-12_14:49:32_hourly
SANOID_TYPES=yearly,monthly,daily,hourly
```
**post script env variables**:
```
SANOID_SCRIPT=post
SANOID_TARGET=sanoid-test-0/b/bb
SANOID_TARGETS=sanoid-test-0/b/bb
SANOID_SNAPNAME=autosnap_2020-02-12_14:49:32_yearly
SANOID_SNAPNAMES=autosnap_2020-02-12_14:49:32_yearly,autosnap_2020-02-12_14:49:32_monthly,autosnap_2020-02-12_14:49:32_daily,autosnap_2020-02-12_14:49:32_hourly
SANOID_TYPES=yearly,monthly,daily,hourly
SANOID_PRE_FAILURE=0
```
----------
# Syncoid
@ -186,7 +290,7 @@ As of 1.4.18, syncoid also automatically supports and enables resume of interrup
This is the bandwidth limit in bytes (kbytes, mbytes, etc) per second imposed upon the source. This is mainly used if the target does not have mbuffer installed, but bandwidth limits are desired.
+ --target-bw-limit <limit t|g|m|k>
+ --target-bwlimit <limit t|g|m|k>
This is the bandwidth limit in bytes (kbytes, mbytesm etc) per second imposed upon the target. This is mainly used if the source does not have mbuffer installed, but bandwidth limits are desired.
@ -202,6 +306,10 @@ As of 1.4.18, syncoid also automatically supports and enables resume of interrup
This argument tells syncoid to restrict itself to existing snapshots, instead of creating a semi-ephemeral syncoid snapshot at execution time. Especially useful in multi-target (A->B, A->C) replication schemes, where you might otherwise accumulate a large number of foreign syncoid snapshots.
+ --keep-sync-snap
This argument tells syncoid to skip pruning old snapshots created and used by syncoid for replication if '--no-sync-snap' isn't specified.
+ --create-bookmark
This argument tells syncoid to create a zfs bookmark for the newest snapshot after it got replicated successfully. The bookmark name will be equal to the snapshot name. Only works in combination with the --no-sync-snap option. This can be very useful for irregular replication where the last matching snapshot on the source was already deleted but the bookmark remains so a replication is still possible.

89
findoid
View File

@ -4,16 +4,26 @@
# from http://www.gnu.org/licenses/gpl-3.0.html on 2014-11-17. A copy should also be available in this
# project's Git repository at https://github.com/jimsalterjrs/sanoid/blob/master/LICENSE.
$::VERSION = '2.0.2';
use strict;
use warnings;
use Getopt::Long qw(:config auto_version auto_help);
use Pod::Usage;
my $zfs = 'zfs';
my %args = getargs(@ARGV);
my %args = ('path' => '');
GetOptions(\%args, "path=s") or pod2usage(2);
my $progversion = '1.4.7';
if ($args{'version'}) { print "$progversion\n"; exit 0; }
if ($args{'path'} eq '') {
if (scalar(@ARGV) < 1) {
warn "file path missing!\n";
pod2usage(2);
exit 127;
} else {
$args{'path'} = $ARGV[0];
}
}
my $dataset = getdataset($args{'path'});
@ -136,62 +146,21 @@ sub getdataset {
return $bestmatch;
}
sub getargs {
my @args = @_;
my %args;
__END__
my %novaluearg;
my %validarg;
push my @validargs, ('debug','version');
foreach my $item (@validargs) { $validarg{$item} = 1; }
push my @novalueargs, ('debug','version');
foreach my $item (@novalueargs) { $novaluearg{$item} = 1; }
=head1 NAME
while (my $rawarg = shift(@args)) {
my $arg = $rawarg;
my $argvalue;
if ($rawarg =~ /=/) {
# user specified the value for a CLI argument with =
# instead of with blank space. separate appropriately.
$argvalue = $arg;
$arg =~ s/=.*$//;
$argvalue =~ s/^.*=//;
}
if ($rawarg =~ /^--/) {
# doubledash arg
$arg =~ s/^--//;
if (! $validarg{$arg}) { die "ERROR: don't understand argument $rawarg.\n"; }
if ($novaluearg{$arg}) {
$args{$arg} = 1;
} else {
# if this CLI arg takes a user-specified value and
# we don't already have it, then the user must have
# specified with a space, so pull in the next value
# from the array as this value rather than as the
# next argument.
if ($argvalue eq '') { $argvalue = shift(@args); }
$args{$arg} = $argvalue;
}
} elsif ($arg =~ /^-/) {
# singledash arg
$arg =~ s/^-//;
if (! $validarg{$arg}) { die "ERROR: don't understand argument $rawarg.\n"; }
if ($novaluearg{$arg}) {
$args{$arg} = 1;
} else {
# if this CLI arg takes a user-specified value and
# we don't already have it, then the user must have
# specified with a space, so pull in the next value
# from the array as this value rather than as the
# next argument.
if ($argvalue eq '') { $argvalue = shift(@args); }
$args{$arg} = $argvalue;
}
} else {
# bare arg
$args{'path'} = $arg;
}
}
findoid - ZFS file version listing tool
return %args;
}
=head1 SYNOPSIS
findoid [options] FILE
FILE local path to file for version listing
Options:
--path=FILE alternative to specify file path to list versions for
--help Prints this helptext
--version Prints the version number

4
packages/debian/postinst Executable file
View File

@ -0,0 +1,4 @@
#!/bin/bash
# remove old cache file
[ -f /var/cache/sanoidsnapshots.txt ] && rm /var/cache/sanoidsnapshots.txt || true

217
sanoid
View File

@ -11,15 +11,20 @@ use strict;
use warnings;
use Config::IniFiles; # read samba-style conf file
use Data::Dumper; # debugging - print contents of hash
use File::Path; # for rmtree command in use_prune
use File::Path 'make_path';
use Getopt::Long qw(:config auto_version auto_help);
use Pod::Usage; # pod2usage
use Time::Local; # to parse dates in reverse
use Capture::Tiny ':all';
my %args = ("configdir" => "/etc/sanoid");
my %args = (
"configdir" => "/etc/sanoid",
"cache-dir" => "/var/cache/sanoid",
"run-dir" => "/var/run/sanoid"
);
GetOptions(\%args, "verbose", "debug", "cron", "readonly", "quiet",
"monitor-health", "force-update", "configdir=s",
"configdir=s", "cache-dir=s", "run-dir=s",
"monitor-health", "force-update",
"monitor-snapshots", "take-snapshots", "prune-snapshots", "force-prune",
"monitor-capacity"
) or pod2usage(2);
@ -30,10 +35,13 @@ if (keys %args < 2) {
$args{'verbose'} = 1;
}
my $pscmd = '/bin/ps';
# for compatibility reasons, older versions used hardcoded command paths
$ENV{'PATH'} = $ENV{'PATH'} . ":/bin:/sbin";
my $zfs = '/sbin/zfs';
my $zpool = '/sbin/zpool';
my $pscmd = 'ps';
my $zfs = 'zfs';
my $zpool = 'zpool';
my $conf_file = "$args{'configdir'}/sanoid.conf";
my $default_conf_file = "$args{'configdir'}/sanoid.defaults.conf";
@ -41,9 +49,15 @@ my $default_conf_file = "$args{'configdir'}/sanoid.defaults.conf";
# parse config file
my %config = init($conf_file,$default_conf_file);
my $cache_dir = $args{'cache-dir'};
my $run_dir = $args{'run-dir'};
make_path($cache_dir);
make_path($run_dir);
# if we call getsnaps(%config,1) it will forcibly update the cache, TTL or no TTL
my $forcecacheupdate = 0;
my $cache = '/var/cache/sanoidsnapshots.txt';
my $cache = "$cache_dir/snapshots.txt";
my $cacheTTL = 900; # 15 minutes
my %snaps = getsnaps( \%config, $cacheTTL, $forcecacheupdate );
my %pruned;
@ -317,11 +331,13 @@ sub prune_snapshots {
if ($config{$dataset}{'pruning_script'}) {
$ENV{'SANOID_TARGET'} = $dataset;
$ENV{'SANOID_SNAPNAME'} = $snapname;
$ENV{'SANOID_SCRIPT'} = 'prune';
if ($args{'verbose'}) { print "executing pruning_script '".$config{$dataset}{'pruning_script'}."' on dataset '$dataset'\n"; }
my $ret = runscript('pruning_script',$dataset);
delete $ENV{'SANOID_TARGET'};
delete $ENV{'SANOID_SNAPNAME'};
delete $ENV{'SANOID_SCRIPT'};
}
} else {
warn "could not remove $snap : $?";
@ -356,7 +372,7 @@ sub take_snapshots {
my %datestamp = get_date();
my $forcecacheupdate = 0;
my @newsnaps;
my %newsnapsgroup;
# get utc timestamp of the current day for DST check
my $daystartUtc = timelocal(0, 0, 0, $datestamp{'mday'}, ($datestamp{'mon'}-1), $datestamp{'year'});
@ -378,9 +394,9 @@ sub take_snapshots {
if ($config{$section}{'process_children_only'}) { next; }
my $path = $config{$section}{'path'};
my @types = ('yearly','monthly','weekly','daily','hourly','frequently');
my @types = ('yearly','monthly','weekly','daily','hourly','frequently');
foreach my $type (@types) {
foreach my $type (@types) {
if ($config{$section}{$type} > 0) {
my $newestage; # in seconds
@ -500,55 +516,58 @@ sub take_snapshots {
my $maxage = time()-$lastpreferred;
if ( $newestage > $maxage ) {
# update to most current possible datestamp
%datestamp = get_date();
# print "we should have had a $type snapshot of $path $maxage seconds ago; most recent is $newestage seconds old.\n";
my $flags = "";
# use zfs (atomic) recursion if specified in config
if ($config{$section}{'zfs_recursion'}) {
$flags .= "r";
}
if ($handleDst) {
$flags .= "d";
if (!exists $newsnapsgroup{$path}) {
$newsnapsgroup{$path} = {
'recursive' => $config{$section}{'zfs_recursion'},
'handleDst' => $handleDst,
'datasets' => [$path], # for later atomic grouping, currently only a one element array
'types' => []
};
}
if ($flags ne "") {
push(@newsnaps, "$path\@autosnap_$datestamp{'sortable'}_$type\@$flags");
} else {
push(@newsnaps, "$path\@autosnap_$datestamp{'sortable'}_$type");
}
push(@{$newsnapsgroup{$path}{'types'}}, $type);
}
}
}
}
if ( (scalar(@newsnaps)) > 0) {
foreach my $snap ( @newsnaps ) {
my $extraMessage = "";
my @split = split '@', $snap, -1;
my $recursiveFlag = 0;
my $dstHandling = 0;
if (scalar(@split) == 3) {
my $flags = $split[2];
if (index($flags, "r") != -1) {
$recursiveFlag = 1;
$extraMessage = " (zfs recursive)";
chop $snap;
}
if (index($flags, "d") != -1) {
$dstHandling = 1;
chop $snap;
}
chop $snap;
if (%newsnapsgroup) {
while ((my $path, my $snapData) = each(%newsnapsgroup)) {
my $recursiveFlag = $snapData->{recursive};
my $dstHandling = $snapData->{handleDst};
my @datasets = @{$snapData->{datasets}};
my $dataset = $datasets[0];
my @types = @{$snapData->{types}};
# same timestamp for all snapshots types (daily, hourly, ...)
my %datestamp = get_date();
my @snapshots;
foreach my $type (@types) {
my $snapname = "autosnap_$datestamp{'sortable'}_$type";
push(@snapshots, $snapname);
}
my $dataset = $split[0];
my $snapname = $split[1];
my $datasetString = join(",", @datasets);
my $typeString = join(",", @types);
my $snapshotString = join(",", @snapshots);
my $extraMessage = "";
if ($recursiveFlag) {
$extraMessage = " (zfs recursive)";
}
my $presnapshotfailure = 0;
my $ret = 0;
if ($config{$dataset}{'pre_snapshot_script'}) {
$ENV{'SANOID_TARGET'} = $dataset;
$ENV{'SANOID_SNAPNAME'} = $snapname;
$ENV{'SANOID_TARGETS'} = $datasetString;
$ENV{'SANOID_SNAPNAME'} = $snapshots[0];
$ENV{'SANOID_SNAPNAMES'} = $snapshotString;
$ENV{'SANOID_TYPES'} = $typeString;
$ENV{'SANOID_SCRIPT'} = 'pre';
if ($args{'verbose'}) { print "executing pre_snapshot_script '".$config{$dataset}{'pre_snapshot_script'}."' on dataset '$dataset'\n"; }
if (!$args{'readonly'}) {
@ -556,7 +575,11 @@ sub take_snapshots {
}
delete $ENV{'SANOID_TARGET'};
delete $ENV{'SANOID_TARGETS'};
delete $ENV{'SANOID_SNAPNAME'};
delete $ENV{'SANOID_SNAPNAMES'};
delete $ENV{'SANOID_TYPES'};
delete $ENV{'SANOID_SCRIPT'};
if ($ret != 0) {
# warning was already thrown by runscript function
@ -564,47 +587,58 @@ sub take_snapshots {
$presnapshotfailure = 1;
}
}
if ($args{'verbose'}) { print "taking snapshot $snap$extraMessage\n"; }
if (!$args{'readonly'}) {
my $stderr;
my $exit;
($stderr, $exit) = tee_stderr {
if ($recursiveFlag) {
system($zfs, "snapshot", "-r", "$snap");
} else {
system($zfs, "snapshot", "$snap");
}
};
$exit == 0 or do {
if ($dstHandling) {
if ($stderr =~ /already exists/) {
$exit = 0;
$snap =~ s/_([a-z]+)$/dst_$1/g;
if ($args{'verbose'}) { print "taking dst snapshot $snap$extraMessage\n"; }
if ($recursiveFlag) {
system($zfs, "snapshot", "-r", "$snap") == 0
or warn "CRITICAL ERROR: $zfs snapshot -r $snap failed, $?";
} else {
system($zfs, "snapshot", "$snap") == 0
or warn "CRITICAL ERROR: $zfs snapshot $snap failed, $?";
foreach my $snap (@snapshots) {
$snap = "$dataset\@$snap";
if ($args{'verbose'}) { print "taking snapshot $snap$extraMessage\n"; }
if (!$args{'readonly'}) {
my $stderr;
my $exit;
($stderr, $exit) = tee_stderr {
if ($recursiveFlag) {
system($zfs, "snapshot", "-r", "$snap");
} else {
system($zfs, "snapshot", "$snap");
}
};
$exit == 0 or do {
if ($dstHandling) {
if ($stderr =~ /already exists/) {
$exit = 0;
$snap =~ s/_([a-z]+)$/dst_$1/g;
if ($args{'verbose'}) { print "taking dst snapshot $snap$extraMessage\n"; }
if ($recursiveFlag) {
system($zfs, "snapshot", "-r", "$snap") == 0
or warn "CRITICAL ERROR: $zfs snapshot -r $snap failed, $?";
} else {
system($zfs, "snapshot", "$snap") == 0
or warn "CRITICAL ERROR: $zfs snapshot $snap failed, $?";
}
}
}
}
};
};
$exit == 0 or do {
if ($recursiveFlag) {
warn "CRITICAL ERROR: $zfs snapshot -r $snap failed, $?";
} else {
warn "CRITICAL ERROR: $zfs snapshot $snap failed, $?";
}
};
$exit == 0 or do {
if ($recursiveFlag) {
warn "CRITICAL ERROR: $zfs snapshot -r $snap failed, $?";
} else {
warn "CRITICAL ERROR: $zfs snapshot $snap failed, $?";
}
};
}
}
if ($config{$dataset}{'post_snapshot_script'}) {
if (!$presnapshotfailure or $config{$dataset}{'force_post_snapshot_script'}) {
$ENV{'SANOID_TARGET'} = $dataset;
$ENV{'SANOID_SNAPNAME'} = $snapname;
$ENV{'SANOID_TARGETS'} = $datasetString;
$ENV{'SANOID_SNAPNAME'} = $snapshots[0];
$ENV{'SANOID_SNAPNAMES'} = $snapshotString;
$ENV{'SANOID_TYPES'} = $typeString;
$ENV{'SANOID_SCRIPT'} = 'post';
$ENV{'SANOID_PRE_FAILURE'} = $presnapshotfailure;
if ($args{'verbose'}) { print "executing post_snapshot_script '".$config{$dataset}{'post_snapshot_script'}."' on dataset '$dataset'\n"; }
if (!$args{'readonly'}) {
@ -612,7 +646,12 @@ sub take_snapshots {
}
delete $ENV{'SANOID_TARGET'};
delete $ENV{'SANOID_TARGETS'};
delete $ENV{'SANOID_SNAPNAME'};
delete $ENV{'SANOID_SNAPNAMES'};
delete $ENV{'SANOID_TYPES'};
delete $ENV{'SANOID_SCRIPT'};
delete $ENV{'SANOID_PRE_FAILURE'};
}
}
}
@ -1365,10 +1404,10 @@ sub get_zpool_capacity {
sub checklock {
# take argument $lockname.
#
# read /var/run/$lockname.lock for a pid on first line and a mutex on second line.
# read $run_dir/$lockname.lock for a pid on first line and a mutex on second line.
#
# check process list to see if the pid from /var/run/$lockname.lock is still active with
# the original mutex found in /var/run/$lockname.lock.
# check process list to see if the pid from $run_dir/$lockname.lock is still active with
# the original mutex found in $run_dir/$lockname.lock.
#
# return:
# 0 if lock is present and valid for another process
@ -1380,7 +1419,7 @@ sub checklock {
#
my $lockname = shift;
my $lockfile = "/var/run/$lockname.lock";
my $lockfile = "$run_dir/$lockname.lock";
if (! -e $lockfile) {
# no lockfile
@ -1402,7 +1441,7 @@ sub checklock {
close FH;
# if we didn't get exactly 2 items from the lock file there is a problem
if (scalar(@lock) != 2) {
warn "WARN: deleting invalid $lockfile\n"
warn "WARN: deleting invalid $lockfile\n";
unlink $lockfile;
return 1
}
@ -1437,11 +1476,11 @@ sub checklock {
sub removelock {
# take argument $lockname.
#
# make sure /var/run/$lockname.lock actually belongs to me (contains my pid and mutex)
# make sure $run_dir/$lockname.lock actually belongs to me (contains my pid and mutex)
# and remove it if it does, die if it doesn't.
my $lockname = shift;
my $lockfile = "/var/run/$lockname.lock";
my $lockfile = "$run_dir/$lockname.lock";
if (checklock($lockname) == 2) {
unlink $lockfile;
@ -1456,11 +1495,11 @@ sub removelock {
sub writelock {
# take argument $lockname.
#
# write a lockfile to /var/run/$lockname.lock with first line
# write a lockfile to $run_dir/$lockname.lock with first line
# being my pid and second line being my mutex.
my $lockname = shift;
my $lockfile = "/var/run/$lockname.lock";
my $lockfile = "$run_dir/$lockname.lock";
# die honorably rather than overwriting a valid, existing lock
if (! checklock($lockname)) {
@ -1663,6 +1702,8 @@ Assumes --cron --verbose if no other arguments (other than configdir) are specif
Options:
--configdir=DIR Specify a directory to find config file sanoid.conf
--cache-dir=DIR Specify a directory to store the zfs snapshot cache
--run-dir=DIR Specify a directory for temporary files such as lock files
--cron Creates snapshots and purges expired snapshots
--verbose Prints out additional information during a sanoid run

View File

@ -3,30 +3,35 @@
# It should go in /etc/sanoid. #
######################################
# name your backup modules with the path to their ZFS dataset - no leading slash.
[zpoolname/datasetname]
# pick one or more templates - they're defined (and editable) below. Comma separated, processed in order.
# in this example, template_demo's daily value overrides template_production's daily value.
use_template = production,demo
## name your backup modules with the path to their ZFS dataset - no leading slash.
#[zpoolname/datasetname]
# # pick one or more templates - they're defined (and editable) below. Comma separated, processed in order.
# # in this example, template_demo's daily value overrides template_production's daily value.
# use_template = production,demo
#
# # if you want to, you can override settings in the template directly inside module definitions like this.
# # in this example, we override the template to only keep 12 hourly and 1 monthly snapshot for this dataset.
# hourly = 12
# monthly = 1
#
## you can also handle datasets recursively.
#[zpoolname/parent]
# use_template = production
# recursive = yes
# # if you want sanoid to manage the child datasets but leave this one alone, set process_children_only.
# process_children_only = yes
#
## you can selectively override settings for child datasets which already fall under a recursive definition.
#[zpoolname/parent/child]
# # child datasets already initialized won't be wiped out, so if you use a new template, it will
# # only override the values already set by the parent template, not replace it completely.
# use_template = demo
# if you want to, you can override settings in the template directly inside module definitions like this.
# in this example, we override the template to only keep 12 hourly and 1 monthly snapshot for this dataset.
hourly = 12
monthly = 1
# you can also handle datasets recursively.
[zpoolname/parent]
# you can also handle datasets recursively in an atomic way without the possibility to override settings for child datasets.
[zpoolname/parent2]
use_template = production
recursive = yes
# if you want sanoid to manage the child datasets but leave this one alone, set process_children_only.
process_children_only = yes
# you can selectively override settings for child datasets which already fall under a recursive definition.
[zpoolname/parent/child]
# child datasets already initialized won't be wiped out, so if you use a new template, it will
# only override the values already set by the parent template, not replace it completely.
use_template = demo
recursive = zfs
@ -91,8 +96,8 @@
daily_crit = 4d
[template_scripts]
### dataset and snapshot name will be supplied as environment variables
### for all pre/post/prune scripts ($SANOID_TARGET, $SANOID_SNAPNAME)
### information about the snapshot will be supplied as environment variables,
### see the README.md file for details about what is passed when.
### run script before snapshot
pre_snapshot_script = /path/to/script.sh
### run script after snapshot

View File

@ -19,6 +19,7 @@ use_template =
process_children_only =
skip_children =
# See "Sanoid script hooks" in README.md for information about scripts.
pre_snapshot_script =
post_snapshot_script =
pruning_script =
@ -108,5 +109,6 @@ yearly_warn = 0
yearly_crit = 0
# default limits for capacity checks (if set to 0, limit will not be checked)
# for overriding these values one needs to specify them in a root pool section! ([tank]\n ...)
capacity_warn = 80
capacity_crit = 95

104
syncoid
View File

@ -25,7 +25,7 @@ GetOptions(\%args, "no-command-checks", "monitor-version", "compress=s", "dumpsn
"source-bwlimit=s", "target-bwlimit=s", "sshkey=s", "sshport=i", "sshcipher|c=s", "sshoption|o=s@",
"debug", "quiet", "no-stream", "no-sync-snap", "no-resume", "exclude=s@", "skip-parent", "identifier=s",
"no-clone-handling", "no-privilege-elevation", "force-delete", "no-clone-rollback", "no-rollback",
"create-bookmark", "preserve-recordsize", "pv-options=s" => \$pvoptions,
"create-bookmark", "pv-options=s" => \$pvoptions, "keep-sync-snap", "preserve-recordsize",
"mbuffer-size=s" => \$mbuffer_size) or pod2usage(2);
my %compressargs = %{compressargset($args{'compress'} || 'default')}; # Can't be done with GetOptions arg, as default still needs to be set
@ -38,6 +38,16 @@ if (length $args{'sendoptions'}) {
pod2usage(2);
exit 127;
}
if (defined $args{'recursive'}) {
foreach my $option(@sendoptions) {
if ($option->{option} eq 'R') {
warn "invalid argument combination, zfs send -R and --recursive aren't compatible!";
pod2usage(2);
exit 127;
}
}
}
}
my @recvoptions = ();
@ -555,7 +565,10 @@ sub syncdataset {
}
$exit == 0 or do {
if ($stdout =~ /\Qused in the initial send no longer exists\E/) {
if (
$stdout =~ /\Qused in the initial send no longer exists\E/ ||
$stdout =~ /incremental source [0-9xa-f]+ no longer exists/
) {
if (!$quiet) { print "WARN: resetting partially receive state because the snapshot source no longer exists\n"; }
resetreceivestate($targethost,$targetfs,$targetisroot);
# do an normal sync cycle
@ -842,9 +855,11 @@ sub syncdataset {
};
}
} else {
# prune obsolete sync snaps on source and target (only if this run created ones).
pruneoldsyncsnaps($sourcehost,$sourcefs,$newsyncsnap,$sourceisroot,keys %{ $snaps{'source'}});
pruneoldsyncsnaps($targethost,$targetfs,$newsyncsnap,$targetisroot,keys %{ $snaps{'target'}});
if (!defined $args{'keep-sync-snap'}) {
# prune obsolete sync snaps on source and target (only if this run created ones).
pruneoldsyncsnaps($sourcehost,$sourcefs,$newsyncsnap,$sourceisroot,keys %{ $snaps{'source'}});
pruneoldsyncsnaps($targethost,$targetfs,$newsyncsnap,$targetisroot,keys %{ $snaps{'target'}});
}
}
} # end syncdataset()
@ -1072,11 +1087,11 @@ sub checkcommands {
my $resumechkcmd = "$zpoolcmd get -o value -H feature\@extensible_dataset";
if ($debug) { print "DEBUG: checking availability of zfs resume feature on source...\n"; }
$avail{'sourceresume'} = system("$sourcessh $resumechkcmd $srcpool 2>/dev/null | grep '\\(active\\|enabled\\)' >/dev/null 2>&1");
$avail{'sourceresume'} = system("$sourcessh $sourcesudocmd $resumechkcmd $srcpool 2>/dev/null | grep '\\(active\\|enabled\\)' >/dev/null 2>&1");
$avail{'sourceresume'} = $avail{'sourceresume'} == 0 ? 1 : 0;
if ($debug) { print "DEBUG: checking availability of zfs resume feature on target...\n"; }
$avail{'targetresume'} = system("$targetssh $resumechkcmd $dstpool 2>/dev/null | grep '\\(active\\|enabled\\)' >/dev/null 2>&1");
$avail{'targetresume'} = system("$targetssh $targetsudocmd $resumechkcmd $dstpool 2>/dev/null | grep '\\(active\\|enabled\\)' >/dev/null 2>&1");
$avail{'targetresume'} = $avail{'targetresume'} == 0 ? 1 : 0;
if ($avail{'sourceresume'} == 0 || $avail{'targetresume'} == 0) {
@ -1112,7 +1127,7 @@ sub iszfsbusy {
foreach my $process (@processes) {
# if ($debug) { print "DEBUG: checking process $process...\n"; }
if ($process =~ /zfs *(receive|recv).*\Q$fs\E/) {
if ($process =~ /zfs *(receive|recv).*\Q$fs\E\Z/) {
# there's already a zfs receive process for our target filesystem - return true
if ($debug) { print "DEBUG: process $process matches target $fs!\n"; }
return 1;
@ -1425,20 +1440,53 @@ sub targetexists {
sub getssh {
my $fs = shift;
my $rhost;
my $rhost = "";
my $isroot;
my $socket;
my $remoteuser = "";
# if we got passed something with an @ in it, we assume it's an ssh connection, eg root@myotherbox
if ($fs =~ /\@/) {
$rhost = $fs;
$fs =~ s/^\S*\@\S*://;
$fs =~ s/^[^\@:]*\@[^\@:]*://;
$rhost =~ s/:\Q$fs\E$//;
my $remoteuser = $rhost;
$remoteuser =~ s/\@.*$//;
$remoteuser = $rhost;
$remoteuser =~ s/\@.*$//;
# do not require a username to be specified
$rhost =~ s/^@//;
} elsif ($fs =~ m{^[^/]*:}) {
# if we got passed something with an : in it, BEFORE any forward slash
# (i.e., not in a dataset name) it MAY be an ssh connection
# but we need to check if there is a pool with that name
my $pool = $fs;
$pool =~ s%/.*$%%;
my ($pools, $error, $exit) = capture {
system("$zfscmd list -d0 -H -oname");
};
$rhost = $fs;
if ($exit != 0) {
warn "Unable to enumerate pools (is zfs available?)";
} else {
foreach (split(/\n/,$pools)) {
if ($_ eq $pool) {
# there's a pool with this name.
$rhost = "";
last;
}
}
}
if ($rhost ne "") {
# there's no pool that might conflict with this
$rhost =~ s/:.*$//;
$fs =~ s/\Q$rhost\E://;
}
}
if ($rhost ne "") {
if ($remoteuser eq 'root' || $args{'no-privilege-elevation'}) { $isroot = 1; } else { $isroot = 0; }
# now we need to establish a persistent master SSH connection
$socket = "/tmp/syncoid-$remoteuser-$rhost-" . time();
$socket = "/tmp/syncoid-$rhost-" . time();
open FH, "$sshcmd -M -S $socket -o ControlPersist=1m $args{'sshport'} $rhost exit |";
close FH;
@ -1475,7 +1523,7 @@ sub getsnaps() {
$fsescaped = escapeshellparam($fsescaped);
}
my $getsnapcmd = "$rhost $mysudocmd $zfscmd get A-Hpd 1 -t snapshot guid,creation $fsescaped";
my $getsnapcmd = "$rhost $mysudocmd $zfscmd get -Hpd 1 -t snapshot guid,creation $fsescaped";
if ($debug) {
$getsnapcmd = "$getsnapcmd |";
print "DEBUG: getting list of snapshots on $fs using $getsnapcmd...\n";
@ -1753,7 +1801,19 @@ sub getsendsize {
}
sub getdate {
my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time);
my @time = localtime(time);
# get timezone info
my $offset = timegm(@time) - timelocal(@time);
my $sign = ''; # + is not allowed in a snapshot name
if ($offset < 0) {
$sign = '-';
$offset = abs($offset);
}
my $hours = int($offset / 3600);
my $minutes = int($offset / 60) - $hours * 60;
my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = @time;
$year += 1900;
my %date;
$date{'unix'} = (((((((($year - 1971) * 365) + $yday) * 24) + $hour) * 60) + $min) * 60) + $sec;
@ -1763,7 +1823,8 @@ sub getdate {
$date{'hour'} = sprintf ("%02u", $hour);
$date{'mday'} = sprintf ("%02u", $mday);
$date{'mon'} = sprintf ("%02u", ($mon + 1));
$date{'stamp'} = "$date{'year'}-$date{'mon'}-$date{'mday'}:$date{'hour'}:$date{'min'}:$date{'sec'}";
$date{'tzoffset'} = sprintf ("GMT%s%02d:%02u", $sign, $hours, $minutes);
$date{'stamp'} = "$date{'year'}-$date{'mon'}-$date{'mday'}:$date{'hour'}:$date{'min'}:$date{'sec'}-$date{'tzoffset'}";
return %date;
}
@ -1887,9 +1948,9 @@ syncoid - ZFS snapshot replication tool
=head1 SYNOPSIS
syncoid [options]... SOURCE TARGET
or syncoid [options]... SOURCE USER@HOST:TARGET
or syncoid [options]... USER@HOST:SOURCE TARGET
or syncoid [options]... USER@HOST:SOURCE USER@HOST:TARGET
or syncoid [options]... SOURCE [[USER]@]HOST:TARGET
or syncoid [options]... [[USER]@]HOST:SOURCE TARGET
or syncoid [options]... [[USER]@]HOST:SOURCE [[USER]@]HOST:TARGET
SOURCE Source ZFS dataset. Can be either local or remote
TARGET Target ZFS dataset. Can be either local or remote
@ -1906,13 +1967,14 @@ Options:
--pv-options=OPTIONS Configure how pv displays the progress bar, default '-p -t -e -r -b'
--no-stream Replicates using newest snapshot instead of intermediates
--no-sync-snap Does not create new snapshot, only transfers existing
--keep-sync-snap Don't destroy created sync snapshots
--create-bookmark Creates a zfs bookmark for the newest snapshot on the source after replication succeeds (only works with --no-sync-snap)
--preserve-recordsize Preserves the recordsize on initial sends to the target
--no-clone-rollback Does not rollback clones on target
--no-rollback Does not rollback clones or snapshots on target (it probably requires a readonly target)
--exclude=REGEX Exclude specific datasets which match the given regular expression. Can be specified multiple times
--sendoptions=OPTIONS Use advanced options for zfs send (the arguments are filterd as needed), e.g. syncoid --sendoptions="Lc e" sets zfs send -L -c -e ...
--recvoptions=OPTIONS Use advanced options for zfs receive (the arguments are filterd as needed), e.g. syncoid --recvoptions="ux recordsize o compression=lz4" sets zfs receive -u -x recordsize -o compression=lz4 ...
--sendoptions=OPTIONS Use advanced options for zfs send (the arguments are filtered as needed), e.g. syncoid --sendoptions="Lc e" sets zfs send -L -c -e ...
--recvoptions=OPTIONS Use advanced options for zfs receive (the arguments are filtered as needed), e.g. syncoid --recvoptions="ux recordsize o compression=lz4" sets zfs receive -u -x recordsize -o compression=lz4 ...
--sshkey=FILE Specifies a ssh key to use to connect
--sshport=PORT Connects to remote on a particular port
--sshcipher|c=CIPHER Passes CIPHER to ssh to use a particular cipher set