Currently, when upgrading the RPM, if the upstream `zrepl.yml` is
changed in the update, the user's `zrepl.yml` will be renamed to
`zrepl.yml.rpmsave` and the new `zrepl.yml` is installed in its place.
This commit changes the behavior so that the user's config remains in
place and the new upstream config is installed to `zrepl.yml.rpmnew`
instead.
---
This was never a problem before because
`packaging/systemd-default-zrepl.yml` was the same since the initial
release. RPM leaves edited config files alone with the upstream config
file doesn't change. However, v0.7.0 changed the file with:
9c63736489. So with this update, RPM
detects an upstream config change during the update and moves the user's
config according to `%config`'s rules
(https://www.cl.cam.ac.uk/~jw35/docs/rpm_config.html).
`%config(noreplace)` prevents this from happening.
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
Extends the draft-release command to automatically upload release
artifacts after creating the draft release. Adds sanity checks to
verify that artifacts have been downloaded and signed before proceeding.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Creates new skill to automatically update docs/supporters.rst
- Extracts contributors from git history and changelog.rst
- Supports time ranges (--since) and commit ranges (v0.6.0..v0.7.0)
- Handles new contributors and recurring contributors (moves them up)
- Records processed range in RST comment
- Integrated into release workflow in README.md
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Before this PR we platformtests in CI only against the ZFS version that
shipped with the CircleCI machine image (ZFS 2.2).
Changes in this PR:
- add an `command` for building OpenZFS `.deb`'s from a Git checkout,
and for caching those `.deb`s in CircleCI
- use that command in the platform test build matrix. ZFS versions:
**2.2.9**, **2.3.5**, and **2.4.0**
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The error message when destroying a held snapshot no longer is `dataset
is busy` but `it's being held. Run 'zfs holds -r <snapshot>' to see
holders.`
The existing `destroySnapshotsErrorRegexp` parses both formats
correctly, but downstream test assertions in platformtest were checking
for the literal `dataset is busy` string.
refs
- the PR that adds ZFS 2.3 & 2.4 to CI:
https://github.com/zrepl/zrepl/pull/921
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
- Add `image` matrix parameter to the `platformtest` job so it runs on
both `ubuntu-2204:current` and `ubuntu-2404:current`
- Covers both current Ubuntu LTS releases for platform testing
PR
- https://github.com/zrepl/zrepl/pull/913
didn't update the `release-docker` flow to use `uv`, thereby breaking
it.
## Changes
- Create `.uv-version` file to centralize UV version between Dockerfile
and CircleCI
- Update build container to use `uv`
While at it, make life with the build container better:
- Create `zrepl_build` user in the build container as same UID/GID as
the caller of `make release-docker`
- Allow Ctrl-C to cancel `make release-docker` in interactive shells
Consolidate `quickcheck-docs` and `publish-zrepl-github-io` jobs into a
single parameterized `run-docs-publish-sh` job with a `push` boolean.
Don't run `make docs` anymore, it's run by `publish.sh` internally
anyway.
The cimg includes `make`, and we don't need Go to build the docs.
Drive-by update to `cimg/base:current` - we pinned all important build
inputs for docs using `uv`, and the inputs we use from the `cimg`
(git, make) are stable.
In commit 4f950bb60 we switched the publish workflow to use HTTPS and
changed CircleCI config accordingly. However, that wasn't working. The
error we get after merge to `master`, when we run the job, is: `The key
you are authenticating with has been marked as read only.`
The reason is that CircleCI configures SSH URL rewriting, which takes
precedence.
Fix by using git's `url.<base>.insteadOf` config to rewrite both HTTPS
and SSH URLs to use HTTPS with the GitHub token. This ensures the token
is used regardless of any SSH configuration.
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This PR simplifies how we build and publish docs:
- **Publish from `master` branch, retire `stable` branch.** The `stable`
branch was a manual step in the release process and often out of date.
Docs are now built and published directly from `master`.
Release-specific docs are available in the `zrepl-noarch.tar` asset on
each GitHub release.
- **build dependencies**: use `uv` for dependency management
- **zrepl.github.io: retire multi-version docs**: before this PR we used
`sphinx-multiversion` to publish multiple docs versions to
`zrepl.github.io`. This was never worth the pain, so, this PR removes it
in order to simplify stuff. Old docs are available in the GitHub
releases, and the docs now have a version dropdown that links there for
a hand-curated set of versions.
- **GitHub pages repo checkout**: use HTTPS because that's what I use
these days for all things GitHub. Switch CircleCI to a fine-grained PAT.
Refs
- docs bug https://github.com/zrepl/zrepl/issues/895
- links to config examples should work again after this PR
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
A number of tools including `lnav` do a crude string matching against
the word "error". For zrepl this results in benign lines being colored
red. It would be better if the logged message was something like
"command ran successfully".
refs
- #872
---------
Co-authored-by: Christian Schwarz <me@cschwarz.com>
Before this PR, config parsing would accept duplicate job names. `zrepl
daemon` would later fail to start with a panic. But tools like `zrepl
configcheck` would pass.
This PR adds a check to ensure job names are unique.
Similarly, internal job names were not being rejected by config parsing
Move that check to parse-time as well.
Last, drive-by change: remove `internal/config/config_include_test.go`
introduced in #856 . These aren't necessary because there is already a
wildcard test for all valid configs.
This PR adds the complimentary "invalid config" wildcard test.
This PR allows for the distribution of zrepl job definitions across
multiple YAML files that are included from the main config.
```
global: ...
include:
- ./zrepl.yml.d
- /opt/zrepl.yml
- ...
```
Refer to the docs changes for details.
Co-authored-by: Christian Schwarz <me@cschwarz.com>
Co-authored-by: Orsiris de Jong <ozy@netpower.fr>
This PR obsoletes https://github.com/zrepl/zrepl/pull/847.
Changes:
* Rearrange dashboard into sections sections: About, Top, Replication,
Internals.
* consistently force selection of a single prom_job_name (no more
multi-value)
* Replace the repeated Gauge panel for failed replications with a
'status history' panel.
* Use `zrepl_start_time` as the metric underlying the `prom_job_name`
variable.
* Replace `zrepl_job_name` with internal dashboard variable
`zrepl_replication_jobid` and use that _only_ in the `Replication`
section of the dashboard
* Update to Grafana v12.4
Added installation instructions for openSUSE as discussed in #776. Sorry
for the delay.
I decided against a solution involving lsb_release, since openSUSE
MicroOS, being a minimal OS, ships without it. Hope the edit is OK.
---------
Co-authored-by: Christian Schwarz <me@cschwarz.com>
**Problem 1**: Users following the EasyRSA script in the docs with their
more recent distro version of EasyRSA find that the generated certs no
longer include a subject-alternative-name (SAN), which is required as of
Go 1.15 / zrepl 0.3.0.
**Problem 2**: Newer openssl versions with the old EasyRSA prompt the
user for a CA password in `init-pki` and depending on EasyRSA version
there are other interactive prompts at later commands in the script.
**Changes**:
- upgrade the pinned EasyRSA version to the latest release
- use `--auto-san` option to make EasyRSA set the SAN. This has the
benefit of also working for IP addresses.
I checked the easy-rsa git history and `--subject-alt-name` appears to
have been present in all 3.X releases, so, adding the flag shouldn't
break users of older EasyRSA 3.X.
(And these are exemplary instructions anyway.)
refs
- fixes https://github.com/zrepl/zrepl/issues/900
- obsoletes https://github.com/zrepl/zrepl/pull/906
- obsoletes https://github.com/zrepl/zrepl/pull/877
Co-authored-by: Andrew Lorimer <andrew@lorimer.id.au>
The `make rpm-build` started failing with
```
find-debuginfo: starting
Extracting debug info from 1 files
*** ERROR: No build ID note found in /build/src/artifacts/rpmbuild/BUILD/zrepl-v0.6.1.47.gbae2bd2-build/BUILDROOT/usr/bin/zrepl
error: Bad exit status from /var/tmp/rpm-tmp.bYHLzG (%install)
```
This must be a change in Fedora (prob new Fedora major release).
Seize the opportunity to
1. stop building from `:latest`, and
2. build with an LTS RPM distro to improve zrepl RPM compatibility (no
problems so far AFAIK but this seems sensible)
While investigating https://github.com/zrepl/zrepl/issues/700
I checked in on `zrepl status` dependencies and found that
`cview`, which was/is a fork of tview, appears to be unmaintained.
We switched to it 4.5 years ago in a58ce74.
Checking now, `github.com/rivo/tview` seems to be somewhat maintained
again.
I also checked what k9s uses because that tool came to mind as a Go
terminal UI app.
It does use `tview`, but, a fork that has diverged substantially.
Maybe in another 4.5 years stuff the ecosystem has consolidated...
refs https://github.com/zrepl/zrepl/issues/700
It pains me to do it, but, especially with hooks, the Protect
settings are too restrictive.
I wish there were a systemd API that allowed us to self-sandbox,
using these settings, _after_ parsing the config.
fixes https://github.com/zrepl/zrepl/issues/735
This PR adds a new field optional field `timestamp_location` that allows
the user to specify a timezone different than the default UTC for use in
the snapshot suffix.
I took @mjasnik 's PR https://github.com/zrepl/zrepl/pull/785 and
refactored+extended it as follows:
* move all formatting logic into its own package
* disallow `dense` and `human` with formats != UTC to protect users from
stupidity
* document behavior more clearly
* regression test for existing users
Go upgrade:
- Go 1.23 is current => use that for release builds
- Go 1.22 is less than one year old, it's desirable to support it.
- The [`Go Toolchains`](https://go.dev/doc/toolchain) stuff is available
in both of these (would also be in Go 1.21). That is quite nice stuff,
but required some changes to how we versions we use in CircleCI and
the `release-docker` Makefile target.
Protobuf upgrade:
- Go to protobuf GH release website
- Download latest locally
- run `sha256sum`
- replace existing pinned hashes
- `make generate`
Deps upgrade:
- `go get -t -u all`
- repository moves aren't handled well automatically, fix manually
- repeat until no changes
fixes https://github.com/zrepl/zrepl/issues/742
Before this PR, when chaining replication from
A => B => C, if B had placeholders and the `filesystems`
included these placeholders, we'd incorrectly
fail the planning phase with error
`sender does not have any versions`.
The non-placeholder child filesystems of these placeholders
would then fail to replicate because of the
initial-replication-dependency-tracking that we do, i.e.,
their parent failed to initially replication, hence
they fail to replicate as well
(`parent(s) failed during initial replication`).
We can do better than that because we have the information
whether a sender-side filesystem is a placeholder.
This PR makes the planner act on that information.
The outcome is that placeholders are replicated as
placeholders (albeit the receiver remains in control
of how these placeholders are created, i.e., `recv.placeholders`)
The mechanism to do it is:
1. Don't plan any replication steps for filesystems that
are placeholders on the sender.
2. Ensure that, if a receiving-side filesystem exists, it
is indeed a placeholder.
Check (2) may seem overly restrictive, but, the goal here
is not just to mirror all non-placeholder filesystems, but
also to mirror the hierarchy.
Testing performed:
- [x] confirm with issue reporter that this PR fixes their issue
- [x] add a regression test that fails without the changes in this PR
From https://github.com/zrepl/zrepl/issues/691
The last_n prune rule keeps everything, regardless of if it matches the
regex or not, if there are less than count snapshot. The expectation
would be to never keep non-regex snapshots, regardless of number.
Because some jobs add client identity to root_fs and other jobs don't do
that,
we can't reliable detect overlapping of filesystems. And and the same
time we
need an ability to use equal or overlapped root_fs for different jobs.
For
instance see this config:
```
- name: "zdisk"
type: "sink"
root_fs: "zdisk/zrepl"
serve:
type: "local"
listener_name: "zdisk"
```
and
```
- name: "remote-to-zdisk"
type: "pull"
connect:
type: "tls"
root_fs: "zdisk/zrepl/remote"
```
As you can see, two jobs have overlapped root_fs, but actually datasets
are not
overlapped, because job `zdisk` save everything under
`zdisk/zrepl/localhost`,
because it adds client identity. So they actually use two different
filesystems:
`zdisk/zrepl/localhost` and `zdisk/zrepl/remote`. And we can't detect
this
situation during config check. So let's just remove this check, because
it's
admin's duty to configure correct root_fs's.
---------
Co-authored-by: Christian Schwarz <me@cschwarz.com>
Command used:
```
cd build
go get -u github.com/golangci/golangci-lint/cmd/golangci-lint
go mod tidy
```
Further, golangci-lint requires go 1.20 to build, so, use that as the lowest version in the CI.
The sphinxcontrib-versioning seems unmaintainted and I can't
get the fork that we used before this PR working on Python 3.10.
The situation wrt maintenance doesn't seem much better for
sphinx-multiversion, but, at least I could get it to work
with current sphinx versions.
The main problem with sphinx-multiversion is that it doesn't render
anything at `/`. I.e., `https://zrepl.github.io/configuration.html` will
be 404.
That's different from `sphinxcontrib-versioning`, and thus switching
to sphinx-multiversion would break URLs.
We host on GitHub pages and don't control the webserver,
so, we can't use webserver-level redirects to keep the URLs working.
We could create JS-level redirects, or `http-equiv`, but that's ugly as
well.
The simplest solution was to fork sphinx-multiversion and hard-code
zrepl's specific needs into that fork.
The fork is based off v0.2.4 and pinned via requirements.txt.
Here are its unique commits:
https://github.com/Holzhaus/sphinx-multiversion/compare/master...zrepl:sphinx-multiversion:zrepl
We should revisit `sphinx-polyversion` in the future once its docs
improve.
See
https://github.com/Holzhaus/sphinx-multiversion/issues/88#issuecomment-1606221194
This PR updates the various Python packages, as I couldn't get
sphinx-multiversion to work with the (very old) versions that were
pinned in `requirements.txt` prior to this PR.
This PR's `requirements.txt` is from a clean Python 3.10 venv on Ubuntu
22.10 after running
```
pip install sphinx sphinx-rtd-theme
pip install 'git+https://github.com/zrepl/sphinx-multiversion/@52c915d7ad898d9641ec48c8bbccb7d4f079db93#egg=sphinx_multiversion'
```
Before this PR, we would panic in the `check` phase of `endpoint.Send()`'s `TryBatchDestroy` call in the following cases: the current protection strategy does NOT produce a tentative replication cursor AND
* `FromVersion` is a tentative cursor bookmark
* `FromVersion` is a snapshot, and there exists a tentative cursor bookmark for that snapshot
* `FromVersion` is a bookmark != tentative cursor bookmark, but there exists a tentative cursor bookmark for the same snapshot as the `FromVersion` bookmark
In those cases, the `check` concluded that we would delete `FromVersion`.
It came to that conclusion because the tentative cursor isn't part of `obsoleteAbs` if the protection strategy doesn't produce a tentative replication cursor.
The scenarios above can happen if the user changes the protection strategy from "with tentative cursor" to one "without tentative replication cursor", while there is a tentative replication cursor on disk.
The workaround was to rename the tentative cursor.
In all cases above, `TryBatchDestroy` would have destroyed the tentative cursor.
In case 1, that would fail the `Send` step and potentially break replication if the cursor is the last common bookmark. The `check` conclusion was correct.
In cases 2 and 3, deleting the tentative cursor would have been fine because `FromVersion` was a different entity than the tentative cursor. So, destroying the tentative cursor would be the right call.
The solution in this PR is as follows:
* add the `FromVersion` to the `liveAbs` set of live abstractions
* rewrite the `check` closure to use the full dataset path (`fullpath`) to identify the concrete ZFS object instead of the `zfs.FilesystemVersionEqualIdentity`, which is only identified by matching GUID.
* Holds have no dataset path and are not the `FromVersion` in any case, so disregard them.
fixes#666
This PR adds a Prometheus counter called
`zrepl_zfs_list_unmatched_user_specified_dataset_count`.
Monitor for increases of the counter to detect filesystem filter rules that
have no effect because they don't match any local filesystem.
An example use case for this is the following story:
1. Someone sets up zrepl with `filesystems` filter for `zroot/pg14<`.
2. During the upgrade to Postgres 15, they rename the dataset to `zroot/pg15`,
but forget to update the zrepl `filesystems` filter.
3. zrepl will not snapshot / replicate the `zroot/pg15<` datasets.
Since `filesystems` rules are always evaluated on the side that has the datasets,
we can smuggle this functionality into the `zfs` module's `ZFSList` function that
is used by all jobs with a `filesystems` filter.
Dashboard changes:
- histogram with increase in $__interval, one row per job
- table with increase in $__range
- explainer text box, so, people know what the previous two are about
We had to re-arrange some panels, hence the Git diff isn't great.
closes https://github.com/zrepl/zrepl/pull/653
Co-authored-by: Christian Schwarz <me@cschwarz.com>
Co-authored-by: Goran Mekić <meka@tilda.center>
CircleCI artifacts are available publicly.
And regarding expiration of artifacts, it doesn't really
matter because I delete minio artifacts after 30d as well.
For this kind of debugging, we switched to env vars a while ago.
For example, ZREPL_RPC_DEBUG.
I don't think we have a substitute for the RPCLog stuff.
However, NetConnLogger is still in the codebase.
obsoletes https://github.com/zrepl/zrepl/pull/661
Originally, I had a patch that would replace all usages of
time.Duration in package config with the new config.Duration
types, but:
1. these are all timeouts/retry intervals that have default values.
Most users don't touch them, and if they do, they don't need
day or week units.
2. go-yaml's error reporting for yaml.Unmarshaler is inferior to
built-in types (line numbers are missing, so the error would not have
sufficient context)
fixes https://github.com/zrepl/zrepl/issues/486
The previous commits were done in response to updating to
the version that we now pin in this commit.
We do the update after the fixes so that each commit builds.
Before upgrading to Go 1.19, these platform tests would sproadically
fail due to the reason outlined in the comment
github.com/zrepl/zrepl/platformtest/tests.SendStreamMultipleCloseAfterEOF
github.com/zrepl/zrepl/platformtest/tests.SendStreamCloseAfterEOFRead
They are useful, not least to debug issues with debugging
SIGSYS caused by overly restrictive settings in the unit file.
(See previous commit for an example.)
Go 1.19 uses it during startup.
From the Go changelog:
> On Unix operating systems, Go programs that import package os now
> automatically increase the open file limit (RLIMIT_NOFILE) to the
> maximum allowed value; that is, they change the soft limit to match the
> hard limit. This corrects artificially low limits set on some systems
> for compatibility with very old C programs using the select system call.
> Go programs are not helped by that limit, and instead even simple
> programs like gofmt often ran out of file descriptors on such systems
> when processing many files in parallel. One impact of this change is
> that Go programs that in turn execute very old C programs in child
> processes may run those programs with too high a limit. This can be
> corrected by setting the hard limit before invoking the Go program.
A newer version of staticheck found these:
> SA4029: sort.StringSlice is a type, not a function, and
> sort.StringSlice(variants) doesn't sort your values; consider using
> sort.Strings instead (staticcheck)
Go 1.18 deprecated net.Error.Temporary().
This commit cleans up places where we use it incorrectly.
Also, the rpc layer defines some errors that implement
interface { Temporary() bool }
I added comments to all of the implementations to indicate
whether they will be required if net.Error.Temporary is ever
ever removed in the future.
For HandshakeError, the Temporary() return value is actually
important. I moved & rewrote a (previously misplaced) comment
there.
The ReadStreamError changes were
1. necessary to pacify newer staticcheck and
2. technically, an error can implement Temporary()
without being net.Err. This applies to some syscall
errors in the standard library.
Reading list for those interested:
- https://github.com/golang/go/issues/45729
- https://groups.google.com/g/golang-nuts/c/-JcZzOkyqYI
- https://man7.org/linux/man-pages/man2/accept.2.html
Note: This change was prompted by staticheck:
> SA1019: neterr.Temporary has been deprecated since Go 1.18 because it
> shouldn't be used: Temporary errors are not well-defined. Most
> "temporary" errors are timeouts, and the few exceptions are surprising.
> Do not use this method. (staticcheck)
The previous commit c743c7b03f
broke the hooks unit tests.
GitHub was not configured to require passing tests for master merge.
Didn't notice it locally due to Go's test caching.
I amended this before pushing this change.
Before this change, resuming from an unencrypted dataset with
send.raw=true specified wouldn't work with zrepl due to overly
restrictive resume token checking.
An initial PR to fix this was made in https://github.com/zrepl/zrepl/pull/503
but it didn't address the core of the problem.
The core of the problem was that zrepl assumed that if a resume token
contained `rawok=true, compressok=true`, the resulting send would be
encrypted. But if the sender dataset was unencrypted, such a resume would
actually result in an unencrypted send.
Which could be totally legitimate but zrepl failed to recognize that.
BACKGROUND
==========
The following snippets of OpenZFS code are insightful regarding how the
various ${X}ok values in the resume token are handled:
- https://github.com/openzfs/zfs/blob/6c3c5fcfbe27d9193cd131753cc7e47ee2784621/module/zfs/dmu_send.c#L1947-L2012
- https://github.com/openzfs/zfs/blob/6c3c5fcfbe27d9193cd131753cc7e47ee2784621/module/zfs/dmu_recv.c#L877-L891
- https://github.com/openzfs/zfs/blob/6c3c5fc/lib/libzfs/libzfs_sendrecv.c#L1663-L1672
Basically, some zfs send flags make the DMU send code set some DMU send
stream featureflags, although it's not a pure mapping, i.e, which DMU
send stream flags are used depends somewhat on the dataset (e.g., is it
encrypted or not, or, does it use zstd or not).
Then, the receiver looks at some (but not all) feature flags and maps
them to ${X}ok dataset zap attributes.
These are funnelled back to the sender 1:1 through the resume_token.
And the sender turns them into lzc flags.
As an example, let's look at zfs send --raw.
if the sender requests a raw send on an unencrypted dataset, the send
stream (and hence the resume token) will not have the raw stream
featureflag set, and hence the resume token will not have the rawok
field set. Instead, it will have compressok, embedok, and depending
on whether large blocks are present in the dataset, largeblockok set.
WHAT'S ZREPL'S ROLE IN THIS?
============================
zrepl provides a virtual encrypted sendflag that is like `raw`,
but further ensures that we only send encrypted datasets.
For any other resume token stuff, it shoudn't do any checking,
because it's a futile effort to keep up with ZFS send/recv features
that are orthogonal to encryption.
CHANGES MADE IN THIS COMMIT
===========================
- Rip out a bunch of needless checking that zrepl would do during
planning. These checks were there to give better error messages,
but actually, the error messages created by the endpoint.Sender.Send
RPC upon send args validation failure are good enough.
- Add platformtests to validate all combinations of
(Unencrypted/Encrypted FS) x (send.encrypted = true | false) x (send.raw = true | false)
for cases both non-resuming and resuming send.
Additional manual testing done:
1. With zrepl 0.5, setup with unencrypted dataset, send.raw=true specified, no send.encrypted specified.
2. Observe that regular non-resuming send works, but resuming doesn't work.
3. Upgrade zrepl to this change.
4. Observe that both regular and resuming send works.
closes https://github.com/zrepl/zrepl/pull/613
Use it from a top-level test case to queue the
execution of sub-tests after this test case is complete.
Note that the testing harness executes the subtest
_after_ the current top-level test. Hence, the subtest
cannot use any ZFS state of the top-level test.
There were and still is too many words. It's a very white paper vibe.
Docs needs to be more brief, exact, and on-point.
closes https://github.com/zrepl/zrepl/pull/618
Previously with unordered list, and 'are considered'
left if unsure whether one or all files are 'considered'.
In reality, the first valid is used, so an ordered list and
perhaps better wording communicates this fact.
refs https://github.com/zrepl/zrepl/pull/618
The goroutine that does endTask() for
"list-abstractions-streamed-producer" can be preempted
after it has closed the out and outErrs channel,
but before it calls endTask().
If the parent ("handler") then gets scheduled and
and ends itself, it will observe an active child task
"list-abstractions-streamed-producer".
This is easy to demo by injecting a sleep here:
--- a/endpoint/endpoint_zfs_abstraction.go
+++ b/endpoint/endpoint_zfs_abstraction.go
@@ -575,6 +576,7 @@ func ListAbstractionsStreamed(ctx context.Context, query ListZFSHoldsAndBookmark
ctx, endTask := trace.WithTask(ctx, "list-abstractions-streamed-producer")
go func() {
defer endTask()
+ defer time.Sleep(10 * time.Second)
defer close(out)
defer close(outErrs)
fixes https://github.com/zrepl/zrepl/issues/607
It may be desirable to check that a config is valid without checking for
the existence of certificate files (e.g. when validating a config inside
a sandbox without access to the cert files).
This will be very useful for NixOS so that we can check the config file
at nix-build time (e.g. potentially without proper permissions to read cert
files for a TLS connection).
fixes https://github.com/zrepl/zrepl/issues/467
closes https://github.com/zrepl/zrepl/pull/587
Config:
```
- type: push
...
conflict_resolution:
initial_replication: most_recent | all | fali
```
The ``initial_replication`` option determines which snapshots zrepl
replicates if the filesystem has not been replicated before.
If ``most_recent`` (the default), the initial replication will only
transfer the most recent snapshot, while ignoring previous snapshots.
If all snapshots should be replicated, specify ``all``.
Use ``fail`` to make replication of the filesystem fail in case
there is no corresponding fileystem on the receiver.
Code-Level Changes, apart from the obvious:
- Rework IncrementalPath()'s return signature.
Now returns an error for initial replications as well.
- Rename & rework it's consumer, resolveConflict().
Co-authored-by: Graham Christensen <graham@grahamc.com>
Fixes https://github.com/zrepl/zrepl/issues/550
Fixes https://github.com/zrepl/zrepl/issues/187
Closes https://github.com/zrepl/zrepl/pull/592
Juding from the (now deleted) comments in go.mod, this might break 1.12
build. If it does, the CI will catch it as it currently build using
1.17 and 1.12.
fixes https://github.com/zrepl/zrepl/issues/586
CircleCI fails like so:
#!/bin/bash -eo pipefail
./lazy.sh docdep
pip3 is /home/circleci/.pyenv/shims/pip3
Installing doc build dependencies
Obtaining sphinxcontrib-versioning from git+git://github.com/rwblair/sphinxcontrib-versioning.git@7e3885a389a809e17ea55261316b7b0e98dbf98f#egg=sphinxcontrib-versioning (from -r ./docs/requirements.txt (line 28))
Cloning git://github.com/rwblair/sphinxcontrib-versioning.git (to revision 7e3885a389a809e17ea55261316b7b0e98dbf98f) to ./src/sphinxcontrib-versioning
Running command git clone --filter=blob:none --quiet git://github.com/rwblair/sphinxcontrib-versioning.git /home/circleci/project/src/sphinxcontrib-versioning
fatal: remote error:
The unauthenticated git protocol on port 9418 is no longer supported.
Please see https://github.blog/2021-09-01-improving-git-protocol-security-github/ for more information.
error: subprocess-exited-with-error
× git clone --filter=blob:none --quiet git://github.com/rwblair/sphinxcontrib-versioning.git /home/circleci/project/src/sphinxcontrib-versioning did not run successfully.
│ exit code: 128
╰─> See above for output.
note: This error originates from a subprocess, and is likely not a problem with pip.
error: subprocess-exited-with-error
× git clone --filter=blob:none --quiet git://github.com/rwblair/sphinxcontrib-versioning.git /home/circleci/project/src/sphinxcontrib-versioning did not run successfully.
│ exit code: 128
╰─> See above for output.
note: This error originates from a subprocess, and is likely not a problem with pip.
Exited with code exit status 1
CircleCI received exit code 1
fixes https://github.com/zrepl/zrepl/issues/504
Problem:
plain send + recv with root_fs encrypted + placeholders causes plain recvs
whereas user would expect encrypt-on-recv
Reason:
We create placeholder filesytems with -o encryption=off.
Thus, children received below those placeholders won't inherit
encryption of root_fs.
Fix:
We'll have three values for `recv.placeholders.encryption: unspecified (default) | off | inherit`.
When we create a placeholder, we will fail the operation if `recv.placeholders.encryption = unspecified`.
The exception is if the placeholder filesystem is to encode the client identity ($root_fs/$client_identity) in a pull job.
Those are created in `inherit` mode if the config field is `unspecified` so that users who don't need
placeholders are not bothered by these details.
Future Work:
Automatically warn existing users of encrypt-on-recv about the problem
if they are affected.
The problem that I hit during implementation of this is that the
`encryption` prop's `source` doesn't quite behave like other props:
`source` is `default` for `encryption=off` and `-` when `encryption=on`.
Hence, we can't use `source` to distinguish the following 2x2 cases:
(1) placeholder created with explicit -o encryption=off
(2) placeholder created without specifying -o encryption
with
(A) an encrypted parent at creation time
(B) an unencrypted parent at creation time
- Substitute full words for both string name 'gridspec' and short form 'grid spec'
- Fix alignment and make spacing more consistent
- Fix fall of snapshots into buckets for the example to really reflect right-exclusiveness
closes https://github.com/zrepl/zrepl/pull/535
We had too many spurious test failures in the past.
But on a developer machine, the tests don't usually fail because the
system isn't loaded as much.
So, only disable test on CircleCI.
When we need artifacts, we use MinIO anyways.
And we have accumulated about 1TiB of (free) CircleCI artifact storage.
Don't need to waste space unnecessarily.
zrepl daemon panics when the snap job triggers
fixup for f5f269bfd5 (bandwidth limiting)
fixes#521
Oct 01 16:14:56 cstp zrepl[56563]: panic: invalid config`BandwidthLimit` field invalid: BucketCapacity must not be zero
Oct 01 16:14:56 cstp zrepl[56563]: panic: end span: span still has active child spans
Oct 01 16:14:56 cstp zrepl[56563]: goroutine 38 [running]:
Oct 01 16:14:56 cstp zrepl[56563]: github.com/zrepl/zrepl/daemon/logging/trace.WithSpan.func2()
Oct 01 16:14:56 cstp zrepl[56563]: /home/cs/zrepl/zrepl/daemon/logging/trace/trace.go:341 +0x2ea
Oct 01 16:14:56 cstp zrepl[56563]: github.com/zrepl/zrepl/daemon/logging/trace.WithTaskAndSpan.func1()
Oct 01 16:14:56 cstp zrepl[56563]: /home/cs/zrepl/zrepl/daemon/logging/trace/trace_convenience.go:40 +0x2e
Oct 01 16:14:56 cstp zrepl[56563]: panic(0xcee9c0, 0xc000676730)
Oct 01 16:14:56 cstp zrepl[56563]: /home/cs/go1.16.6/src/runtime/panic.go:965 +0x1b9
Oct 01 16:14:56 cstp zrepl[56563]: github.com/zrepl/zrepl/endpoint.NewSender(0xf5bbc0, 0xc0003840c0, 0xc0000b2c90, 0x4, 0xc0002c5958, 0x0, 0x0, 0x0, 0xc000068cf8)
Oct 01 16:14:56 cstp zrepl[56563]: /home/cs/zrepl/zrepl/endpoint/endpoint.go:68 +0x1ec
Oct 01 16:14:56 cstp zrepl[56563]: github.com/zrepl/zrepl/daemon/job.(*SnapJob).doPrune(0xc00039e000, 0xf6e3b8, 0xc0006541b0)
Oct 01 16:14:56 cstp zrepl[56563]: /home/cs/zrepl/zrepl/daemon/job/snapjob.go:179 +0x198
Oct 01 16:14:56 cstp zrepl[56563]: github.com/zrepl/zrepl/daemon/job.(*SnapJob).Run(0xc00039e000, 0xf6e3b8, 0xc0001d83c0)
Oct 01 16:14:56 cstp zrepl[56563]: /home/cs/zrepl/zrepl/daemon/job/snapjob.go:127 +0x329
Oct 01 16:14:56 cstp zrepl[56563]: github.com/zrepl/zrepl/daemon.(*jobs).start.func1(0xc0006a4100, 0xf6e3b8, 0xc00022a0f0, 0xf72d18, 0xc00039e000)
Oct 01 16:14:56 cstp zrepl[56563]: /home/cs/zrepl/zrepl/daemon/daemon.go:255 +0x15b
Oct 01 16:14:56 cstp zrepl[56563]: created by github.com/zrepl/zrepl/daemon.(*jobs).start
Oct 01 16:14:56 cstp zrepl[56563]: /home/cs/zrepl/zrepl/daemon/daemon.go:251 +0x425
Oct 01 16:14:56 cstp systemd[1]: zrepl.service: Main process exited, code=exited, status=2/INVALIDARGUMENT
Oct 01 16:14:56 cstp systemd[1]: zrepl.service: Failed with result 'exit-code'.
This commit was motivated by https://github.com/zrepl/zrepl/issues/495
where, on FreeBSD with OpenZFS 2.0, a SendStream.Close() call might wait indefinitely for `zfs send` to exit.
The reason is that, due to the refactoring done for redacted send & recv
(https://github.com/openzfs/zfs/commit/30af21b02569ac192f52ce6e6511015f8a8d5729),
the `dump_bytes` function, which writes to the pipe, executes in a separate thread (synctask taskq) iff not `HAVE_LARGE_STACKS`.
The `zfs send` process/thread waits for that taskq thread using an uninterruptible primitive.
So when we SIGKILL `zfs send`, that signal doesn't reach the right thread to interrupt the pipe write.
Theoretically this affects both Linux and FreeBSD, but most Linux users `HAVE_LARGE_STACKS` and since https://github.com/penzfs/zfs/pull/12350/files OpenZFS on FreeBSD `HAVE_LARGE_STACKS` as well.
However, at least until FreeBSD 13.1, possibly for the entire 13 lifecycle, we're going to have to live with that oddity.
Measures taken in this commit:
- Report the behavior as an upstream bug https://github.com/openzfs/zfs/issues/12500
- Change SendStream code so that it closes zrepl's read-end of the pipe (see comment in code)
- Clean up and make explicit SendStream's state handling
- Write extensive platformtests for SendStream
- They pass on my Linux install and on FreeBSD 12
- FreeBSD 13 still needs testing.
fixes https://github.com/zrepl/zrepl/issues/495
I generally like when snippets are provided in a way which could be used without running as root, and uses sudo when applicable. This change allows for this.
It will, however print out one extra line, which is possible to remove by adding '>/dev/null' after '/etc/apt/sources.list.d/zrepl.list'.
closes#461
This was merged to master prematurely as the job components are not decoupled well enough
for these signals to be useful yet.
This reverts commit 2c8c2cfa14.
closes#452
Addition by @problame: move getPipeCapacityHint() into platform-specific
code. This has the added benefit of not recognizing the envvar as an
envconst on platform that do not support resizing pipes. => won't show
up in (zrepl status --raw).Global.Envconst
fixes#424
cloes #449
A fairly common pattern when presented with JSON is to pipe it to `jq`.
This also allows one to `zrepl status --mode raw > log` and operate on
the JSON there.
closes#442
Primary goals:
- Scrollable output ( fixes#245 )
- Sending job signals from status view
- Filtering of output by filesystem
Implementation:
- original TUI framework: github.com/rivo/tview
- but: tview is quasi-unmaintained, didn't support some features
- => use fork https://gitlab.com/tslocum/cview
- however, don't buy into either too much to avoid lock-in
- instead: **port over the existing status UI drawing code
and adjust it to produce strings instead of directly
drawing into the termbox buffer**
Co-authored-by: Calistoc <calistoc@protonmail.com>
Co-authored-by: InsanePrawn <insane.prawny@gmail.com>
fixes#245fixes#220
From:
github.com/golang/protobuf v1.3.2
google.golang.org/grpc v1.17.0
To:
github.com/golang/protobuf v1.4.3
google.golang.org/grpc v1.35.0
google.golang.org/protobuf v1.25.0
About the two protobuf packages:
https://developers.google.com/protocol-buffers/docs/reference/go/faq
> Version v1.4.0 and higher of github.com/golang/protobuf wrap the new
implementation and permit programs to adopt the new API incrementally. For
example, the well-known types defined in github.com/golang/protobuf/ptypes are
simply aliases of those defined in the newer module. Thus,
google.golang.org/protobuf/types/known/emptypb and
github.com/golang/protobuf/ptypes/empty may be used interchangeably.
Notable Code Changes in zrepl:
- generate protobufs now contain a mutex so we can't copy them by value
anymore
- grpc.WithDialer is deprecated => use grpc.WithContextDialer instead
Go1.12 is now actually required by some of the dependencies.
Unless we're using the global source for math/rand, (*rand.Rand).Read
should not be called concurrently. We seed the rng in daemon.Run to
avoid ambiguity or hiding global side effects inside packages.
closes#414
GO111MODULE=on golangci-lint run ./...
endpoint/endpoint.go:487:9: S1039: unnecessary use of fmt.Sprintf (gosimple)
panic(fmt.Sprintf("ClientIdentityKey context value must be set"))
^
platformtest/platformtest_ops.go:259:41: S1039: unnecessary use of fmt.Sprintf (gosimple)
return nil, &LineError{scan.Text(), fmt.Sprintf("unexpected tokens at EOL")}
^
platformtest/platformtest_ops.go:266:41: S1039: unnecessary use of fmt.Sprintf (gosimple)
return nil, &LineError{scan.Text(), fmt.Sprintf("unexpected tokens at EOL")}
^
util/optionaldeadline/optionaldeadline_test.go:97:50: SA1029: should not use built-in type string as key for value; define your own type to avoid collisions (staticcheck)
pctx := context.WithValue(context.Background(), "key", "value")
^
rpc/rpc_debug.go:8:5: var `debugEnabled` is unused (unused)
rpc/dataconn/dataconn_debug.go:8:5: var `debugEnabled` is unused (unused)
rpc/dataconn/frameconn/frameconn.go:42:9: S1039: unnecessary use of fmt.Sprintf (gosimple)
panic(fmt.Sprintf("frame header is 8 bytes long"))
^
platformtest/platformtest_ops.go:322:40: S1039: unnecessary use of fmt.Sprintf (gosimple)
return nil, &LineError{scan.Text(), fmt.Sprintf("unexpected tokens at EOL")}
- `timeoutconn` handles state, yet calls to Read/Write make a copy of
that state (non-pointer receiver) so any outbound calls will not have
the state updated
- Even without the copy issue, the renew methods can in edge cases set a
new deadline _after_ DisableTimeouts have been called, consider the
following racy behavior:
1. `renewReadDeadline` is called, checks `renewDeadlinesDisabled`
(not disabled)
2. `DisableTimeouts` is called, sets `renewDeadlinesDisabled`
3. `DisableTimeouts` invokes `c.SetDeadline`
4. `renewReadDeadline` invokes `c.SetReadDeadline`
To fix the above, the `Conn` receiver was made to be a pointer
everywhere and access to renewDeadlinesDisabled is now guarded
by an RWMutex instead of using atomics.
closes#415
Before this change, the step correlation code returned early in several cases:
- did not set f.planning.done in the cases where it was a no-op
- did not set f.planning.err in the cases where correlation did not
succeed
Reported-by: InsanePrawn <insane.prawny@gmail.com>
fixes build failure for aarch64
```
make rpm-docker GOARCH=arm64 GOOS=linux
+ /usr/lib/rpm/brp-compress
+ /usr/lib/rpm/brp-strip /usr/bin/strip
/usr/bin/strip: Unable to recognise the format of the input file `/build/src/artifacts/rpmbuild/BUILDROOT/zrepl-v0.3.0.38.g53028ed5-1.aarch64/usr/bin/zrepl'
error: Bad exit status from /var/tmp/rpm-tmp.g8Fdbs (%install)
Bad exit status from /var/tmp/rpm-tmp.g8Fdbs (%install)
```
package is now at 95% code coverage and the additional tests codify
all behavior specified in the docs
There is a slight change in behavior:
Intervals are now [duration) instead of (duration].
If the leftmost interval is not keep=all, the most recently created
snapshot will be destroyed if there are other snapshots within
that first interval.
Since we recommend keep=all all over the docs, and zrepl 0.3
will put holds on that snapshot if it is being replicated,
I feel like this is an acceptable change in behavior.
refs #292
fixup of 0bbe2befce
- `make formatcheck` outputs the list of files that need formatting
- `make format` applies the formatting and now also outputs the diffs.
Signed-off-by: InsanePrawn <insane.prawny@gmail.com>
- Emit warning and return 1 when no cmd is specified
- get docdep requirements from lazy.sh parent dir, not $GOPATH/src/zrepl/zrepl
- Explicitly warn when gopath is not in PATH
- Fix `release` cmd
- replace `! -z` with `-n`
Signed-off-by: InsanePrawn <insane.prawny@gmail.com>
* The SendStream.Close() was not called by dataconn.Server, which left the zfs send process dangling.
* When the source job's ctx interceptor closed the task, the dangling zfs send was detect by the trace package and panicked.
020-07-25T19:54:41-04:00 [ERRO][latitude][rpc.data][cyZj$J3Ca$J3Ca.CJwB]: cannot write send stream err="frameconn: shutting down"
panic: end task: 1 active child tasks: end task: task still has active child tasks
goroutine 196966 [running]:
github.com/zrepl/zrepl/daemon/logging/trace.WithTask.func1.1(0xc000320680, 0xcde000)
/home/jeremy/go/src/github.com/zrepl/zrepl/daemon/logging/trace/trace.go:221 +0x2f7
github.com/zrepl/zrepl/daemon/logging/trace.WithTask.func1()
/home/jeremy/go/src/github.com/zrepl/zrepl/daemon/logging/trace/trace.go:237 +0x38
github.com/zrepl/zrepl/daemon/logging/trace.WithTaskAndSpan.func1()
/home/jeremy/go/src/github.com/zrepl/zrepl/daemon/logging/trace/trace_convenience.go:41 +0x37
github.com/zrepl/zrepl/daemon/job.(*PassiveSide).Run.func1(0xdcf780, 0xc0000a3560, 0xdc65a0, 0xc00035e620, 0xc0000a34d0)
/home/jeremy/go/src/github.com/zrepl/zrepl/daemon/job/passive.go:194 +0x2e7
github.com/zrepl/zrepl/rpc.NewServer.func3(0xdcf780, 0xc0001ce4b0, 0xdc65e0, 0xc00035e600, 0xc0000a34d0)
/home/jeremy/go/src/github.com/zrepl/zrepl/rpc/rpc_server.go:82 +0xd5
github.com/zrepl/zrepl/rpc/dataconn.(*Server).serveConn(0xc0000a2ba0, 0xc00018eca0)
/home/jeremy/go/src/github.com/zrepl/zrepl/rpc/dataconn/dataconn_server.go:149 +0x3be
github.com/zrepl/zrepl/rpc/dataconn.(*Server).Serve.func3(0xc0000b8180, 0xc0000a2ba0, 0xc00018eca0)
/home/jeremy/go/src/github.com/zrepl/zrepl/rpc/dataconn/dataconn_server.go:108 +0x5d
created by github.com/zrepl/zrepl/rpc/dataconn.(*Server).Serve
/home/jeremy/go/src/github.com/zrepl/zrepl/rpc/dataconn/dataconn_server.go:106 +0x24a
2020-07-25T19:58:55-04:00 [ERRO][latitude][rpc.data][Pt4F$gCWT$gCWT.fzhc]: cannot write send stream err="frameconn: shutting down"
panic: end task: 1 active child tasks: end task: task still has active child tasks
fixes#348
This commit
- adds a configuration in which no step holds, replication cursors, etc. are created
- removes the send.step_holds.disable_incremental setting
- creates a new config option `replication` for active-side jobs
- adds the replication.protection.{initial,incremental} settings, each
of which can have values
- `guarantee_resumability`
- `guarantee_incremental`
- `guarantee_nothing`
(refer to docs/configuration/replication.rst for semantics)
The `replication` config from an active side is sent to both endpoint.Sender and endpoint.Receiver
for each replication step. Sender and Receiver then act accordingly.
For `guarantee_incremental`, we add the new `tentative-replication-cursor` abstraction.
The necessity for that abstraction is outlined in https://github.com/zrepl/zrepl/issues/340.
fixes https://github.com/zrepl/zrepl/issues/340
This "breaks" the use case of receiving an unencrypted send into an encrypted receiver by setting the receiver's `root_fs`'s `encryption=on`.
"breaks" in air-quotes because we have not yet released a version of
zrepl with encrypted send support.
We will bring back the featured outlined above in a future release.
See https://github.com/zrepl/zrepl/issues/342#issuecomment-657231818 and following.
- uses version metric for 'instances up'
- displays active task count
- displays send abstractions cache entry count
- in general, graphs have a shorter y axis for better overview
fixes#332
This is a stop-gap solution until we re-write the pruner to support
rules for removing step holds.
Note that disabling step holds for incremental sends does not affect
zrepl's guarantee that incremental replication is always possible:
Suppose you yank the external drive during an incremental @from -> @to step:
* restarting that step or future incrementals @from -> @to_later` will be possible
because the replication cursor bookmark points to @from until the step is complete
* resuming @from -> @to will work as long as the pruner on your internal pool doesn't come around to destroy @to.
* in that case, the replication algorithm should determine that the resumable state
on the receiving side isuseless because @to no longer exists on the sending side,
and consequently clear it, and restart an incremental step @from -> @to_later
refs #288
- drop HintMostRecentCommonAncestor rpc call
- it is wrong to put faith into the active side of the replication to always make that call
(we might not trust it, ref pull setup)
- clean up step holds + step bookmarks + replication cursor bookmarks on
send RPC instead
- this makes it symmetric with Receive RPC
- use a cache (endpoint.sendAbstractionsCache) to avoid the cost of
listing the on-disk endpoint abstractions state on every step
The "create" methods for endpoint abstractions (CreateReplicationCursor, HoldStep) are now fully
idempotent and return an Abstraction.
Notes about endpoint.sendAbstractionsCache:
- fills lazily from disk state on first `Get` operation
- fill from disk is generally only attempted once
- unless the `ListAbstractions` fails, in which case the fill from
disk is retried on next `Get` (the current `Get` will observe a
subset of the actual on-disk abstractions)
- the `Invalidate` method is called
- it is a global (zrepl process-wide) cache
fixes#316
- discovered during investigation of #316
- this is not the fix for #316, as a malicious receiver who doesn't
implement the behavior added by this patch could still cause leakage
of step holds on the sender
refs #316
package trace:
- introduce the concept of tasks and spans, tracked as linked list within ctx
- see package-level docs for an overview of the concepts
- **main feature 1**: unique stack of task and span IDs
- makes it easy to follow a series of log entries in concurrent code
- **main feature 2**: ability to produce a chrome://tracing-compatible trace file
- either via an env variable or a `zrepl pprof` subcommand
- this is not a CPU profile, we already have go pprof for that
- but it is very useful to visually inspect where the
replication / snapshotter / pruner spends its time
( fixes#307 )
usage in package daemon/logging:
- goal: every log entry should have a trace field with the ID stack from package trace
- make `logging.GetLogger(ctx, Subsys)` the authoritative `logger.Logger` factory function
- the context carries a linked list of injected fields which
`logging.GetLogger` adds to the logger it returns
- `logging.GetLogger` also uses package `trace` to get the
task-and-span-stack and injects it into the returned logger's fields
- prior to this patch, context cancellation would leave rpc.Server open
- did not make problems because context was only cancelled by SIGINT,
which was immediately followed by os.Exit
The motivation for this recatoring are based on two independent issues:
- @JMoVS found that the changes merged as part of #259 slowed his OS X
based installation down significantly.
Analysis of the zfs command logging introduced in #296 showed that
`zfs holds` took most of the execution time, and they pointed out
that not all of those `zfs holds` invocations were actually necessary.
I.e.: zrepl was inefficient about retrieving information from ZFS.
- @InsanePrawn found that failures on initial replication would lead
to step holds accumulating on the sending side, i.e. they would never
be cleaned up in the HintMostRecentCommonAncestor RPC handler.
That was because we only sent that RPC if there was a most recent
common ancestor detected during replication planning.
@InsanePrawn prototyped an implementation of a `zrepl zfs-abstractions release`
command to mitigate the situation.
As part of that development work and back-and-forth with @problame,
it became evident that the abstractions that #259 built on top of
zfs in package endpoint (step holds, replication cursor,
last-received-hold), were not well-represented for re-use in the
`zrepl zfs-abstractions release` subocommand prototype.
This commit refactors package endpoint to address both of these issues:
- endpoint abstractions now share an interface `Abstraction` that, among
other things, provides a uniform `Destroy()` method.
However, that method should not be destroyed directly but instead
the package-level `BatchDestroy` function should be used in order
to allow for a migration to zfs channel programs in the future.
- endpoint now has a query facitilty (`ListAbstractions`) which is
used to find on-disk
- step holds and bookmarks
- replication cursors (v1, v2)
- last-received-holds
By describing the query in a struct, we can centralized the retrieval
of information via the ZFS CLI and only have to be clever once.
We are "clever" in the following ways:
- When asking for hold-based abstractions, we only run `zfs holds` on
snapshot that have `userrefs` > 0
- To support this functionality, add field `UserRefs` to zfs.FilesystemVersion
and retrieve it anywhere we retrieve zfs.FilesystemVersion from ZFS.
- When asking only for bookmark-based abstractions, we only run
`zfs list -t bookmark`, not with snapshots.
- Currently unused (except for CLI) per-filesystem concurrent lookup
- Option to only include abstractions with CreateTXG in a specified range
- refactor `endpoint`'s various ZFS info retrieval methods to use
`ListAbstractions`
- rename the `zrepl holds list` command to `zrepl zfs-abstractions list`
- make `zrepl zfs-abstractions list` consume endpoint.ListAbstractions
- Add a `ListStale` method which, given a query template,
lists stale holds and bookmarks.
- it uses replication cursor has different modes
- the new `zrepl zfs-abstractions release-{all,stale}` commands can be used
to remove abstractions of package endpoint
- Adjust HintMostRecentCommonAncestor RPC for stale-holds cleanup:
- send it also if no most recent common ancestor exists between sender and receiver
- have the sender clean up its abstractions when it receives the RPC
with no most recent common ancestor, using `ListStale`
- Due to changed semantics, bump the protocol version.
- Adjust HintMostRecentCommonAncestor RPC for performance problems
encountered by @JMoVS
- by default, per (job,fs)-combination, only consider cleaning
step holds in the createtxg range
`[last replication cursor,conservatively-estimated-receive-side-version)`
- this behavior ensures resumability at cost proportional to the
time that replication was donw
- however, as explained in a comment, we might leak holds if
the zrepl daemon stops running
- that trade-off is acceptable because in the presumably rare
this might happen the user has two tools at their hand:
- Tool 1: run `zrepl zfs-abstractions release-stale`
- Tool 2: use env var `ZREPL_ENDPOINT_SENDER_HINT_MOST_RECENT_STEP_HOLD_CLEANUP_MODE`
to adjust the lower bound of the createtxg range (search for it in the code).
The env var can also be used to disable hold-cleanup on the
send-side entirely.
supersedes closes#293
supersedes closes#282fixes#280fixes#278
Additionaly, we fixed a couple of bugs:
- zfs: fix half-nil error reporting of dataset-does-not-exist for ZFSListChan and ZFSBookmark
- endpoint: Sender's `HintMostRecentCommonAncestor` handler would not
check whether access to the specified filesystem was allowed.
- **Resumable Send & Recv Support**
No knobs required, automatically used where supported.
- **Hold-Protected Send & Recv**
Automatic ZFS holds to ensure that we can always resume a replication step.
- **Encrypted Send & Recv Support** for OpenZFS native encryption.
Configurable at the job level, i.e., for all filesystems a job is responsible for.
- **Receive-side hold on last received dataset**
The counterpart to the replication cursor bookmark on the send-side.
Ensures that incremental replication will always be possible between a sender and receiver.
Design Doc
----------
`replication/design.md` doc describes how we use ZFS holds and bookmarks to ensure that a single replication step is always resumable.
The replication algorithm described in the design doc introduces the notion of job IDs (please read the details on this design doc).
We reuse the job names for job IDs and use `JobID` type to ensure that a job name can be embedded into hold tags, bookmark names, etc.
This might BREAK CONFIG on upgrade.
Protocol Version Bump
---------------------
This commit makes backwards-incompatible changes to the replication/pdu protobufs.
Thus, bump the version number used in the protocol handshake.
Replication Cursor Format Change
--------------------------------
The new replication cursor bookmark format is: `#zrepl_CURSOR_G_${this.GUID}_J_${jobid}`
Including the GUID enables transaction-safe moving-forward of the cursor.
Including the job id enables that multiple sending jobs can send the same filesystem without interfering.
The `zrepl migrate replication-cursor:v1-v2` subcommand can be used to safely destroy old-format cursors once zrepl has created new-format cursors.
Changes in This Commit
----------------------
- package zfs
- infrastructure for holds
- infrastructure for resume token decoding
- implement a variant of OpenZFS's `entity_namecheck` and use it for validation in new code
- ZFSSendArgs to specify a ZFS send operation
- validation code protects against malicious resume tokens by checking that the token encodes the same send parameters that the send-side would use if no resume token were available (i.e. same filesystem, `fromguid`, `toguid`)
- RecvOptions support for `recv -s` flag
- convert a bunch of ZFS operations to be idempotent
- achieved through more differentiated error message scraping / additional pre-/post-checks
- package replication/pdu
- add field for encryption to send request messages
- add fields for resume handling to send & recv request messages
- receive requests now contain `FilesystemVersion To` in addition to the filesystem into which the stream should be `recv`d into
- can use `zfs recv $root_fs/$client_id/path/to/dataset@${To.Name}`, which enables additional validation after recv (i.e. whether `To.Guid` matched what we received in the stream)
- used to set `last-received-hold`
- package replication/logic
- introduce `PlannerPolicy` struct, currently only used to configure whether encrypted sends should be requested from the sender
- integrate encryption and resume token support into `Step` struct
- package endpoint
- move the concepts that endpoint builds on top of ZFS to a single file `endpoint/endpoint_zfs.go`
- step-holds + step-bookmarks
- last-received-hold
- new replication cursor + old replication cursor compat code
- adjust `endpoint/endpoint.go` handlers for
- encryption
- resumability
- new replication cursor
- last-received-hold
- client subcommand `zrepl holds list`: list all holds and hold-like bookmarks that zrepl thinks belong to it
- client subcommand `zrepl migrate replication-cursor:v1-v2`
There's plenty of room for improvement here.
For example, detect if we're past the last step without size estimation
and compute the remaining sum of bytes to be replicated from there on.
Symptom: zrepl log message:
rpc error: code = Unavailable desc = transport is closing
Underlying Problem:
* rpc.NewServer was not using grpchelper.NewServer and not setting Server KeepaliveParams by itself
* and even grpchelper.NewServer didn't set a KeepaliveEnforcementPolicy
* However, KeepaliveEnforcementPolicy is necessary if the client keepalive is configured with non-default values
* .. which grpchelper.ClientConn does, and that method is used by rpc.NewClient
* rpc.Client was sending pings
* lacking server-side KeepaliveEnforcementPolicy caused grpc-hard-coded `pingStrikes` counter to go past limit of 2:
https://github.com/grpc/grpc-go/blob/021bd5734e43b1b11073ea326de29af4de3dfa9b/internal/transport/http2_server.go#L726
How was this debugged:
* GRPC_GO_LOG_VERBOSITY_LEVEL=99 GRPC_GO_LOG_SEVERITY_LEVEL=info PATH=/root/mockpath:$PATH zrepl daemon
* with a patch on grpc package to get more log messages on pingStrikes increases:
diff --git a/internal/transport/http2_server.go b/internal/transport/http2_server.go
index 8b04b039..f68f55ea 100644
--- a/internal/transport/http2_server.go
+++ b/internal/transport/http2_server.go
@@ -214,6 +214,7 @@ func newHTTP2Server(conn net.Conn, config *ServerConfig) (_ ServerTransport, err
if kep.MinTime == 0 {
kep.MinTime = defaultKeepalivePolicyMinTime
}
+ errorf("effective keepalive enforcement policy: %#v", kep)
done := make(chan struct{})
t := &http2Server{
ctx: context.Background(),
@@ -696,6 +697,7 @@ func (t *http2Server) handlePing(f *http2.PingFrame) {
t.controlBuf.put(pingAck)
now := time.Now()
+ errorf("transport:ping handlePing, last ping %s ago", now.Sub(t.lastPingAt))
defer func() {
t.lastPingAt = now
}()
@@ -713,11 +715,13 @@ func (t *http2Server) handlePing(f *http2.PingFrame) {
// Keepalive shouldn't be active thus, this new ping should
// have come after at least defaultPingTimeout.
if t.lastPingAt.Add(defaultPingTimeout).After(now) {
+ errorf("transport:ping strike ns < 1 && !t.kep.PermitWithoutStream")
t.pingStrikes++
}
} else {
// Check if keepalive policy is respected.
if t.lastPingAt.Add(t.kep.MinTime).After(now) {
+ errorf("transport:ping strike !(ns < 1 && !t.kep.PermitWithoutStream) kep.MinTime=%s ns=%d", t.kep.MinTime, ns)
t.pingStrikes++
}
}
fixes#181
from go-netssh changelog:
dial: better error handling if ssh command exits with non-zero exit status
SSHError.Error() relied on go-rwccmd behavior of returning io.EOF if the
ssh binary exited with status code 0.
We no longe ruse go-rwccmd => capture Stderr ourselves using zrepl's
circlog (depending on zrepl is not pretty, but since this package is supposedly
only used by zrepl ATM, this is fine)
refs https://github.com/zrepl/zrepl/issues/237
Verified once again that grpc.DialContext is indeed non-blocking.
However, it checks in a defer stmt that the passed dial is not ctx.Done().
That is highly unusual if the dial is non-blocking.
But it might still happen, maybe because of machine suspend during the function call and before the defer stmt is executed.
panic:
context deadline exceeded
goroutine 49 [running]:
github.com/zrepl/zrepl/rpc/grpcclientidentity/grpchelper.ClientConn(0x1906ea0, 0xc0003ea1e0, 0x1921620, 0xc0002da660, 0x0)
/gopath/src/github.com/zrepl/zrepl/rpc/grpcclientidentity/grpchelper/authlistener_grpc_adaptor_wrapper.go:49 +0x38c
github.com/zrepl/zrepl/rpc.NewClient(0x1906f00, 0xc0002d60f0, 0x1921620, 0xc0002da640, 0x1921620, 0xc0002da660, 0x1921620, 0xc0002da6a0, 0x1921620)
/gopath/src/github.com/zrepl/zrepl/rpc/rpc_client.go:53 +0x199
github.com/zrepl/zrepl/daemon/job.(*modePush).ConnectEndpoints(0xc0000d1e90, 0x1921620, 0xc0002da640, 0x1921620, 0xc0002da660, 0x1921620, 0xc0002da6a0, 0x1906f00, 0xc0002d60f0)
/gopath/src/github.com/zrepl/zrepl/daemon/job/active.go:105 +0x15d
github.com/zrepl/zrepl/daemon/job.(*ActiveSide).do(0xc0000d6120, 0x1918720, 0xc00020f170)
/gopath/src/github.com/zrepl/zrepl/daemon/job/active.go:356 +0x236
github.com/zrepl/zrepl/daemon/job.(*ActiveSide).Run(0xc0000d6120, 0x1918720, 0xc00009c660)
/gopath/src/github.com/zrepl/zrepl/daemon/job/active.go:347 +0x289
github.com/zrepl/zrepl/daemon.(*jobs).start.func1(0xc0000fc880, 0x1921620, 0xc0002da120, 0x191a320, 0xc0000d6120, 0x1918720, 0xc0002d6a80)
* Add Close() in closeState to identify the first closer
* Non-first closers get an error
* Reads and Writes from the Conn get an error if the conn was closed
during the Read / Write was running
* The first closer starts _separate_ goroutine draining the c.frameReads channel
* The first closer then waits for the goroutine that fills c.frameReads
to exit
refs 3bfe0c16d0fixes#174
readFrames would block on `reads <-`
but only after that would stream.Conn.readFrames close c.waitReadFramesDone
which was too late because stream.Conn.Close would wait for c.waitReadFramesDone to be closed before draining the channel
^^^^^^ (not frameconn.Conn, that closed successfully)
195 @ 0x1032ae0 0x1006cab 0x1006c81 0x1006a65 0x15505be 0x155163e 0x1060bc1
0x15505bd github.com/zrepl/zrepl/rpc/dataconn/stream.readFrames+0x16d github.com/zrepl/zrepl/rpc/dataconn/stream/stream.go:220
0x155163d github.com/zrepl/zrepl/rpc/dataconn/stream.(*Conn).readFrames+0xbd github.com/zrepl/zrepl/rpc/dataconn/stream/stream_conn.go:71
195 @ 0x1032ae0 0x10078c8 0x100789e 0x100758b 0x1552678 0x1557a4b 0x1556aec 0x1060bc1
0x1552677 github.com/zrepl/zrepl/rpc/dataconn/stream.(*Conn).Close+0x77 github.com/zrepl/zrepl/rpc/dataconn/stream/stream_conn.go:191
0x1557a4a github.com/zrepl/zrepl/rpc/dataconn.(*Server).serveConn.func1+0x5a github.com/zrepl/zrepl/rpc/dataconn/dataconn_server.go:93
0x1556aeb github.com/zrepl/zrepl/rpc/dataconn.(*Server).serveConn+0x87b github.com/zrepl/zrepl/rpc/dataconn/dataconn_server.go:176
* stack-based execution model, documented in documentation
* circbuf for capturing hook output
* built-in hooks for postgres and mysql
* refactor docs, too much info on the jobs page, too difficult
to discover snapshotting & hooks
Co-authored-by: Ross Williams <ross@ross-williams.net>
Co-authored-by: Christian Schwarz <me@cschwarz.com>
fixes#74
ATM, the replication logic sends all dry-run requests in parallel,
which might overwhelm the ZFS pool on the sending side.
Since we use rpc/dataconn for dry sends, this also opens one TCP
connection per dry-run request.
Use a sempahore to limit the degree of concurrency where we know it is a
problem ATM.
As indicated by the comments, the cleaner solution would involve some
kind of 'resource exhaustion' error code.
refs #161
refs #164
As became clear in #126 (comment) the current zrepl:placeholder user
property makes it hard to rename datasets because the placeholder status
is determined by computing a hash of the dataset path and comparing it
with the property value.
This PR
- changes zrepl:placeholder to a simple on|off property
- derives property status from the property source (local),avoiding the
original problem of property inheritance in the original implementation
- provides a migration command that, based on the current zrepl
configuration, migrates property values to the new on|off schema
refs #150
treat handshake errors as permanent on the client
The issue was observed by 100% CPU usage due to lack ofrate-limiting in
dataconn.ReqPing retries=> safeguard that
Retry handling is broken since the gRPC changes (wrong error classification).
Will come back at some point, hopefully by merging the replication
driver retry infrastructure.
However, the simpler architecture allows an easy fix for the problem
that the pruner practically gave up on the first error it encountered.
fixes#123
We assumed that `zfs recv -F FS` would basically replace FS inplace, leaving its children untouched.
That is in fact not the case, it only works if `zfs send -R` is set, which we don't do.
Thus, implement the required functionality manually.
This solves a `zfs recv` error that would occur when a filesystem previously created as placeholder on the receiving side becomes a non-placeholder filesystem (likely due to config change on the sending side):
zfs send pool1/foo@1 | zfs recv -F pool1/bar
cannot receive new filesystem stream:
destination has snapshots (eg. pool1/bar)
must destroy them to overwrite it
* use pseudo-depdencies in build/build.go to convince dep
* update Travis, Dockerfile and Docs
* build.Dockerfile image now contains the Go build dependencies
* => faster builds
* bump pdu file after protoc update
fixes#106
* Remove explicity state machine code for all but replication.Replication
* Introduce explicit error types that satisfy interfaces which provide
sufficient information for replication.Replication to make intelligent
retry + queuing decisions
* Temporary()
* LocalToFS()
* Remove the queue and replace it with a simple array that we sort each
time (yay no generics :( )
Pruner now backs off as soon as there is an error, making that error the
Error field in the pruner report.
The error is also stored in the specific *fs that failed, and we
maintain an error counter per *fs to de-prioritize those fs that failed.
Like with replication, the de-prioritization on errors is to avoid '
getting stuck' with an individual filesystem until the watchdog hits.
An fsrep.Replication is either Ready, Retry or in a terminal state.
The queue prefers Ready over Retry:
Ready is sorted by nextStepDate to progress evenly..
Retry is sorted by error count, to de-prioritize filesystems that fail
often. This way we don't get stuck with individual filesystems
and lose other working filesystems to the watchdog.
fsrep.Replication no longer blocks in Retry state, we have
replication.WorkingWait for that.
ActiveSide.do() can only run sequentially, i.e. we cannot run
replication and pruning in parallel. Why?
* go-streamrpc only allows one active request at a time
(this is bad design and should be fixed at some point)
* replication and pruning are implemented independently, but work on the
same resources (snapshots)
A: pruning might destroy a snapshot that is planned to be replicated
B: replication might replicate snapshots that should be pruned
We do not have any resource management / locking for A and B, but we
have a use case where users don't want their machine fill up with
snapshots if replication does not work.
That means we _have_ to run the pruners.
A further complication is that we cannot just cancel the replication
context after a timeout and move on to the pruner: it could be initial
replication and we don't know how long it will take.
(And we don't have resumable send & recv yet).
With the previous commits, we can implement the watchdog using context
cancellation.
Note that the 'MadeProgress()' calls can only be placed right before
non-error state transition. Otherwise, we could end up in a live-lock.
We have the problem that there are legitimate use cases where a user
does not want their machine to fill up with snapshots, even if it means
unreplicated must be destroyed. This can be expressed by *not*
configuring the keep rule `not_replicated` for the snapshot-creating
side. This commit only addresses push mode because we don't support
pruning in the source job. We adivse users in the docs to use push mode
if they have above use case, so this is fine - at least for 0.1.
Ideally, the replication.Replication would communicate to the pruner
which snapshots are currently part of the replication plan, and then
we'd need some conflict resolution to determine whether it's more
important to destroy the snapshots or to replicate them (destroy should
win?).
However, we don't have the infrastructure for this yet (we could parse
the replication report, but that's just ugly). And we want to get 0.1
out, so showtime for a dirty hack:
We start replication, and ideally, replication and pruning is done
before new snapshot have been taken. If so: great. However, what happens
if snapshots have been taken and we are not done with replication and /
or pruning?
* If replicatoin is making progress according to its state, let it run.
This covers the *important* situation of initial replication, where
replication may easily take longer than a single snapshotting interval.
* If replication is in an error state, cancel it through context
cancellation.
* As with the pruner below, the main problem here is that
status output will only contain "context cancelled" after the
cancellation, instead of showing the reason why it was cancelled.
Not nice, but oh well, the logs provide enough detail for this
niche situation...
* If we are past replication, we're still pruning
* Leave the local (send-side) pruning alone.
Again, we only implement this hack for push, so we know sender is
local, and it will only fail hard, not retry.
* If the remote (receiver-side) pruner is in an error state, cancel it
through context cancellation.
* Otherwise, let it run.
Note that every time we "let it run", we tolerate a temporary excess of
snapshots, but given sufficiently aggressive timeouts and the assumption
that the snapshot interval is much greater than the timeouts, this is
not a significant problem in practice.
If concurrent updates strictly modify *different* members of the tasks
struct, the copying + lock-drop still constitutes a race condition:
The last updater always wins and sets tasks to its copy + changes.
This eliminates the other updater's changes.
- handle wakeups in Planning state
- fsrep.Replication yields immediately in RetryWait
- once the queue only contains fsrep.Replication in retryWait:
transition replication.Replication into WorkingWait state
- handle wakeups in WorkingWait state, too
1. Change config format to support multiple types
of snapshotting modes.
2. Implement a hacky way to support periodic or completely
manual snaphots.
In manual mode, the user has to trigger replication using the wakeup
mechanism after they took snapshots using their own tooling.
As indicated by the comment, a more general solution would be desirable,
but we want to get the release out and 'manual' mode is a feature that
some people requested...
The new local transport uses socketpair() and a switchboard based on
client identities.
The special local job type is gone, which is good since it does not fit
into the 'Active/Passive side ' + 'mode' concept used to implement the
duality of push/sink | pull/source.
Just because taking one snapshot fails does not mean snapper needs to
stop for all others.
Since users are advised to monitor error logs, snapshot-taking errors
can still be addressed.
The ErrorWait mode allows a potential future Report / Status command to
distinguish normal waits from error waits.
Also:
- Defensive measures in control http server (1s timeouts)
(prevent the leak, even if request body is not closed)
- Add prometheus metrics to track control socket latencies
(were used for debugging)
A bookmark with a well-known name is used to track which version was
last successfully received by the receiver.
The createtxg that can be retrieved from the bookmark using `zfs get` is
used to set the Replicated attribute of each snap on the sender:
If the snap's CreateTXG > the cursor's, it is not yet replicated,
otherwise it has been.
There is an optional config option to change the behvior to
`CreateTXG >= the cursor's`, and the implementation defaults to that.
The reason: While things work just fine with `CreateTXG > the cursor's`,
ZFS does not provide size estimates in a `zfs send` dry run
(see acd2418).
However, to enable the use case of keeping the snapshot only around for
the replication, the config flag exists.
Since we don't implement screen-scraping of ZFS output ATM, this is
better than nothing, as user's may be able to figure out what' sthe
problem from the logs / status reports.
Control status will be replaced by job-specific output at some point.
Task was not useful anymore with state machine, may reintroduce
something similar at a later point, but consider alternatives:
- opentracing.io
- embedding everything in ctx
- activity stack would work easily
- log entries via proxy logger.Logger object
- progress reporting should be in status reports of individial jobs
Summary:
* Logging is still bad
* test output in a lot of placed
* FIXMEs every where
Test Plan: None, just review
Differential Revision: https://phabricator.cschwarz.com/D2
* reproducible tarball
* includes go version
* sha512sum
The sha512 sum file should be signed manually, don't want that in the
Makefile because we may build in docker.
The old approach with ZFSList would keep the two-dimensional array of
lines and their fields in memory (for a short time), which could easily
consume 100s of MiB with > 10000 snapshots / bookmarks (see #34)
fixes#61
Issue #56 shows zombie SSH processes.
We fix this by actually Close()ing the RWC in job pull.
If this fixes#56 it also fixes#6 --- it's the same issue.
Additionally, debugging around this revealed another issue: just
Close()ing the sshbytestream in job source will apparently outpace the
normal data stream of stdin and stdout (URG or PUSH flags?). leading
to ugly errors in the logs.
With proper TCP connections, we would simply set the connection to
linger and close it, letting the kernel handle the final timeout. Meh.
refs #56
refs #6
- make TCPOutlet fully asynchronous, dropping messages if connection is
not fast enough
- syslog is just fine for now, local anyways
- stdout same thing
refs #26
An instance of Task tracks a single thread of activity that is part of a Job.
While the docs already use this terminology of tasks being composed of jobs,
the code did not have an object to represent these semantics.
Now it does:
* A task t is initialized with a root activity, which is its name
* t can t.Enter() and t.Finish() an activity, building
a stack of activities
* t's code can get a logger t.Log() whose logTaskField is set to the
concatenated stack of activities
* t's code can update IO progress it made since leaving idle state
* t's code's log output vie t.Log() is captured since leaving idle
state
* FIXME: find a way to bound that buffer
refs #10
refs #48
Didn't notice it because vendor/ was already populated on my dev
machine, but did notice it in Docker build.
Docker build now consumes devsetup like regular user, so this should
catch future problems.
Remove remaining curl|shit functionality from lazy.sh (no checkout logic
needed anymore).
refs #35
sphinx-versioning only build branches / commits with a 'docs/conf.py',
otherwise:
=> Gathering info about the remote git repository...
=> Getting list of all remote branches/tags...
=> Found: docs_theme master resumable_send_recv 0.0.1 0.0.2
=> With docs: 0.0.2
=> Root ref master not found in: 0.0.2
refs #35
Version is autodetected on build using git
If it cannot be detected with git, an override must be provided.
For tracability of distros, the distroy packagers should override as
well, which is why I added a README entry for package mainatiners.
refs #35
Abandons stderr special-casing:
* looks weird on shell and IO redirection to same file because of
interleaving of stdout and stderr
* better than a separate dedicated outlet because it does not require
additional configuration
fixes#28
BREAK SEMANTICS CONFIG
* Idempotent clone_and_build.sh does everything
* Add documentation for how to build in Docker
Had to sacrificy go generate because stringer apparently can't handle
vendor directory used by go dep, fails with error
on go generate rpc/frame_layer.go
refs #37
In contrast to any 'something<' mapping, a '<' mapping cannot be unique
Thus, '<' mappings are thus just an append to target, which is exactly
what we get when trimming empty prefix ''.
Otherwise, given mapping
{ "<": "storage/backups/app-srv" }
Before (clearly a conflict)
zroot => storage/backups/app-srv
storage => storage/backups/app-srv
After:
zroot => storage/backups/app-srv/zroot
storage => storage/backups/app-srv/storage
However, mapping directly with subtree wildcard is still possible, just
not with the root wildcard
{
"<" "storage/backups/app-srv"
"zroot/var/db<": "storage/db_replication/app-srv"
}
fixes#22
While filesystems is also not the right term (since it excludes ZVOLs),
we want to stay consistent with comments & terminology used in docs.
BREAK CONFIG
fixes#17
description: Generate GitHub release notes from docs/changelog.rst and create a draft release with artifacts
argument-hint: <version-tag, e.g. v0.7.0>
model: sonnet
allowed-tools: Read, Write
---
# Task
Create a draft GitHub release for zrepl version $ARGUMENTS and upload release artifacts.
## Inputs
Here is the current changelog RST source:
!`cat docs/changelog.rst`
## Instructions
### Phase 1: Pre-flight Sanity Checks
Before creating the release, verify that the previous build steps have been completed.
Run these checks using bash commands:
```bash
test -d artifacts/release &&echo"✓ artifacts/release directory exists"||echo"✗ Missing artifacts/release directory - run: make download-circleci-release JOB_NUM=<num>"
test -f artifacts/release/sha512sum.txt &&echo"✓ sha512sum.txt exists"||echo"✗ Missing sha512sum.txt - run: make download-circleci-release JOB_NUM=<num>"
test -f artifacts/release/sha512sum.txt.asc &&echo"✓ sha512sum.txt.asc exists (signed)"||echo"✗ Missing signature - run: make verify-and-sign"
ls artifacts/release/zrepl-* >/dev/null 2>&1&&echo"✓ Release artifacts present"||echo"✗ No release artifacts found"
```
If ANY check fails (shows ✗), **STOP** and print the error messages. Do not proceed with release creation.
### Phase 2: Generate Release Notes
2. Extract the changelog section for version $ARGUMENTS from the RST source above.
3. Extract all GitHub usernames mentioned in that changelog section (look for @username patterns or other contributor attributions).
4. Write a short "Highlights" section (2-4 bullets) summarizing the most impactful user-facing changes in plain language. Do NOT include commit/issue links.
5. Write a "Contributors" thank you line for all GitHub users found in step 3. Format as: "Thanks to @user1, @user2, and @user3 for their contributions to this release!"
6. Write a "Breaking Changes" section. Convert RST formatting to plain Markdown text (no commit or issue links):
-`:commit:\`abc123\`` → just remove it or describe what it changed
-`:issue:\`123\`` → just remove it or describe the issue in plain text
-`:ref:\`display text <anchor>\`` → just use `display text` as plain text
-`:repomasterlink:\`path\`` → just mention the path without a link
- RST links `` `text <url>`_ `` → just use `text` without the link
If there are no breaking changes, write "No breaking changes. vX.Y.Z-1 is interoperable with vX.Y.Z." (fill in actual versions).
7. Assemble the full release notes using the template below. **IMPORTANT**: Do NOT include a detailed changelog list. Only include what is specified in the template.
8. Write the result to `artifacts/release-notes.md`.
9. Validate all links in the release notes by checking HTTP status codes with curl:
- Extract all URLs from the markdown file
- Check each URL with `curl -sI -w "%{http_code}" -o /dev/null <url>`
- Report any broken links (non-200 status codes)
- If any links are broken, stop and notify the user before creating the release
### Phase 3: Create Draft Release and Upload Artifacts
13. Print the release URL and confirm that artifacts were uploaded successfully.
## Template
```
The full changelog is [on the docs site](https://zrepl.github.io/changelog.html).
## Highlights
{2-4 bullet plain-language summary of the most impactful changes}
{Thank all GitHub users mentioned in the changelog, e.g., "Thanks to @user1, @user2, and @user3 for their contributions to this release!"}
## Breaking Changes
{breaking changes, or "No breaking changes."}
## New Users
We provide [quick-start guides](https://zrepl.github.io/quickstart.html) for different usage scenarios.
We also recommend studying the [overview section of the configuration chapter](https://zrepl.github.io/configuration/overview.html).
## Testing & Upgrading
* Read the [Changelog](https://zrepl.github.io/changelog.html)
* [Run the platform tests](https://zrepl.github.io/usage.html#platform-tests) on a test system.
* Download & deploy the `zrepl` binary / distro package.
## Donations
zrepl is a spare-time project primarily developed by [Christian Schwarz](https://cschwarz.com).
Express your support through a donation to keep maintenance and feature development going.
[](https://patreon.com/zrepl) [](https://github.com/sponsors/problame) [](https://liberapay.com/zrepl/donate) [](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=R5QSXJVYHGX96)
[](https://patreon.com/zrepl)
[](https://github.com/sponsors/problame)
[](https://liberapay.com/zrepl/donate)
[](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=R5QSXJVYHGX96)
zrepl is a ZFS filesystem backup & replication solution written in Go.
zrepl is a one-stop ZFS backup & replication solution.
## User Documentation
**User Documentation** cab be found at [zrepl.github.io](https://zrepl.github.io).
**User Documentation** can be found at [zrepl.github.io](https://zrepl.github.io).
## Bug Reports
@@ -12,43 +22,207 @@ zrepl is a ZFS filesystem backup & replication solution written in Go.
## Feature Requests
1. Does you feature request require default values / some kind of configuration?
1. Does your feature request require default values / some kind of configuration?
If so, think of an expressive configuration example.
2. Think of at least one use case that generalizes from your concrete application.
3. Open an issue on GitHub with example conf & use case attached.
4.**Optional**: [Contact Christian Schwarz](https://cschwarz.com) for contract work.
The above does not apply if you already implemented everything.
Check out the *Coding Workflow* section below for details.
## Developer Documentation
## Development
### Overall Architecture
zrepl is written in [Go](https://golang.org) and uses [Go modules](https://github.com/golang/go/wiki/Modules) to manage dependencies.
The documentation is written in [ReStructured Text](http://docutils.sourceforge.net/rst.html) using the [Sphinx](https://www.sphinx-doc.org) framework.
The application architecture is documented as part of the user docs in the *Implementation* section (`docs/content/impl`).
Make sure to develop an understanding how zrepl is typically used by studying the user docs first.
### Building
### Project Structure
#### Go Code
```
├── cmd
│ ├── sampleconf # example configuration
├── docs
│ ├── content # hugo-based documentation -> sources for ./public_git
│ ├── deploy.sh # shell script for automated rendering & deploy to zrepl.github.io repo
│ ├── public_git # used by above shell script
│ └── themes
│ └── docdock # submodule of our docdock theme fork
├── jobrun # OBSOLETE
├── logger # logger package used by zrepl
├── rpc # rpc protocol implementation
├── scratchpad # small example programs demoing some internal packages. probably OBSOLETE
├── sshbytestream # io.ReadWriteCloser over SSH
├── util
└── zfs # ZFS wrappers, filesystemm diffing
Dependencies:
* Go
* GNU Make
* Git
* wget (`make generate`)
* unzip (`make generate`)
Some Go code is **generated**, and generated code is committed to the source tree.
Therefore, building does not require having code generation tools set up.
When making changes that require code to be (re-)generated, run `make generate`.
I downloads and installs pinned versions of the code generation tools into `./build/install`.
There is a CI check that ensures Git state is clean, i.e., code generation has been done by a PR and is deterministic.
#### Docs
Install [uv](https://docs.astral.sh/uv/getting-started/installation/), then run `make docs`.
uv automatically manages Python and dependencies.
### Testing
The **test suite** is split into pure **Go tests** (`make test-go`) and **platform tests** that interact with ZFS and thus generally **require root privileges** (`sudo make test-platform`).
Platform tests run on their own pool with the name `zreplplatformtest`, which is created using the file vdev in `/tmp`.
For a full **code coverage** profile, run `make test-go COVER=1 && sudo make test-platform && make cover-merge`.
An HTML report can be generated using `make cover-html`.
### Circle CI
We use CircleCI for automated build & test pre- and post-merge.
There are two workflows:
*`ci` runs for every commit / branch / tag pushed to GitHub.
It is supposed to run very fast (<5min and provides quick feedback to developers).
It runs formatting checks, lints and tests on the most important OSes / architectures.
*`release` runs
* on manual triggers through the CircleCI API (in order to produce a release)
* periodically on `master`
Artifacts are stored in CircleCI.
### Releasing
All zrepl releases are git-tagged and then published as a GitHub Release.
There is a git tag for each zrepl release, usually `vMAJOR.MINOR.0`.
We don't move git tags once the release has been published.
The procedure to issue a release is as follows:
* Prepare the release (as a PR to `master`):
* Finalize `docs/changelog.rst` for the release.
* Update the supporters list based on contributors for this release:
```bash
claude --permission-mode default '/update-supporters v0.6.1..v0.7.0'
```
Replace version tags with the appropriate range for your release.
* Merge the PR. Docs are auto-published to zrepl.github.io on merge.
* Tag the release:
* Git tag the release on the `master` branch (e.g., `vMAJOR.MINOR.0`).
```
make tag-release ZREPL_TAG_VERSION=v0.7.0
```
* Push the tag.
* Build and publish:
* Run the `release` pipeline against the `master` branch (trigger via CircleCI UI).
This URL: https://app.circleci.com/pipelines/github/zrepl/zrepl?branch=master.
Example pipeline: https://app.circleci.com/pipelines/github/zrepl/zrepl/8547
* Download artifacts using this handy makefile target.
Note: `JOB_NUM` must be the **job number** from the `release-upload` job, **not the pipeline number**.
- `go get -tags tools -u example.com/tool ; go mod tidy`
- review whether we're ready to switch to `go tool`: https://github.com/zrepl/zrepl/pull/909
* Open an issue when starting to hack on a new feature
* Commits should reference the issue they are related to
* Docs improvements not documenting new features do not require an issue.
Now run `make generate`.
Run `make lint` and `make vet`.
Update the CI configuration `.circleci/config.yml`:
- Update Go version references (we reference the minimum and max supported version)
- Set `Makefile` `RELEASE_GOVERSION` to the new Go version
- Update the pinned ZFS release tags in the `platformtest` matrix (`zfs_release` parameter) to the latest patch releases of each OpenZFS branch (currently 2.2, 2.3, 2.4)
- Update the pinned Ubuntu machine image for `platformtest` (e.g. `ubuntu-2404:2025.09.1`) — this is pinned rather than `current` so the ZFS build cache stays valid across runs
Update docs build tooling:
- Update `uv` version in `.circleci/config.yml` (search for `astral.sh/uv/` and cache keys containing the version)
- Check if there's now a CircleCI orb for uv that we could use
- Update version constraints in `pyproject.toml` and the `uv` lockfile (see [uv docs on dependencies](https://docs.astral.sh/uv/concepts/projects/dependencies/)):
- Test locally with `make docs`
Kick a full CI pipeline run (`do_ci=true` and `do_release=true`).
Merge PR with merge commit.
## Notes to Distro Package Maintainers
* The `Makefile` in this project is not suitable for builds in distros.
* Run the platform tests (Docs -> Usage -> Platform Tests) **on a test system** to validate that zrepl's abstractions on top of ZFS work with the system ZFS.
* Ship a default config that adheres to your distro's `hier` and logging system.
* Ship a service manager file and _please_ try to upstream it to this repository.
* `dist/systemd` contains a Systemd unit template.
* Ship other material provided in `./dist`, e.g. in `/usr/share/zrepl/`.
* Have a look at the `Makefile`'s `ZREPL_VERSION` variable and how it passed to Go's `ldFlags`.
This is how `zrepl version` knows what version number to show.
Your build system should set the `ldFlags` flags appropriately and add a prefix or suffix that indicates that the given zrepl binary is a distro build, not an official one.
* Make sure you are informed about new zrepl versions, e.g. by subscribing to GitHub's release RSS feed.
d="m 182.98145,31.572266 q -4.0625,0 -5.85938,-2.109375 -1.77734,-2.070313 -1.77734,-7.011719 V 5.4199219 h -5.80078 V 1.0253906 h 11.52343 V 22.451172 q 0,2.558594 0.80078,3.652344 0.78125,1.074218 2.71485,1.074218 h 4.57031 v 4.394532 z"
d="m 26.768151,290.81306 q 0.251492,0 0.444418,0.0413 l -0.07579,0.57188 q -0.21015,-0.0482 -0.409966,-0.0482 -0.299723,0 -0.558105,0.16537 -0.254937,0.16536 -0.399631,0.45819 -0.144694,0.28939 -0.144694,0.64424 v 2.01882 h -0.620118 v -3.78271 h 0.509874 l 0.0689,0.69246 h 0.02756 q 0.217041,-0.37551 0.513319,-0.56844 0.296279,-0.19292 0.644233,-0.19292 z"
d="m 48.427466,294.22715 q 0.120578,0 0.282498,-0.0241 0.161919,-0.0241 0.244602,-0.0551 v 0.47542 q -0.08613,0.0379 -0.268718,0.0723 -0.179145,0.0379 -0.36518,0.0379 -1.109321,0 -1.109321,-1.16788 v -2.20142 h -0.53399 v -0.29628 l 0.54088,-0.24804 0.248047,-0.80616 h 0.368625 v 0.86817 h 1.092096 v 0.48231 h -1.092096 v 2.18419 q 0,0.32729 0.15503,0.50299 0.158474,0.1757 0.437527,0.1757 z"
d="m 58.380348,294.66467 v -2.41845 q 0,-0.46165 -0.206706,-0.68558 -0.20326,-0.22737 -0.640787,-0.22737 -0.582222,0 -0.847494,0.32383 -0.261827,0.3204 -0.261827,1.04731 v 1.96026 h -0.620117 v -3.78271 h 0.499538 l 0.09302,0.51676 h 0.03445 q 0.1757,-0.27905 0.492649,-0.43063 0.316948,-0.15503 0.699354,-0.15503 0.695909,0 1.033529,0.33762 0.337619,0.33761 0.337619,1.05075 v 2.46324 z"
* |feature| Add configurable timezone for snapshot name timestamps. The default remains UTC. (:commit:`b9b9ad1`, thanks, `@mjasnik <https://github.com/mjasnik>`_).
* |feature| Improve Grafana dashboard with additional panels and refinements (:commit:`3f75127`, thanks, `@deajan <https://github.com/deajan>`_).
* |bugfix| Fix replication of placeholder filesystems (:commit:`5615f49`).
* |bugfix| Avoid using the word "error" in info-level log lines about ZFS command execution (:commit:`d3b2295`, thanks, `@bakhtiyarneyman <https://github.com/bakhtiyarneyman>`_).
* |bugfix| Detect duplicate and internal job names at config parse time.
zrepl will now refuse to load a config with duplicate or internally reserved job names, instead of failing later at daemon startup.
* |docs| Improve documentation on ``send_properties`` configuration and NFS/SMB considerations (:commit:`b5d8538`, thanks, `@Malvineous <https://github.com/Malvineous>`_).
* |docs| Warn more prominently about the risks of the ``not_replicated`` keep rule (:commit:`affe00a`, thanks, `@wxiaoguang <https://github.com/wxiaoguang>`_).
* |feature| zrepl status: use a ``*`` to indicate which filesystem is currently replicating
* |feature| include daemon environment variables in zrepl status (currently only in ``--raw``)
* |bugfix| **fix encrypt-on-receive + placeholders use case** (:issue:`504`)
* Before this fix, **plain sends** to a receiver with an encrypted ``root_fs``**could be received unencrypted** if zrepl needed to create placeholders on the receiver.
* Existing zrepl users should :ref:`read the docs <job-recv-options--placeholder>` and check ``zfs get -r encryption,zrepl:placeholder PATH_TO_ROOTFS`` on the receiver.
* Thanks to `@mologie <https://github.com/mologie>`_ and `@razielgn <https://github.com/razielgn>`_ for reporting and testing!
* |bugfix| Rename mis-spelled :ref:`send option <job-send-options>```embbeded_data`` to ``embedded_data``.
* |bugfix| zrepl status: replication step numbers should start at 1
* |bugfix| incorrect bandwidth averaging in ``zrepl status``.
* |bugfix| FreeBSD with OpenZFS 2.0: zrepl would wait indefinitely for zfs send to exit on timeouts.
* |bugfix| fix ``strconv.ParseInt: value out of range`` bug (and use the control RPCs).
* |docs| improve description of multiple pruning rules.
* |docs| quickstart: make users aware that prune rules apply to all snapshots.
* |maint| some platformtests were broken.
* |maint| FreeBSD: release armv7 and arm64 binaries.
* |maint| apt repo: update instructions due to ``apt-key`` deprecation.
Note to all users: please read up on the following OpenZFS bugs, as you might be affected:
*`ZFS send/recv with ashift 9->12 leads to data corruption <https://github.com/openzfs/zfs/issues/12762>`_.
* Various bugs with encrypted send/recv (`Leadership meeting notes <https://openzfs.topicbox.com/groups/developer/T24bdaa2886c6cbf5-Mc039a11c3f1507ea0664817b/december-openzfs-leadership-meeting>`_)
Finally, I'd like to point you to the `GitHub discussion <https://github.com/zrepl/zrepl/discussions/547>`_ about which bugfixes and features should be prioritized in zrepl 0.6 and beyond!
0.4.0
-----
* |feature| support setting zfs send / recv flags in the config (send: ``-wLcepbS`` , recv: ``-ox`` ).
Config docs :ref:`here <job-send-options>` and :ref:`here <job-recv-options>` .
* |feature| parallel replication is now configurable (disabled by default, :ref:`config docs here <replication-option-concurrency>` ).
* |feature| New ``zrepl status`` UI:
* Interactive job selection.
* Interactively ``zrepl signal`` jobs.
* Filter filesystems in the job view by name.
* An approximation of the old UI is still included as `--mode legacy` but will be removed in a future release of zrepl.
* |bugfix| Actually use concurrency when listing zrepl abstractions & doing size estimation.
These operations were accidentally made sequential in zrepl 0.3.
* |bugfix| Job hang-up during second replication attempt.
* |bugfix| Data races conditions in the dataconn rpc stack.
* |maint| Update to protobuf v1.25 and grpc 1.35.
For users who skipped the 0.3.1 update: please make sure your pruning grid config is correct.
The following bugfix in 0.3.1 :issue:`caused problems for some users <400>`:
* |bugfix| pruning: ``grid``: add all snapshots that do not match the regex to the rule's destroy list.
0.3.1
-----
Mostly a bugfix release for :ref:`zrepl 0.3 <release-0.3>`.
* |feature| pruning: add optional ``regex`` field to ``last_n`` rule
* |docs| pruning: ``grid`` : improve documentation and add an example
* |bugfix| pruning: ``grid``: add all snapshots that do not match the regex to the rule's destroy list.
This brings the implementation in line with the docs.
* |bugfix| ``easyrsa`` script in docs
* |bugfix| platformtest: fix skipping encryption-only tests on systems that don't support encryption
* |bugfix| replication: report AttemptDone if no filesystems are replicated
* |feature| status + replication: warning if replication succeeeded without any filesystem being replicated
* Continuous deployment of that new `stable` branch to zrepl.github.io.
.._release-0.3:
0.3
---
This is a big one! Headlining features:
***Resumable Send & Recv Support**
No knobs required, automatically used where supported.
***Encrypted Send & Recv Support** for OpenZFS native encryption,
:ref:`configurable <job-send-options>` at the job level, i.e., for all filesystems a job is responsible for.
***Replication Guarantees**
Automatic use of ZFS holds and bookmarks to protect a replicated filesystem from losing synchronization between sender and receiver.
By default, zrepl guarantees that incremental replication will always be possible and interrupted steps will always be resumable.
..TIP::
We highly recommend studying the updated :ref:`overview section of the configuration chapter <overview-how-replication-works>` to understand how replication works.
..TIP::
Go 1.15 changed the default TLS validation policy to **require Subject Alternative Names (SAN) in certificates**.
The openssl commands we provided in the quick-start guides up to and including the zrepl 0.3 docs seem not to work properly.
If you encounter certificate validation errors regarding SAN and wish to continue to use your old certificates, start the zrepl daemon with env var ``GODEBUG=x509ignoreCN=0``.
Alternatively, generate new certificates with SANs (see :ref:`both options int the TLS transport docs <transport-tcp+tlsclientauth-certgen>` ).
Quick-start guides:
* We have added :ref:`another quick-start guide for a typical workstation use case for zrepl <quickstart-backup-to-external-disk>`.
Check it out to learn how you can use zrepl to back up your workstation's OpenZFS natively-encrypted root filesystem to an external disk.
Additional changelog:
* |break| Go 1.15 TLS changes mentioned above.
* |break| |break_config| **more restrictive job names than in prior zrepl versions**
Starting with this version, job names are going to be embedded into ZFS holds and bookmark names (see :ref:`this section for details <zrepl-zfs-abstractions>`).
Therefore you might need to adjust your job names.
**Note that jobs** cannot be renamed easily **once you start using zrepl 0.3.**
* zrepl now manages the :ref:`replication cursor bookmark <zrepl-zfs-abstractions>` per job-filesystem tuple instead of a single replication cursor per filesystem.
In the future, this will permit multiple sending jobs to send from the same filesystems.
* ZFS does not allow bookmark renaming, thus we cannot migrate the old replication cursors.
* zrepl 0.3 will automatically create cursors in the new format for new replications, and warn if it still finds ones in the old format.
* Run ``zrepl migrate replication-cursor:v1-v2`` to safely destroy old-format cursors.
The migration will ensure that only those old-format cursors are destroyed that have been superseeded by new-format cursors.
* |feature| New option ``listen_freebind`` (tcp, tls, prometheus listener)
* A significant reduction or sudden stall of the receive rate (e.g. recv pool has other I/O to do)
would cause a ``writev I/O timeout`` error after approximately ten seconds.
0.1
---
This release is a milestone for zrepl and required significant refactoring if not rewrites of substantial parts of the application.
It breaks both configuration and transport format, and thus requires manual intervention and updates on both sides of a replication setup.
..DANGER::
The changes in the pruning system for this release require you to explicitly define **keep rules**:
for any snapshot that you want to keep, at least one rule must match.
This is different from previous releases where pruning only affected snapshots with the configured snapshotting prefix.
Make sure that snapshots to be kept or ignored by zrepl are covered, e.g. by using the ``regex`` keep rule.
:ref:`Learn more in the config docs... <prune>`
Notes to Package Maintainers
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* Notify users about config changes and migrations (see changes attributed with |break| and |mig| below)
* If the daemon crashes, the stack trace produced by the Go runtime and possibly diagnostic output of zrepl will be written to stderr.
This behavior is independent from the ``stdout`` outlet type.
Please make sure the stderr output of the daemon is captured somewhere.
To conserve precious stack traces, make sure that multiple service restarts do not directly discard previous stderr output.
* Make it obvious for users how to set the ``GOTRACEBACK`` environment variable to ``GOTRACEBACK=crash``.
This functionality will cause SIGABRT on panics and can be used to capture a coredump of the panicking process.
To that extend, make sure that your package build system, your OS's coredump collection and the Go delve debugger work together.
Use your build system to package the Go program in `this tutorial on Go coredumps and the delve debugger <https://rakyll.org/coredumps/>`_ , and make sure the symbol resolution etc. work on coredumps captured from the binary produced by your build system. (Special focus on symbol stripping, etc.)
* Consider using the ``zrepl configcheck`` subcommand in startup scripts to abort a restart that would fail due to an invalid config.
* The :ref:`placeholder property <replication-placeholder-property>` now uses ``on|off`` as values
instead of hashes of the dataset path. This permits renames of the sink filesystem without
updating all placeholder properties.
* Relevant for 0.0.X-0.1-rc* to 0.1 migrations
* Make sure your config is valid with ``zrepl configcheck``
* Run ``zrepl migrate 0.0.X:0.1:placeholder``
* |feature| :issue:`55` : Push replication (see :ref:`push job <job-push>` and :ref:`sink job <job-sink>`)
* |feature| :ref:`TCP Transport <transport-tcp>`
* |feature| :ref:`TCP + TLS client authentication transport <transport-tcp+tlsclientauth>`
* |feature| :issue:`111`: RPC protocol rewrite
* |break| Protocol breakage; Update and restart of all zrepl daemons is required.
* Use `gRPC <https://grpc.io/>`_ for control RPCs and a custom protocol for bulk data transfer.
* Automatic retries for network-temporary errors
* Limited to errors during replication for this release.
Addresses the common problem of ISP-forced reconnection at night, but will become
way more useful with resumable send & recv support.
Pruning errors are handled per FS, i.e., a prune RPC is attempted at least once per FS.
* |feature| Proper timeout handling for the :ref:`SSH transport <transport-ssh+stdinserver>`
* |break| Requires Go 1.11 or later.
* |break| |break_config|: mappings are no longer supported
* Receiving sides (``pull`` and ``sink`` job) specify a single ``root_fs``.
Received filesystems are then stored *per client* in ``${root_fs}/${client_identity}``.
See :ref:`job-overview` for details.
* |feature| |break| |break_config| Manual snapshotting + triggering of replication
* |feature| :issue:`69`: include manually created snapshots in replication
* |break_config| ``manual`` and ``periodic``:ref:`snapshotting types <job-snapshotting-spec>`
* |feature| ``zrepl signal wakeup JOB`` subcommand to trigger replication + pruning
* |feature| ``zrepl signal reset JOB`` subcommand to abort current replication + pruning
* |feature| |break| |break_config| New pruning system
* The active side of a replication (pull or push) decides what to prune for both sender and receiver.
The RPC protocol is used to execute the destroy operations on the remote side.
* New pruning policies (see :ref:`configuration documentation <prune>` )
* The decision what snapshots shall be pruned is now made based on *keep rules*
* |feature| :issue:`68`: keep rule ``not_replicated`` prevents divergence of sender and receiver
* |feature| |break| Bookmark pruning is no longer necessary
* Per filesystem, zrepl creates a single bookmark (``#zrepl_replication_cursor``) and moves it forward with the most recent successfully replicated snapshot on the receiving side.
* Old bookmarks created by prior versions of zrepl (named like their corresponding snapshot) must be deleted manually.
* |break_config| ``keep_bookmarks`` parameter of the ``grid`` keep rule has been removed
initial_replication: most_recent | all | fail # default: most_recent
...
.._conflict_resolution-initial_replication:
``initial_replication`` option
------------------------------
The ``initial_replication`` option determines how many snapshots zrepl replicates if the filesystem has not been replicated before.
If ``most_recent`` (the default), the initial replication will only transfer the most recent snapshot, while ignoring previous snapshots.
If all snapshots should be replicated, specify ``all``.
Use ``fail`` to make replication of the filesystem fail in case there is no corresponding fileystem on the receiver.
For example, suppose there are snapshosts ``tank@1``, ``tank@2``, ``tank@3`` on a sender.
Then ``most_recent`` will replicate just ``@3``, but ``all`` will replicate ``@1``, ``@2``, and ``@3``.
If initial replication is interrupted, and there is at least one (maybe partial) snapshot on the receiver, zrepl will always resume in **incremental mode**.
And that is regardless of where the initial replication was interrupted.
For example, if ``initial_replication: all`` and the transfer of ``@1`` is interrupted, zrepl would retry/resume at ``@1``.
And even if the user changes the config to ``initial_replication: most_recent`` before resuming, **incremental mode** will still resume at ``@1``.
- unique name of the job :issue:`(must not change)<327>`
* - ``connect``
- |connect-transport|
* - ``filesystems``
- |filter-spec| for filesystems to be snapshotted and pushed to the sink
* - ``send``
- |send-options|
* - ``snapshotting``
- |snapshotting-spec|
* - ``pruning``
- |pruning-spec|
* - ``replication``
- |replication-options|
* - ``conflict_resolution``
- |conflict-resolution-options|
Example config: :sampleconf:`/push.yml`
.._job-sink:
Job Type ``sink``
-----------------
..list-table::
:widths:20 80
:header-rows:1
* - Parameter
- Comment
* - ``type``
- = ``sink``
* - ``name``
- unique name of the job :issue:`(must not change)<327>`
* - ``serve``
- |serve-transport|
* - ``root_fs``
- ZFS filesystems are received to
``$root_fs/$client_identity/$source_path``
Example config: :sampleconf:`/sink.yml`
.._job-pull:
Job Type ``pull``
-----------------
..list-table::
:widths:20 80
:header-rows:1
* - Parameter
- Comment
* - ``type``
- = ``pull``
* - ``name``
- unique name of the job :issue:`(must not change)<327>`
* - ``connect``
- |connect-transport|
* - ``root_fs``
- ZFS filesystems are received to
``$root_fs/$source_path``
* - ``interval``
- | Interval at which to pull from the source job (e.g. ``10m``).
| ``manual`` disables periodic pulling, replication then only happens on :ref:`wakeup <cli-signal-wakeup>`.
* - ``pruning``
- |pruning-spec|
* - ``replication``
- |replication-options|
* - ``conflict_resolution``
- |conflict-resolution-options|
Example config: :sampleconf:`/pull.yml`
.._job-source:
Job Type ``source``
-------------------
..list-table::
:widths:20 80
:header-rows:1
* - Parameter
- Comment
* - ``type``
- = ``source``
* - ``name``
- unique name of the job :issue:`(must not change)<327>`
* - ``serve``
- |serve-transport|
* - ``filesystems``
- |filter-spec| for filesystems to be snapshotted and exposed to connecting clients
* - ``send``
- |send-options|
* - ``snapshotting``
- |snapshotting-spec|
Example config: :sampleconf:`/source.yml`
.._replication-local:
Local replication
-----------------
If you have the need for local replication (most likely between two local storage pools), you can use the :ref:`local transport type <transport-local>` to connect a local push job to a local sink job.
Example config: :sampleconf:`/local.yml`.
.._job-snap:
Job Type ``snap`` (snapshot & prune only)
-----------------------------------------
Job type that only takes snapshots and performs pruning on the local machine.
..list-table::
:widths:20 80
:header-rows:1
* - Parameter
- Comment
* - ``type``
- = ``snap``
* - ``name``
- unique name of the job :issue:`(must not change)<327>`
Monitoring endpoints are configured in the ``global.monitoring`` section of the config file.
.._monitoring-prometheus:
Prometheus & Grafana
--------------------
zrepl can expose `Prometheus metrics <https://prometheus.io/docs/instrumenting/exposition_formats/>`_ via HTTP.
The ``listen`` attribute is a `net.Listen <https://golang.org/pkg/net/#Listen>`_ string for tcp, e.g. ``:9811`` or ``127.0.0.1:9811`` (port 9811 was reserved to zrepl `on the official list <https://github.com/prometheus/prometheus/wiki/Default-port-allocations/_compare/43e495dd251ee328ac0d08b58084665b5c0f7a7e...459195059b55b414193ebeb80c5ba463d2606951>`_).
The ``listen_freebind`` attribute is :ref:`explained here <listen-freebind-explanation>`.
The Prometheus monitoring job appears in the ``zrepl control`` job list and may be specified **at most once**.
zrepl also ships with an importable `Grafana <https://grafana.com>`_ dashboard that consumes the Prometheus metrics:
see :repomasterlink:`dist/grafana`.
The dashboard also contains some advice on which metrics are important to monitor.
..NOTE::
At the time of writing, there is no stability guarantee on the exported metrics.
The active side (:ref:`push <job-push>` and :ref:`pull <job-pull>` job) executes the replication and pruning logic:
1. Wakeup after snapshotting (``push`` job) or pull interval ticker (``pull`` job).
2. Connect to the passive side and instantiate an RPC client.
3. Replicate data from the sender to the receiver.
4. Prune on sender & receiver.
..TIP::
The progress of the active side can be watched live using ``zrepl status``.
.._overview-passive-side--client-identity:
How the Passive Side Works
--------------------------
The passive side (:ref:`sink <job-sink>` and :ref:`source <job-source>`) waits for connections from the active side,
on the :ref:`transport <transport>` specified with ``serve`` in the job configuration.
The respective transport then perfoms authentication & authorization, resulting in a stable *client identity*.
The passive side job uses this *client identity* as follows:
* In ``sink`` jobs, to map requests from different *client identities* to their respective sub-filesystem tree ``root_fs/${client_identity}``.
**In the future, ``source`` might embed the client identity in :ref:`zrepl's ZFS abstraction names <zrepl-zfs-abstractions>`, to support multi-host replication.*
..TIP::
The use of the client identity in the ``sink`` job implies that it must be usable as a ZFS ZFS filesystem name component.
.._overview-how-replication-works:
How Replication Works
---------------------
One of the major design goals of the replication module is to avoid any duplication of the nontrivial logic.
As such, the code works on abstract senders and receiver **endpoints**, where typically one will be implemented by a local program object and the other is an RPC client instance.
Regardless of push- or pull-style setup, the logic executes on the active side, i.e. in the ``push`` or ``pull`` job.
The following high-level steps take place during replication and can be monitored using ``zrepl status``:
* Plan the replication:
* Compare sender and receiver filesystem snapshots
* Build the **replication plan**
* Per filesystem, compute a diff between sender and receiver snapshots
* Build a list of **replication steps**
* If possible, use incremental and resumable sends
* Otherwise, use full send of most recent snapshot on sender
* Retry on errors that are likely temporary (i.e. network failures).
* Give up on filesystems where a permanent error was received over RPC.
* Execute the plan
* Perform replication steps in the following order:
Among all filesystems with pending replication steps, pick the filesystem whose next replication step's snapshot is the oldest.
* Create placeholder filesystems on the receiving side to mirror the dataset paths on the sender to ``root_fs/${client_identity}``.
* Acquire send-side *step-holds* on the step's `from` and `to` snapshots.
* Perform the replication step.
* Move the **replication cursor** bookmark on the sending side (see below).
* Move the **last-received-hold** on the receiving side (see below).
* Release the send-side step-holds.
The idea behind the execution order of replication steps is that if the sender snapshots all filesystems simultaneously at fixed intervals, the receiver will have all filesystems snapshotted at time ``T1`` before the first snapshot at ``T2 = T1 + $interval`` is replicated.
ZFS Background Knowledge
^^^^^^^^^^^^^^^^^^^^^^^^
This section gives some background knowledge about ZFS features that zrepl uses to provide guarantees for a replication filesystem.
Specifically, zrepl guarantees by default that **incremental replication is always possible and that started replication steps can always be resumed if they are interrupted.**
**ZFS Send Modes & Bookmarks**
ZFS supports full sends (``zfs send fs@to``) and incremental sends (``zfs send -i @from fs@to``).
Full sends are used to create a new filesystem on the receiver with the send-side state of ``fs@to``.
Incremental sends only transfer the delta between ``@from`` and ``@to``.
Incremental sends require that ``@from`` be present on the receiving side when receiving the incremental stream.
Incremental sends can also use a ZFS bookmark as *from* on the sending side (``zfs send -i #bm_from fs@to``), where ``#bm_from`` was created using ``zfs bookmark fs@from fs#bm_from``.
The receiving side must always have the actual snapshot ``@from``, regardless of whether the sending side uses ``@from`` or a bookmark of it.
.._zfs-background-knowledge-plain-vs-raw-sends:
**Plain and raw sends**
By default, ``zfs send`` sends the most generic, backwards-compatible data stream format (so-called 'plain send').
If the sent uses newer features, e.g. compression or encryption, ``zfs send`` has to un-do these operations on the fly to produce the plain send stream.
If the receiver uses newer features (e.g. compression or encryption inherited from the parent FS), it applies the necessary transformations again on the fly during ``zfs recv``.
Flags such as ``-e``, ``-c`` and ``-L`` tell ZFS to produce a send stream that is closer to how the data is stored on disk.
Sending with those flags removes computational overhead from sender and receiver.
However, the receiver will not apply certain transformations, e.g., it will not compress with the receive-side ``compression`` algorithm.
The ``-w`` (``--raw``) flag produces a send stream that is as *raw* as possible.
For unencrypted datasets, its current effect is the same as ``-Lce``.
Encrypted datasets can only be sent plain (unencrypted) or raw (encrypted) using the ``-w`` flag.
**Resumable Send & Recv**
The ``-s`` flag for ``zfs recv`` tells zfs to save the partially received send stream in case it is interrupted.
To resume the replication, the receiving side filesystem's ``receive_resume_token`` must be passed to a new ``zfs send -t <value> | zfs recv`` command.
A full send can only be resumed if ``@to`` still exists.
An incremental send can only be resumed if ``@to`` still exists *and* either ``@from`` still exists *or* a bookmark ``#fbm`` of ``@from`` still exists.
**ZFS Holds**
ZFS holds prevent a snapshot from being deleted through ``zfs destroy``, letting the destroy fail with a ``datset is busy`` error.
Holds are created and referred to by a *tag*. They can be thought of as a named, persistent lock on the snapshot.
.._zrepl-zfs-abstractions:
ZFS Abstractions Managed By zrepl
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
With the background knowledge from the previous paragraph, we now summarize the different on-disk ZFS objects that zrepl manages to provide its functionality.
.._replication-placeholder-property:
**Placeholder filesystems** on the receiving side are regular ZFS filesystems with the ZFS property ``zrepl:placeholder=on``.
Placeholders allow the receiving side to mirror the sender's ZFS dataset hierarchy without replicating every filesystem at every intermediary dataset path component.
Consider the following example: ``S/H/J`` shall be replicated to ``R/sink/job/S/H/J``, but neither ``S/H`` nor ``S`` shall be replicated.
ZFS requires the existence of ``R/sink/job/S`` and ``R/sink/job/S/H`` in order to receive into ``R/sink/job/S/H/J``.
Thus, zrepl creates the parent filesystems as placeholders on the receiving side.
If at some point ``S/H`` and ``S`` shall be replicated, the receiving side invalidates the placeholder flag automatically.
The ``zrepl test placeholder`` command can be used to check whether a filesystem is a placeholder.
.._replication-cursor-and-last-received-hold:
The **replication cursor** bookmark and **last-received-hold** are managed by zrepl to ensure that future replications can always be done incrementally.
The replication cursor is a send-side bookmark of the most recent successfully replicated snapshot,
and the last-received-hold is a hold of that snapshot on the receiving side.
Both are moved atomically after the receiving side has confirmed that a replication step is complete.
The replication cursor has the format ``#zrepl_CUSOR_G_<GUID>_J_<JOBNAME>``.
The last-received-hold tag has the format ``zrepl_last_received_J_<JOBNAME>``.
Encoding the job name in the names ensures that multiple sending jobs can replicate the same filesystem to different receivers without interference.
.._tentative-replication-cursor-bookmarks:
**Tentative replication cursor bookmarks** are short-lived bookmarks that protect the atomic moving-forward of the replication cursor and last-received-hold (see :issue:`this issue <340>`).
They are only necessary if step holds are not used as per the :ref:`replication.protection <replication-option-protection>` setting.
The tentative replication cursor has the format ``#zrepl_CUSORTENTATIVE_G_<GUID>_J_<JOBNAME>``.
The ``zrepl zfs-abstraction list`` command provides a listing of all bookmarks and holds managed by zrepl.
.._step-holds:
**Step holds** are zfs holds managed by zrepl to ensure that a replication step can always be resumed if it is interrupted, e.g., due to network outage.
zrepl creates step holds before it attempts a replication step and releases them after the receiver confirms that the replication step is complete.
For an initial replication ``full @initial_snap``, zrepl puts a zfs hold on ``@initial_snap``.
For an incremental send ``@from -> @to``, zrepl puts a zfs hold on both ``@from`` and ``@to``.
Note that ``@from`` is not strictly necessary for resumability -- a bookmark on the sending side would be sufficient --, but size-estimation in currently used OpenZFS versions only works if ``@from`` is a snapshot.
The hold tag has the format ``zrepl_STEP_J_<JOBNAME>``.
A job only ever has one active send per filesystem.
Thus, there are never more than two step holds for a given pair of ``(job,filesystem)``.
**Step bookmarks** are zrepl's equivalent for holds on bookmarks (ZFS does not support putting holds on bookmarks).
They are intended for a situation where a replication step uses a bookmark ``#bm`` as incremental ``from`` where ``#bm`` is not managed by zrepl.
To ensure resumability, zrepl copies ``#bm`` to step bookmark ``#zrepl_STEP_G_<GUID>_J_<JOBNAME>``.
If the replication is interrupted and ``#bm`` is deleted by the user, the step bookmark remains as an incremental source for the resumable send.
Note that zrepl does not yet support creating step bookmarks because the `corresponding ZFS feature for copying bookmarks <https://github.com/openzfs/zfs/pull/9571>`_ is not yet widely available .
Subscribe to zrepl :issue:`326` for details.
The ``zrepl zfs-abstraction list`` command provides a listing of all bookmarks and holds managed by zrepl.
..NOTE::
More details can be found in the design document :repomasterlink:`replication/design.md`.
Caveats With Complex Setups (More Than 2 Jobs or Machines)
Most users are served well with a single sender and a single receiver job.
This section documents considerations for more complex setups.
..ATTENTION::
Before you continue, make sure you have a working understanding of :ref:`how zrepl works <overview-how-replication-works>`
and :ref:`what zrepl does to ensure <zrepl-zfs-abstractions>` that replication between sender and receiver is always
possible without conflicts.
This will help you understand why certain kinds of multi-machine setups do not (yet) work.
..NOTE::
If you can't find your desired configuration, have questions or would like to see improvements to multi-job setups, please `open an issue on GitHub <https://github.com/zrepl/zrepl/issues/new>`_.
Multiple Jobs on One Machine
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
As a general rule, multiple jobs configured on one machine **must operate on disjoint sets of filesystems**.
Otherwise, concurrently running jobs might interfere when operating on the same filesystem.
On your setup, ensure that
* all ``filesystems`` filter specifications are disjoint
* no ``root_fs`` is a prefix or equal to another ``root_fs``
* For ``sink`` jobs, consider all possible ``root_fs/${client_identity}``.
* no ``filesystems`` filter matches any ``root_fs``
**Exceptions to the rule**:
* A ``snap`` and ``push`` job on the same machine can match the same ``filesystems``.
To avoid interference, only one of the jobs should be pruning snapshots on the sender, the other one should keep all snapshots.
Since the jobs won't coordinate, errors in the log are to be expected, but :ref:`zrepl's ZFS abstractions <zrepl-zfs-abstractions>` ensure that ``push`` and ``sink`` can always replicate incrementally.
This scenario is detailed in one of the :ref:`quick-start guides <quickstart-backup-to-external-disk>`.
Two Or More Machines
^^^^^^^^^^^^^^^^^^^^
This section might be relevant to users who wish to *fan-in* (N machines replicate to 1) or *fan-out* (replicate 1 machine to N machines).
**Working setups**:
***Fan-in: N servers replicated to one receiver, disjoint dataset trees.**
* This is the common use case of a centralized backup server.
* Implementation:
* N ``push`` jobs (one per sender server), 1 ``sink`` (as long as the different push jobs have a different :ref:`client identity <overview-passive-side--client-identity>`)
* N ``source`` jobs (one per sender server), N ``pull`` on the receiver server (unique names, disjoint ``root_fs``)
* The ``sink`` job automatically constrains each client to a disjoint sub-tree of the sink-side dataset hierarchy ``${root_fs}/${client_identity}``.
Therefore, the different clients cannot interfere.
* The ``pull`` job only pulls from one host, so it's up to the zrepl user to ensure that the different ``pull`` jobs don't interfere.
.._fan-out-replication:
***Fan-out: 1 server replicated to N receivers**
* Can be implemented either in a pull or push fashion.
* **pull setup**: 1 ``pull`` job on each receiver server, each with a corresponding **unique** ``source`` job on the sender server.
***push setup**: 1 ``sink`` job on each receiver server, each with a corresponding **unique**``push`` job on the sender server.
* It is critical that we have one sending-side job (``source``, ``push``) per receiver.
The reason is that :ref:`zrepl's ZFS abstractions <zrepl-zfs-abstractions>` (``zrepl zfs-abstraction list``) include the name of the ``source``/``push`` job, but not the receive-side job name or client identity (see :issue:`380`).
As a counter-example, suppose we used multiple ``pull`` jobs with only one ``source`` job.
All ``pull`` jobs would share the same :ref:`replication cursor bookmark <replication-cursor-and-last-received-hold>` and trip over each other, breaking incremental replication guarantees quickly.
The anlogous problem exists for 1 ``push`` to N ``sink`` jobs.
* The ``filesystems`` matched by the sending side jobs (``source``, ``push``) need not necessarily be disjoint.
For this to work, we need to avoid interference between snapshotting and pruning of the different sending jobs.
The solution is to centralize sender-side snapshot management in a separate ``snap`` job.
Snapshotting in the ``source``/``push`` job should then be disabled (``type: manual``).
And sender-side pruning (``keep_sender``) needs to be disabled in the active side (``pull`` / ``push``), since that'll be done by the ``snap job``.
***Restore limitations**: when restoring from one of the ``pull`` targets (e.g., using ``zfs send -R``), the replication cursor bookmarks don't exist on the restored system.
This can break incremental replication to all other receive-sides after restore.
* See :ref:`the fan-out replication quick-start guide <quickstart-fan-out-replication>` for an example of this setup.
**Setups that do not work**:
* N ``pull`` identities, 1 ``source`` job. Tracking :issue:`380`.
Pruning must happen on both sides of a replication or the systems would inevitably run out of disk space at some point.
Typically, the requirements to temporal resolution and maximum retention time differ per side.
For example, when using zrepl to back up a busy database server, you will want high temporal resolution (snapshots every 10 min) for the last 24h in case of administrative disasters, but cannot afford to store them for much longer because you might have high turnover volume in the database.
On the receiving side, you may have more disk space available, or need to comply with other backup retention policies.
zrepl uses a set of **keep rules** per sending and receiving side to determine which snapshots shall be kept per filesystem.
**A snapshot that is not kept by any rule is destroyed.**
The keep rules are **evaluated on the active side** (:ref:`push <job-push>` or :ref:`pull job <job-pull>`) of the replication setup, for both active and passive side, after replication completed or was determined to have failed permanently.
Example Configuration:
::
jobs:
- type: push
name: ...
connect: ...
filesystems: {
"<": true,
"tmp": false
}
snapshotting:
type: periodic
prefix: zrepl_
interval: 10m
pruning:
keep_sender:
- type: not_replicated
# make sure manually created snapshots by the administrator are kept
- type: regex
regex: "^manual_.*"
- type: grid
grid: 1x1h(keep=all) | 24x1h | 14x1d
regex: "^zrepl_.*"
keep_receiver:
- type: grid
grid: 1x1h(keep=all) | 24x1h | 35x1d | 6x30d
regex: "^zrepl_.*"
# manually created snapshots will be kept forever on receiver
- type: regex
regex: "^manual_.*"
..DANGER::
You might have **existing snapshots** of filesystems affected by pruning which you want to keep, i.e. not be destroyed by zrepl.
Make sure to actually add the necessary ``regex`` keep rules on both sides, like with ``manual`` in the example above.
.._prune-keep-not-replicated:
Policy ``not_replicated``
-------------------------
::
jobs:
- type: push
pruning:
keep_sender:
- type: not_replicated
...
``not_replicated`` keeps all snapshots that have not been replicated to the receiving side.
It only makes sense to specify this rule for the ``keep_sender``.
The reason is that, by definition, all snapshots on the receiver have already been replicated to there from the sender.
To determine whether a sender-side snapshot has already been replicated, zrepl uses the :ref:`replication cursor bookmark <replication-cursor-and-last-received-hold>` which corresponds to the most recent successfully replicated snapshot.
..ATTENTION::
If you use ``not_replicated`` in your pruning rules, make sure to :ref:`monitor <monitoring>` replication.
If your replication gets stuck then ``not_replicated`` causes snapshots to pile uip on the sender.
ZFS and especially the ``zfs`` management command are known to degrade in performance with a lot of snapshots.
Such degradation impacts zrepl, any other scripts, and human ability to manage your zpool.
.._prune-keep-retention-grid:
Policy ``grid``
---------------
::
jobs:
- type: pull
pruning:
keep_receiver:
- type: grid
regex: "^zrepl_.*"
grid: 1x1h(keep=all) | 24x1h | 35x1d | 6x30d
│ │ │
└─ 1 repetition of a one-hour interval with keep=all
│ │
└─ 24 repetitions of a one-hour interval with keep=1
│
└─ 6 repetitions of a 30-day interval with keep=1
...
The retention grid can be thought of as a time-based sieve that thins out snapshots as they get older.
The ``grid`` field specifies a list of adjacent time intervals.
Each interval is a bucket with a maximum capacity of ``keep`` snapshots.
The following procedure happens during pruning:
#. The list of snapshots is filtered by the regular expression in ``regex``.
Only snapshots names that match the regex are considered for this rule, all others will be pruned unless another rule keeps them.
#. The snapshots that match ``regex`` are placed onto a time axis according to their ``creation`` date.
The youngest snapshot is on the left, the oldest on the right.
#. The first buckets are placed "under" that axis so that the ``grid`` spec's first bucket's left edge aligns with youngest snapshot.
#. All subsequent buckets are placed adjacent to their predecessor bucket.
#. Now each snapshot on the axis either falls into one bucket or it is older than our rightmost bucket.
Buckets are left-inclusive and right-exclusive which means that a snapshot on the edge of bucket will always 'fall into the right one'.
#. Snapshots older than the rightmost bucket are **not kept** by the grid specification.
#. For each bucket, we only keep the ``keep`` oldest snapshots.
The syntax to describe the bucket list is as follows:
::
Repeat x Duration (keep=all)
* The **duration** specifies the length of the interval.
* The **keep** count specifies the number of snapshots that fit into the bucket.
It can be either a positive integer or ``all`` (all snapshots are kept).
* The **repeat** count repeats the bucket definition for the specified number of times.
**Example**:
::
Assume the following grid specification:
grid: 1x1h(keep=all) | 2x2h | 1x3h
This grid specification produces the following constellation of buckets:
``last_n`` filters the snapshot list by ``regex``, then keeps the last ``count`` snapshots in that list (last = youngest = most recent creation date)
All snapshots that don't match ``regex`` or exceed ``count`` in the filtered list are destroyed unless matched by other rules.
.._prune-keep-regex:
Policy ``regex``
----------------
::
jobs:
- type: push
pruning:
keep_receiver:
# keep all snapshots with prefix zrepl_ or manual_
- type: regex
regex: "^(zrepl|manual)_.*"
- type: push
snapshotting:
prefix: zrepl_
pruning:
keep_sender:
# keep all snapshots that were not created by zrepl
- type: regex
negate: true
regex: "^zrepl_.*"
``regex`` keeps all snapshots whose names are matched by the regular expression in ``regex``.
Like all other regular expression fields in prune policies, zrepl uses Go's `regexp.Regexp <https://golang.org/pkg/regexp/#Compile>`_ Perl-compatible regular expressions (`Syntax <https://golang.org/pkg/regexp/syntax>`_).
The optional `negate` boolean field inverts the semantics: Use it if you want to keep all snapshots that *do not* match the given regex.
.._prune-workaround-source-side-pruning:
Source-side snapshot pruning
----------------------------
A :ref:`source jobs<job-source>` takes snapshots on the system it runs on.
The corresponding :ref:`pull job <job-pull>` on the replication target connects to the source job and replicates the snapshots.
Afterwards, the pull job coordinates pruning on both sender (the source job side) and receiver (the pull job side).
There is no built-in way to define and execute pruning on the source side independently of the pull side.
The source job will continue taking snapshots which will not be pruned until the pull side connects.
This means that **extended replication downtime will fill up the source's zpool with snapshots**.
If the above is a conceivable situation for you, consider using :ref:`push mode <job-push>`, where pruning happens on the same side where snapshots are taken.
Workaround using ``snap`` job
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
As a workaround (see GitHub :issue:`102` for development progress), a pruning-only :ref:`snap job <job-snap>` can be defined on the source side:
The snap job is in charge of snapshot creation & destruction, whereas the source job's role is reduced to just serving snapshots.
However, since, jobs are run independently, it is possible that the snap job will prune snapshots that are queued for replication / destruction by the remote pull job that connects to the source job.
Symptoms of such race conditions are spurious replication and destroy errors.
The ``protection`` variable controls the degree to which a replicated filesystem is protected from getting out of sync through a zrepl pruner or external tools that destroy snapshots.
zrepl can guarantee :ref:`resumability <step-holds>` or just :ref:`incremental replication <replication-cursor-and-last-received-hold>`.
``guarantee_resumability`` is the **default** value and guarantees that a replication step is always resumable and that incremental replication will always be possible.
The implementation uses replication cursors, last-received-hold and step holds.
``guarantee_incremental`` only guarantees that incremental replication will always be possible.
If a step ``from -> to`` is interrupted and its `to` snapshot is destroyed, zrepl will remove the half-received ``to``'s resume state and start a new step ``from -> to2``.
The implementation uses replication cursors, tentative replication cursors and last-received-hold.
``guarantee_nothing`` does not make any guarantees with regards to keeping sending and receiving side in sync.
No bookmarks or holds are created to protect sender and receiver from diverging.
**Tradeoffs**
Using ``guarantee_incremental`` instead of ``guarantee_resumability`` obviously removes the resumability guarantee.
This means that replication progress is no longer monotonic which might lead to a replication setup that never makes progress if mid-step interruptions are too frequent (e.g. frequent network outages).
However, the advantage and :issue:`reason for existence <288>` of the ``incremental`` mode is that it allows the pruner to delete snapshots of interrupted replication steps
which is useful if replication happens so rarely (or fails so frequently) that the amount of disk space exclusively referenced by the step's snapshots becomes intolerable.
..NOTE::
When changing this flag, obsoleted zrepl-managed bookmarks and holds will be destroyed on the next replication step that is attempted for each filesystem.
.._replication-option-concurrency:
``concurrency`` option
----------------------
The ``concurrency`` options control the maximum amount of concurrency during replication.
The default values allow some concurrency during size estimation but no parallelism for the actual replication.
*``concurrency.steps`` (default = 1) controls the maximum number of concurrently executed :ref:`replication steps <overview-how-replication-works>`.
The planning step for each file system is counted as a single step.
*``concurrency.size_estimates`` (default = 4) controls the maximum number of concurrent step size estimations done by the job.
Note that initial replication cannot start replicating child filesystems before the parent filesystem's initial replication step has completed.
Some notes on tuning these values:
* Disk: Size estimation is less I/O intensive than step execution because it does not need to access the data blocks.
* CPU: Size estimation is usually a dense CPU burst whereas step execution CPU utilization is stretched out over time because of disk IO.
Faster disks, sending a compressed dataset in :ref:`plain mode <zfs-background-knowledge-plain-vs-raw-sends>` and the zrepl transport mode all contribute to higher CPU requirements.
* Network bandwidth: Size estimation does not consume meaningful amounts of bandwidth, step execution does.
*:ref:`zrepl ZFS abstractions <zrepl-zfs-abstractions>`: for each replication step zrepl needs to update its ZFS abstractions through the ``zfs`` command which often waits multiple seconds for the zpool to sync.
Thus, if the actual send & recv time of a step is small compared to the time spent on zrepl ZFS abstractions then increasing step execution concurrency will result in a lower overall turnaround time.
:ref:`Source<job-source>` and :ref:`push<job-push>` jobs have an optional ``send`` configuration section.
::
jobs:
- type: push
filesystems: ...
send:
# flags from the table below go here
...
The following table specifies the list of (boolean) options.
Flags with an entry in the ``zfs send`` column map directly to the zfs send CLI flags.
zrepl does not perform feature checks for these flags.
If you enable a flag that is not supported by the installed version of ZFS, the zfs error will show up at runtime in the logs and zrepl status.
See the `upstream man page <https://openzfs.github.io/openzfs-docs/man/8/zfs-send.8.html>`_ (``man zfs-send``) for their semantics.
..list-table::
:widths:20 10 70
:header-rows:1
* - ``send.``
-``zfs send``
- Comment
* - ``encrypted``
-
- Specific to zrepl, :ref:`see below <job-send-options-encrypted>`.
* - ``bandwidth_limit``
-
- Specific to zrepl, :ref:`see below <job-send-recv-options--bandwidth-limit>`.
* - ``raw``
- ``-w``
- Use ``encrypted`` to only allow encrypted sends. Mixed sends are not supported.
* - ``send_properties``
- ``-p``
- **Be careful**, read the :ref:`note on property replication below <job-note-property-replication>`.
* - ``backup_properties``
- ``-b``
- **Be careful**, read the :ref:`note on property replication below <job-note-property-replication>`.
* - ``large_blocks``
- ``-L``
- **Potential data loss on OpenZFS < 2.0**, see :ref:`warning below <job-send-options-large-blocks>`.
* - ``compressed``
- ``-c``
-
* - ``embedded_data``
- ``-e``
-
* - ``saved``
- ``-S``
-
.._job-send-options-encrypted:
``encrypted``
-------------
The ``encrypted`` option controls whether the matched filesystems are sent as `OpenZFS native encryption <http://open-zfs.org/wiki/ZFS-Native_Encryption>`_ raw sends.
More specifically, if ``encrypted=true``, zrepl
* checks for any of the filesystems matched by ``filesystems`` whether the ZFS ``encryption`` property indicates that the filesystem is actually encrypted with ZFS native encryption and
* invokes the ``zfs send`` subcommand with the ``-w`` option (raw sends) and
* expects the receiving side to support OpenZFS native encryption (recv will fail otherwise)
Filesystems matched by ``filesystems`` that are not encrypted are not sent and will cause error log messages.
If ``encrypted=false``, zrepl expects that filesystems matching ``filesystems`` are not encrypted or have loaded encryption keys.
..NOTE::
Use ``encrypted`` instead of ``raw`` to make your intent clear that zrepl must only replicate filesystems that are actually encrypted by OpenZFS native encryption.
It is meant as a safeguard to prevent unintended sends of unencrypted filesystems in raw mode.
.._job-send-options-properties:
``send_properties``
-------------------
Sends the dataset properties along with snapshots.
Please be careful with this option and read the :ref:`note on property replication below <job-note-property-replication>`.
.._job-send-options-backup-properties:
``backup_properties``
---------------------
When properties are modified on a filesystem that was received from a send stream with ``send.properties=true``, ZFS archives the original received value internally.
This also applies to :ref:`inheriting or overriding properties during zfs receive <job-recv-options--inherit-and-override>`.
When sending those received filesystems another hop, the ``backup_properties`` flag instructs ZFS to send the original property values rather than the current locally set values.
This is useful for replicating properties across multiple levels of backup machines.
**Example:**
Suppose we want to flow snapshots from Machine A to B, then from B to C.
A will enable the :ref:`properties send option <job-send-options-properties>`.
B will want to override :ref:`critical properties such as mountpoint or canmount <job-note-property-replication>`.
But the job that replicates from B to C should be sending the original property values received from A.
Thus, B sets the ``backup_properties`` option.
Please be careful with this option and read the :ref:`note on property replication below <job-note-property-replication>`.
.._job-send-options-large-blocks:
``large_blocks``
----------------
This flag should not be changed after initial replication.
Prior to `OpenZFS commit 7bcb7f08 <https://github.com/openzfs/zfs/pull/10383/files#diff-4c1e47568f46fb63546e984943b09a3e6b051e2242649523f7835bbdfe2a9110R337-R342>`_
it was possible to change this setting which resulted in **data loss on the receiver**.
The commit in question is included in OpenZFS 2.0 and works around the problem by prohibiting receives of incremental streams with a flipped setting.
..WARNING::
This bug has **not been fixed in the OpenZFS 0.8 releases** which means that changing this flag after initial replication might cause **data loss** on the receiver.
.._job-recv-options:
Recv Options
~~~~~~~~~~~~
:ref:`Sink<job-sink>` and :ref:`pull<job-pull>` jobs have an optional ``recv`` configuration section:
``override`` maps directly to the `zfs recv -o flag <https://openzfs.github.io/openzfs-docs/man/8/zfs-recv.8.html>`_.
Property name-value pairs specified in this map will apply to all received filesystems, regardless of whether the send stream contains properties or not.
``inherit`` maps directly to the `zfs recv -x flag <https://openzfs.github.io/openzfs-docs/man/8/zfs-recv.8.html>`_.
Property names specified in this list will be inherited from the receiving side's parent filesystem (e.g. ``root_fs``).
With both options, the sending side's property value is still stored on the receiver, but the local override or inherit is the one that takes effect.
You can send the original properties from the first receiver to another receiver using :ref:`send.backup_properties<job-send-options-backup-properties>`.
.._job-note-property-replication:
A Note on Property Replication
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If a send stream contains properties, as per ``send.send_properties`` or ``send.backup_properties``,
the default ZFS behavior is to use those properties on the receiving side, verbatim.
In many use cases for zrepl, this can have devastating consequences.
For example, when backing up a filesystem that has ``mountpoint=/`` to a storage server,
that storage server's root filesystem will be shadowed by the received file system on some platforms.
Also, many scripts and tools use ZFS user properties for configuration and do not check the property source (``local`` vs. ``received``).
If they are installed on the receiving side as well as the sending side, property replication could have unintended effects.
**zrepl currently does not provide any automatic safe-guards for property replication:**
* Make sure to read the entire man page on zfs recv (`man zfs recv <https://openzfs.github.io/openzfs-docs/man/8/zfs-recv.8.html>`_) before enabling this feature.
* Use ``recv.properties.override`` whenever possible, e.g. for ``mountpoint=none`` or ``canmount=off``.
* Use ``recv.properties.inherit`` if that makes more sense to you.
Below is an **non-exhaustive list of problematic properties**.
Please open a pull request if you find a property that is missing from this list.
(Both with regards to core ZFS tools and other software in the broader ecosystem.)
Mount behaviour
---------------
*``mountpoint``
*``canmount``
*``overlay``
Note: Before `OpenZFS 2.0.5 <https://github.com/openzfs/zfs/issues/11416>`_, inheriting or overriding the ``mountpoint`` property on ZVOLs fails in ``zfs recv``.
If you are on such an older version, consider creating separate zrepl jobs for your ZVOL and filesystem datasets.
Systemd
-------
With systemd, you should also consider the properties processed by the `zfs-mount-generator <https://manpages.debian.org/buster-backports/zfsutils-linux/zfs-mount-generator.8.en.html>`_ .
Most notably:
*``org.openzfs.systemd:ignore``
*``org.openzfs.systemd:wanted-by``
*``org.openzfs.systemd:required-by``
Encryption
----------
If the sender filesystems are encrypted but the sender does :ref:`plain sends <zfs-background-knowledge-plain-vs-raw-sends>`
and property replication is enabled, the receiver must :ref:`inherit the following properties<job-recv-options--inherit-and-override>`:
*``keylocation``
*``keyformat``
*``encryption``
Sharing
-------
You may not want the replicated filesystem shared in the same way as the source is.
*``sharenfs``
*``sharesmb``
.._job-recv-options--placeholder:
Placeholders
~~~~~~~~~~~~
::
placeholder:
encryption: unspecified | off | inherit
During replication, zrepl :ref:`creates placeholder datasets <replication-placeholder-property>` on the receiving side if the sending side's ``filesystems`` filter creates gaps in the dataset hierarchy.
This is generally fully transparent to the user.
However, with OpenZFS Native Encryption, placeholders require zrepl user attention.
Specifically, the problem is that, when zrepl attempts to create the placeholder dataset on the receiver, and that placeholder's parent dataset is encrypted, ZFS wants to inherit encryption to the placeholder.
This is relevant to two use cases that zrepl supports:
1.**encrypted-send-to-untrusted-receiver** In this use case, the sender sends an :ref:`encrypted send stream <job-send-options-encrypted>` and the receiver doesn't have the key loaded.
2.**send-plain-encrypt-on-receive** The receive-side ``root_fs`` dataset is encrypted, and the senders are unencrypted.
The key of ``root_fs`` is loaded, and the goal is that the plain sends (e.g., from production) are encrypted on-the-fly during receive, with ``root_fs``'s key.
For **encrypted-send-to-untrusted-receiver**, the placeholder datasets need to be created with ``-o encryption=off``.
Without it, creation would fail with an error, indicating that the placeholder's parent dataset's key needs to be loaded.
But we don't trust the receiver, so we can't expect that to ever happen.
However, for **send-plain-encrypt-on-receive**, we cannot set ``-o encryption=off``.
The reason is that if we did, any of the (non-placeholder) child datasets below the placeholder would inherit ``encryption=off``, thereby silently breaking our encrypt-on-receive use case.
So, to cover this use case, we need to create placeholders without specifying ``-o encryption``.
This will make ``zfs create`` inherit the encryption mode from the parent dataset, and thereby transitively from ``root_fs``.
The zrepl config provides the `recv.placeholder.encryption` knob to control this behavior.
In ``undefined`` mode (default), placeholder creation bails out and asks the user to configure a behavior.
In ``off`` mode, the placeholder is created with ``encryption=off``, i.e., **encrypted-send-to-untrusted-rceiver** use case.
In ``inherit`` mode, the placeholder is created without specifying ``-o encryption`` at all, i.e., the **send-plain-encrypt-on-receive** use case.
Common Options
~~~~~~~~~~~~~~
.._job-send-recv-options--bandwidth-limit:
Bandwidth Limit (send & recv)
-----------------------------
::
bandwidth_limit:
max: 23.5 MiB # -1 is the default and disabled rate limiting
bucket_capacity: # token bucket capacity in bytes; defaults to 128KiB
Both ``send`` and ``recv`` can be limited to a maximum bandwidth through ``bandwidth_limit``.
For most users, it should be sufficient to just set ``bandwidth_limit.max``.
The ``bandwidth_limit.bucket_capacity`` refers to the `token bucket size <https://github.com/juju/ratelimit>`_.
The bandwidth limit only applies to the payload data, i.e., the ZFS send stream.
It does not account for transport protocol overheads.
The scope is the job level, i.e., all :ref:`concurrent <replication-option-concurrency>` sends or incoming receives of a job share the bandwidth limit.
You can configure zrepl to take snapshots of the filesystems in the ``filesystems`` field specified in ``push``, ``source`` and ``snap`` jobs.
The following snapshotting types are supported:
..list-table::
:widths:20 70
:header-rows:1
* - ``snapshotting.type``
- Comment
* - ``periodic``
- Ensure that snapshots are taken at a particular interval.
* - ``cron``
- Use cron spec to take snapshots at particular points in time.
* - ``manual``
- zrepl does not take any snapshots by itself.
The ``periodic`` and ``cron`` snapshotting types share some common options and behavior:
***Naming:** The snapshot names are composed of a user-defined ``prefix`` followed by a UTC date formatted like ``20060102_150405_000`` by default.
We either use UTC or timestamp with timezone information because it will avoid name conflicts when switching time zones or between summer and winter time.
Note that if timestamp with time zone information is used, the "+" of the timezone (i.e. +02:00) is replaced by "_" (i.e. _02:00) to conform to allowed characters in ZFS snapshots.
***Hooks:** You can configure hooks to run before or after zrepl takes the snapshots. See :ref:`below <job-snapshotting-hooks>` for details.
***Push replication:** After creating all snapshots, the snapshotter will wake up the replication part of the job, if it's a ``push`` job.
Note that snapshotting is decoupled from replication, i.e., if it is down or takes too long, snapshots will still be taken.
Note further that other jobs are not woken up by snapshotting.
..NOTE::
There is **no concept of ownership** of the snapshots that are created by ``periodic`` or ``cron``.
Thus, there is no distinction between zrepl-created snapshots and user-created snapshots during replication or pruning.
In particular, pruning will take all snapshots into consideration by default.
To constrain pruning to just zrepl-created snapshots:
1. Assign a unique `prefix` to the snapshotter and
2. Use the ``regex`` functionality of the various pruning ``keep`` rules to just consider snapshots with that prefix.
There is currently no way to constrain replication to just zrepl-created snapshots.
Follow and comment at :issue:`403` if you need this functionality.
..NOTE::
The ``zrepl signal wakeup JOB`` subcommand does not trigger snapshotting.
``periodic`` Snapshotting
-------------------------
::
jobs:
- ...
filesystems: { ... }
snapshotting:
type: periodic
prefix: zrepl_
interval: 10m
# Timestamp format that is used as snapshot suffix.
# Can be any of "dense" (default), "human", "iso-8601", "unix-seconds" or a custom Go time format (see https://go.dev/src/time/format.go)
timestamp_format: dense
# Specifies in which time zone the snapshot suffix is generated, optional, defaults to UTC, used only if time zone information is part of timestamp_format.
# Can be "UTC" (default), "Local" (time zone information from the OS) or any of the IANA Time Zone names (see https://nodatime.org/TimeZones)
timestamp_location: UTC
hooks: ...
pruning: ...
The ``periodic`` snapshotter ensures that snapshots are taken in the specified ``interval``.
If you use zrepl for backup, this translates into your recovery point objective (RPO).
To meet your RPO, you still need to monitor that replication, which happens asynchronously to snapshotting, actually works.
It is desirable to get all ``filesystems`` snapshotted simultaneously because it results in a more consistent backup.
To accomplish this while still maintaining the ``interval``, the ``periodic`` snapshotter attempts to get the snapshotting rhythms in sync.
To find that sync point, the most recent snapshot, created by the snapshotter, in any of the matched ``filesystems`` is used.
A filesystem that does not have snapshots by the snapshotter has lower priority than filesystem that do, and thus might not be snapshotted (and replicated) until it is snapshotted at the next sync point.
The snapshotter uses the ``prefix`` to identify which snapshots it created.
# Timestamp format that is used as snapshot suffix.
# Can be any of "dense" (default), "human", "iso-8601", "unix-seconds" or a custom Go time format (see https://go.dev/src/time/format.go)
timestamp_format: dense
# Specifies in which time zone the snapshot suffix is generated, optional, defaults to UTC, used only if time zone information is part of timestamp_format.
# Can be "UTC" (default), "Local" (time zone information from the OS) or any of the IANA Time Zone names (see https://nodatime.org/TimeZones)
timestamp_location: UTC
pruning: ...
In ``cron`` mode, the snapshotter takes snaphots at fixed points in time.
See https://en.wikipedia.org/wiki/Cron for details on the syntax.
zrepl uses the ``the github.com/robfig/cron/v3`` Go package for parsing.
An optional field for "seconds" is supported to take snapshots at sub-minute frequencies.
``manual`` Snapshotting
-----------------------
::
jobs:
- type: push
snapshotting:
type: manual
...
In ``manual`` mode, zrepl does not take snapshots by itself.
Manual snapshotting is most useful if you have existing infrastructure for snapshot management.
Or, if you want to decouple snapshot management from replication using a zrepl ``snap`` job.
See :ref:`this quickstart guide <quickstart-backup-to-external-disk>` for an example.
To trigger replication after taking snapshots, use the ``zrepl signal wakeup JOB`` command.
.._job-snapshotting-timestamp_format:
Timestamp Format
----------------
The ``cron`` and ``periodic`` snapshotter support configuring a custom timestamp format that is used as suffix for the snapshot name.
By default, the current time is converted to UTC and then formatted using the ``dense`` format.
Both time zone and format can be customized by setting ``timestamp_format`` and/or ``timestamp_location``.
``timestamp_location`` is fed verbatim into `time.LoadLocation <https://pkg.go.dev/time#LoadLocation>`_.
``timestamp_format`` supports the following values (case-insensitive).
*``dense`` => Go format string ``20060102_150405_000``
*``human`` => Go format string ``2006-01-02_15:04:05``
*``iso-8601`` => Go format string ``2006-01-02T15:04:05.000Z07:00``
*``unix-seconds`` => Unix seconds since the epoch, formatted as a decimal string (`time.Time.Unix <https://pkg.go.dev/time#Time.Unix>`_)
* Any custom Go time format accepted by `time.Time#Format <https://go.dev/src/time/format.go>`_.
The ``dense``, ``human``, and ``unix-seconds`` formats require the ``timestamp_location`` to be ``UTC`` because they do not contain timezone information.
The ``+`` character is `not allowed in ZFS snapshot names <https://github.com/openzfs/zfs/blob/1d3ba0bf01020f5459b1c28db3979129088924c0/module/zcommon/zfs_namecheck.c#L334-L351>`_.
Format strings are disallowed to contain the ``+`` character (it's not a character that is interpreted by ``time.Time.Format``, so there's no reason to use it).
If a format _produces_ a ``+``, that ``+`` is replaced by an `_`` character.
.._job-snapshotting-hooks:
Pre- and Post-Snapshot Hooks
----------------------------
Jobs with `periodic snapshots <job-snapshotting-spec_>`_ can run hooks before and/or after taking the snapshot specified in ``snapshotting.hooks``:
Hooks are called per filesystem before and after the snapshot is taken (pre- and post-edge).
Pre-edge invocations are in configuration order, post-edge invocations in reverse order, i.e. like a stack.
If a pre-snapshot invocation fails, ``err_is_fatal=true`` cuts off subsequent hooks, does not take a snapshot, and only invokes post-edges corresponding to previous successful pre-edges.
``err_is_fatal=false`` logs the failed pre-edge invocation but does not affect subsequent hooks nor snapshotting itself.
Post-edges are only invoked for hooks whose pre-edges ran without error.
Note that hook failures for one filesystem never affect other filesystems.
The optional ``timeout`` parameter specifies a period after which zrepl will kill the hook process and report an error.
The default is 30 seconds and may be specified in any units understood by `time.ParseDuration <https://golang.org/pkg/time/#ParseDuration>`_.
The optional ``filesystems`` filter which limits the filesystems the hook runs for. This uses the same |filter-spec| as jobs.
Most hook types take additional parameters, please refer to the respective subsections below.
- Flush and read-Lock MySQL tables while taking the snapshot.
.._job-hook-type-command:
``command`` Hooks
~~~~~~~~~~~~~~~~~
::
jobs:
- type: push
filesystems: {
"<": true,
"tmp": false
}
snapshotting:
type: periodic
prefix: zrepl_
interval: 10m
hooks:
- type: command
path: /etc/zrepl/hooks/zrepl-notify.sh
timeout: 30s
err_is_fatal: false
- type: command
path: /etc/zrepl/hooks/special-snapshot.sh
filesystems: {
"tank/special": true
}
...
``command`` hooks take a ``path`` to an executable script or binary to be executed before and after the snapshot.
``path`` must be absolute (e.g. ``/etc/zrepl/hooks/zrepl-notify.sh``).
No arguments may be specified; create a wrapper script if zrepl must call an executable that requires arguments.
The process standard output is logged at level INFO. Standard error is logged at level WARN.
The following environment variables are set:
*``ZREPL_HOOKTYPE``: either "pre_snapshot" or "post_snapshot"
*``ZREPL_FS``: the ZFS filesystem name being snapshotted
*``ZREPL_SNAPNAME``: the zrepl-generated snapshot name (e.g. ``zrepl_20380119_031407_000``)
*``ZREPL_DRYRUN``: set to ``"true"`` if a dry run is in progress so scripts can print, but not run, their commands
An empty template hook can be found in :sampleconf:`/hooks/template.sh`.
.._job-hook-type-postgres-checkpoint:
``postgres-checkpoint`` Hook
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Connects to a Postgres server and executes the ``CHECKPOINT`` statement pre-snapshot.
Checkpointing applies the WAL contents to all data files and syncs the data files to disk.
This is not required for a consistent database backup: it merely forward-pays the "cost" of WAL replay to the time of snapshotting instead of at restore.
However, the Postgres manual recommends against checkpointing during normal operation.
Further, the operation requires Postgres superuser privileges.
zrepl users must decide on their own whether this hook is useful for them (it likely isn't).
..ATTENTION::
Note that WALs and Postgres data directory (with all database data files) must be on the same filesystem to guarantee a correct point-in-time backup with the ZFS snapshot.
* pre-snapshot ``FLUSH TABLES WITH READ LOCK`` to lock all tables in all databases in the MySQL server we connect to (`docs <https://dev.mysql.com/doc/refman/8.0/en/flush.html#flush-tables-with-read-lock>`_)
*`spiped <http://www.tarsnap.com/spiped.html>`_: think of it as an encrypted pipe between two servers
* SSH
*`sshuttle <https://sshuttle.readthedocs.io/en/stable/overview.html>`_: VPN-like solution, but using SSH
*`SSH port forwarding <https://help.ubuntu.com/community/SSH/OpenSSH/PortForwarding>`_: Systemd user unit & make it start before the zrepl service.
Serve
~~~~~
::
jobs:
- type: sink
serve:
type: tcp
listen: ":8888"
listen_freebind: true # optional, default false
clients: {
"192.168.122.123" : "mysql01",
"192.168.122.42" : "mx01",
"2001:0db8:85a3::8a2e:0370:7334": "gateway",
# CIDR masks require a '*' in the client identity string
# that is expanded to the client's IP address
"10.23.42.0/24": "cluster-*"
"fde4:8dba:82e1::/64": "san-*"
}
...
.._listen-freebind-explanation:
``listen_freebind`` controls whether the socket is allowed to bind to non-local or unconfigured IP addresses (Linux ``IP_FREEBIND`` , FreeBSD ``IP_BINDANY``).
Enable this option if you want to ``listen`` on a specific IP address that might not yet be configured when the zrepl daemon starts.
Connect
~~~~~~~
::
jobs:
- type: push
connect:
type: tcp
address: "10.23.42.23:8888"
dial_timeout: # optional, default 10s
...
.._transport-tcp+tlsclientauth:
``tls`` Transport
-----------------
The ``tls`` transport uses TCP + TLS with client authentication using client certificates.
The client identity is the common name (CN) presented in the client certificate.
It is recommended to set up a dedicated CA infrastructure for this transport, e.g. using OpenVPN's `EasyRSA <https://github.com/OpenVPN/easy-rsa>`_.
For a simple 2-machine setup, mutual TLS might also be sufficient.
We provide :ref:`copy-pastable instructions to generate the certificates below <transport-tcp+tlsclientauth-certgen>`.
The implementation uses `Go's TLS library <https://golang.org/pkg/crypto/tls/>`_.
Since Go binaries are statically linked, you or your distribution need to recompile zrepl when vulnerabilities in that library are disclosed.
All file paths are resolved relative to the zrepl daemon's working directory.
Specify absolute paths if you are unsure what directory that is (or find out from your init system).
If intermediate CAs are used, the **full chain** must be present in either in the ``ca`` file or the individual ``cert`` files.
Regardless, the client's certificate must be first in the ``cert`` file, with each following certificate directly certifying the one preceding it (see `TLS's specification <https://tools.ietf.org/html/rfc5246#section-7.4.2>`_).
This is the common default when using a CA management tool.
..NOTE::
As of Go 1.15 (zrepl 0.3.0 and newer), the Go TLS / x509 library **requrires Subject Alternative Names**
be present in certificates. You might need to re-generate your certificates using one of the :ref:`two alternatives
Note further that zrepl continues to use the CommonName field to assign client identities.
Hence, we recommend to keep the Subject Alternative Name and the CommonName in sync.
Serve
~~~~~
::
jobs:
- type: sink
root_fs: "pool2/backup_laptops"
serve:
type: tls
listen: ":8888"
listen_freebind: true # optional, default false
ca: /etc/zrepl/ca.crt
cert: /etc/zrepl/prod.fullchain
key: /etc/zrepl/prod.key
client_cns:
- "laptop1"
- "homeserver"
The ``ca`` field specified the certificate authority used to validate client certificates.
The ``client_cns`` list specifies a list of accepted client common names (which are also the client identities for this transport).
The ``listen_freebind`` field is :ref:`explained here <listen-freebind-explanation>`.
Connect
~~~~~~~
::
jobs:
- type: pull
connect:
type: tls
address: "server1.foo.bar:8888"
ca: /etc/zrepl/ca.crt
cert: /etc/zrepl/backupserver.fullchain
key: /etc/zrepl/backupserver.key
server_cn: "server1"
dial_timeout: # optional, default 10s
The ``ca`` field specifies the CA which signed the server's certificate (``serve.cert``).
The ``server_cn`` specifies the expected common name (CN) of the server's certificate.
It overrides the hostname specified in ``address``.
The connection fails if either do not match.
.._transport-tcp+tlsclientauth-certgen:
.._transport-tcp+tlsclientauth-2machineopenssl:
Mutual-TLS between Two Machines
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
For a two-machine setup, self-signed certificates distributed using an out-of-band mechanism will also work just fine:
Suppose you have a push-mode setup, with `backups.example.com` running the :ref:`sink job <job-sink>`, and `prod.example.com` running the :ref:`push job <job-push>`.
Run the following OpenSSL commands on each host, substituting HOSTNAME in both filenames and the interactive input prompt by OpenSSL:
echo cert for host $host available at pki/issued/$host.crt
echo key for host $host available at pki/private/$host.key
done
echo ca cert available at pki/ca.crt
.._transport-ssh+stdinserver:
``ssh+stdinserver`` Transport
-----------------------------
``ssh+stdinserver`` uses the ``ssh`` command and some features of the server-side SSH ``authorized_keys`` file.
It is less efficient than other transports because the data passes through two more pipes.
However, it is fairly convenient to set up and allows the zrepl daemon to not be directly exposed to the internet, because all traffic passes through the system's SSH server.
The concept is inspired by `git shell <https://git-scm.com/docs/git-shell>`_ and `Borg Backup <https://borgbackup.readthedocs.io/en/stable/deployment.html>`_.
The implementation is provided by the Go package ``github.com/problame/go-netssh``.
..NOTE::
``ssh+stdinserver`` generally provides inferior error detection and handling compared to the ``tcp`` and ``tls`` transports.
When encountering such problems, consider using ``tcp`` or ``tls`` transports, or help improve package go-netssh.
.._transport-ssh+stdinserver-serve:
Serve
~~~~~
::
jobs:
- type: source
serve:
type: stdinserver
client_identities:
- "client1"
- "client2"
...
First of all, note that ``type=stdinserver`` in this case:
Currently, only ``connect.type=ssh+stdinserver`` can connect to a ``serve.type=stdinserver``, but we want to keep that option open for future extensions.
The serving job opens a UNIX socket named after ``client_identity`` in the runtime directory.
In our example above, that is ``/var/run/zrepl/stdinserver/client1`` and ``/var/run/zrepl/stdinserver/client2``.
On the same machine, the ``zrepl stdinserver $client_identity`` command connects to ``/var/run/zrepl/stdinserver/$client_identity``.
It then passes its stdin and stdout file descriptors to the zrepl daemon via *cmsg(3)*.
zrepl daemon in turn combines them into an object implementing ``net.Conn``:
a ``Write()`` turns into a write to stdout, a ``Read()`` turns into a read from stdin.
Interactive use of the ``stdinserver`` subcommand does not make much sense.
However, we can force its execution when a user with a particular SSH pubkey connects via SSH.
This can be achieved with an entry in the ``authorized_keys`` file of the serving zrepl daemon.
# dial_timeout: 10s # optional, default 10s, max time.Duration until initial handshake is completed
The connecting zrepl daemon
#. Creates a pipe
#. Forks
#. In the forked process
#. Replaces forked stdin and stdout with the corresponding pipe ends
#. Executes the ``ssh`` binary found in ``$PATH``.
#. The identity file (``-i``) is set to ``$identity_file``.
#. The remote user, host and port correspond to those configured.
#. Further options can be specified using the ``options`` field, which appends each entry in the list to the command line using ``-o $entry``.
#. Wraps the pipe ends in a ``net.Conn`` and returns it to the RPC layer.
As discussed in the section above, the connecting zrepl daemon expects that ``zrepl stdinserver $client_identity`` is executed automatically via an ``authorized_keys`` file entry.
The ``known_hosts`` file used by the ssh command must contain an entry for ``connect.host`` prior to starting zrepl.
Thus, run the following on the pulling host's command line (substituting ``connect.host``):
The environment variables of the underlying SSH process are cleared. ``$SSH_AUTH_SOCK`` will not be available.
It is suggested to create a separate, unencrypted SSH key solely for that purpose.
.._transport-local:
``local`` Transport
-------------------
The local transport can be used to implement :ref:`local replication <replication-local>`, i.e., push replication between a push and sink job defined in the same configuration file.
The ``listener_name`` is analogous to a hostname and must match between ``serve`` and ``connect``.
The ``client_identity`` is used by the sink as documented above.
The SSH transport connects to the remote server using the SSH binary in
`$PATH` and the parameters specified in the `zrepl` config file.
However, instead of a traditional interactive SSH session, `zrepl` expects
another instance of `zrepl` on the other side of the connection; You may be
familiar with this concept from [git shell](https://git-scm.com/docs/git-shell)
or [Borg Backup](https://borgbackup.readthedocs.io/en/stable/deployment.html).
Check the examples for instructions on how to set this up on your machines!
{{% panel %}}
The environment variables of the underlying SSH process are cleared. `$SSH_AUTH_SOCK` will not be available. We suggest creating a separate, unencrypted SSH key.
The following design aspects may convince you that `zrepl` is superior to a hacked-together shell script solution.
## Language
`zrepl` is written in Go, a real programming language with type safety,
reasonable performance, testing infrastructure and an (opinionated) idea of
software engineering.
* key parts & algorithms of `zrepl` are covered by unit tests
* zrepl is noticably faster than comparable shell scripts
## RPC protocol
While it is tempting to just issue a few `ssh remote 'zfs send ...' | zfs recv`, this has a number of drawbacks:
* The snapshot streams need to be compatible.
* Communication is still unidirectional. Thus, you will most likely
* either not take advantage of features such as *compressed send & recv*
* or issue additional `ssh` commands in advance to figure out what features are supported on the other side.
* Advanced logic in shell scripts is ugly to read, poorly testable and a pain to maintain.
`zrepl` takes a different approach:
* Define an RPC protocol.
* Establish an encrypted, authenticated, bidirectional communication channel...
* ... with `zrepl` running at both ends of it.
This has several obvious benefits:
* No blank root shell access is given to the other side.
* Instead, access control lists (ACLs) are used to grant permissions to *authenticated* peers.
* The transport mechanism is decoupled from the remaining logic, keeping it extensible (e.g. TCP+TLS)
{{% panel %}}
Currently, the bidirectional communication channel is multiplexed on top of a single SSH connection.
Local replication is of course handled efficiently via simple method calls
See TODO for details.
{{% / panel %}}
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.