Compare commits

...

67 Commits

Author SHA1 Message Date
Christian Schwarz 180eaea195 docs: 0.3.1 changelog 2020-11-01 14:19:03 +01:00
Christian Schwarz 69ed2d7117 docs + readme: fix Patreon badge 2020-11-01 14:18:36 +01:00
Christian Schwarz c420f3c909 [#381] zfs: ListFilesystemVersions: make list filesystems version invocation deterministic
fixes #381
ref #379
2020-11-01 13:59:21 +01:00
Christian Schwarz 0a2dea05a9 [#385] status + replication: warning if replication succeeeded without any filesystem being replicated
refs #385
refs #384
2020-11-01 13:51:28 +01:00
Christian Schwarz 293c89d392 [#385] replication: report AttemptDone if no filesystems are replicated
fixes #385
2020-11-01 13:51:28 +01:00
Jeremy Bryan Smith bb5ef0c8b2 docs: fix link to template.sh sample hook file 2020-11-01 10:45:17 +01:00
Christian Schwarz 8f44cdc284 build: rpm: don't strip binaries
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)
```
2020-11-01 10:23:50 +01:00
Christian Schwarz 53028ed50a docs: gen-sphinx-versioning-flags.py: implement the stable branch logic used for 0.3.0 release 2020-09-12 14:03:45 +02:00
Christian Schwarz 17e152c601 docs: build zrepl.github.io on circleci 2020-09-12 14:03:45 +02:00
Christian Schwarz 2ba6aabd4a build: circleci: quickcheck-docs: upload artifacts 2020-09-06 17:28:16 +02:00
Christian Schwarz 98207c904d docs + README: document new build & release process 2020-09-06 17:28:16 +02:00
Christian Schwarz d2b825a8ae build: daily release builds of branches stable, master, problame/circleci-build 2020-09-06 17:28:16 +02:00
Christian Schwarz 913b8b37fe build: circleci: include pipeline number in minio url 2020-09-06 17:22:44 +02:00
Christian Schwarz f494c5ba31 build: pin Go version in circleci 2020-09-06 17:15:32 +02:00
Christian Schwarz b9503685f0 build: parametrized pipeline (no more approval for release build), embed exact go version info in artifacts
refs #378
refs #377
2020-09-06 15:39:34 +02:00
Christian Schwarz 0f4143c0e0 docs: update supporters 2020-09-05 17:49:35 +02:00
Christian Schwarz 8839ed1f95 docs: update multi-job & multi-host setup section 2020-09-05 17:45:18 +02:00
Christian Schwarz 41b4038ad5 docs: add example setup 'local disk backup' to jobs overview table 2020-09-05 17:44:46 +02:00
Christian Schwarz 88d21eb23a Makefile: docs: treat Sphinx warnings as errors 2020-09-05 16:41:22 +02:00
Christian Schwarz e0be7e4d4f docs: installation/rpm-repos: remove ineffective literalinclude directive 2020-09-05 16:40:33 +02:00
Christian Schwarz 0c189265e8 platformtests: fix skipping encryption-only tests on systems that don't support encryption
(Or split the test into two tests  of which one is skipped depending on encryption support)
2020-09-05 16:04:34 +02:00
Christian Schwarz b1f8cdf385 [#373] pruning: add optional regex field to last_n rule
fixes #373
2020-09-02 22:45:44 +02:00
Christian Schwarz 428a60870a pruning: cleanup retention grid impl + tests + correct docs
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
2020-09-02 22:45:44 +02:00
Christian Schwarz af2d6579c5 [#347] zfscmd: fix dangling trace Task on .Start() failure
fixes #347
2020-09-02 22:45:44 +02:00
Christian Schwarz 0f3da73ef1 [#347] zfscmd + zfs: define .Start() semantics, apply to call sites in pkg zfs
fixes #347
2020-09-02 22:45:44 +02:00
Christian Schwarz fecc9416ab [#347] package trace: envconst-configurable debug mode 2020-09-02 22:45:44 +02:00
Christian Schwarz a7915db4c3 [#347] package trace: printing debugString before instead of at panic (fixup e500d9e) 2020-09-02 22:45:44 +02:00
Christian Schwarz 3a4e841c73 [#292] pruning: grid: add all snapshots that do not match the regex to the rule's destroy list
Before this patch, multiple grids with disjoint regexes would result in
no snapshots being destroyed at all.

fixes #292
2020-09-02 22:45:44 +02:00
Christian Schwarz bcf6ff1c08 [#292] pruning: add func MustNewKeepGrid 2020-09-02 22:45:44 +02:00
Christian Schwarz ad7a104ab4 [#292] pruning: simplify table test driver 2020-09-02 22:45:44 +02:00
Christian Schwarz 639359f393 [#292] pruning: last_n: use snap name as fallback when creation is equal 2020-09-02 22:45:44 +02:00
Christian Schwarz 91e310b7e3 build: rpm + deb targets, build-in-docker targets, CircleCI pipeline rewrite
Co-authored-by: Armin Wehrfritz <dkxls23@gmail.com>
2020-09-02 21:34:52 +02:00
Christian Schwarz 5b30ad01ce make formatcheck: exit with non-zero status code if unformatted files are found
fixup of 7a5883d404
2020-09-02 21:33:36 +02:00
chenrui 8f4f9338a9 build: remove travis config
as I did not find it got referenced in the PR builds
2020-09-01 19:21:59 +02:00
Christian Schwarz 67bbce3c36 Merge pull request #375 from InsanePrawn/make_formatcheck
Add `make formatcheck` and reformat all files
2020-09-01 00:12:10 +02:00
InsanePrawn 180c3d9ae1 Reformat all files with make format.
Signed-off-by: InsanePrawn <insane.prawny@gmail.com>
2020-08-31 23:57:45 +02:00
InsanePrawn 7a5883d404 circleci: Add make formatcheck
Signed-off-by: InsanePrawn <insane.prawny@gmail.com>
2020-08-31 23:57:45 +02:00
InsanePrawn c90acefacb Makefile: Add formatcheck target
- `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>
2020-08-31 23:53:27 +02:00
InsanePrawn d6799e08d8 lazy.sh: various fixes (#372)
- 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>
2020-08-31 16:40:13 +02:00
Christian Schwarz c44dccc34b build: circleci: switch to 'machine' executor for test-build-in-docker job
refs #356
2020-08-26 11:47:17 +02:00
Christian Schwarz 7f1695c457 docs: transport: fix easyrsa script (fixup of 6b4c6fc) 2020-08-23 20:36:43 +02:00
Christian Schwarz e500d9ee26 package trace: track activeChildTasks in a set if debugEnabled=true
refs #358
2020-08-23 20:13:58 +02:00
Christian Schwarz d5ce578929 [#358] platformtest: ReceiveForceIntoEncryptedErr: fix trace panic caused by dangling open zfs send process
```
panic: end task: 1 active child tasks: end task: task still has active child tasks

goroutine 1 [running]:
github.com/zrepl/zrepl/daemon/logging/trace.WithTask.func1.1(0xc000020680, 0xc000080f00)
	/home/cs/zrepl/zrepl/daemon/logging/trace/trace.go:221 +0x2f7
github.com/zrepl/zrepl/daemon/logging/trace.WithTask.func1()
	/home/cs/zrepl/zrepl/daemon/logging/trace/trace.go:237 +0x38
main.HarnessRun(0x7ffe0b49ad3a, 0x11, 0x7ffe0b49acf2, 0x1a, 0xc800000, 0x7ffe0b49ad19, 0x16, 0x1, 0x0, 0x0, ...)
	/home/cs/zrepl/zrepl/platformtest/harness/harness.go:168 +0xe54
main.main()
	/home/cs/zrepl/zrepl/platformtest/harness/harness.go:41 +0x2ef
```

fixes #358
2020-08-23 20:13:58 +02:00
Christian Schwarz 3f8fe3a368 [#346] do not rely on creation property for filesystem version diff
fixes #346
fixes #358
2020-08-23 19:21:55 +02:00
Christian Schwarz 6b4c6fc062 [#357] docs: update quickstart + tls transport to produce keypairs with subject alternative names
fixes #357
2020-08-22 03:05:30 +02:00
Christian Schwarz e239d6f633 build: make platformtest-* usable
* use plain `go build` instead of the `go test` + `func TestMain` hack
* add target cover-platform and use that to generate coverage reports
2020-08-21 23:01:35 +02:00
InsanePrawn 0bbe2befce docs: prune: add prune interval visualisation
fixes #122

Co-Authored-By: Christian Schwarz <me@cschwarz.com>

Signed-off-by: InsanePrawn <insane.prawny@gmail.com>
2020-08-21 22:05:05 +02:00
InsanePrawn fa4e048169 readme: fix typo
Signed-off-by: InsanePrawn <insane.prawny@gmail.com>
2020-08-21 22:05:05 +02:00
Christian Schwarz 4f9f21f7f2 logger: fix go-1.15-discovered conversion from int to string
logger/datastructures.go:85:10: conversion from Level (int) to string yields a string of one rune
2020-08-12 21:44:02 +02:00
Christian Schwarz 480176ba2d rpc/dataconn: fix go1.15-discovered recursive Error() method impl
rpc/dataconn/dataconn_client.go:69:9: Sprintf format %s with arg e causes recursive Error method call
2020-08-12 21:41:31 +02:00
Christian Schwarz 1190c0f6d2 docs: supporters: update 2020-08-12 21:38:23 +02:00
Christian Schwarz 720a284db5 dist/grafana: fix endpoint abstractions cache metric panel
fixup of 30cdc14
2020-08-04 01:36:31 +02:00
Hans Schulz 83fdffbcef replication: prometheus metric for number of failed replications in last attempt
- package replication: metric
- Grafana panel
- wiring
- changelog

Signed-off-by: Christian Schwarz <me@cschwarz.com>

closes #341
2020-08-04 01:19:44 +02:00
Christian Schwarz 0ee7a49d31 [#289] zfs: workaround for OpenZFS 0.7 dry send info with zero estimated size
fixes #289
2020-07-26 20:32:35 +02:00
Christian Schwarz 02db5994fe [#345] fix broken identification of parent-fs for initial replication ordering
fixup of 02807279

fixes #345
2020-07-26 20:32:35 +02:00
Christian Schwarz 1d7a84e8ae build: extract debian binary packaging workflow trigger into a reusable script 2020-07-26 20:32:35 +02:00
Christian Schwarz a0e3dc7040 [#348] replication: add platformtest to check behavior on recv fail while still sending
Regression test for #348
2020-07-26 20:32:35 +02:00
Christian Schwarz 43495d70c7 [#348] replication: return errors if .Send rpc returns a nil SendRes
discovered while developing the next commit in this series
2020-07-26 20:32:35 +02:00
Christian Schwarz fa586b493c [#348] fix crash on early-recv error
* 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
2020-07-26 20:32:35 +02:00
Christian Schwarz 30cdc1430e replication + endpoint: replication guarantees: guarantee_{resumability,incremental,nothing}
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
2020-07-26 20:32:35 +02:00
Christian Schwarz 27673a23e9 config: add test for fromdefaults behavior 2020-07-26 20:32:35 +02:00
Christian Schwarz 95fc299733 daemon/job: test that sample configs are buildable 2020-07-26 20:32:35 +02:00
Christian Schwarz 4e702eedc9 cmd: zfs-abstraction list --json: fix panic
(was panicking because `abstractions` is in fact a channel
2020-07-26 20:32:35 +02:00
Christian Schwarz 8ff83f2f1a [#342] endpoint: always create unencrypted placeholder filesystems
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.
2020-07-26 20:32:35 +02:00
Christian Schwarz 4b8f0ad112 docs: supporters: update 2020-06-22 13:36:00 +02:00
Brian Candler dbc8bbeb6a docs: config: prune: example: keep manual snapshots on receiver
Fixes #335
closes #336

Signed-off-by: Christian Schwarz <me@cschwarz.com>
2020-06-22 12:32:03 +02:00
Christian Schwarz b3e856f40d docs: changelog: 0.3: fix broken issue link 2020-06-22 12:30:42 +02:00
115 changed files with 3824 additions and 1557 deletions
+348 -123
View File
@@ -1,74 +1,79 @@
version: 2.0 version: 2.1
workflows:
version: 2
build:
jobs:
- build-1.11
- build-1.12
- build-1.13
- build-1.14
- build-latest
- test-build-in-docker
jobs:
# build-latest serves as the template commands:
# we use YAML anchors & aliases to exchange the docker image (and hence Go version used for the build) setup-home-local-bin:
build-latest: &build-latest
description: Builds zrepl
parameters:
image:
description: "the docker image that the job should use"
type: string
docker:
- image: circleci/golang:latest
environment:
# required by lazy.sh
TERM: xterm
working_directory: /go/src/github.com/zrepl/zrepl
steps: steps:
- run: - run:
name: Setup environment variables shell: /bin/bash -euo pipefail
command: | command: |
# used by pip (for docs) mkdir -p "$HOME/.local/bin"
echo 'export PATH="$HOME/.local/bin:$PATH"' >> $BASH_ENV line='export PATH="$HOME/.local/bin:$PATH"'
# we use modules if grep "$line" $BASH_ENV >/dev/null; then
echo 'export GO111MODULE=on' >> $BASH_ENV echo "$line" >> $BASH_ENV
fi
invoke-lazy-sh:
parameters:
subcommand:
type: string
steps:
- run:
environment:
TERM: xterm
command: ./lazy.sh <<parameters.subcommand>>
apt-update-and-install-common-deps:
steps:
- run: sudo apt update && sudo apt install gawk make
restore-cache-gomod:
steps:
- restore_cache: - restore_cache:
keys: key: go-mod-v4-{{ checksum "go.sum" }}
- source save-cache-gomod:
- protobuf steps:
- checkout
- save_cache: - save_cache:
key: source key: go-mod-v4-{{ checksum "go.sum" }}
paths: paths:
- ".git" - "/go/pkg/mod"
# install deps install-godep:
- run: wget https://github.com/protocolbuffers/protobuf/releases/download/v3.6.1/protoc-3.6.1-linux-x86_64.zip steps:
- run: echo "6003de742ea3fcf703cfec1cd4a3380fd143081a2eb0e559065563496af27807 protoc-3.6.1-linux-x86_64.zip" | sha256sum -c - apt-update-and-install-common-deps
- run: sudo unzip -d /usr protoc-3.6.1-linux-x86_64.zip - invoke-lazy-sh:
subcommand: godep
install-docdep:
steps:
- apt-update-and-install-common-deps
- run: sudo apt install python3 python3-pip libgirepository1.0-dev
- invoke-lazy-sh:
subcommand: docdep
download-and-install-minio-client:
steps:
- setup-home-local-bin
- restore_cache:
key: minio-client-v2
- run:
shell: /bin/bash -eo pipefail
command: |
if which mc; then exit 0; fi
sudo curl -sSL https://dl.min.io/client/mc/release/linux-amd64/archive/mc.RELEASE.2020-08-20T00-23-01Z \
-o "$HOME/.local/bin/mc"
sudo chmod +x "$HOME/.local/bin/mc"
- save_cache: - save_cache:
key: protobuf key: minio-client-v2
paths: paths:
- "/usr/include/google/protobuf" - "$HOME/.local/bin/mc"
- run: sudo apt update && sudo apt install python3 python3-pip libgirepository1.0-dev gawk
- run: ./lazy.sh devsetup
- run: make zrepl-bin
- run: make vet
- run: make lint
- run: make release
- run: make test-go
# cannot run test-platform because circle-ci runs in linux containers
- store_artifacts:
path: ./artifacts/release
when: always
upload-minio:
parameters:
src:
type: string
dst:
type: string
steps:
- run: - run:
shell: /bin/bash -eo pipefail shell: /bin/bash -eo pipefail
when: always when: always
@@ -78,78 +83,298 @@ jobs:
exit 0 exit 0
fi fi
set -u # from now on set -u # from now on
# Download and install minio
curl -sSL https://dl.minio.io/client/mc/release/linux-amd64/mc -o ${GOPATH}/bin/mc
chmod +x ${GOPATH}/bin/mc
mc config host add --api s3v4 zrepl-minio https://minio.cschwarz.com ${MINIO_ACCESS_KEY} ${MINIO_SECRET_KEY}
# Upload artifacts
echo "$CIRCLE_BUILD_URL" > ./artifacts/release/cirlceci_build_url
mc cp -r artifacts/release "zrepl-minio/zrepl-ci-artifacts/${CIRCLE_SHA1}/${CIRCLE_JOB}/"
mc config host add --api s3v4 zrepl-minio https://minio.cschwarz.com ${MINIO_ACCESS_KEY} ${MINIO_SECRET_KEY}
# keep in sync with set-github-minio-status
jobprefix=zrepl-ci-artifacts/${CIRCLE_SHA1}-pipeline-<<pipeline.number>>/${CIRCLE_JOB}
# Upload artifacts
mkdir -p ./artifacts
mc cp -r <<parameters.src>> "zrepl-minio/$jobprefix/<<parameters.dst>>"
set-github-minio-status:
parameters:
context:
type: string
description:
type: string
minio-dst:
type: string
steps:
- run:
shell: /bin/bash -eo pipefail
command: |
if [ -n "$CIRCLE_PR_NUMBER" ]; then # CIRCLE_PR_NUMBER is guaranteed to be only present in forked PRs (external)
echo "Forked PR detected. Sry, can't trust you with credentials to external artifact store, use CircleCI's instead."
exit 0
fi
set -u # from now on
# keep in sync with with upload-minio command
jobprefix=zrepl-ci-artifacts/${CIRCLE_SHA1}-pipeline-<<pipeline.number>>/${CIRCLE_JOB}
# Push Artifact Link to GitHub # Push Artifact Link to GitHub
REPO="zrepl/zrepl" REPO="zrepl/zrepl"
COMMIT="${CIRCLE_SHA1}" COMMIT="${CIRCLE_SHA1}"
JOB_NAME="${CIRCLE_JOB}" JOB_NAME="${CIRCLE_JOB}"
CONTEXT="<<parameters.context>>"
DESCRIPTION="<<parameters.description>>"
TARGETURL=https://minio.cschwarz.com/minio/"$jobprefix"/"<<parameters.minio-dst>>"
curl "https://api.github.com/repos/$REPO/statuses/$COMMIT" \ curl "https://api.github.com/repos/$REPO/statuses/$COMMIT" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-H "Authorization: token $GITHUB_COMMIT_STATUS_TOKEN" \ -H "Authorization: token $GITHUB_COMMIT_STATUS_TOKEN" \
-X POST \ -X POST \
-d '{"context":"zrepl/publish-ci-artifacts", "state": "success", "description":"CI Build Artifacts for '"$JOB_NAME"'", "target_url":"https://minio.cschwarz.com/minio/zrepl-ci-artifacts/'"$COMMIT"'/"}' -d '{"context":"'"$CONTEXT"'", "state": "success", "description":"'"$DESCRIPTION"'", "target_url":"'"$TARGETURL"'"}'
- run:
shell: /bin/bash -euo pipefail
command: |
# Trigger Debian Package Build
curl -v -X POST https://api.github.com/repos/zrepl/debian-binary-packaging/dispatches \
-H 'Accept: application/vnd.github.v3+json' \
-H "Authorization: token $ZREPL_DEBIAN_BINARYPACKAGIN_TRIGGER_BUILD_GITHUB_TOKEN" \
--data '{"event_type": "push", "client_payload": { "zrepl_main_repo_commit": "'"$CIRCLE_SHA1"'", "go_version": "'"${CIRCLE_JOB##build-}"'" }}'
build-1.11: trigger-pipeline:
<<: *build-latest parameters:
docker: body_no_shell_subst:
- image: circleci/golang:1.11 type: string
build-1.12:
<<: *build-latest
docker:
- image: circleci/golang:1.12
build-1.13:
<<: *build-latest
docker:
- image: circleci/golang:1.13
build-1.14:
<<: *build-latest
docker:
- image: circleci/golang:1.14
# this job tries to mimic the build-in-docker instructions
# given in docs/installation.rst
#
# However, CircleCI doesn't support volume mounts, so we have to copy
# the source into the build-container by modifying the Dockerfile
test-build-in-docker:
description: Check that build-in-docker works
docker:
- image: circleci/golang:latest
environment:
working_directory: /go/src/github.com/zrepl/zrepl
steps: steps:
- checkout - run: |
- setup_remote_docker curl -X POST https://circleci.com/api/v2/project/github/zrepl/zrepl/pipeline \
- run: -H 'Content-Type: application/json' \
name: (hacky) circleci doesn't allow volume mounts, so copy src to container -H 'Accept: application/json' \
command: echo "ADD . /src" >> build.Dockerfile -H "Circle-Token: $ZREPL_BOT_CIRCLE_TOKEN" \
- run: --data '<<parameters.body_no_shell_subst>>'
name: (hacky) commit modified Dockerfile to avoid failing git clean check in Makefile
command: git -c user.name='circleci' -c user.email='circleci@localhost' commit -m 'CIRCLECI modified Dockerfile with zrepl src' --author 'autoauthor <circleci@localhost>' -- build.Dockerfile parameters:
- run: do_ci:
name: build the build image (build deps) type: boolean
command: docker build -t zrepl_build -f build.Dockerfile . default: true
- run:
name: try compiling do_release:
command: docker run -it zrepl_build make release type: boolean
default: false
release_docker_baseimage_tag:
type: string
default: "1.15"
workflows:
version: 2
ci:
when: << pipeline.parameters.do_ci >>
jobs:
- quickcheck-docs
- quickcheck-go: &quickcheck-go-smoketest
name: quickcheck-go-amd64-linux-1.15
goversion: &latest-go-release "1.15"
goos: linux
goarch: amd64
- test-go-on-latest-go-release:
goversion: *latest-go-release
- quickcheck-go:
requires:
- quickcheck-go-amd64-linux-1.15 #quickcheck-go-smoketest.name
matrix: &quickcheck-go-matrix
alias: quickcheck-go-matrix
parameters:
goversion: [*latest-go-release, "1.11"]
goos: ["linux", "freebsd"]
goarch: ["amd64", "arm64"]
exclude:
# don't re-do quickcheck-go-smoketest
- goversion: *latest-go-release
goos: linux
goarch: amd64
# not supported by Go 1.11
- goversion: "1.11"
goos: freebsd
goarch: arm64
release:
when: << pipeline.parameters.do_release >>
jobs:
- release-build
- release-deb:
requires:
- release-build
- release-rpm:
requires:
- release-build
- release-upload:
requires:
- release-build
- release-deb
- release-rpm
periodic:
triggers:
- schedule:
cron: "45 15 * * *"
filters:
branches:
only:
- master
- stable
- problame/circleci-build
jobs:
- periodic-trigger-pipeline
zrepl.github.io:
jobs:
- publish-zrepl-github-io:
filters:
branches:
only:
- stable
jobs:
quickcheck-docs:
docker:
- image: cimg/base:2020.08
steps:
- checkout
- install-docdep
- run: make docs
- store_artifacts:
path: artifacts
- download-and-install-minio-client
- upload-minio:
src: artifacts
dst: ""
- set-github-minio-status:
context: artifacts/${CIRCLE_JOB}
description: artifacts of CI job ${CIRCLE_JOB}
minio-dst: ""
quickcheck-go:
parameters:
goversion:
type: string
goos:
type: string
goarch:
type: string
docker:
- image: circleci/golang:<<parameters.goversion>>
environment:
GOOS: <<parameters.goos>>
GOARCH: <<parameters.goarch>>
steps:
- checkout
- restore-cache-gomod
- run: go mod download
- run: cd build && go mod download
- save-cache-gomod
- install-godep
- run: make formatcheck
- run: make generate-platform-test-list
- run: make zrepl-bin test-platform-bin
- run: make vet
- run: make lint
- download-and-install-minio-client
- run: rm -f artifacts/generate-platform-test-list
- store_artifacts:
path: artifacts
- upload-minio:
src: artifacts
dst: ""
- set-github-minio-status:
context: artifacts/${CIRCLE_JOB}
description: artifacts of CI job ${CIRCLE_JOB}
minio-dst: ""
test-go-on-latest-go-release:
parameters:
goversion:
type: string
docker:
- image: circleci/golang:<<parameters.goversion>>
steps:
- checkout
- restore-cache-gomod
- run: make test-go
# don't save-cache-gomod here, test-go doesn't pull all the dependencies
release-build:
machine: true
steps:
- checkout
- run: make release-docker RELEASE_DOCKER_BASEIMAGE_TAG=<<pipeline.parameters.release_docker_baseimage_tag>>
- persist_to_workspace:
root: .
paths: [.]
release-deb:
machine: true
steps:
- attach_workspace:
at: .
- run: make debs-docker
- persist_to_workspace:
root: .
paths:
- "artifacts/*.deb"
release-rpm:
machine: true
steps:
- attach_workspace:
at: .
- run: make rpms-docker
- persist_to_workspace:
root: .
paths:
- "artifacts/*.rpm"
release-upload:
docker:
- image: cimg/base:2020.08
steps:
- attach_workspace:
at: .
- store_artifacts:
path: artifacts
- download-and-install-minio-client
- upload-minio:
src: artifacts
dst: ""
- set-github-minio-status:
context: artifacts/release
description: CI-generated release artifacts
minio-dst: ""
periodic-trigger-pipeline:
docker:
- image: cimg/base:2020.08
steps:
- trigger-pipeline:
body_no_shell_subst: '{"branch":"master", "parameters": { "do_ci": true, "do_release": true }}'
- trigger-pipeline:
body_no_shell_subst: '{"branch":"stable", "parameters": { "do_ci": true, "do_release": true }}'
publish-zrepl-github-io:
docker:
- image: cimg/python:3.7
steps:
- checkout
- invoke-lazy-sh:
subcommand: docdep
- run:
command: |
git config --global user.email "me@cschwarz.com"
git config --global user.name "zrepl-github-io-ci"
# https://circleci.com/docs/2.0/add-ssh-key/#adding-multiple-keys-with-blank-hostnames
- run: ssh-add -D
# the default circleci ssh config only additional ssh keys for Host !github.com
- run:
command: |
cat > ~/.ssh/config \<<EOF
Host *
IdentityFile /home/circleci/.ssh/id_rsa_458e62c517f6c480e40452126ce47421
EOF
- add_ssh_keys:
fingerprints:
# deploy key for zrepl.github.io
- "45:8e:62:c5:17:f6:c4:80:e4:04:52:12:6c:e4:74:21"
- run: bash -x docs/publish.sh -c -a
+10
View File
@@ -0,0 +1,10 @@
#!/usr/bin/env bash
set -euo pipefail
COMMIT="$1"
GO_VERSION="$2"
curl -v -X POST https://api.github.com/repos/zrepl/debian-binary-packaging/dispatches \
-H 'Accept: application/vnd.github.v3+json' \
-H "Authorization: token $GITHUB_ACCESS_TOKEN" \
--data '{"event_type": "push", "client_payload": { "zrepl_main_repo_commit": "'"$COMMIT"'", "go_version": "'"$GO_VERSION"'" }}'
-70
View File
@@ -1,70 +0,0 @@
dist: xenial
services:
- docker
env: # for allow_failures: https://docs.travis-ci.com/user/customizing-the-build/
matrix:
include:
- language: go
name: "Build in Docker (docs/installation.rst)"
script:
- sudo docker build -t zrepl_build -f build.Dockerfile .
- |
sudo docker run -it --rm \
-v "${PWD}:/go/src/github.com/zrepl/zrepl" \
--user "$(id -u):$(id -g)" \
zrepl_build make vendordeps release
- &zrepl_build_template
language: go
go_import_path: github.com/zrepl/zrepl
before_install:
- wget https://github.com/protocolbuffers/protobuf/releases/download/v3.6.1/protoc-3.6.1-linux-x86_64.zip
- echo "6003de742ea3fcf703cfec1cd4a3380fd143081a2eb0e559065563496af27807 protoc-3.6.1-linux-x86_64.zip" | sha256sum -c
- sudo unzip -d /usr protoc-3.6.1-linux-x86_64.zip
- ./lazy.sh godep
- make vendordeps
script:
- make
- make vet
- make test
- make lint
- make artifacts/zrepl-freebsd-amd64
- make artifacts/zrepl-linux-amd64
- make artifacts/zrepl-darwin-amd64
go:
- "1.11"
- <<: *zrepl_build_template
go:
- "1.12"
- <<: *zrepl_build_template
go:
- "master"
- &zrepl_docs_template
language: python
python:
- "3.4"
install:
- sudo apt-get install libgirepository1.0-dev
- pip install -r docs/requirements.txt
script:
- make docs
- <<: *zrepl_docs_template
python:
- "3.5"
- <<: *zrepl_docs_template
python:
- "3.6"
- <<: *zrepl_docs_template
python:
- "3.7"
allow_failures:
- <<: *zrepl_build_template
go:
- "master"
+120 -12
View File
@@ -28,6 +28,8 @@ GO_BUILDFLAGS := $(GO_MOD_READONLY) $(GO_EXTRA_BUILDFLAGS)
GO_BUILD := $(GO_ENV_VARS) $(GO) build $(GO_BUILDFLAGS) -ldflags $(GO_LDFLAGS) GO_BUILD := $(GO_ENV_VARS) $(GO) build $(GO_BUILDFLAGS) -ldflags $(GO_LDFLAGS)
GOLANGCI_LINT := golangci-lint GOLANGCI_LINT := golangci-lint
GOCOVMERGE := gocovmerge GOCOVMERGE := gocovmerge
RELEASE_DOCKER_BASEIMAGE_TAG ?= 1.15
RELEASE_DOCKER_BASEIMAGE ?= golang:$(RELEASE_DOCKER_BASEIMAGE_TAG)
ifneq ($(GOARM),) ifneq ($(GOARM),)
ZREPL_TARGET_TUPLE := $(GOOS)-$(GOARCH)v$(GOARM) ZREPL_TARGET_TUPLE := $(GOOS)-$(GOARCH)v$(GOARM)
@@ -57,6 +59,85 @@ ifeq (SIGN, 1)
endif endif
@echo "ZREPL RELEASE ARTIFACTS AVAILABLE IN artifacts/release" @echo "ZREPL RELEASE ARTIFACTS AVAILABLE IN artifacts/release"
release-docker: $(ARTIFACTDIR)
sed 's/FROM.*!SUBSTITUTED_BY_MAKEFILE/FROM $(RELEASE_DOCKER_BASEIMAGE)/' build.Dockerfile > artifacts/release-docker.Dockerfile
docker build -t zrepl_release --pull -f artifacts/release-docker.Dockerfile .
docker run --rm -i -v $(CURDIR):/src -u $$(id -u):$$(id -g) \
zrepl_release \
make release GOOS=$(GOOS) GOARCH=$(GOARCH) GOARM=$(GOARM)
debs-docker:
$(MAKE) _debs_or_rpms_docker _DEB_OR_RPM=deb
rpms-docker:
$(MAKE) _debs_or_rpms_docker _DEB_OR_RPM=rpm
_debs_or_rpms_docker: # artifacts/_zrepl.zsh_completion artifacts/bash_completion docs zrepl-bin
$(MAKE) $(_DEB_OR_RPM)-docker GOOS=linux GOARCH=amd64
$(MAKE) $(_DEB_OR_RPM)-docker GOOS=linux GOARCH=arm64
$(MAKE) $(_DEB_OR_RPM)-docker GOOS=linux GOARCH=arm GOARM=7
$(MAKE) $(_DEB_OR_RPM)-docker GOOS=linux GOARCH=386
rpm: $(ARTIFACTDIR) # artifacts/_zrepl.zsh_completion artifacts/bash_completion docs zrepl-bin
$(eval _ZREPL_RPM_VERSION := $(subst -,.,$(_ZREPL_VERSION)))
$(eval _ZREPL_RPM_TOPDIR_ABS := $(CURDIR)/$(ARTIFACTDIR)/rpmbuild)
rm -rf "$(_ZREPL_RPM_TOPDIR_ABS)"
mkdir "$(_ZREPL_RPM_TOPDIR_ABS)"
mkdir -p "$(_ZREPL_RPM_TOPDIR_ABS)"/{SPECS,RPMS,BUILD,BUILDROOT}
sed "s/^Version:.*/Version: $(_ZREPL_RPM_VERSION)/g" \
packaging/rpm/zrepl.spec > $(_ZREPL_RPM_TOPDIR_ABS)/SPECS/zrepl.spec
# see /usr/lib/rpm/platform
ifeq ($(GOARCH),amd64)
$(eval _ZREPL_RPMBUILD_TARGET := x86_64)
else ifeq ($(GOARCH), 386)
$(eval _ZREPL_RPMBUILD_TARGET := i386)
else ifeq ($(GOARCH), arm64)
$(eval _ZREPL_RPMBUILD_TARGET := aarch64)
else ifeq ($(GOARCH), arm)
$(eval _ZREPL_RPMBUILD_TARGET := armv7hl)
else
$(eval _ZREPL_RPMBUILD_TARGET := $(GOARCH))
endif
rpmbuild \
--build-in-place \
--define "_sourcedir $(CURDIR)" \
--define "_topdir $(_ZREPL_RPM_TOPDIR_ABS)" \
--define "_zrepl_binary_filename zrepl-$(ZREPL_TARGET_TUPLE)" \
--target $(_ZREPL_RPMBUILD_TARGET) \
-bb "$(_ZREPL_RPM_TOPDIR_ABS)"/SPECS/zrepl.spec
cp "$(_ZREPL_RPM_TOPDIR_ABS)"/RPMS/$(_ZREPL_RPMBUILD_TARGET)/zrepl-$(_ZREPL_RPM_VERSION)*.rpm $(ARTIFACTDIR)/
rpm-docker:
docker build -t zrepl_rpm_pkg --pull -f packaging/rpm/Dockerfile .
docker run --rm -i -v $(CURDIR):/build/src -u $$(id -u):$$(id -g) \
zrepl_rpm_pkg \
make rpm GOOS=$(GOOS) GOARCH=$(GOARCH) GOARM=$(GOARM)
deb: $(ARTIFACTDIR) # artifacts/_zrepl.zsh_completion artifacts/bash_completion docs zrepl-bin
cp packaging/deb/debian/changelog.template packaging/deb/debian/changelog
sed -i 's/DATE_DASH_R_OUTPUT/$(shell date -R)/' packaging/deb/debian/changelog
VERSION="$(subst -,.,$(_ZREPL_VERSION))"; \
export VERSION="$${VERSION#v}"; \
sed -i 's/VERSION/'"$$VERSION"'/' packaging/deb/debian/changelog
ifeq ($(GOARCH), arm)
$(eval DEB_HOST_ARCH := armhf)
else ifeq ($(GOARCH), 386)
$(eval DEB_HOST_ARCH := i386)
else
$(eval DEB_HOST_ARCH := $(GOARCH))
endif
export ZREPL_DPKG_ZREPL_BINARY_FILENAME=zrepl-$(ZREPL_TARGET_TUPLE); \
dpkg-buildpackage -b --no-sign --host-arch $(DEB_HOST_ARCH)
cp ../*.deb artifacts/
deb-docker:
docker build -t zrepl_debian_pkg --pull -f packaging/deb/Dockerfile .
docker run --rm -i -v $(CURDIR):/build/src -u $$(id -u):$$(id -g) \
zrepl_debian_pkg \
make deb GOOS=$(GOOS) GOARCH=$(GOARCH) GOARM=$(GOARM)
# expects `release` target to have run before # expects `release` target to have run before
NOARCH_TARBALL := $(ARTIFACTDIR)/zrepl-noarch.tar NOARCH_TARBALL := $(ARTIFACTDIR)/zrepl-noarch.tar
wrapup-and-checksum: wrapup-and-checksum:
@@ -147,26 +228,41 @@ zrepl-bin:
generate-platform-test-list: generate-platform-test-list:
$(GO_BUILD) -o $(ARTIFACTDIR)/generate-platform-test-list ./platformtest/tests/gen $(GO_BUILD) -o $(ARTIFACTDIR)/generate-platform-test-list ./platformtest/tests/gen
test-platform-bin: COVER_PLATFORM_BIN_PATH := $(ARTIFACTDIR)/platformtest-cover-$(ZREPL_TARGET_TUPLE)
cover-platform-bin:
$(GO_ENV_VARS) $(GO) test $(GO_BUILDFLAGS) \ $(GO_ENV_VARS) $(GO) test $(GO_BUILDFLAGS) \
-c -o "$(ARTIFACTDIR)/platformtest-$(ZREPL_TARGET_TUPLE)" \ -c -o "$(COVER_PLATFORM_BIN_PATH)" \
-covermode=atomic -cover -coverpkg github.com/zrepl/zrepl/... \ -covermode=atomic -cover -coverpkg github.com/zrepl/zrepl/... \
./platformtest/harness ./platformtest/harness
cover-platform:
# do not track dependency on cover-platform-bin to allow build of binary outside of test VM
export _TEST_PLATFORM_CMD="$(COVER_PLATFORM_BIN_PATH) \
-test.coverprofile \"$(ARTIFACTDIR)/platformtest.cover\" \
-test.v \
__DEVEL--i-heard-you-like-tests"; \
$(MAKE) _test-or-cover-platform-impl
TEST_PLATFORM_BIN_PATH := $(ARTIFACTDIR)/platformtest-$(ZREPL_TARGET_TUPLE)
test-platform-bin:
$(GO_BUILD) -o "$(TEST_PLATFORM_BIN_PATH)" ./platformtest/harness
test-platform:
export _TEST_PLATFORM_CMD="\"$(TEST_PLATFORM_BIN_PATH)\""; \
$(MAKE) _test-or-cover-platform-impl
ZREPL_PLATFORMTEST_POOLNAME := zreplplatformtest ZREPL_PLATFORMTEST_POOLNAME := zreplplatformtest
ZREPL_PLATFORMTEST_IMAGEPATH := /tmp/zreplplatformtest.pool.img ZREPL_PLATFORMTEST_IMAGEPATH := /tmp/zreplplatformtest.pool.img
ZREPL_PLATFORMTEST_MOUNTPOINT := /tmp/zreplplatformtest.pool ZREPL_PLATFORMTEST_MOUNTPOINT := /tmp/zreplplatformtest.pool
ZREPL_PLATFORMTEST_ZFS_LOG := /tmp/zreplplatformtest.zfs.log ZREPL_PLATFORMTEST_ZFS_LOG := /tmp/zreplplatformtest.zfs.log
# ZREPL_PLATFORMTEST_STOP_AND_KEEP := -failure.stop-and-keep-pool # ZREPL_PLATFORMTEST_STOP_AND_KEEP := -failure.stop-and-keep-pool
ZREPL_PLATFORMTEST_ARGS := ZREPL_PLATFORMTEST_ARGS :=
test-platform: $(ARTIFACTDIR) # do not track dependency on test-platform-bin to allow build of platformtest outside of test VM _test-or-cover-platform-impl: $(ARTIFACTDIR)
ifndef _TEST_PLATFORM_CMD
$(error _TEST_PLATFORM_CMD is undefined, caller 'cover-platform' or 'test-platform' should have defined it)
endif
rm -f "$(ZREPL_PLATFORMTEST_ZFS_LOG)" rm -f "$(ZREPL_PLATFORMTEST_ZFS_LOG)"
rm -f "$(ARTIFACTDIR)/platformtest.cover" rm -f "$(ARTIFACTDIR)/platformtest.cover"
platformtest/logmockzfs/logzfsenv "$(ZREPL_PLATFORMTEST_ZFS_LOG)" `which zfs` \ platformtest/logmockzfs/logzfsenv "$(ZREPL_PLATFORMTEST_ZFS_LOG)" `which zfs` \
"$(ARTIFACTDIR)/platformtest-$(ZREPL_TARGET_TUPLE)" \ $(_TEST_PLATFORM_CMD) \
-test.coverprofile "$(ARTIFACTDIR)/platformtest.cover" \
-test.v \
__DEVEL--i-heard-you-like-tests \
-poolname "$(ZREPL_PLATFORMTEST_POOLNAME)" \ -poolname "$(ZREPL_PLATFORMTEST_POOLNAME)" \
-imagepath "$(ZREPL_PLATFORMTEST_IMAGEPATH)" \ -imagepath "$(ZREPL_PLATFORMTEST_IMAGEPATH)" \
-mountpoint "$(ZREPL_PLATFORMTEST_MOUNTPOINT)" \ -mountpoint "$(ZREPL_PLATFORMTEST_MOUNTPOINT)" \
@@ -178,22 +274,31 @@ cover-merge: $(ARTIFACTDIR)
cover-html: cover-merge cover-html: cover-merge
$(GO) tool cover -html "$(ARTIFACTDIR)/merged.cover" -o "$(ARTIFACTDIR)/merged.cover.html" $(GO) tool cover -html "$(ARTIFACTDIR)/merged.cover" -o "$(ARTIFACTDIR)/merged.cover.html"
test-full: cover-full:
test "$$(id -u)" = "0" || echo "MUST RUN AS ROOT" 1>&2 test "$$(id -u)" = "0" || echo "MUST RUN AS ROOT" 1>&2
$(MAKE) test-go COVER=1 $(MAKE) test-go COVER=1
$(MAKE) test-platform $(MAKE) cover-platform-bin
$(MAKE) cover-platform
$(MAKE) cover-html $(MAKE) cover-html
##################### DEV TARGETS ##################### ##################### DEV TARGETS #####################
# not part of the build, must do that manually # not part of the build, must do that manually
.PHONY: generate format .PHONY: generate formatcheck format
generate: generate-platform-test-list generate: generate-platform-test-list
protoc -I=replication/logic/pdu --go_out=plugins=grpc:replication/logic/pdu replication/logic/pdu/pdu.proto protoc -I=replication/logic/pdu --go_out=plugins=grpc:replication/logic/pdu replication/logic/pdu/pdu.proto
$(GO_ENV_VARS) $(GO) generate $(GO_BUILDFLAGS) -x ./... $(GO_ENV_VARS) $(GO) generate $(GO_BUILDFLAGS) -x ./...
GOIMPORTS := goimports -srcdir . -local 'github.com/zrepl/zrepl'
FINDSRCFILES := find . -type f -name '*.go' -not -path "./vendor/*" -not -name '*.pb.go' -not -name '*_enumer.go'
formatcheck:
@# goimports doesn't have a knob to exit with non-zero status code if formatting is needed
@# see https://go-review.googlesource.com/c/tools/+/237378
@ affectedfiles=$$($(GOIMPORTS) -l $(shell $(FINDSRCFILES)) | tee /dev/stderr | wc -l); test "$$affectedfiles" = 0
format: format:
goimports -srcdir . -local 'github.com/zrepl/zrepl' -w $(shell find . -type f -name '*.go' -not -path "./vendor/*" -not -name '*.pb.go' -not -name '*_enumer.go') @ $(GOIMPORTS) -w -d $(shell $(FINDSRCFILES))
##################### NOARCH ##################### ##################### NOARCH #####################
.PHONY: noarch $(ARTIFACTDIR)/bash_completion $(ARTIFACTDIR)/_zrepl.zsh_completion $(ARTIFACTDIR)/go_env.txt docs docs-clean .PHONY: noarch $(ARTIFACTDIR)/bash_completion $(ARTIFACTDIR)/_zrepl.zsh_completion $(ARTIFACTDIR)/go_env.txt docs docs-clean
@@ -217,11 +322,14 @@ $(ARTIFACTDIR)/_zrepl.zsh_completion:
$(ARTIFACTDIR)/go_env.txt: $(ARTIFACTDIR)/go_env.txt:
$(GO_ENV_VARS) $(GO) env > $@ $(GO_ENV_VARS) $(GO) env > $@
$(GO) version >> $@
docs: $(ARTIFACTDIR)/docs docs: $(ARTIFACTDIR)/docs
# https://www.sphinx-doc.org/en/master/man/sphinx-build.html
make -C docs \ make -C docs \
html \ html \
BUILDDIR=../artifacts/docs \ BUILDDIR=../artifacts/docs \
SPHINXOPTS="-W --keep-going -n"
docs-clean: docs-clean:
make -C docs \ make -C docs \
+59 -82
View File
@@ -1,7 +1,7 @@
[![GitHub license](https://img.shields.io/github/license/zrepl/zrepl.svg)](https://github.com/zrepl/zrepl/blob/master/LICENSE) [![GitHub license](https://img.shields.io/github/license/zrepl/zrepl.svg)](https://github.com/zrepl/zrepl/blob/master/LICENSE)
[![Language: Go](https://img.shields.io/badge/language-Go-6ad7e5.svg)](https://golang.org/) [![Language: Go](https://img.shields.io/badge/language-Go-6ad7e5.svg)](https://golang.org/)
[![User Docs](https://img.shields.io/badge/docs-web-blue.svg)](https://zrepl.github.io) [![User Docs](https://img.shields.io/badge/docs-web-blue.svg)](https://zrepl.github.io)
[![Donate via Patreon](https://img.shields.io/endpoint.svg?url=https%3A%2F%2Fshieldsio-patreon.herokuapp.com%2Fzrepl%2Fpledges&style=flat&color=yellow)](https://www.patreon.com/zrepl) [![Donate via Patreon](https://img.shields.io/endpoint.svg?url=https%3A%2F%2Fshieldsio-patreon.vercel.app%2Fapi%3Fusername%3Dzrepl%26type%3Dpatrons&style=flat&color=yellow)](https://patreon.com/zrepl)
[![Donate via GitHub Sponsors](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&style=flat&color=yellow)](https://github.com/sponsors/problame) [![Donate via GitHub Sponsors](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&style=flat&color=yellow)](https://github.com/sponsors/problame)
[![Donate via Liberapay](https://img.shields.io/liberapay/patrons/zrepl.svg?logo=liberapay)](https://liberapay.com/zrepl/donate) [![Donate via Liberapay](https://img.shields.io/liberapay/patrons/zrepl.svg?logo=liberapay)](https://liberapay.com/zrepl/donate)
[![Donate via PayPal](https://img.shields.io/badge/donate-paypal-yellow.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=R5QSXJVYHGX96) [![Donate via PayPal](https://img.shields.io/badge/donate-paypal-yellow.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=R5QSXJVYHGX96)
@@ -21,7 +21,7 @@ zrepl is a one-stop ZFS backup & replication solution.
## Feature Requests ## 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. If so, think of an expressive configuration example.
2. Think of at least one use case that generalizes from your concrete application. 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. 3. Open an issue on GitHub with example conf & use case attached.
@@ -30,20 +30,12 @@ zrepl is a one-stop ZFS backup & replication solution.
The above does not apply if you already implemented everything. The above does not apply if you already implemented everything.
Check out the *Coding Workflow* section below for details. Check out the *Coding Workflow* section below for details.
## Package Maintainer Information ## Building, Releasing, Downstream-Packaging
* Follow the steps in `docs/installation.rst -> Compiling from Source` and read the Makefile / shell scripts used in this process. This section provides an overview of the zrepl build & release process.
* Make sure your distro is compatible with the paths in `docs/installation.rst`. Check out `docs/installation/compile-from-source.rst` for build-from-source instructions.
* 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/`.
* Use `make release ZREPL_VERSION='mydistro-1.2.3_1'`
* Your distro's name and any versioning supplemental to zrepl's (e.g. package revision) should be in this string
* Use `sudo make test-platform` **on a test system** to validate that zrepl's abstractions on top of ZFS work with the system ZFS.
* Make sure you are informed about new zrepl versions, e.g. by subscribing to GitHub's release RSS feed.
## Developer Documentation ### Overview
zrepl is written in [Go](https://golang.org) and uses [Go modules](https://github.com/golang/go/wiki/Modules) to manage dependencies. 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 documentation is written in [ReStructured Text](http://docutils.sourceforge.net/rst.html) using the [Sphinx](https://www.sphinx-doc.org) framework.
@@ -60,60 +52,61 @@ An HTML report can be generated using `make cover-html`.
**Code generation** is triggered by `make generate`. Generated code is committed to the source tree. **Code generation** is triggered by `make generate`. Generated code is committed to the source tree.
### Project Structure ### Build & Release Process
``` **The `Makefile` is catering to the needs of developers & CI, not distro packagers**.
├── artifacts # build artifcats generate by make It provides phony targets for
├── cli # wrapper around CLI package cobra * local development (building, running tests, etc)
├── client # all subcommands that are not `daemon` * building a release in Docker (used by the CI & release management)
├── config # config data types (=> package yaml-config) * building .deb and .rpm packages out of the release artifacts.
│   └── samples
├── daemon # the implementation of `zrepl daemon` subcommand
│   ├── filters
│   ├── hooks # snapshot hooks
│   ├── job # job implementations
│   ├── logging # logging outlets + formatters
│   ├── nethelpers
│   ├── prometheus
│   ├── pruner # pruner implementation
│   ├── snapper # snapshotter implementation
├── dist # supplemental material for users & package maintainers
├── docs # sphinx-based documentation
│   ├── **/*.rst # documentation in reStructuredText
│   ├── sphinxconf
│   │   └── conf.py # sphinx config (see commit 445a280 why its not in docs/)
│   ├── requirements.txt # pip3 requirements to build documentation
│   ├── publish.sh # shell script for automated rendering & deploy to zrepl.github.io repo
│   └── public_git # checkout of zrepl.github.io managed by above shell script
├── endpoint # implementation of replication endpoints (=> package replication)
├── logger # our own logger package
├── platformtest # test suite for our zfs abstractions (error classification, etc)
├── pruning # pruning rules (the logic, not the actual execution)
│   └── retentiongrid
├── replication
│ ├── driver # the driver of the replication logic (status reporting, error handling)
│ ├── logic # planning & executing replication steps via rpc
| |   └── pdu # the generated gRPC & protobuf code used in replication (and endpoints)
│ └── report # the JSON-serializable report datastructures exposed to the client
├── rpc # the hybrid gRPC + ./dataconn RPC client: connects to a remote replication.Endpoint
│ ├── dataconn # Bulk data-transfer RPC protocol
│ ├── grpcclientidentity # adaptor to inject package transport's 'client identity' concept into gRPC contexts
│ ├── netadaptor # adaptor to convert a package transport's Connecter and Listener into net.* primitives
│ ├── transportmux # TCP connecter and listener used to split control & data traffic
│ └── versionhandshake # replication protocol version handshake perfomed on newly established connections
├── tlsconf # abstraction for Go TLS server + client config
├── transport # transport implementations
│ ├── fromconfig
│ ├── local
│ ├── ssh
│ ├── tcp
│ └── tls
├── util
├── version # abstraction for versions (filled during build by Makefile)
└── zfs # zfs(8) wrappers
```
### Coding Workflow **Build tooling & dependencies** are documented as code in `lazy.sh`.
Go dependencies are then fetched by the go command and pip dependencies are pinned through a `requirements.txt`.
**We use CircleCI for continuous integration**.
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.
Artifacts are published to minio.cschwarz.com (see GitHub Commit Status).
* `release` runs
* on manual triggers through the CircleCI API (in order to produce a release)
* periodically on `master`
Artifacts are published to minio.cschwarz.com (see GitHub Commit Status).
**Releases** are issued via Git tags + GitHub Releases feature.
The procedure to issue a release is as follows:
* Issue the source release:
* Git tag the release on the `master` branch.
* Push the tag.
* Run `./docs/publish.sh` to re-build & push zrepl.github.io.
* Issue the official binary release:
* Run the `release` pipeline (triggered via CircleCI API)
* Download the artifacts to the release manager's machine.
* Create a GitHub release, edit the changelog, upload all the release artifacts, including .rpm and .deb files.
* Issue the GitHub release.
* Add the .rpm and .deb files to the official zrepl repos, publish those.
**Official binary releases are not re-built when Go receives an update. If the Go update is critical to zrepl (e.g. a Go security update that affects zrepl), we'd issue a new source release**.
The rationale for this is that whereas distros provide a mechanism for this (`$zrepl_source_release-$distro_package_revision`), GitHub Releases doesn't which means we'd need to update the existing GitHub release's assets, which nobody would notice (no RSS feed updates, etc.).
Downstream packagers can read the changelog to determine whether they want to push that minor release into their distro or simply skip it.
### Additional Notes to Distro Package Maintainers
* Use `sudo make test-platform-bin && sudo make test-platform` **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.
## Contributing Code
* Open an issue when starting to hack on a new feature * Open an issue when starting to hack on a new feature
* Commits should reference the issue they are related to * Commits should reference the issue they are related to
@@ -123,9 +116,6 @@ An HTML report can be generated using `make cover-html`.
Backward-incompatible changes must be documented in the git commit message and are listed in `docs/changelog.rst`. Backward-incompatible changes must be documented in the git commit message and are listed in `docs/changelog.rst`.
* Config-breaking changes must contain a line `BREAK CONFIG` in the commit message
* Other breaking changes must contain a line `BREAK` in the commit message
### Glossary & Naming Inconsistencies ### Glossary & Naming Inconsistencies
In ZFS, *dataset* refers to the objects *filesystem*, *ZVOL* and *snapshot*. <br /> In ZFS, *dataset* refers to the objects *filesystem*, *ZVOL* and *snapshot*. <br />
@@ -142,16 +132,3 @@ variables and types are often named *dataset* when they in fact refer to a *file
There will not be a big refactoring (an attempt was made, but it's destroying too much history without much gain). There will not be a big refactoring (an attempt was made, but it's destroying too much history without much gain).
However, new contributions & patches should fix naming without further notice in the commit message. However, new contributions & patches should fix naming without further notice in the commit message.
### RPC debugging
Optionally, there are various RPC-related environment variables, that if set to something != `""` will produce additional debug output on stderr:
https://github.com/zrepl/zrepl/blob/master/rpc/rpc_debug.go#L11
https://github.com/zrepl/zrepl/blob/master/rpc/dataconn/dataconn_debug.go#L11
https://github.com/zrepl/zrepl/blob/master/rpc/dataconn/stream/stream_debug.go#L11
https://github.com/zrepl/zrepl/blob/master/rpc/dataconn/heartbeatconn/heartbeatconn_debug.go#L11
+1 -1
View File
@@ -1,4 +1,4 @@
FROM golang:latest FROM !SUBSTITUTED_BY_MAKEFILE
RUN apt-get update && apt-get install -y \ RUN apt-get update && apt-get install -y \
python3-pip \ python3-pip \
+1
View File
@@ -7,6 +7,7 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag" "github.com/spf13/pflag"
"github.com/zrepl/zrepl/daemon/logging/trace" "github.com/zrepl/zrepl/daemon/logging/trace"
"github.com/zrepl/zrepl/config" "github.com/zrepl/zrepl/config"
+5
View File
@@ -480,6 +480,11 @@ func (t *tui) renderReplicationReport(rep *report.Report, history *bytesProgress
t.newline() t.newline()
} }
if len(latest.Filesystems) == 0 {
t.write("NOTE: no filesystems were considered for replication!")
t.newline()
}
var maxFSLen int var maxFSLen int
for _, fs := range latest.Filesystems { for _, fs := range latest.Filesystems {
if len(fs.Info.Name) > maxFSLen { if len(fs.Info.Name) > maxFSLen {
+1 -1
View File
@@ -63,7 +63,7 @@ func doZabsList(ctx context.Context, sc *cli.Subcommand, args []string) error {
defer line.Lock().Unlock() defer line.Lock().Unlock()
if zabsListFlags.Json { if zabsListFlags.Json {
enc.SetIndent("", " ") enc.SetIndent("", " ")
if err := enc.Encode(abstractions); err != nil { if err := enc.Encode(a); err != nil {
panic(err) panic(err)
} }
fmt.Println() fmt.Println()
+28 -20
View File
@@ -52,11 +52,12 @@ func (j JobEnum) Name() string {
} }
type ActiveJob struct { type ActiveJob struct {
Type string `yaml:"type"` Type string `yaml:"type"`
Name string `yaml:"name"` Name string `yaml:"name"`
Connect ConnectEnum `yaml:"connect"` Connect ConnectEnum `yaml:"connect"`
Pruning PruningSenderReceiver `yaml:"pruning"` Pruning PruningSenderReceiver `yaml:"pruning"`
Debug JobDebugSettings `yaml:"debug,optional"` Debug JobDebugSettings `yaml:"debug,optional"`
Replication *Replication `yaml:"replication,optional,fromdefaults"`
} }
type PassiveJob struct { type PassiveJob struct {
@@ -76,18 +77,7 @@ type SnapJob struct {
} }
type SendOptions struct { type SendOptions struct {
Encrypted bool `yaml:"encrypted"` Encrypted bool `yaml:"encrypted,optional,default=false"`
StepHolds SendOptionsStepHolds `yaml:"step_holds,optional"`
}
type SendOptionsStepHolds struct {
DisableIncremental bool `yaml:"disable_incremental,optional"`
}
var _ yaml.Defaulter = (*SendOptions)(nil)
func (l *SendOptions) SetDefault() {
*l = SendOptions{Encrypted: false}
} }
type RecvOptions struct { type RecvOptions struct {
@@ -98,10 +88,13 @@ type RecvOptions struct {
// Reencrypt bool `yaml:"reencrypt"` // Reencrypt bool `yaml:"reencrypt"`
} }
var _ yaml.Defaulter = (*RecvOptions)(nil) type Replication struct {
Protection *ReplicationOptionsProtection `yaml:"protection,optional,fromdefaults"`
}
func (l *RecvOptions) SetDefault() { type ReplicationOptionsProtection struct {
*l = RecvOptions{} Initial string `yaml:"initial,optional,default=guarantee_resumability"`
Incremental string `yaml:"incremental,optional,default=guarantee_resumability"`
} }
type PushJob struct { type PushJob struct {
@@ -111,6 +104,9 @@ type PushJob struct {
Send *SendOptions `yaml:"send,fromdefaults,optional"` Send *SendOptions `yaml:"send,fromdefaults,optional"`
} }
func (j *PushJob) GetFilesystems() FilesystemsFilter { return j.Filesystems }
func (j *PushJob) GetSendOptions() *SendOptions { return j.Send }
type PullJob struct { type PullJob struct {
ActiveJob `yaml:",inline"` ActiveJob `yaml:",inline"`
RootFS string `yaml:"root_fs"` RootFS string `yaml:"root_fs"`
@@ -118,6 +114,10 @@ type PullJob struct {
Recv *RecvOptions `yaml:"recv,fromdefaults,optional"` Recv *RecvOptions `yaml:"recv,fromdefaults,optional"`
} }
func (j *PullJob) GetRootFS() string { return j.RootFS }
func (j *PullJob) GetAppendClientIdentity() bool { return false }
func (j *PullJob) GetRecvOptions() *RecvOptions { return j.Recv }
type PositiveDurationOrManual struct { type PositiveDurationOrManual struct {
Interval time.Duration Interval time.Duration
Manual bool Manual bool
@@ -155,6 +155,10 @@ type SinkJob struct {
Recv *RecvOptions `yaml:"recv,optional,fromdefaults"` Recv *RecvOptions `yaml:"recv,optional,fromdefaults"`
} }
func (j *SinkJob) GetRootFS() string { return j.RootFS }
func (j *SinkJob) GetAppendClientIdentity() bool { return true }
func (j *SinkJob) GetRecvOptions() *RecvOptions { return j.Recv }
type SourceJob struct { type SourceJob struct {
PassiveJob `yaml:",inline"` PassiveJob `yaml:",inline"`
Snapshotting SnapshottingEnum `yaml:"snapshotting"` Snapshotting SnapshottingEnum `yaml:"snapshotting"`
@@ -162,6 +166,9 @@ type SourceJob struct {
Send *SendOptions `yaml:"send,optional,fromdefaults"` Send *SendOptions `yaml:"send,optional,fromdefaults"`
} }
func (j *SourceJob) GetFilesystems() FilesystemsFilter { return j.Filesystems }
func (j *SourceJob) GetSendOptions() *SendOptions { return j.Send }
type FilesystemsFilter map[string]bool type FilesystemsFilter map[string]bool
type SnapshottingEnum struct { type SnapshottingEnum struct {
@@ -317,6 +324,7 @@ type PruneKeepNotReplicated struct {
type PruneKeepLastN struct { type PruneKeepLastN struct {
Type string `yaml:"type"` Type string `yaml:"type"`
Count int `yaml:"count"` Count int `yaml:"count"`
Regex string `yaml:"regex,optional"`
} }
type PruneKeepRegex struct { // FIXME rename to KeepRegex type PruneKeepRegex struct { // FIXME rename to KeepRegex
+90
View File
@@ -0,0 +1,90 @@
package config
import (
"fmt"
"testing"
"github.com/stretchr/testify/require"
"github.com/zrepl/yaml-config"
)
type A struct {
B *B `yaml:"b,optional,fromdefaults"`
A1 string `yaml:"a1,optional"`
}
type B struct {
C *C `yaml:"c,optional,fromdefaults"`
D string `yaml:"d,default=ddd"`
E string `yaml:"e,optional"`
}
type C struct {
Q string `yaml:"q,optional"`
R string `yaml:"r,default=r"`
}
func TestDepFromDefaults(t *testing.T) {
type testcase struct {
name string
yaml string
expect *A
}
tcs := []testcase{
{
name: "empty",
yaml: `{}`,
expect: &A{
B: &B{
C: &C{
R: "r",
},
D: "ddd",
},
},
},
{
name: "a1 set",
yaml: `{"a1":"blah"}`,
expect: &A{
A1: "blah",
B: &B{
C: &C{
R: "r",
},
D: "ddd",
},
},
},
{
name: "D set",
yaml: `
b:
d: 4d
`,
expect: &A{
B: &B{
D: "4d",
C: &C{
R: "r",
},
},
},
},
}
for tci := range tcs {
t.Run(fmt.Sprintf("%d-%s", tci, tcs[tci].name), func(t *testing.T) {
tc := tcs[tci]
var a A
err := yaml.UnmarshalStrict([]byte(tc.yaml), &a)
require.NoError(t, err)
require.Equal(t, tc.expect, &a)
})
}
}
+3 -3
View File
@@ -60,9 +60,9 @@ jobs:
}) })
t.Run("encrypted_unspecified", func(t *testing.T) { t.Run("encrypted_unspecified", func(t *testing.T) {
c, err := testConfig(t, fill(encrypted_unspecified)) c = testValidConfig(t, fill(encrypted_unspecified))
assert.Error(t, err) encrypted := c.Jobs[0].Ret.(*PushJob).Send.Encrypted
assert.Nil(t, c) assert.Equal(t, false, encrypted)
}) })
t.Run("send_not_specified", func(t *testing.T) { t.Run("send_not_specified", func(t *testing.T) {
+2 -2
View File
@@ -37,7 +37,7 @@ func (t *RetentionIntervalList) UnmarshalYAML(u func(interface{}, bool) error) (
return err return err
} }
intervals, err := parseRetentionGridIntervalsString(in) intervals, err := ParseRetentionIntervalSpec(in)
if err != nil { if err != nil {
return err return err
} }
@@ -102,7 +102,7 @@ func parseRetentionGridIntervalString(e string) (intervals []RetentionInterval,
} }
func parseRetentionGridIntervalsString(s string) (intervals []RetentionInterval, err error) { func ParseRetentionIntervalSpec(s string) (intervals []RetentionInterval, err error) {
ges := strings.Split(s, "|") ges := strings.Split(s, "|")
intervals = make([]RetentionInterval, 0, 7*len(ges)) intervals = make([]RetentionInterval, 0, 7*len(ges))
@@ -52,12 +52,15 @@ jobs:
} }
send: send:
encrypted: true encrypted: true
# disable incremental step holds so that replication:
# - we can yank out the backup drive during replication protection:
# - thereby sacrificing resumability initial: guarantee_resumability
# - in exchange for the replicating snapshot not sticking around until we reconnect the backup drive # Downgrade protection to guarantee_incremental which uses zfs bookmarks instead of zfs holds.
step_holds: # Thus, when we yank out the backup drive during replication
disable_incremental: true # - we might not be able to resume the interrupted replication step because the partially received `to` snapshot of a `from`->`to` step may be pruned any time
# - but in exchange we get back the disk space allocated by `to` when we prune it
# - and because we still have the bookmarks created by `guarantee_incremental`, we can still do incremental replication of `from`->`to2` in the future
incremental: guarantee_incremental
snapshotting: snapshotting:
type: manual type: manual
pruning: pruning:
+1
View File
@@ -12,6 +12,7 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"github.com/zrepl/zrepl/daemon/logging/trace" "github.com/zrepl/zrepl/daemon/logging/trace"
"github.com/zrepl/zrepl/endpoint" "github.com/zrepl/zrepl/endpoint"
"github.com/zrepl/zrepl/util/envconst" "github.com/zrepl/zrepl/util/envconst"
+1
View File
@@ -10,6 +10,7 @@ import (
"text/template" "text/template"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/zrepl/zrepl/daemon/logging/trace" "github.com/zrepl/zrepl/daemon/logging/trace"
"github.com/zrepl/zrepl/config" "github.com/zrepl/zrepl/config"
+36 -29
View File
@@ -9,10 +9,10 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/log" "github.com/prometheus/common/log"
"github.com/zrepl/zrepl/daemon/logging/trace" "github.com/zrepl/zrepl/daemon/logging/trace"
"github.com/zrepl/zrepl/config" "github.com/zrepl/zrepl/config"
"github.com/zrepl/zrepl/daemon/filters"
"github.com/zrepl/zrepl/daemon/job/reset" "github.com/zrepl/zrepl/daemon/job/reset"
"github.com/zrepl/zrepl/daemon/job/wakeup" "github.com/zrepl/zrepl/daemon/job/wakeup"
"github.com/zrepl/zrepl/daemon/pruner" "github.com/zrepl/zrepl/daemon/pruner"
@@ -35,9 +35,10 @@ type ActiveSide struct {
prunerFactory *pruner.PrunerFactory prunerFactory *pruner.PrunerFactory
promRepStateSecs *prometheus.HistogramVec // labels: state promRepStateSecs *prometheus.HistogramVec // labels: state
promPruneSecs *prometheus.HistogramVec // labels: prune_side promPruneSecs *prometheus.HistogramVec // labels: prune_side
promBytesReplicated *prometheus.CounterVec // labels: filesystem promBytesReplicated *prometheus.CounterVec // labels: filesystem
promReplicationErrors prometheus.Gauge
tasksMtx sync.Mutex tasksMtx sync.Mutex
tasks activeSideTasks tasks activeSideTasks
@@ -145,23 +146,24 @@ func (m *modePush) ResetConnectBackoff() {
func modePushFromConfig(g *config.Global, in *config.PushJob, jobID endpoint.JobID) (*modePush, error) { func modePushFromConfig(g *config.Global, in *config.PushJob, jobID endpoint.JobID) (*modePush, error) {
m := &modePush{} m := &modePush{}
var err error
fsf, err := filters.DatasetMapFilterFromConfig(in.Filesystems) m.senderConfig, err = buildSenderConfig(in, jobID)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "cannot build filesystem filter") return nil, errors.Wrap(err, "sender config")
} }
m.senderConfig = &endpoint.SenderConfig{ replicationConfig, err := logic.ReplicationConfigFromConfig(in.Replication)
FSF: fsf, if err != nil {
Encrypt: &zfs.NilBool{B: in.Send.Encrypted}, return nil, errors.Wrap(err, "field `replication`")
DisableIncrementalStepHolds: in.Send.StepHolds.DisableIncremental,
JobID: jobID,
} }
m.plannerPolicy = &logic.PlannerPolicy{ m.plannerPolicy = &logic.PlannerPolicy{
EncryptedSend: logic.TriFromBool(in.Send.Encrypted), EncryptedSend: logic.TriFromBool(in.Send.Encrypted),
ReplicationConfig: *replicationConfig,
} }
if m.snapper, err = snapper.FromConfig(g, fsf, in.Snapshotting); err != nil { if m.snapper, err = snapper.FromConfig(g, m.senderConfig.FSF, in.Snapshotting); err != nil {
return nil, errors.Wrap(err, "cannot build snapper") return nil, errors.Wrap(err, "cannot build snapper")
} }
@@ -173,7 +175,6 @@ type modePull struct {
receiver *endpoint.Receiver receiver *endpoint.Receiver
receiverConfig endpoint.ReceiverConfig receiverConfig endpoint.ReceiverConfig
sender *rpc.Client sender *rpc.Client
rootFS *zfs.DatasetPath
plannerPolicy *logic.PlannerPolicy plannerPolicy *logic.PlannerPolicy
interval config.PositiveDurationOrManual interval config.PositiveDurationOrManual
} }
@@ -247,26 +248,19 @@ func modePullFromConfig(g *config.Global, in *config.PullJob, jobID endpoint.Job
m = &modePull{} m = &modePull{}
m.interval = in.Interval m.interval = in.Interval
m.rootFS, err = zfs.NewDatasetPath(in.RootFS) replicationConfig, err := logic.ReplicationConfigFromConfig(in.Replication)
if err != nil { if err != nil {
return nil, errors.New("RootFS is not a valid zfs filesystem path") return nil, errors.Wrap(err, "field `replication`")
}
if m.rootFS.Length() <= 0 {
return nil, errors.New("RootFS must not be empty") // duplicates error check of receiver
} }
m.plannerPolicy = &logic.PlannerPolicy{ m.plannerPolicy = &logic.PlannerPolicy{
EncryptedSend: logic.DontCare, EncryptedSend: logic.DontCare,
ReplicationConfig: *replicationConfig,
} }
m.receiverConfig = endpoint.ReceiverConfig{ m.receiverConfig, err = buildReceiverConfig(in, jobID)
JobID: jobID, if err != nil {
RootWithoutClientComponent: m.rootFS, return nil, err
AppendClientIdentity: false, // !
UpdateLastReceivedHold: true,
}
if err := m.receiverConfig.Validate(); err != nil {
return nil, errors.Wrap(err, "cannot build receiver config")
} }
return m, nil return m, nil
@@ -307,6 +301,14 @@ func activeSide(g *config.Global, in *config.ActiveJob, configJob interface{}) (
ConstLabels: prometheus.Labels{"zrepl_job": j.name.String()}, ConstLabels: prometheus.Labels{"zrepl_job": j.name.String()},
}, []string{"filesystem"}) }, []string{"filesystem"})
j.promReplicationErrors = prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: "zrepl",
Subsystem: "replication",
Name: "filesystem_errors",
Help: "number of filesystems that failed replication in the latest replication attempt, or -1 if the job failed before enumerating the filesystems",
ConstLabels: prometheus.Labels{"zrepl_job": j.name.String()},
})
j.connecter, err = fromconfig.ConnecterFromConfig(g, in.Connect) j.connecter, err = fromconfig.ConnecterFromConfig(g, in.Connect)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "cannot build client") return nil, errors.Wrap(err, "cannot build client")
@@ -331,6 +333,7 @@ func (j *ActiveSide) RegisterMetrics(registerer prometheus.Registerer) {
registerer.MustRegister(j.promRepStateSecs) registerer.MustRegister(j.promRepStateSecs)
registerer.MustRegister(j.promPruneSecs) registerer.MustRegister(j.promPruneSecs)
registerer.MustRegister(j.promBytesReplicated) registerer.MustRegister(j.promBytesReplicated)
registerer.MustRegister(j.promReplicationErrors)
} }
func (j *ActiveSide) Name() string { return j.name.String() } func (j *ActiveSide) Name() string { return j.name.String() }
@@ -365,7 +368,7 @@ func (j *ActiveSide) OwnedDatasetSubtreeRoot() (rfs *zfs.DatasetPath, ok bool) {
_ = j.mode.(*modePush) // make sure we didn't introduce a new job type _ = j.mode.(*modePush) // make sure we didn't introduce a new job type
return nil, false return nil, false
} }
return pull.rootFS.Copy(), true return pull.receiverConfig.RootWithoutClientComponent.Copy(), true
} }
func (j *ActiveSide) SenderConfig() *endpoint.SenderConfig { func (j *ActiveSide) SenderConfig() *endpoint.SenderConfig {
@@ -463,6 +466,10 @@ func (j *ActiveSide) do(ctx context.Context) {
GetLogger(ctx).Info("start replication") GetLogger(ctx).Info("start replication")
repWait(true) // wait blocking repWait(true) // wait blocking
repCancel() // always cancel to free up context resources repCancel() // always cancel to free up context resources
replicationReport := j.tasks.replicationReport()
j.promReplicationErrors.Set(float64(replicationReport.GetFailedFilesystemsCountInLatestAttempt()))
endSpan() endSpan()
} }
+1
View File
@@ -5,6 +5,7 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/zrepl/zrepl/endpoint" "github.com/zrepl/zrepl/endpoint"
"github.com/zrepl/zrepl/transport" "github.com/zrepl/zrepl/transport"
) )
+56
View File
@@ -0,0 +1,56 @@
package job
import (
"github.com/pkg/errors"
"github.com/zrepl/zrepl/config"
"github.com/zrepl/zrepl/daemon/filters"
"github.com/zrepl/zrepl/endpoint"
"github.com/zrepl/zrepl/zfs"
)
type SendingJobConfig interface {
GetFilesystems() config.FilesystemsFilter
GetSendOptions() *config.SendOptions // must not be nil
}
func buildSenderConfig(in SendingJobConfig, jobID endpoint.JobID) (*endpoint.SenderConfig, error) {
fsf, err := filters.DatasetMapFilterFromConfig(in.GetFilesystems())
if err != nil {
return nil, errors.Wrap(err, "cannot build filesystem filter")
}
return &endpoint.SenderConfig{
FSF: fsf,
Encrypt: &zfs.NilBool{B: in.GetSendOptions().Encrypted},
JobID: jobID,
}, nil
}
type ReceivingJobConfig interface {
GetRootFS() string
GetAppendClientIdentity() bool
GetRecvOptions() *config.RecvOptions
}
func buildReceiverConfig(in ReceivingJobConfig, jobID endpoint.JobID) (rc endpoint.ReceiverConfig, err error) {
rootFs, err := zfs.NewDatasetPath(in.GetRootFS())
if err != nil {
return rc, errors.New("root_fs is not a valid zfs filesystem path")
}
if rootFs.Length() <= 0 {
return rc, errors.New("root_fs must not be empty") // duplicates error check of receiver
}
rc = endpoint.ReceiverConfig{
JobID: jobID,
RootWithoutClientComponent: rootFs,
AppendClientIdentity: in.GetAppendClientIdentity(),
}
if err := rc.Validate(); err != nil {
return rc, errors.Wrap(err, "cannot build receiver config")
}
return rc, nil
}
+36
View File
@@ -2,12 +2,16 @@ package job
import ( import (
"fmt" "fmt"
"path"
"path/filepath"
"testing" "testing"
"github.com/kr/pretty"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/zrepl/zrepl/config" "github.com/zrepl/zrepl/config"
"github.com/zrepl/zrepl/transport/tls"
) )
func TestValidateReceivingSidesDoNotOverlap(t *testing.T) { func TestValidateReceivingSidesDoNotOverlap(t *testing.T) {
@@ -108,3 +112,35 @@ jobs:
} }
} }
func TestSampleConfigsAreBuiltWithoutErrors(t *testing.T) {
paths, err := filepath.Glob("../../config/samples/*")
if err != nil {
t.Errorf("glob failed: %+v", err)
}
for _, p := range paths {
if path.Ext(p) != ".yml" {
t.Logf("skipping file %s", p)
continue
}
t.Run(p, func(t *testing.T) {
c, err := config.ParseConfig(p)
if err != nil {
t.Errorf("error parsing %s:\n%+v", p, err)
}
t.Logf("file: %s", p)
t.Log(pretty.Sprint(c))
tls.FakeCertificateLoading(t)
jobs, err := JobsFromConfig(c)
t.Logf("jobs: %#v", jobs)
assert.NoError(t, err)
})
}
}
+7 -22
View File
@@ -6,10 +6,10 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"github.com/zrepl/zrepl/daemon/logging/trace" "github.com/zrepl/zrepl/daemon/logging/trace"
"github.com/zrepl/zrepl/config" "github.com/zrepl/zrepl/config"
"github.com/zrepl/zrepl/daemon/filters"
"github.com/zrepl/zrepl/daemon/logging" "github.com/zrepl/zrepl/daemon/logging"
"github.com/zrepl/zrepl/daemon/snapper" "github.com/zrepl/zrepl/daemon/snapper"
"github.com/zrepl/zrepl/endpoint" "github.com/zrepl/zrepl/endpoint"
@@ -48,19 +48,9 @@ func (m *modeSink) SnapperReport() *snapper.Report { return nil }
func modeSinkFromConfig(g *config.Global, in *config.SinkJob, jobID endpoint.JobID) (m *modeSink, err error) { func modeSinkFromConfig(g *config.Global, in *config.SinkJob, jobID endpoint.JobID) (m *modeSink, err error) {
m = &modeSink{} m = &modeSink{}
rootDataset, err := zfs.NewDatasetPath(in.RootFS) m.receiverConfig, err = buildReceiverConfig(in, jobID)
if err != nil { if err != nil {
return nil, errors.New("root dataset is not a valid zfs filesystem path") return nil, err
}
m.receiverConfig = endpoint.ReceiverConfig{
JobID: jobID,
RootWithoutClientComponent: rootDataset,
AppendClientIdentity: true, // !
UpdateLastReceivedHold: true,
}
if err := m.receiverConfig.Validate(); err != nil {
return nil, errors.Wrap(err, "cannot build receiver config")
} }
return m, nil return m, nil
@@ -74,18 +64,13 @@ type modeSource struct {
func modeSourceFromConfig(g *config.Global, in *config.SourceJob, jobID endpoint.JobID) (m *modeSource, err error) { func modeSourceFromConfig(g *config.Global, in *config.SourceJob, jobID endpoint.JobID) (m *modeSource, err error) {
// FIXME exact dedup of modePush // FIXME exact dedup of modePush
m = &modeSource{} m = &modeSource{}
fsf, err := filters.DatasetMapFilterFromConfig(in.Filesystems)
m.senderConfig, err = buildSenderConfig(in, jobID)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "cannot build filesystem filter") return nil, errors.Wrap(err, "send options")
}
m.senderConfig = &endpoint.SenderConfig{
FSF: fsf,
Encrypt: &zfs.NilBool{B: in.Send.Encrypted},
DisableIncrementalStepHolds: in.Send.StepHolds.DisableIncremental,
JobID: jobID,
} }
if m.snapper, err = snapper.FromConfig(g, fsf, in.Snapshotting); err != nil { if m.snapper, err = snapper.FromConfig(g, m.senderConfig.FSF, in.Snapshotting); err != nil {
return nil, errors.Wrap(err, "cannot build snapper") return nil, errors.Wrap(err, "cannot build snapper")
} }
+1 -2
View File
@@ -7,6 +7,7 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"github.com/zrepl/zrepl/daemon/logging/trace" "github.com/zrepl/zrepl/daemon/logging/trace"
"github.com/zrepl/zrepl/config" "github.com/zrepl/zrepl/config"
@@ -175,8 +176,6 @@ func (j *SnapJob) doPrune(ctx context.Context) {
FSF: j.fsfilter, FSF: j.fsfilter,
// FIXME encryption setting is irrelevant for SnapJob because the endpoint is only used as pruner.Target // FIXME encryption setting is irrelevant for SnapJob because the endpoint is only used as pruner.Target
Encrypt: &zfs.NilBool{B: true}, Encrypt: &zfs.NilBool{B: true},
// FIXME DisableIncrementalStepHolds setting is irrelevant for SnapJob because the endpoint is only used as pruner.Target
DisableIncrementalStepHolds: false,
}) })
j.pruner = j.prunerFactory.BuildLocalPruner(ctx, sender, alwaysUpToDateReplicationCursorHistory{sender}) j.pruner = j.prunerFactory.BuildLocalPruner(ctx, sender, alwaysUpToDateReplicationCursorHistory{sender})
log.Info("start pruning") log.Info("start pruning")
+1
View File
@@ -9,6 +9,7 @@ import (
"github.com/mattn/go-isatty" "github.com/mattn/go-isatty"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/zrepl/zrepl/config" "github.com/zrepl/zrepl/config"
"github.com/zrepl/zrepl/daemon/logging/trace" "github.com/zrepl/zrepl/daemon/logging/trace"
"github.com/zrepl/zrepl/logger" "github.com/zrepl/zrepl/logger"
+47 -8
View File
@@ -93,11 +93,15 @@ package trace
import ( import (
"context" "context"
"fmt" "fmt"
"os"
runtimedebug "runtime/debug"
"strings" "strings"
"time" "time"
"github.com/kr/pretty"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"github.com/zrepl/zrepl/util/chainlock" "github.com/zrepl/zrepl/util/chainlock"
) )
@@ -120,15 +124,19 @@ func RegisterMetrics(r prometheus.Registerer) {
} }
type traceNode struct { type traceNode struct {
id string // NOTE: members with prefix debug are only valid if the package variable debugEnabled is true.
annotation string
parentTask *traceNode id string
annotation string
parentTask *traceNode
debugCreationStack string // debug: stack trace of when the traceNode was created
mtx chainlock.L mtx chainlock.L
activeChildTasks int32 // only for task nodes, insignificant for span nodes activeChildTasks int32 // only for task nodes, insignificant for span nodes
parentSpan *traceNode debugActiveChildTasks map[*traceNode]bool // debug: set of active child tasks
activeChildSpan *traceNode // nil if task or span doesn't have an active child span parentSpan *traceNode
activeChildSpan *traceNode // nil if task or span doesn't have an active child span
startedAt time.Time startedAt time.Time
endedAt time.Time endedAt time.Time
@@ -137,6 +145,14 @@ type traceNode struct {
func (s *traceNode) StartedAt() time.Time { return s.startedAt } func (s *traceNode) StartedAt() time.Time { return s.startedAt }
func (s *traceNode) EndedAt() time.Time { return s.endedAt } func (s *traceNode) EndedAt() time.Time { return s.endedAt }
// caller must hold mtx
func (s *traceNode) debugString() string {
if !debugEnabled {
return "<debugEnabled=false>"
}
return pretty.Sprint(s)
}
// Returned from WithTask or WithSpan. // Returned from WithTask or WithSpan.
// Must be called once the task or span ends. // Must be called once the task or span ends.
// See package-level docs for nesting rules. // See package-level docs for nesting rules.
@@ -196,8 +212,16 @@ func WithTask(ctx context.Context, taskName string) (context.Context, DoneFunc)
endedAt: time.Time{}, endedAt: time.Time{},
} }
if debugEnabled {
this.debugCreationStack = string(runtimedebug.Stack())
this.debugActiveChildTasks = map[*traceNode]bool{}
}
if parentTask != nil { if parentTask != nil {
this.parentTask.activeChildTasks++ this.parentTask.activeChildTasks++
if debugEnabled {
this.parentTask.debugActiveChildTasks[this] = true
}
parentTask.mtx.Unlock() parentTask.mtx.Unlock()
} }
@@ -218,7 +242,11 @@ func WithTask(ctx context.Context, taskName string) (context.Context, DoneFunc)
defer this.mtx.Lock().Unlock() defer this.mtx.Lock().Unlock()
if this.activeChildTasks != 0 { if this.activeChildTasks != 0 {
panic(errors.Wrapf(ErrTaskStillHasActiveChildTasks, "end task: %v active child tasks", this.activeChildTasks)) if debugEnabled {
// the debugString can be quite long and panic won't print it completely
fmt.Fprintf(os.Stderr, "going to panic due to activeChildTasks:\n%s\n", this.debugString())
}
panic(errors.WithMessagef(ErrTaskStillHasActiveChildTasks, "end task: %v active child tasks\n", this.activeChildTasks))
} }
// support idempotent task ends // support idempotent task ends
@@ -229,8 +257,15 @@ func WithTask(ctx context.Context, taskName string) (context.Context, DoneFunc)
if this.parentTask != nil { if this.parentTask != nil {
this.parentTask.activeChildTasks-- this.parentTask.activeChildTasks--
if debugEnabled {
delete(this.parentTask.debugActiveChildTasks, this)
}
if this.parentTask.activeChildTasks < 0 { if this.parentTask.activeChildTasks < 0 {
panic("impl error: parent task with negative activeChildTasks count") if debugEnabled {
// the debugString can be quite long and panic won't print it completely
fmt.Fprintf(os.Stderr, "going to panic due to activeChildTasks < 0:\n%s\n", this.parentTask.debugString())
}
panic(fmt.Sprintf("impl error: parent task with negative activeChildTasks count: %v", this.parentTask.activeChildTasks))
} }
} }
return false return false
@@ -279,6 +314,10 @@ func WithSpan(ctx context.Context, annotation string) (context.Context, DoneFunc
endedAt: time.Time{}, endedAt: time.Time{},
} }
if debugEnabled {
this.debugCreationStack = string(runtimedebug.Stack())
}
parentSpan.mtx.HoldWhile(func() { parentSpan.mtx.HoldWhile(func() {
if parentSpan.activeChildSpan != nil { if parentSpan.activeChildSpan != nil {
panic(ErrAlreadyActiveChildSpan) panic(ErrAlreadyActiveChildSpan)
+3 -1
View File
@@ -3,9 +3,11 @@ package trace
import ( import (
"fmt" "fmt"
"os" "os"
"github.com/zrepl/zrepl/util/envconst"
) )
const debugEnabled = false var debugEnabled = envconst.Bool("ZREPL_TRACE_DEBUG_ENABLED", false)
func debug(format string, args ...interface{}) { func debug(format string, args ...interface{}) {
if !debugEnabled { if !debugEnabled {
+4 -4
View File
@@ -8,10 +8,10 @@ import (
"time" "time"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/zrepl/zrepl/daemon/logging/trace" "github.com/zrepl/zrepl/daemon/logging/trace"
"github.com/zrepl/zrepl/config" "github.com/zrepl/zrepl/config"
"github.com/zrepl/zrepl/daemon/filters"
"github.com/zrepl/zrepl/daemon/hooks" "github.com/zrepl/zrepl/daemon/hooks"
"github.com/zrepl/zrepl/daemon/logging" "github.com/zrepl/zrepl/daemon/logging"
"github.com/zrepl/zrepl/logger" "github.com/zrepl/zrepl/logger"
@@ -49,7 +49,7 @@ type args struct {
ctx context.Context ctx context.Context
prefix string prefix string
interval time.Duration interval time.Duration
fsf *filters.DatasetMapFilter fsf zfs.DatasetFilter
snapshotsTaken chan<- struct{} snapshotsTaken chan<- struct{}
hooks *hooks.List hooks *hooks.List
dryRun bool dryRun bool
@@ -109,7 +109,7 @@ func getLogger(ctx context.Context) Logger {
return logging.GetLogger(ctx, logging.SubsysSnapshot) return logging.GetLogger(ctx, logging.SubsysSnapshot)
} }
func PeriodicFromConfig(g *config.Global, fsf *filters.DatasetMapFilter, in *config.SnapshottingPeriodic) (*Snapper, error) { func PeriodicFromConfig(g *config.Global, fsf zfs.DatasetFilter, in *config.SnapshottingPeriodic) (*Snapper, error) {
if in.Prefix == "" { if in.Prefix == "" {
return nil, errors.New("prefix must not be empty") return nil, errors.New("prefix must not be empty")
} }
@@ -383,7 +383,7 @@ func wait(a args, u updater) state {
} }
} }
func listFSes(ctx context.Context, mf *filters.DatasetMapFilter) (fss []*zfs.DatasetPath, err error) { func listFSes(ctx context.Context, mf zfs.DatasetFilter) (fss []*zfs.DatasetPath, err error) {
return zfs.ZFSListMapping(ctx, mf) return zfs.ZFSListMapping(ctx, mf)
} }
+2 -2
View File
@@ -5,7 +5,7 @@ import (
"fmt" "fmt"
"github.com/zrepl/zrepl/config" "github.com/zrepl/zrepl/config"
"github.com/zrepl/zrepl/daemon/filters" "github.com/zrepl/zrepl/zfs"
) )
// FIXME: properly abstract snapshotting: // FIXME: properly abstract snapshotting:
@@ -32,7 +32,7 @@ func (s *PeriodicOrManual) Report() *Report {
return nil return nil
} }
func FromConfig(g *config.Global, fsf *filters.DatasetMapFilter, in config.SnapshottingEnum) (*PeriodicOrManual, error) { func FromConfig(g *config.Global, fsf zfs.DatasetFilter, in config.SnapshottingEnum) (*PeriodicOrManual, error) {
switch v := in.Ret.(type) { switch v := in.Ret.(type) {
case *config.SnapshottingPeriodic: case *config.SnapshottingPeriodic:
snapper, err := PeriodicFromConfig(g, fsf, v) snapper, err := PeriodicFromConfig(g, fsf, v)
Symlink
+1
View File
@@ -0,0 +1 @@
packaging/deb/debian
+175 -15
View File
@@ -69,6 +69,139 @@
"title": "Panel Title", "title": "Panel Title",
"type": "text" "type": "text"
}, },
{
"cacheTimeout": null,
"colorBackground": true,
"colorPostfix": false,
"colorPrefix": false,
"colorValue": false,
"colors": [
"#bf1b00",
"#508642",
"#bf1b00"
],
"datasource": "${DS_PROMETHEUS}",
"description": "Number of filesystems that failed replications",
"format": "none",
"gauge": {
"maxValue": 100,
"minValue": 0,
"show": false,
"thresholdLabels": false,
"thresholdMarkers": true
},
"gridPos": {
"h": 3,
"w": 24,
"x": 0,
"y": 10
},
"id": 50,
"interval": null,
"links": [],
"mappingType": 1,
"mappingTypes": [
{
"name": "value to text",
"value": 1
},
{
"name": "range to text",
"value": 2
}
],
"maxDataPoints": 100,
"nullPointMode": "connected",
"nullText": null,
"postfix": "",
"postfixFontSize": "50%",
"prefix": "",
"prefixFontSize": "50%",
"rangeMaps": [
{
"from": "",
"text": "",
"to": ""
}
],
"repeat": "zrepl_job_name",
"repeatDirection": "h",
"scopedVars": {
"zrepl_job_name": {
"selected": false,
"text": "desktop_to_homesrv",
"value": "desktop_to_homesrv"
}
},
"sparkline": {
"fillColor": "rgba(31, 118, 189, 0.18)",
"full": true,
"lineColor": "rgb(31, 120, 193)",
"show": true
},
"tableColumn": "__name__",
"targets": [
{
"expr": "zrepl_replication_filesystem_errors{job=\"$prom_job_name\",zrepl_job=\"$zrepl_job_name\"}",
"format": "time_series",
"groupBy": [
{
"params": [
"$__interval"
],
"type": "time"
},
{
"params": [
"null"
],
"type": "fill"
}
],
"instant": true,
"interval": "",
"intervalFactor": 1,
"legendFormat": "",
"orderByTime": "ASC",
"policy": "default",
"refId": "A",
"resultFormat": "time_series",
"select": [
[
{
"params": [
"value"
],
"type": "field"
},
{
"params": [],
"type": "mean"
}
]
],
"tags": []
}
],
"thresholds": "0,1",
"title": "Failed replications $zrepl_job_name",
"transparent": false,
"type": "singlestat",
"valueFontSize": "80%",
"valueMaps": [
{
"op": "=",
"text": "All failed",
"value": "-1"
},
{
"op": "=",
"text": "All OK",
"value": "0"
}
],
"valueName": "avg"
},
{ {
"aliasColors": {}, "aliasColors": {},
"bars": false, "bars": false,
@@ -87,7 +220,7 @@
"h": 4, "h": 4,
"w": 12, "w": 12,
"x": 0, "x": 0,
"y": 10 "y": 13
}, },
"hiddenSeries": false, "hiddenSeries": false,
"id": 48, "id": 48,
@@ -181,7 +314,7 @@
"h": 5, "h": 5,
"w": 12, "w": 12,
"x": 12, "x": 12,
"y": 10 "y": 13
}, },
"hiddenSeries": false, "hiddenSeries": false,
"id": 44, "id": 44,
@@ -273,7 +406,7 @@
"h": 5, "h": 5,
"w": 12, "w": 12,
"x": 0, "x": 0,
"y": 14 "y": 17
}, },
"hiddenSeries": false, "hiddenSeries": false,
"id": 42, "id": 42,
@@ -373,7 +506,7 @@
"h": 4, "h": 4,
"w": 12, "w": 12,
"x": 12, "x": 12,
"y": 15 "y": 18
}, },
"hiddenSeries": false, "hiddenSeries": false,
"id": 22, "id": 22,
@@ -465,7 +598,7 @@
"h": 5, "h": 5,
"w": 12, "w": 12,
"x": 0, "x": 0,
"y": 19 "y": 22
}, },
"hiddenSeries": false, "hiddenSeries": false,
"id": 33, "id": 33,
@@ -573,7 +706,7 @@
"h": 5, "h": 5,
"w": 12, "w": 12,
"x": 12, "x": 12,
"y": 19 "y": 22
}, },
"hiddenSeries": false, "hiddenSeries": false,
"id": 23, "id": 23,
@@ -665,7 +798,7 @@
"h": 5, "h": 5,
"w": 12, "w": 12,
"x": 0, "x": 0,
"y": 24 "y": 27
}, },
"hiddenSeries": false, "hiddenSeries": false,
"id": 41, "id": 41,
@@ -758,7 +891,7 @@
"h": 5, "h": 5,
"w": 12, "w": 12,
"x": 12, "x": 12,
"y": 24 "y": 27
}, },
"hiddenSeries": false, "hiddenSeries": false,
"id": 47, "id": 47,
@@ -787,7 +920,7 @@
"stack": false, "stack": false,
"steppedLine": false, "steppedLine": false,
"targets": [{ "targets": [{
"expr": "zrepl_endpoint_send_abstractions_cache_entry_count", "expr": "zrepl_endpoint_abstractions_cache_entry_count",
"format": "time_series", "format": "time_series",
"intervalFactor": 1, "intervalFactor": 1,
"refId": "A" "refId": "A"
@@ -796,7 +929,7 @@
"timeFrom": null, "timeFrom": null,
"timeRegions": [], "timeRegions": [],
"timeShift": null, "timeShift": null,
"title": "send abstractions cache entry count (should not be zero and not grow unboundedly)", "title": "zfs abstractions cache entry count (should not be zero and not grow unboundedly)",
"tooltip": { "tooltip": {
"shared": true, "shared": true,
"sort": 0, "sort": 0,
@@ -850,7 +983,7 @@
"h": 5, "h": 5,
"w": 12, "w": 12,
"x": 0, "x": 0,
"y": 29 "y": 32
}, },
"hiddenSeries": false, "hiddenSeries": false,
"id": 17, "id": 17,
@@ -943,7 +1076,7 @@
"h": 5, "h": 5,
"w": 12, "w": 12,
"x": 0, "x": 0,
"y": 34 "y": 37
}, },
"hiddenSeries": false, "hiddenSeries": false,
"id": 19, "id": 19,
@@ -1044,6 +1177,33 @@
"tagsQuery": "", "tagsQuery": "",
"type": "query", "type": "query",
"useTags": false "useTags": false
},
{
"allValue": null,
"current": {
"text": "All",
"value": [
"$__all"
]
},
"datasource": "${DS_PROMETHEUS}",
"definition": "label_values(zrepl_replication_filesystem_errors{job=\"$prom_job_name\"}, zrepl_job)",
"hide": 2,
"includeAll": true,
"label": "Zrepl Job Name",
"multi": true,
"name": "zrepl_job_name",
"options": [],
"query": "label_values(zrepl_replication_filesystem_errors{job=\"$prom_job_name\"}, zrepl_job)",
"refresh": 1,
"regex": "",
"skipUrlSync": false,
"sort": 1,
"tagValuesQuery": "",
"tags": [],
"tagsQuery": "",
"type": "query",
"useTags": false
}] }]
}, },
"time": { "time": {
@@ -1076,6 +1236,6 @@
}, },
"timezone": "", "timezone": "",
"title": "zrepl 0.3", "title": "zrepl 0.3",
"uid": "etJuvBmGz", "uid": "etQuvBnGz",
"version": 6 "version": 7
} }
+48 -16
View File
@@ -27,6 +27,33 @@ We use the following annotations for classifying changes:
* |bugfix| Change that fixes a bug, no regressions or incompatibilities expected. * |bugfix| Change that fixes a bug, no regressions or incompatibilities expected.
* |docs| Change to the documentation. * |docs| Change to the documentation.
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
* |docs| update multi-job & multi-host setup section
* RPM Packaging
* CI infrastructure rework
* Continuous deployment of that new `stable` branch to zrepl.github.io.
.. NOTE::
| zrepl is a spare-time project primarily developed by `Christian Schwarz <https://cschwarz.com>`_.
| You can support maintenance and feature development through one of the following services:
| |Donate via Patreon| |Donate via GitHub Sponsors| |Donate via Liberapay| |Donate via PayPal|
| Note that PayPal processing fees are relatively high for small donations.
| For SEPA wire transfer and **commercial support**, please `contact Christian directly <https://cschwarz.com>`_.
.. _release-0.3:
0.3 0.3
--- ---
@@ -34,27 +61,38 @@ This is a big one! Headlining features:
* **Resumable Send & Recv Support** * **Resumable Send & Recv Support**
No knobs required, automatically used where supported. No knobs required, automatically used where supported.
* **Hold-Protected Send & Recv**
Automatic ZFS holds to ensure that we can always use resumable send&recv for a replication step.
* **Encrypted Send & Recv Support** for OpenZFS native encryption, * **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. :ref:`configurable <job-send-options>` at the job level, i.e., for all filesystems a job is responsible for.
* **Receive-side hold on last received dataset** * **Replication Guarantees**
The counterpart to the replication cursor bookmark on the send-side. Automatic use of ZFS holds and bookmarks to protect a replicated filesystem from losing synchronization between sender and receiver.
Ensures that incremental replication will always be possible between a sender and receiver. By default, zrepl guarantees that incremental replication will always be possible and interrupted steps will always be resumable.
.. TIP:: .. TIP::
We highly recommend studying the :ref:`overview section of the configuration chapter <overview-how-replication-works>` to understand how replication works. 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: Additional changelog:
* |break| Go 1.15 TLS changes mentioned above.
* |break| |break_config| **more restrictive job names than in prior zrepl versions** * |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:`here<replication-cursor-and-last-received-hold>` and :ref:`here<step-holds-and-bookmarks>`). 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. Therefore you might need to adjust your job names.
**Note that jobs** :issue:`cannot be renamed easily` **once you start using zrepl 0.3.** **Note that jobs** cannot be renamed easily **once you start using zrepl 0.3.**
* |break| |mig| replication cursor representation changed * |break| |mig| replication cursor representation changed
* zrepl now manages the :ref:`replication cursor bookmark <replication-cursor-and-last-received-hold>` per job-filesystem tuple instead of a single replication cursor per filesystem. * 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. 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. * 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. * 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.
@@ -62,6 +100,7 @@ Additional changelog:
The migration will ensure that only those old-format cursors are destroyed that have been superseeded by new-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) * |feature| New option ``listen_freebind`` (tcp, tls, prometheus listener)
* |feature| :issue:`341` Prometheus metric for failing replications + corresponding Grafana panel
* |feature| :issue:`265` transport/tcp: support for CIDR masks in client IP whitelist * |feature| :issue:`265` transport/tcp: support for CIDR masks in client IP whitelist
* |feature| documented subcommand to generate ``bash`` and ``zsh`` completions * |feature| documented subcommand to generate ``bash`` and ``zsh`` completions
* |feature| :issue:`307` ``chrome://trace`` -compatible activity tracing of zrepl daemon activity * |feature| :issue:`307` ``chrome://trace`` -compatible activity tracing of zrepl daemon activity
@@ -77,13 +116,6 @@ Additional changelog:
* **[MAINTAINER NOTICE]** New platform tests in this version, please make sure you run them for your distro! * **[MAINTAINER NOTICE]** New platform tests in this version, please make sure you run them for your distro!
* **[MAINTAINER NOTICE]** Please add the shell completions to the zrepl packages. * **[MAINTAINER NOTICE]** Please add the shell completions to the zrepl packages.
.. NOTE::
| zrepl is a spare-time project primarily developed by `Christian Schwarz <https://cschwarz.com>`_.
| You can support maintenance and feature development through one of the following services:
| |Donate via Patreon| |Donate via GitHub Sponsors| |Donate via Liberapay| |Donate via PayPal|
| Note that PayPal processing fees are relatively high for small donations.
| For SEPA wire transfer and **commercial support**, please `contact Christian directly <https://cschwarz.com>`_.
0.2.1 0.2.1
----- -----
+1
View File
@@ -12,6 +12,7 @@ Configuration
configuration/transports configuration/transports
configuration/filter_syntax configuration/filter_syntax
configuration/sendrecvoptions configuration/sendrecvoptions
configuration/replication
configuration/snapshotting configuration/snapshotting
configuration/prune configuration/prune
configuration/logging configuration/logging
+74 -55
View File
@@ -59,8 +59,8 @@ Note that snapshot-creation denoted by "(snap)" is orthogonal to whether a job i
| Pull mode | ``pull`` | ``source`` | * Central backup-server for many nodes | | Pull mode | ``pull`` | ``source`` | * Central backup-server for many nodes |
| | | (snap) | * Remote server to NAS behind NAT | | | | (snap) | * Remote server to NAS behind NAT |
+-----------------------+--------------+----------------------------------+------------------------------------------------------------------------------------+ +-----------------------+--------------+----------------------------------+------------------------------------------------------------------------------------+
| Local replication | | ``push`` + ``sink`` in one config | * Backup FreeBSD boot pool | | Local replication | | ``push`` + ``sink`` in one config | * Backup to :ref:`locally attached disk <quickstart-backup-to-external-disk>` |
| | | with :ref:`local transport <transport-local>` | | | | | with :ref:`local transport <transport-local>` | * Backup FreeBSD boot pool |
+-----------------------+--------------+----------------------------------+------------------------------------------------------------------------------------+ +-----------------------+--------------+----------------------------------+------------------------------------------------------------------------------------+
| Snap & prune-only | ``snap`` | N/A | * | Snapshots & pruning but no replication | | Snap & prune-only | ``snap`` | N/A | * | Snapshots & pruning but no replication |
| | (snap) | | | required | | | (snap) | | | required |
@@ -80,16 +80,19 @@ The active side (:ref:`push <job-push>` and :ref:`pull <job-pull>` job) executes
.. TIP:: .. TIP::
The progress of the active side can be watched live using the ``zrepl status`` subcommand. The progress of the active side can be watched live using the ``zrepl status`` subcommand.
.. _overview-passive-side--client-identity:
How the Passive Side Works How the Passive Side Works
-------------------------- --------------------------
The passive side (:ref:`sink <job-sink>` and :ref:`source <job-source>`) waits for connections from the corresponding active side, The passive side (:ref:`sink <job-sink>` and :ref:`source <job-source>`) waits for connections from the corresponding active side,
using the transport listener type specified in the ``serve`` field of the job configuration. using the transport listener type specified in the ``serve`` field of the job configuration.
Each transport listener provides a client's identity to the passive side job. When a client connects, the transport listener performS listener-specific access control (cert validation, IP ACLs, etc)
It uses the client identity for access control: and determines the *client identity*.
The passive side job then uses this client identity as follows:
* The ``sink`` job maps requests from different client identities to their respective sub-filesystem tree ``root_fs/${client_identity}``. * The ``sink`` job maps requests from different client identities to their respective sub-filesystem tree ``root_fs/${client_identity}``.
* The ``source`` job has a whitelist of client identities that are allowed pull access. * The ``source`` might, in the future, embed the client identity in :ref:`zrepl's ZFS abstraction names <zrepl-zfs-abstractions>` in order to support multi-host replication.
.. TIP:: .. TIP::
The implementation of the ``sink`` job requires that the connecting client identities be a valid ZFS filesystem name components. The implementation of the ``sink`` job requires that the connecting client identities be a valid ZFS filesystem name components.
@@ -132,23 +135,10 @@ The following high-level steps take place during replication and can be monitore
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. 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.
Placeholder Filesystems
^^^^^^^^^^^^^^^^^^^^^^^
.. _replication-placeholder-property:
**Placeholder filesystems** on the receiving side are regular ZFS filesystems with the placeholder 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.
ZFS Background Knowledge ZFS Background Knowledge
^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^
This section gives some background knowledge about ZFS features that zrepl uses to provide guarantees for a replication filesystem.
This section gives some background knowledge about ZFS features that zrepl uses to guarantee that Specifically, zrepl guarantees by default that **incremental replication is always possible and that started replication steps can always be resumed if they are interrupted.**
**incremental replication is always possible and that started replication steps can always be resumed if they are interrupted.**
**ZFS Send Modes & Bookmarks** **ZFS Send Modes & Bookmarks**
ZFS supports full sends (``zfs send fs@to``) and incremental sends (``zfs send -i @from fs@to``). ZFS supports full sends (``zfs send fs@to``) and incremental sends (``zfs send -i @from fs@to``).
@@ -166,41 +156,56 @@ An incremental send can only be resumed if ``@to`` still exists *and* either ``@
**ZFS Holds** **ZFS Holds**
ZFS holds prevent a snapshot from being deleted through ``zfs destroy``, letting the destroy fail with a ``datset is busy`` error. 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 user-defined *tag*. They can be thought of as a named, persistent lock on the snapshot. 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: .. _replication-cursor-and-last-received-hold:
Guaranteeing That Incremental Sends Are Always Possible The **replication cursor** bookmark and **last-received-hold** are managed by zrepl to ensure that future replications can always be done incrementally.
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
**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, 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. and the last-received-hold is a hold of that snapshot on the receiving side.
Both are moved aomically after the receiving side has confirmed that a replication step is complete. 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 replication cursor has the format ``#zrepl_CUSOR_G_<GUID>_J_<JOBNAME>``.
The last-received-hold tag has the format ``zrepl_last_received_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. 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 boomkarks 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. The ``zrepl zfs-abstraction list`` command provides a listing of all bookmarks and holds managed by zrepl.
.. _step-holds-and-bookmarks: .. _step-holds:
Guaranteeing That Sends Are Always Resumable
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
**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. **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. 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 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``. 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. 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>``. The hold tag has the format ``zrepl_STEP_J_<JOBNAME>``.
A job only ever has one active send per filesystem. 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)``. 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). **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`` that is not managed by zrepl. 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>``. 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. 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 . 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 .
@@ -227,39 +232,53 @@ Limitations
Multiple Jobs & More than 2 Machines Multiple Jobs & More than 2 Machines
------------------------------------ ------------------------------------
The quick-start guides focus on simple setups with a single sender and a single receiver.
This section documents considerations for more complex setups.
.. ATTENTION:: .. ATTENTION::
When using multiple jobs across single or multiple machines, the following rules are critical to avoid race conditions & data loss: 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.
1. The sets of ZFS filesystems matched by the ``filesystems`` filter fields must be disjoint across all jobs configured on a machine. .. NOTE::
2. The ZFS filesystem subtrees of jobs with ``root_fs`` must be disjoint.
3. Across all zrepl instances on all machines in the replication domain, there must be a 1:1 correspondence between active and passive jobs.
Explanations & exceptions to above rules are detailed below. 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>`_.
If you 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.
No Overlapping On your setup, ensure that
^^^^^^^^^^^^^^
Jobs run independently of each other. * all ``filesystems`` filter specifications are disjoint
If two jobs match the same filesystem with their ``filesystems`` filter, they will operate on that filesystem independently and potentially in parallel. * no ``root_fs`` is a prefix or equal to another ``root_fs``
For example, if job A prunes snapshots that job B is planning to replicate, the replication will fail because B assumed the snapshot to still be present. * no ``filesystems`` filter maches any ``root_fs``
However, the next replication attempt will re-examine the situation from scratch and should work.
N push jobs to 1 sink **Exceptions to the rule**:
^^^^^^^^^^^^^^^^^^^^^
The :ref:`sink job <job-sink>` namespaces by client identity. * A ``snap`` and ``push`` job on the same machine can match the same ``filesystems``.
It is thus safe to push to one sink job with different client identities. To avoid interference, only one of the jobs should be pruning snapshots on the sender, the other one should keep all snapshots.
If the push jobs have the same client identity, the filesystems matched by the push jobs must be disjoint to avoid races. 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>`.
N pull jobs from 1 source
^^^^^^^^^^^^^^^^^^^^^^^^^
Multiple pull jobs pulling from the same source have potential for race conditions during pruning: More Than 2 Machines
each pull job prunes the source side independently, causing replication-prune and prune-prune races. ^^^^^^^^^^^^^^^^^^^^
There is currently no way for a pull job to filter which snapshots it should attempt to replicate. 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).
Thus, it is not possible to just manually assert that the prune rules of all pull jobs are disjoint to avoid replication-prune and prune-prune races.
**Working setups**:
* N ``push`` identities, 1 ``sink`` (as long as the different push jobs have a different :ref:`client identity <overview-passive-side--client-identity>`)
* ``sink`` constrains each client to a disjoint sub-tree of the sink-side dataset hierarchy ``${root_fs}/${client_identity}``.
Therefore, the different clients cannot interfere.
**Setups that do not work**:
* N ``pull`` identities, 1 ``source`` job. Tracking :issue:`380`.
+74 -22
View File
@@ -46,6 +46,8 @@ Example Configuration:
grid: 1x1h(keep=all) | 24x1h | 35x1d | 6x30d grid: 1x1h(keep=all) | 24x1h | 35x1d | 6x30d
regex: "^zrepl_.*" regex: "^zrepl_.*"
# manually created snapshots will be kept forever on receiver # manually created snapshots will be kept forever on receiver
- type: regex
regex: "^manual_.*"
.. DANGER:: .. DANGER::
You might have **existing snapshots** of filesystems affected by pruning which you want to keep, i.e. not be destroyed by zrepl. You might have **existing snapshots** of filesystems affected by pruning which you want to keep, i.e. not be destroyed by zrepl.
@@ -82,34 +84,82 @@ Policy ``grid``
- type: grid - type: grid
regex: "^zrepl_.*" regex: "^zrepl_.*"
grid: 1x1h(keep=all) | 24x1h | 35x1d | 6x30d grid: 1x1h(keep=all) | 24x1h | 35x1d | 6x30d
│ │ │ │
└─ one hour interval └─ 1 repetition of a one-hour interval with keep=all
└─ 24 adjacent one-hour intervals └─ 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: 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:
the left edge of the leftmost (first) interval is the ``creation`` date of the youngest snapshot.
All intervals to its right describe time intervals further in the past.
Each interval carries a maximum number of snapshots to keep.
It is specified via ``(keep=N)``, where ``N`` is either ``all`` (all snapshots are kept) or a positive integer.
The default value is **keep=1**.
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 following procedure happens during pruning:
#. The list of snapshots is filtered by the regular expression in ``regex``. #. 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 are not affected. Only snapshots names that match the regex are considered for this rule, all others will be pruned unless another rule keeps them.
#. The filtered list of snapshots is sorted by ``creation`` #. The snapshots that match ``regex`` are placed onto a time axis according to their ``creation`` date.
#. The left edge of the first interval is aligned to the ``creation`` date of the youngest snapshot The youngest snapshot is on the left, the oldest on the right.
#. A list of buckets is created, one for each interval #. The first buckets are placed "under" that axis so that the ``grid`` spec's first bucket's left edge aligns with youngest snapshot.
#. The list of snapshots is split up into the buckets. #. All subsequent buckets are placed adjacent to their predecessor bucket.
#. For each 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 **not kept** by this gridspec.
#. For each bucket, we only keep the ``keep`` oldest snapshots.
#. the contained snapshot list is sorted by creation. The syntax to describe the bucket list is as follows:
#. snapshots from the list, oldest first, are destroyed until the specified ``keep`` count is reached.
#. all remaining snapshots on the list are kept. ::
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**:
::
This grid spec produces the following list of adjacent buckets. For the sake of simplicity,
we subject all snapshots to the grid pruning policy by settings `regex: .*`.
`
grid: 1x1h(keep=all) | 2x2h | 1x3h
regex: .*
`
0h 1h 2h 3h 4h 5h 6h 7h 8h 9h
| | | | | | | | | |
|-Bucket1-|-----Bucket 2------|------Bucket 3-----|-----------Bucket 4----------|
| keep=all| keep=1 | keep=1 | keep=1 |
Let us consider the following set of snapshots @a-zA-C:
| a b c d e f g h i j k l m n o p q r s t u v w x y z A B C D |
The `grid` algorithm maps them to their respective buckets:
Bucket 1: a, b, c
Bucket 2: d,e,f,g,h,i,j
Bucket 3: k,l,m,n,o,p
Bucket 4: q,r, q,r,s,t,u,v,w,x,y,z
None: A,B,C,D
It then applies the per-bucket pruning logic described above which resulting in the
following list of remaining snapshots.
| a b c j p z |
Note that it only makes sense to grow (not shorten) the interval duration for buckets
further in the past since each bucket acts like a low-pass filter for incoming snapshots
and adding a less-low-pass-filter after a low-pass one has no effect.
.. _prune-keep-last-n: .. _prune-keep-last-n:
@@ -125,9 +175,11 @@ Policy ``last_n``
keep_receiver: keep_receiver:
- type: last_n - type: last_n
count: 10 count: 10
regex: ^zrepl_.*$ # optional
... ...
``last_n`` keeps the last ``count`` snapshots (last = youngest = most recent creation date). ``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: .. _prune-keep-regex:
+47
View File
@@ -0,0 +1,47 @@
.. include:: ../global.rst.inc
Replication Options
===================
::
jobs:
- type: push
filesystems: ...
replication:
protection:
initial: guarantee_resumability # guarantee_{resumability,incremental,nothing}
incremental: guarantee_resumability # guarantee_{resumability,incremental,nothing}
...
.. _replication-option-protection:
``protection`` option
--------------------------
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.
-20
View File
@@ -16,8 +16,6 @@ Send Options
filesystems: ... filesystems: ...
send: send:
encrypted: true encrypted: true
step_holds:
disable_incremental: false
... ...
:ref:`Source<job-source>` and :ref:`push<job-push>` jobs have an optional ``send`` configuration section. :ref:`Source<job-source>` and :ref:`push<job-push>` jobs have an optional ``send`` configuration section.
@@ -36,24 +34,6 @@ Filesystems matched by ``filesystems`` that are not encrypted are not sent and w
If ``encryption=false``, zrepl expects that filesystems matching ``filesystems`` are not encrypted or have loaded encryption keys. If ``encryption=false``, zrepl expects that filesystems matching ``filesystems`` are not encrypted or have loaded encryption keys.
.. _job-send-option-step-holds-disable-incremental:
``step_holds.disable_incremental`` option
-----------------------------------------
The ``step_holds.disable_incremental`` variable controls whether the creation of :ref:`step holds <step-holds-and-bookmarks>` should be disabled for incremental replication.
The default value is ``false``.
Disabling step holds has the disadvantage that steps :ref:`might not be resumable <step-holds-and-bookmarks>` if interrupted.
Non-resumability means that replication progress is no longer monotonic which might result in 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 this flag 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 setting this flag to ``true``, existing step holds for the job will be destroyed on the next replication attempt.
.. _job-recv-options: .. _job-recv-options:
Recv Options Recv Options
+1 -1
View File
@@ -131,7 +131,7 @@ The following environment variables are set:
* ``ZREPL_SNAPNAME``: the zrepl-generated snapshot name (e.g. ``zrepl_20380119_031407_000``) * ``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 * ``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`. An empty template hook can be found in :sampleconf:`/hooks/template.sh`.
.. _job-hook-type-postgres-checkpoint: .. _job-hook-type-postgres-checkpoint:
+51 -30
View File
@@ -91,7 +91,8 @@ The ``tls`` transport uses TCP + TLS with client authentication using client cer
The client identity is the common name (CN) presented in the client certificate. 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>`_. 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, see the :ref:`instructions below<transport-tcp+tlsclientauth-2machineopenssl>`. 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/>`_. 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. Since Go binaries are statically linked, you or your distribution need to recompile zrepl when vulnerabilities in that library are disclosed.
@@ -103,6 +104,16 @@ If intermediate CAs are used, the **full chain** must be present in either in th
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>`_). 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. 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
provided below<transport-tcp+tlsclientauth-certgen>`.
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 Serve
~~~~~ ~~~~~
@@ -147,45 +158,25 @@ The ``server_cn`` specifies the expected common name (CN) of the server's certif
It overrides the hostname specified in ``address``. It overrides the hostname specified in ``address``.
The connection fails if either do not match. The connection fails if either do not match.
.. _transport-tcp+tlsclientauth-certgen:
.. _transport-tcp+tlsclientauth-2machineopenssl: .. _transport-tcp+tlsclientauth-2machineopenssl:
Self-Signed Certificates Mutual-TLS between Two Machines
~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Tools like `EasyRSA <https://github.com/OpenVPN/easy-rsa>`_ make it easy to manage CA infrastructure for multiple clients, e.g. a central zrepl backup server (in sink mode).
However, for a two-machine setup, self-signed certificates distributed using an out-of-band mechanism will also work just fine: However, 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>`. 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: Run the following OpenSSL commands on each host, substituting HOSTNAME in both filenames and the interactive input prompt by OpenSSL:
.. code-block:: bash .. code-block:: bash
:emphasize-lines: 1-5,24
openssl req -x509 -sha256 -nodes \ (name=HOSTNAME; openssl req -x509 -sha256 -nodes \
-newkey rsa:4096 \ -newkey rsa:4096 \
-days 365 \ -days 365 \
-keyout HOSTNAME.key \ -keyout $name.key \
-out HOSTNAME.crt -out $name.crt -addext "subjectAltName = DNS:$name" -subj "/CN=$name")
#Generating a 4096 bit RSA private key
#................++++
#.++++
#writing new private key to 'backups.key'
#-----
#You are about to be asked to enter information that will be incorporated
#into your certificate request.
#What you are about to enter is what is called a Distinguished Name or a DN.
#There are quite a few fields but you can leave some blank
#For some fields there will be a default value,
#If you enter '.', the field will be left blank.
#-----
#Country Name (2 letter code) [XX]:
#State or Province Name (full name) []:
#Locality Name (eg, city) [Default City]:
#Organization Name (eg, company) [Default Company Ltd]:
#Organizational Unit Name (eg, section) []:
#Common Name (eg, your name or your server's hostname) []:HOSTNAME
#Email Address []:
Now copy each machine's ``HOSTNAME.crt`` to the other machine's ``/etc/zrepl/HOSTNAME.crt``, for example using `scp`. Now copy each machine's ``HOSTNAME.crt`` to the other machine's ``/etc/zrepl/HOSTNAME.crt``, for example using `scp`.
The serve & connect configuration will thus look like the following: The serve & connect configuration will thus look like the following:
@@ -216,6 +207,36 @@ The serve & connect configuration will thus look like the following:
... ...
Certificate Authority using EasyRSA
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
For more than two machines, it might make sense to set up a CA infrastructure.
Tools like `EasyRSA <https://github.com/OpenVPN/easy-rsa>`_ make this very easy:
::
#!/usr/bin/env bash
set -euo pipefail
HOSTS=(backupserver prod1 prod2 prod3)
curl -L https://github.com/OpenVPN/easy-rsa/releases/download/v3.0.7/EasyRSA-3.0.7.tgz > EasyRSA-3.0.7.tgz
echo "157d2e8c115c3ad070c1b2641a4c9191e06a32a8e50971847a718251eeb510a8 EasyRSA-3.0.7.tgz" | sha256sum -c
rm -rf EasyRSA-3.0.7
tar -xf EasyRSA-3.0.7.tgz
cd EasyRSA-3.0.7
./easyrsa
./easyrsa init-pki
./easyrsa build-ca nopass
for host in "${HOSTS[@]}"; do
./easyrsa build-serverClient-full $host nopass
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: .. _transport-ssh+stdinserver:
``ssh+stdinserver`` Transport ``ssh+stdinserver`` Transport
+6 -8
View File
@@ -58,13 +58,11 @@ for latest_patch in latest_by_major_minor:
cmdline.append("--whitelist-tags") cmdline.append("--whitelist-tags")
cmdline.append(f"^{re.escape(latest_patch.orig)}$") cmdline.append(f"^{re.escape(latest_patch.orig)}$")
# we want to render the latest non-rc version as the default page # we want flexibility to update docs for the latest stable release
# (latest_by_major_minor is already sorted) # => we have a branch for that, called `stable` which we move manually
default_version = latest_by_major_minor[-1] # TODO: in the future, have f"stable-{latest_by_major_minor[-1]}"
for tag in reversed(latest_by_major_minor): default_version = "stable"
if tag.rc == 0: cmdline.extend(["--whitelist-branches", default_version])
default_version = tag
break
cmdline.extend(["--root-ref", f"{default_version}"]) cmdline.extend(["--root-ref", f"{default_version}"])
cmdline.extend(["--banner-main-ref", f"{default_version}"]) cmdline.extend(["--banner-main-ref", f"{default_version}"])
@@ -73,4 +71,4 @@ cmdline.extend(["--sort", "semver"])
cmdline.extend(["--whitelist-branches", "master"]) cmdline.extend(["--whitelist-branches", "master"])
print(" ".join(cmdline)) print(" ".join(cmdline))
+1 -1
View File
@@ -13,7 +13,7 @@
:target: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=R5QSXJVYHGX96 :target: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=R5QSXJVYHGX96
.. |Donate via Liberapay| image:: https://img.shields.io/liberapay/patrons/zrepl.svg?logo=liberapay .. |Donate via Liberapay| image:: https://img.shields.io/liberapay/patrons/zrepl.svg?logo=liberapay
:target: https://liberapay.com/zrepl/donate :target: https://liberapay.com/zrepl/donate
.. |Donate via Patreon| image:: https://img.shields.io/endpoint.svg?url=https%3A%2F%2Fshieldsio-patreon.herokuapp.com%2Fzrepl%2Fpledges&style=flat&color=yellow .. |Donate via Patreon| image:: https://img.shields.io/endpoint.svg?url=https%3A%2F%2Fshieldsio-patreon.vercel.app%2Fapi%3Fusername%3Dzrepl%26type%3Dpatrons&style=flat&color=yellow
:target: https://www.patreon.com/zrepl :target: https://www.patreon.com/zrepl
.. |Donate via GitHub Sponsors| image:: https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&style=flat&color=yellow .. |Donate via GitHub Sponsors| image:: https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&style=flat&color=yellow
:target: https://github.com/sponsors/problame :target: https://github.com/sponsors/problame
+1
View File
@@ -14,6 +14,7 @@ Installation
installation/user-privileges installation/user-privileges
installation/packages installation/packages
installation/apt-repos installation/apt-repos
installation/rpm-repos
installation/compile-from-source installation/compile-from-source
installation/freebsd-jail-with-iocage installation/freebsd-jail-with-iocage
installation/what-next installation/what-next
+2 -2
View File
@@ -7,7 +7,7 @@ Debian / Ubuntu APT repositories
We maintain APT repositories for Debian, Ubuntu and derivatives. We maintain APT repositories for Debian, Ubuntu and derivatives.
The fingerprint of the signing key is ``E101 418F D3D6 FBCB 9D65 A62D 7086 99FC 5F2E BF16``. The fingerprint of the signing key is ``E101 418F D3D6 FBCB 9D65 A62D 7086 99FC 5F2E BF16``.
It is available at `<https://zrepl.cschwarz.com/apt/apt-key.asc>`_ . It is available at `<https://zrepl.cschwarz.com/apt/apt-key.asc>`_ .
Please open an issue `in the packaging repository <https://github.com/zrepl/debian-binary-packaging>`_ if you encounter any issues with the repository. Please open an issue in on GitHub if you encounter any issues with the repository.
The following snippet configure the repository for your Debian or Ubuntu release: The following snippet configure the repository for your Debian or Ubuntu release:
@@ -24,6 +24,6 @@ The following snippet configure the repository for your Debian or Ubuntu release
.. NOTE:: .. NOTE::
Until zrepl reaches 1.0, all APT repositories will be updated to the latest zrepl release immediately. Until zrepl reaches 1.0, the repositories will be updated to the latest zrepl release immediately.
This includes breaking changes between zrepl versions. This includes breaking changes between zrepl versions.
Use ``apt-mark hold zrepl`` to prevent upgrades of zrepl. Use ``apt-mark hold zrepl`` to prevent upgrades of zrepl.
+21 -23
View File
@@ -9,37 +9,35 @@ Producing a release requires **Go 1.11** or newer and **Python 3** + **pip3** +
A tutorial to install Go is available over at `golang.org <https://golang.org/doc/install>`_. A tutorial to install Go is available over at `golang.org <https://golang.org/doc/install>`_.
Python and pip3 should probably be installed via your distro's package manager. Python and pip3 should probably be installed via your distro's package manager.
::
cd to/your/zrepl/checkout
python3 -m venv3
source venv3/bin/activate
./lazy.sh devsetup
make release
# build artifacts are available in ./artifacts/release
The Python venv is used for the documentation build dependencies.
If you just want to build the zrepl binary, leave it out and use `./lazy.sh godep` instead.
Alternatively, you can use the Docker build process: Alternatively, you can use the Docker build process:
it is used to produce the official zrepl `binary releases`_ it is used to produce the official zrepl `binary releases`_
and serves as a reference for build dependencies and procedure: and serves as a reference for build dependencies and procedure:
:: ::
git clone https://github.com/zrepl/zrepl.git && \ cd to/your/zrepl/checkout
cd zrepl && \ # make sure your user has access to the docker socket
sudo docker build -t zrepl_build -f build.Dockerfile . && \ make release-docker
sudo docker run -it --rm \ # if you want .deb or .rpm packages, invoke the follwoing
-v "${PWD}:/src" \ # targets _after_ you invoked release-docker
--user "$(id -u):$(id -g)" \ make deb-docker
zrepl_build make release make rpm-docker
# build artifacts are available in ./artifacts/release
# packages are available in ./artifacts
Alternatively, you can install build dependencies on your local system and then build in your ``$GOPATH``:
::
mkdir -p "${GOPATH}/src/github.com/zrepl/zrepl"
git clone https://github.com/zrepl/zrepl.git "${GOPATH}/src/github.com/zrepl/zrepl"
cd "${GOPATH}/src/github.com/zrepl/zrepl"
python3 -m venv3
source venv3/bin/activate
./lazy.sh devsetup
make release
The Python venv is used for the documentation build dependencies.
If you just want to build the zrepl binary, leave it out and use `./lazy.sh godep` instead.
Either way, all build results are located in the ``artifacts/`` directory.
.. NOTE:: .. NOTE::
It is your job to install the appropriate binary in the zrepl users's ``$PATH``, e.g. ``/usr/local/bin/zrepl``. It is your job to install the built binary in the zrepl users's ``$PATH``, e.g. ``/usr/local/bin/zrepl``.
Otherwise, the examples in the :ref:`quick-start guides <quickstart-toc>` may need to be adjusted. Otherwise, the examples in the :ref:`quick-start guides <quickstart-toc>` may need to be adjusted.
+3 -6
View File
@@ -30,15 +30,12 @@ The following list may be incomplete, feel free to submit a PR with an update:
* - Arch Linux * - Arch Linux
- ``yay install zrepl`` - ``yay install zrepl``
- Available on `AUR <https://aur.archlinux.org/packages/zrepl>`_ - Available on `AUR <https://aur.archlinux.org/packages/zrepl>`_
* - Fedora * - Fedora, CentOS, RHEL, OpenSUSE
- ``dnf install zrepl`` - ``dnf install zrepl``
- Available on `COPR <https://copr.fedorainfracloud.org/coprs/poettlerric/zrepl/>`_ - :ref:`RPM repository config <installation-rpm-repos>`
* - CentOS/RHEL
- ``yum install zrepl``
- Available on `COPR <https://copr.fedorainfracloud.org/coprs/poettlerric/zrepl/>`_
* - Debian + Ubuntu * - Debian + Ubuntu
- ``apt install zrepl`` - ``apt install zrepl``
- APT repository config :ref:`see below <installation-apt-repos>` - :ref:`APT repository config <installation-apt-repos>`
* - OmniOS * - OmniOS
- ``pkg install zrepl`` - ``pkg install zrepl``
- Available since `r151030 <https://pkg.omniosce.org/r151030/extra/en/search.shtml?token=zrepl&action=Search>`_ - Available since `r151030 <https://pkg.omniosce.org/r151030/extra/en/search.shtml?token=zrepl&action=Search>`_
+30
View File
@@ -0,0 +1,30 @@
.. _installation-rpm-repos:
RPM repositories
~~~~~~~~~~~~~~~~
We provide a single RPM repository for all RPM-based Linux distros.
The zrepl binary in the repo is the same as the one published to GitHub.
Since Go binaries are statically linked, the RPM should work about everywhere.
The fingerprint of the signing key is ``F6F6 E8EA 6F2F 1462 2878 B5DE 50E3 4417 826E 2CE6``.
It is available at `<https://zrepl.cschwarz.com/rpm/rpm-key.asc>`_ .
Please open an issue on GitHub if you encounter any issues with the repository.
Copy-paste the following snippet into your shell to set up the zrepl repository.
Then ``dnf install zrepl`` and make sure to confirm that the signing key matches the one shown above.
::
cat > /etc/yum.repos.d/zrepl.repo <<EOF
[zrepl]
name = zrepl
baseurl = https://zrepl.cschwarz.com/rpm/repo
gpgkey = https://zrepl.cschwarz.com/rpm/rpm-key.asc
EOF
.. NOTE::
Until zrepl reaches 1.0, the repository will be updated to the latest zrepl release immediately.
This includes breaking changes between zrepl versions.
If that bothers you, use the `dnf versionlock plugin <https://dnf-plugins-core.readthedocs.io/en/latest/versionlock.html>`_ to pin the version of zrepl on your system.
+32 -7
View File
@@ -1,14 +1,30 @@
#!/bin/bash #!/bin/bash
set -eo pipefail set -eo pipefail
NON_INTERACTIVE=false
DO_CLONE=false
while getopts "ca" arg; do
case "$arg" in
"a")
NON_INTERACTIVE=true
;;
"c")
DO_CLONE=true
;;
*)
echo invalid option
exit 1
;;
esac
done
GHPAGESREPO="git@github.com:zrepl/zrepl.github.io.git" GHPAGESREPO="git@github.com:zrepl/zrepl.github.io.git"
SCRIPTDIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) SCRIPTDIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
PUBLICDIR="${SCRIPTDIR}/public_git" PUBLICDIR="${SCRIPTDIR}/public_git"
checkout_repo_msg() { checkout_repo_msg() {
echo "clone ${GHPAGESREPO} to ${PUBLICDIR}:" echo "clone ${GHPAGESREPO} to ${PUBLICDIR}:"
echo " git clone ${GHPAGESREPO} ${PUBLICDIR}"
git clone "${GHPAGESREPO}" "${PUBLICDIR}"
} }
exit_msg() { exit_msg() {
@@ -25,11 +41,19 @@ cd "$SCRIPTDIR"
if [ ! -d "$PUBLICDIR" ]; then if [ ! -d "$PUBLICDIR" ]; then
checkout_repo_msg checkout_repo_msg
exit 1 if $DO_CLONE; then
git clone "${GHPAGESREPO}" "${PUBLICDIR}"
else
exit 1
fi
fi fi
echo -n "PRESS ENTER to confirm you commited and pushed docs changes and tags to the zrepl repo" if $NON_INTERACTIVE; then
read echo "non-interactive mode"
else
echo -n "PRESS ENTER to confirm you commited and pushed docs changes and tags to the zrepl repo"
read -r
fi
pushd "$PUBLICDIR" pushd "$PUBLICDIR"
@@ -52,9 +76,10 @@ popd
echo "building site" echo "building site"
flags="$(python3 gen-sphinx-versioning-flags.py)"
set -e set -e
sphinx-versioning build \ sphinx-versioning build \
$(python3 gen-sphinx-versioning-flags.py) \ $flags \
docs ./public_git \ docs ./public_git \
-- -c sphinxconf # older conf.py throw errors because they used -- -c sphinxconf # older conf.py throw errors because they used
# version = subprocess.show_output(["git", "describe"]) # version = subprocess.show_output(["git", "describe"])
@@ -66,7 +91,7 @@ git status --porcelain
if [[ "$(git status --porcelain)" != "" ]]; then if [[ "$(git status --porcelain)" != "" ]]; then
CURRENT_COMMIT="${CURRENT_COMMIT}(dirty)" CURRENT_COMMIT="${CURRENT_COMMIT}(dirty)"
fi fi
COMMIT_MSG="sphinx-versioning render from publish.sh - `date -u` - ${CURRENT_COMMIT}" COMMIT_MSG="sphinx-versioning render from publish.sh - $(date -u) - ${CURRENT_COMMIT}"
pushd "$PUBLICDIR" pushd "$PUBLICDIR"
+1 -1
View File
@@ -25,7 +25,7 @@ A few additional requirements:
* We want to be able to put off the backups for more than three weeks, i.e., longer than the lifetime of the automatically created snapshots on our workstation. * We want to be able to put off the backups for more than three weeks, i.e., longer than the lifetime of the automatically created snapshots on our workstation.
**zrepl should use bookmarks and holds to achieve this goal**. **zrepl should use bookmarks and holds to achieve this goal**.
* When we yank out the drive during replication and go on a long vacation, we do *not* want the partially replicated snapshot to stick around as it would hold on to too much disk space over time. * When we yank out the drive during replication and go on a long vacation, we do *not* want the partially replicated snapshot to stick around as it would hold on to too much disk space over time.
Therefore, we want zrepl to deviate from its :ref:`default step-hold behavior <step-holds-and-bookmarks>` and sacrifice resumability, but nonetheless retain the ability to do incremental replication once we return from our vacation. Therefore, we want zrepl to deviate from its :ref:`default behavior <replication-option-protection>` and sacrifice resumability, but nonetheless retain the ability to do incremental replication once we return from our vacation.
**zrepl should provide an easy config knob to disable step holds for incremental replication**. **zrepl should provide an easy config knob to disable step holds for incremental replication**.
The following config snippet implements the setup described above. The following config snippet implements the setup described above.
+11 -14
View File
@@ -51,21 +51,18 @@ To get things going quickly, we skip setting up a CA and generate two self-signe
For convenience, we generate the key pairs on our local machine and distribute them using ssh: For convenience, we generate the key pairs on our local machine and distribute them using ssh:
.. code-block:: bash .. code-block:: bash
:emphasize-lines: 6,13
openssl req -x509 -sha256 -nodes \ (name=backups; openssl req -x509 -sha256 -nodes \
-newkey rsa:4096 \ -newkey rsa:4096 \
-days 365 \ -days 365 \
-keyout backups.key \ -keyout $name.key \
-out backups.crt -out $name.crt -addext "subjectAltName = DNS:$name" -subj "/CN=$name")
# ... and use "backups" as Common Name (CN)
openssl req -x509 -sha256 -nodes \ (name=prod; openssl req -x509 -sha256 -nodes \
-newkey rsa:4096 \ -newkey rsa:4096 \
-days 365 \ -days 365 \
-keyout prod.key \ -keyout $name.key \
-out prod.crt -out $name.crt -addext "subjectAltName = DNS:$name" -subj "/CN=$name")
# ... and use "prod" as Common Name (CN)
ssh root@backups "mkdir /etc/zrepl" ssh root@backups "mkdir /etc/zrepl"
scp backups.key backups.crt prod.crt root@backups:/etc/zrepl scp backups.key backups.crt prod.crt root@backups:/etc/zrepl
@@ -94,4 +91,4 @@ We define a corresponding **sink job** named ``sink`` in ``/etc/zrepl/zrepl.yml`
Go Back To Quickstart Guide Go Back To Quickstart Guide
--------------------------- ---------------------------
:ref:`Click here <quickstart-apply-config>` to go back to the quickstart guide. :ref:`Click here <quickstart-apply-config>` to go back to the quickstart guide.
+5 -1
View File
@@ -32,6 +32,10 @@ We would like to thank the following people and organizations for supporting zre
<div class="fa fa-code" style="width: 1em;"></div> <div class="fa fa-code" style="width: 1em;"></div>
* |supporter-std| `Gordon Schulz <https://github.com/azmodude>`_
* |supporter-std| `@jwittlincohen <https://github.com/jwittlincohen>`_
* |supporter-std| `Michael D. Schmitt <https://waterbendingscroll.dancingdragons.org>`_
* |supporter-std| `Hans Schulz <https://github.com/schulzh>`_
* |supporter-std| Henning Kessler * |supporter-std| Henning Kessler
* |supporter-std| `John Ramsden <https://github.com/johnramsden>`_ * |supporter-std| `John Ramsden <https://github.com/johnramsden>`_
* |supporter-std| `DrLuke <https://github.com/drluke>`_ * |supporter-std| `DrLuke <https://github.com/drluke>`_
@@ -43,7 +47,7 @@ We would like to thank the following people and organizations for supporting zre
* |supporter-gold| `MNX.io <https://mnx.io>`_ * |supporter-gold| `MNX.io <https://mnx.io>`_
* |supporter-std| `Marshall Clyburn <https://github.com/mdclyburn>`_ * |supporter-std| `Marshall Clyburn <https://github.com/mdclyburn>`_
* |supporter-code| `Ross Williams <https://github.com/overhacked>`_ * |supporter-code| `Ross Williams <https://github.com/overhacked>`_
* |supporter-std| Mike T. * |supporter-gold| Mike T.
* |supporter-code| `Justin Scholz <https://github.com/JMoVS>`_ * |supporter-code| `Justin Scholz <https://github.com/JMoVS>`_
* |supporter-code| `InsanePrawn <https://github.com/InsanePrawn>`_ * |supporter-code| `InsanePrawn <https://github.com/InsanePrawn>`_
* |supporter-code| `Ben Woods <https://www.freshports.org/sysutils/zrepl/>`_ * |supporter-code| `Ben Woods <https://www.freshports.org/sysutils/zrepl/>`_
+100 -86
View File
@@ -10,6 +10,7 @@ import (
"github.com/kr/pretty" "github.com/kr/pretty"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/zrepl/zrepl/daemon/logging/trace" "github.com/zrepl/zrepl/daemon/logging/trace"
"github.com/zrepl/zrepl/replication/logic/pdu" "github.com/zrepl/zrepl/replication/logic/pdu"
@@ -21,10 +22,9 @@ import (
) )
type SenderConfig struct { type SenderConfig struct {
FSF zfs.DatasetFilter FSF zfs.DatasetFilter
Encrypt *zfs.NilBool Encrypt *zfs.NilBool
DisableIncrementalStepHolds bool JobID JobID
JobID JobID
} }
func (c *SenderConfig) Validate() error { func (c *SenderConfig) Validate() error {
@@ -40,10 +40,9 @@ func (c *SenderConfig) Validate() error {
// Sender implements replication.ReplicationEndpoint for a sending side // Sender implements replication.ReplicationEndpoint for a sending side
type Sender struct { type Sender struct {
FSFilter zfs.DatasetFilter FSFilter zfs.DatasetFilter
encrypt *zfs.NilBool encrypt *zfs.NilBool
disableIncrementalStepHolds bool jobId JobID
jobId JobID
} }
func NewSender(conf SenderConfig) *Sender { func NewSender(conf SenderConfig) *Sender {
@@ -51,10 +50,9 @@ func NewSender(conf SenderConfig) *Sender {
panic("invalid config" + err.Error()) panic("invalid config" + err.Error())
} }
return &Sender{ return &Sender{
FSFilter: conf.FSF, FSFilter: conf.FSF,
encrypt: conf.Encrypt, encrypt: conf.Encrypt,
disableIncrementalStepHolds: conf.DisableIncrementalStepHolds, jobId: conf.JobID,
jobId: conf.JobID,
} }
} }
@@ -211,37 +209,27 @@ func (s *Sender) Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, io.Rea
return res, nil, nil return res, nil, nil
} }
// create a replication cursor for `From` (usually an idempotent no-op because SendCompleted already created it before) // create holds or bookmarks of `From` and `To` to guarantee one of the following:
var fromReplicationCursor Abstraction // - that the replication step can always be resumed (`holds`),
if sendArgs.From != nil { // - that the replication step can be interrupted and a future replication
// For all but the first replication, this should always be a no-op because SendCompleted already moved the cursor // step with same or different `To` but same `From` is still possible (`bookmarks`)
fromReplicationCursor, err = CreateReplicationCursor(ctx, sendArgs.FS, *sendArgs.FromVersion, s.jobId) // no shadow // - nothing (`none`)
if err == zfs.ErrBookmarkCloningNotSupported { //
getLogger(ctx).Debug("not creating replication cursor from bookmark because ZFS does not support it") // ...
// fallthrough //
} else if err != nil { // ... actually create the abstractions
return nil, nil, errors.Wrap(err, "cannot set replication cursor to `from` version before starting send") replicationGuaranteeOptions, err := replicationGuaranteeOptionsFromPDU(r.GetReplicationConfig().Protection)
} if err != nil {
return nil, nil, err
} }
replicationGuaranteeStrategy := replicationGuaranteeOptions.Strategy(sendArgs.From != nil)
takeStepHolds := sendArgs.FromVersion == nil || !s.disableIncrementalStepHolds liveAbs, err := replicationGuaranteeStrategy.SenderPreSend(ctx, s.jobId, &sendArgs)
if err != nil {
var fromHold, toHold Abstraction return nil, nil, err
// make sure `From` doesn't go away in order to make this step resumable
if sendArgs.From != nil && takeStepHolds {
fromHold, err = HoldStep(ctx, sendArgs.FS, *sendArgs.FromVersion, s.jobId) // no shadow
if err == zfs.ErrBookmarkCloningNotSupported {
getLogger(ctx).Debug("not creating step bookmark because ZFS does not support it")
// fallthrough
} else if err != nil {
return nil, nil, errors.Wrapf(err, "cannot hold `from` version %q before starting send", *sendArgs.FromVersion)
}
} }
if takeStepHolds { for _, a := range liveAbs {
// make sure `To` doesn't go away in order to make this step resumable if a != nil {
toHold, err = HoldStep(ctx, sendArgs.FS, sendArgs.ToVersion, s.jobId) abstractionsCacheSingleton.Put(a)
if err != nil {
return nil, nil, errors.Wrapf(err, "cannot hold `to` version %q before starting send", sendArgs.ToVersion)
} }
} }
@@ -261,7 +249,6 @@ func (s *Sender) Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, io.Rea
// //
// Note further that a resuming send, due to the idempotent nature of func CreateReplicationCursor and HoldStep, // Note further that a resuming send, due to the idempotent nature of func CreateReplicationCursor and HoldStep,
// will never lose its step holds because we just (idempotently re-)created them above, before attempting the cleanup. // will never lose its step holds because we just (idempotently re-)created them above, before attempting the cleanup.
liveAbs := []Abstraction{fromHold, toHold, fromReplicationCursor}
func() { func() {
ctx, endSpan := trace.WithSpan(ctx, "cleanup-stale-abstractions") ctx, endSpan := trace.WithSpan(ctx, "cleanup-stale-abstractions")
defer endSpan() defer endSpan()
@@ -283,22 +270,29 @@ func (s *Sender) Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, io.Rea
for _, staleVersion := range obsoleteAbs { for _, staleVersion := range obsoleteAbs {
for _, mustLiveVersion := range mustLiveVersions { for _, mustLiveVersion := range mustLiveVersions {
isSendArg := zfs.FilesystemVersionEqualIdentity(mustLiveVersion, staleVersion.GetFilesystemVersion()) isSendArg := zfs.FilesystemVersionEqualIdentity(mustLiveVersion, staleVersion.GetFilesystemVersion())
isStepHoldWeMightHaveCreatedWithCurrentValueOf_takeStepHolds := stepHoldBasedGuaranteeStrategy := false
takeStepHolds && staleVersion.GetType() == AbstractionStepHold k := replicationGuaranteeStrategy.Kind()
if isSendArg && isStepHoldWeMightHaveCreatedWithCurrentValueOf_takeStepHolds { switch k {
case ReplicationGuaranteeKindResumability:
stepHoldBasedGuaranteeStrategy = true
case ReplicationGuaranteeKindIncremental:
case ReplicationGuaranteeKindNone:
default:
panic(fmt.Sprintf("this is supposed to be an exhaustive match, got %v", k))
}
isSnapshot := mustLiveVersion.IsSnapshot()
if isSendArg && (!isSnapshot || stepHoldBasedGuaranteeStrategy) {
panic(fmt.Sprintf("impl error: %q would be destroyed because it is considered stale but it is part of of sendArgs=%s", mustLiveVersion.String(), pretty.Sprint(sendArgs))) panic(fmt.Sprintf("impl error: %q would be destroyed because it is considered stale but it is part of of sendArgs=%s", mustLiveVersion.String(), pretty.Sprint(sendArgs)))
} }
} }
} }
} }
sendAbstractionsCacheSingleton.TryBatchDestroy(ctx, s.jobId, sendArgs.FS, keep, check) destroyTypes := AbstractionTypeSet{
}() AbstractionStepHold: true,
// now add the newly created abstractions to the cleaned-up cache AbstractionTentativeReplicationCursorBookmark: true,
for _, a := range liveAbs {
if a != nil {
sendAbstractionsCacheSingleton.Put(a)
} }
} abstractionsCacheSingleton.TryBatchDestroy(ctx, s.jobId, sendArgs.FS, destroyTypes, keep, check)
}()
sendStream, err := zfs.ZFSSend(ctx, sendArgs) sendStream, err := zfs.ZFSSend(ctx, sendArgs)
if err != nil { if err != nil {
@@ -332,36 +326,32 @@ func (p *Sender) SendCompleted(ctx context.Context, r *pdu.SendCompletedReq) (*p
return nil, errors.Wrap(err, "validate `to` exists") return nil, errors.Wrap(err, "validate `to` exists")
} }
log := func(ctx context.Context) Logger { replicationGuaranteeOptions, err := replicationGuaranteeOptionsFromPDU(orig.GetReplicationConfig().Protection)
log := getLogger(ctx).WithField("to_guid", to.Guid).
WithField("fs", fs).
WithField("to", to.RelName)
if from != nil {
log = log.WithField("from", from.RelName).WithField("from_guid", from.Guid)
}
return log
}
toReplicationCursor, err := CreateReplicationCursor(ctx, fs, to, p.jobId)
if err != nil { if err != nil {
if err == zfs.ErrBookmarkCloningNotSupported { return nil, err
log(ctx).Debug("not setting replication cursor, bookmark cloning not supported") }
} else { liveAbs, err := replicationGuaranteeOptions.Strategy(from != nil).SenderPostRecvConfirmed(ctx, p.jobId, fs, to)
msg := "cannot move replication cursor, keeping hold on `to` until successful" if err != nil {
log(ctx).WithError(err).Error(msg) return nil, err
err = errors.Wrap(err, msg) }
// it is correct to not destroy from and to step holds if we can't move the cursor! for _, a := range liveAbs {
return &pdu.SendCompletedRes{}, err if a != nil {
abstractionsCacheSingleton.Put(a)
} }
} else {
sendAbstractionsCacheSingleton.Put(toReplicationCursor)
log(ctx).WithField("to_cursor", toReplicationCursor.String()).Info("successfully created `to` replication cursor")
} }
keep := func(a Abstraction) (keep bool) {
keep := func(a Abstraction) bool { keep = false
return AbstractionEquals(a, toReplicationCursor) for _, k := range liveAbs {
keep = keep || AbstractionEquals(a, k)
}
return keep
} }
sendAbstractionsCacheSingleton.TryBatchDestroy(ctx, p.jobId, fs, keep, nil) destroyTypes := AbstractionTypeSet{
AbstractionStepHold: true,
AbstractionTentativeReplicationCursorBookmark: true,
AbstractionReplicationCursorBookmarkV2: true,
}
abstractionsCacheSingleton.TryBatchDestroy(ctx, p.jobId, fs, destroyTypes, keep, nil)
return &pdu.SendCompletedRes{}, nil return &pdu.SendCompletedRes{}, nil
@@ -437,8 +427,6 @@ type ReceiverConfig struct {
RootWithoutClientComponent *zfs.DatasetPath // TODO use RootWithoutClientComponent *zfs.DatasetPath // TODO use
AppendClientIdentity bool AppendClientIdentity bool
UpdateLastReceivedHold bool
} }
func (c *ReceiverConfig) copyIn() { func (c *ReceiverConfig) copyIn() {
@@ -679,7 +667,7 @@ func (s *Receiver) Receive(ctx context.Context, req *pdu.ReceiveReq, receive io.
f := zfs.NewDatasetPathForest() f := zfs.NewDatasetPathForest()
f.Add(lp) f.Add(lp)
getLogger(ctx).Debug("begin tree-walk") getLogger(ctx).Debug("begin tree-walk")
f.WalkTopDown(func(v zfs.DatasetPathVisit) (visitChildTree bool) { f.WalkTopDown(func(v *zfs.DatasetPathVisit) (visitChildTree bool) {
if v.Path.Equal(lp) { if v.Path.Equal(lp) {
return false return false
} }
@@ -707,7 +695,7 @@ func (s *Receiver) Receive(ctx context.Context, req *pdu.ReceiveReq, receive io.
} }
l := getLogger(ctx).WithField("placeholder_fs", v.Path) l := getLogger(ctx).WithField("placeholder_fs", v.Path)
l.Debug("create placeholder filesystem") l.Debug("create placeholder filesystem")
err := zfs.ZFSCreatePlaceholderFilesystem(ctx, v.Path) err := zfs.ZFSCreatePlaceholderFilesystem(ctx, v.Path, v.Parent.Path)
if err != nil { if err != nil {
l.WithError(err).Error("cannot create placeholder filesystem") l.WithError(err).Error("cannot create placeholder filesystem")
visitErr = err visitErr = err
@@ -863,12 +851,38 @@ func (s *Receiver) Receive(ctx context.Context, req *pdu.ReceiveReq, receive io.
return nil, errors.Wrap(err, msg) return nil, errors.Wrap(err, msg)
} }
if s.conf.UpdateLastReceivedHold { replicationGuaranteeOptions, err := replicationGuaranteeOptionsFromPDU(req.GetReplicationConfig().Protection)
log.Debug("move last-received-hold") if err != nil {
if err := MoveLastReceivedHold(ctx, lp.ToString(), toRecvd, s.conf.JobID); err != nil { return nil, err
return nil, errors.Wrap(err, "cannot move last-received-hold") }
replicationGuaranteeStrategy := replicationGuaranteeOptions.Strategy(ph.FSExists)
liveAbs, err := replicationGuaranteeStrategy.ReceiverPostRecv(ctx, s.conf.JobID, lp.ToString(), toRecvd)
if err != nil {
return nil, err
}
for _, a := range liveAbs {
if a != nil {
abstractionsCacheSingleton.Put(a)
} }
} }
keep := func(a Abstraction) (keep bool) {
keep = false
for _, k := range liveAbs {
keep = keep || AbstractionEquals(a, k)
}
return keep
}
check := func(obsoleteAbs []Abstraction) {
for _, abs := range obsoleteAbs {
if zfs.FilesystemVersionEqualIdentity(abs.GetFilesystemVersion(), toRecvd) {
panic(fmt.Sprintf("would destroy endpoint abstraction around the filesystem version we just received %s", abs))
}
}
}
destroyTypes := AbstractionTypeSet{
AbstractionLastReceivedHold: true,
}
abstractionsCacheSingleton.TryBatchDestroy(ctx, s.conf.JobID, lp.ToString(), destroyTypes, keep, check)
return &pdu.ReceiveRes{}, nil return &pdu.ReceiveRes{}, nil
} }
@@ -6,53 +6,54 @@ import (
"sync" "sync"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"github.com/zrepl/zrepl/daemon/logging/trace" "github.com/zrepl/zrepl/daemon/logging/trace"
"github.com/zrepl/zrepl/util/chainlock" "github.com/zrepl/zrepl/util/chainlock"
) )
var sendAbstractionsCacheMetrics struct { var abstractionsCacheMetrics struct {
count prometheus.Gauge count prometheus.Gauge
} }
func init() { func init() {
sendAbstractionsCacheMetrics.count = prometheus.NewGauge(prometheus.GaugeOpts{ abstractionsCacheMetrics.count = prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: "zrepl", Namespace: "zrepl",
Subsystem: "endpoint", Subsystem: "endpoint",
Name: "send_abstractions_cache_entry_count", Name: "abstractions_cache_entry_count",
Help: "number of send abstractions tracked in the sendAbstractionsCache data structure", Help: "number of abstractions tracked in the abstractionsCache data structure",
}) })
} }
var sendAbstractionsCacheSingleton = newSendAbstractionsCache() var abstractionsCacheSingleton = newAbstractionsCache()
func SendAbstractionsCacheInvalidate(fs string) { func AbstractionsCacheInvalidate(fs string) {
sendAbstractionsCacheSingleton.InvalidateFSCache(fs) abstractionsCacheSingleton.InvalidateFSCache(fs)
} }
type sendAbstractionsCacheDidLoadFSState int type abstractionsCacheDidLoadFSState int
const ( const (
sendAbstractionsCacheDidLoadFSStateNo sendAbstractionsCacheDidLoadFSState = iota // 0-value has meaning abstractionsCacheDidLoadFSStateNo abstractionsCacheDidLoadFSState = iota // 0-value has meaning
sendAbstractionsCacheDidLoadFSStateInProgress abstractionsCacheDidLoadFSStateInProgress
sendAbstractionsCacheDidLoadFSStateDone abstractionsCacheDidLoadFSStateDone
) )
type sendAbstractionsCache struct { type abstractionsCache struct {
mtx chainlock.L mtx chainlock.L
abstractions []Abstraction abstractions []Abstraction
didLoadFS map[string]sendAbstractionsCacheDidLoadFSState didLoadFS map[string]abstractionsCacheDidLoadFSState
didLoadFSChanged *sync.Cond didLoadFSChanged *sync.Cond
} }
func newSendAbstractionsCache() *sendAbstractionsCache { func newAbstractionsCache() *abstractionsCache {
c := &sendAbstractionsCache{ c := &abstractionsCache{
didLoadFS: make(map[string]sendAbstractionsCacheDidLoadFSState), didLoadFS: make(map[string]abstractionsCacheDidLoadFSState),
} }
c.didLoadFSChanged = c.mtx.NewCond() c.didLoadFSChanged = c.mtx.NewCond()
return c return c
} }
func (s *sendAbstractionsCache) Put(a Abstraction) { func (s *abstractionsCache) Put(a Abstraction) {
defer s.mtx.Lock().Unlock() defer s.mtx.Lock().Unlock()
var zeroJobId JobID var zeroJobId JobID
@@ -63,10 +64,10 @@ func (s *sendAbstractionsCache) Put(a Abstraction) {
} }
s.abstractions = append(s.abstractions, a) s.abstractions = append(s.abstractions, a)
sendAbstractionsCacheMetrics.count.Set(float64(len(s.abstractions))) abstractionsCacheMetrics.count.Set(float64(len(s.abstractions)))
} }
func (s *sendAbstractionsCache) InvalidateFSCache(fs string) { func (s *abstractionsCache) InvalidateFSCache(fs string) {
// FIXME: O(n) // FIXME: O(n)
newAbs := make([]Abstraction, 0, len(s.abstractions)) newAbs := make([]Abstraction, 0, len(s.abstractions))
for _, a := range s.abstractions { for _, a := range s.abstractions {
@@ -75,9 +76,9 @@ func (s *sendAbstractionsCache) InvalidateFSCache(fs string) {
} }
} }
s.abstractions = newAbs s.abstractions = newAbs
sendAbstractionsCacheMetrics.count.Set(float64(len(s.abstractions))) abstractionsCacheMetrics.count.Set(float64(len(s.abstractions)))
s.didLoadFS[fs] = sendAbstractionsCacheDidLoadFSStateNo s.didLoadFS[fs] = abstractionsCacheDidLoadFSStateNo
s.didLoadFSChanged.Broadcast() s.didLoadFSChanged.Broadcast()
} }
@@ -86,7 +87,7 @@ func (s *sendAbstractionsCache) InvalidateFSCache(fs string) {
// - only fetches on-disk abstractions once, but every time from the in-memory store // - only fetches on-disk abstractions once, but every time from the in-memory store
// //
// That means that for precise results, all abstractions created by the endpoint must be .Put into this cache. // That means that for precise results, all abstractions created by the endpoint must be .Put into this cache.
func (s *sendAbstractionsCache) GetAndDeleteByJobIDAndFS(ctx context.Context, jobID JobID, fs string, keep func(a Abstraction) bool) (ret []Abstraction) { func (s *abstractionsCache) GetAndDeleteByJobIDAndFS(ctx context.Context, jobID JobID, fs string, types AbstractionTypeSet, keep func(a Abstraction) bool) (ret []Abstraction) {
defer s.mtx.Lock().Unlock() defer s.mtx.Lock().Unlock()
defer trace.WithSpanFromStackUpdateCtx(&ctx)() defer trace.WithSpanFromStackUpdateCtx(&ctx)()
var zeroJobId JobID var zeroJobId JobID
@@ -97,50 +98,50 @@ func (s *sendAbstractionsCache) GetAndDeleteByJobIDAndFS(ctx context.Context, jo
panic("must not pass zero-value fs") panic("must not pass zero-value fs")
} }
s.tryLoadOnDiskSendAbstractions(ctx, fs) s.tryLoadOnDiskAbstractions(ctx, fs)
// FIXME O(n) // FIXME O(n)
var remaining []Abstraction var remaining []Abstraction
for _, a := range s.abstractions { for _, a := range s.abstractions {
aJobId := *a.GetJobID() aJobId := *a.GetJobID()
aFS := a.GetFS() aFS := a.GetFS()
if aJobId == jobID && aFS == fs && !keep(a) { if aJobId == jobID && aFS == fs && types[a.GetType()] && !keep(a) {
ret = append(ret, a) ret = append(ret, a)
} else { } else {
remaining = append(remaining, a) remaining = append(remaining, a)
} }
} }
s.abstractions = remaining s.abstractions = remaining
sendAbstractionsCacheMetrics.count.Set(float64(len(s.abstractions))) abstractionsCacheMetrics.count.Set(float64(len(s.abstractions)))
return ret return ret
} }
// caller must hold s.mtx // caller must hold s.mtx
func (s *sendAbstractionsCache) tryLoadOnDiskSendAbstractions(ctx context.Context, fs string) { func (s *abstractionsCache) tryLoadOnDiskAbstractions(ctx context.Context, fs string) {
for s.didLoadFS[fs] != sendAbstractionsCacheDidLoadFSStateDone { for s.didLoadFS[fs] != abstractionsCacheDidLoadFSStateDone {
if s.didLoadFS[fs] == sendAbstractionsCacheDidLoadFSStateInProgress { if s.didLoadFS[fs] == abstractionsCacheDidLoadFSStateInProgress {
s.didLoadFSChanged.Wait() s.didLoadFSChanged.Wait()
continue continue
} }
if s.didLoadFS[fs] != sendAbstractionsCacheDidLoadFSStateNo { if s.didLoadFS[fs] != abstractionsCacheDidLoadFSStateNo {
panic(fmt.Sprintf("unreachable: %v", s.didLoadFS[fs])) panic(fmt.Sprintf("unreachable: %v", s.didLoadFS[fs]))
} }
s.didLoadFS[fs] = sendAbstractionsCacheDidLoadFSStateInProgress s.didLoadFS[fs] = abstractionsCacheDidLoadFSStateInProgress
defer s.didLoadFSChanged.Broadcast() defer s.didLoadFSChanged.Broadcast()
var onDiskAbs []Abstraction var onDiskAbs []Abstraction
var err error var err error
s.mtx.DropWhile(func() { s.mtx.DropWhile(func() {
onDiskAbs, err = s.tryLoadOnDiskSendAbstractionsImpl(ctx, fs) // no shadow onDiskAbs, err = s.tryLoadOnDiskAbstractionsImpl(ctx, fs) // no shadow
}) })
if err != nil { if err != nil {
s.didLoadFS[fs] = sendAbstractionsCacheDidLoadFSStateNo s.didLoadFS[fs] = abstractionsCacheDidLoadFSStateNo
getLogger(ctx).WithField("fs", fs).WithError(err).Error("cannot list send step abstractions for filesystem") getLogger(ctx).WithField("fs", fs).WithError(err).Error("cannot list abstractions for filesystem")
} else { } else {
s.didLoadFS[fs] = sendAbstractionsCacheDidLoadFSStateDone s.didLoadFS[fs] = abstractionsCacheDidLoadFSStateDone
s.abstractions = append(s.abstractions, onDiskAbs...) s.abstractions = append(s.abstractions, onDiskAbs...)
getLogger(ctx).WithField("fs", fs).WithField("abstractions", onDiskAbs).Debug("loaded step abstractions for filesystem") getLogger(ctx).WithField("fs", fs).WithField("abstractions", onDiskAbs).Debug("loaded step abstractions for filesystem")
} }
@@ -149,7 +150,7 @@ func (s *sendAbstractionsCache) tryLoadOnDiskSendAbstractions(ctx context.Contex
} }
// caller should _not hold s.mtx // caller should _not hold s.mtx
func (s *sendAbstractionsCache) tryLoadOnDiskSendAbstractionsImpl(ctx context.Context, fs string) ([]Abstraction, error) { func (s *abstractionsCache) tryLoadOnDiskAbstractionsImpl(ctx context.Context, fs string) ([]Abstraction, error) {
defer trace.WithSpanFromStackUpdateCtx(&ctx)() defer trace.WithSpanFromStackUpdateCtx(&ctx)()
q := ListZFSHoldsAndBookmarksQuery{ q := ListZFSHoldsAndBookmarksQuery{
@@ -158,9 +159,10 @@ func (s *sendAbstractionsCache) tryLoadOnDiskSendAbstractionsImpl(ctx context.Co
}, },
JobID: nil, JobID: nil,
What: AbstractionTypeSet{ What: AbstractionTypeSet{
AbstractionStepHold: true, AbstractionStepHold: true,
AbstractionStepBookmark: true, AbstractionTentativeReplicationCursorBookmark: true,
AbstractionReplicationCursorBookmarkV2: true, AbstractionReplicationCursorBookmarkV2: true,
AbstractionLastReceivedHold: true,
}, },
Concurrency: 1, Concurrency: 1,
} }
@@ -175,12 +177,12 @@ func (s *sendAbstractionsCache) tryLoadOnDiskSendAbstractionsImpl(ctx context.Co
return abs, nil return abs, nil
} }
func (s *sendAbstractionsCache) TryBatchDestroy(ctx context.Context, jobId JobID, fs string, keep func(a Abstraction) bool, check func(willDestroy []Abstraction)) { func (s *abstractionsCache) TryBatchDestroy(ctx context.Context, jobId JobID, fs string, types AbstractionTypeSet, keep func(a Abstraction) bool, check func(willDestroy []Abstraction)) {
// no s.mtx, we only use the public interface in this function // no s.mtx, we only use the public interface in this function
defer trace.WithSpanFromStackUpdateCtx(&ctx)() defer trace.WithSpanFromStackUpdateCtx(&ctx)()
obsoleteAbs := s.GetAndDeleteByJobIDAndFS(ctx, jobId, fs, keep) obsoleteAbs := s.GetAndDeleteByJobIDAndFS(ctx, jobId, fs, types, keep)
if check != nil { if check != nil {
check(obsoleteAbs) check(obsoleteAbs)
@@ -193,11 +195,11 @@ func (s *sendAbstractionsCache) TryBatchDestroy(ctx context.Context, jobId JobID
getLogger(ctx). getLogger(ctx).
WithField("abstraction", res.Abstraction). WithField("abstraction", res.Abstraction).
WithError(res.DestroyErr). WithError(res.DestroyErr).
Error("cannot destroy stale send step abstraction") Error("cannot destroy abstraction")
} else { } else {
getLogger(ctx). getLogger(ctx).
WithField("abstraction", res.Abstraction). WithField("abstraction", res.Abstraction).
Info("destroyed stale send step abstraction") Info("destroyed abstraction")
} }
} }
if hadErr { if hadErr {
+214
View File
@@ -0,0 +1,214 @@
package endpoint
import (
"context"
"fmt"
"github.com/pkg/errors"
"github.com/zrepl/zrepl/replication/logic/pdu"
"github.com/zrepl/zrepl/zfs"
)
type ReplicationGuaranteeOptions struct {
Initial ReplicationGuaranteeKind
Incremental ReplicationGuaranteeKind
}
func replicationGuaranteeOptionsFromPDU(in *pdu.ReplicationConfigProtection) (o ReplicationGuaranteeOptions, _ error) {
if in == nil {
return o, errors.New("pdu.ReplicationConfigProtection must not be nil")
}
initial, err := replicationGuaranteeKindFromPDU(in.GetInitial())
if err != nil {
return o, errors.Wrap(err, "pdu.ReplicationConfigProtection: field Initial")
}
incremental, err := replicationGuaranteeKindFromPDU(in.GetIncremental())
if err != nil {
return o, errors.Wrap(err, "pdu.ReplicationConfigProtection: field Incremental")
}
o = ReplicationGuaranteeOptions{
Initial: initial,
Incremental: incremental,
}
return o, nil
}
func replicationGuaranteeKindFromPDU(in pdu.ReplicationGuaranteeKind) (k ReplicationGuaranteeKind, _ error) {
switch in {
case pdu.ReplicationGuaranteeKind_GuaranteeNothing:
return ReplicationGuaranteeKindNone, nil
case pdu.ReplicationGuaranteeKind_GuaranteeIncrementalReplication:
return ReplicationGuaranteeKindIncremental, nil
case pdu.ReplicationGuaranteeKind_GuaranteeResumability:
return ReplicationGuaranteeKindResumability, nil
case pdu.ReplicationGuaranteeKind_GuaranteeInvalid:
fallthrough
default:
return k, errors.Errorf("%q", in.String())
}
}
func (o ReplicationGuaranteeOptions) Strategy(incremental bool) ReplicationGuaranteeStrategy {
g := o.Initial
if incremental {
g = o.Incremental
}
return ReplicationGuaranteeFromKind(g)
}
//go:generate enumer -type=ReplicationGuaranteeKind -json -transform=snake -trimprefix=ReplicationGuaranteeKind
type ReplicationGuaranteeKind int
const (
ReplicationGuaranteeKindResumability ReplicationGuaranteeKind = 1 << iota
ReplicationGuaranteeKindIncremental
ReplicationGuaranteeKindNone
)
type ReplicationGuaranteeStrategy interface {
Kind() ReplicationGuaranteeKind
SenderPreSend(ctx context.Context, jid JobID, sendArgs *zfs.ZFSSendArgsValidated) (keep []Abstraction, err error)
ReceiverPostRecv(ctx context.Context, jid JobID, fs string, toRecvd zfs.FilesystemVersion) (keep []Abstraction, err error)
SenderPostRecvConfirmed(ctx context.Context, jid JobID, fs string, to zfs.FilesystemVersion) (keep []Abstraction, err error)
}
func ReplicationGuaranteeFromKind(k ReplicationGuaranteeKind) ReplicationGuaranteeStrategy {
switch k {
case ReplicationGuaranteeKindNone:
return ReplicationGuaranteeNone{}
case ReplicationGuaranteeKindIncremental:
return ReplicationGuaranteeIncremental{}
case ReplicationGuaranteeKindResumability:
return ReplicationGuaranteeResumability{}
default:
panic(fmt.Sprintf("unreachable: %q %T", k, k))
}
}
type ReplicationGuaranteeNone struct{}
func (g ReplicationGuaranteeNone) Kind() ReplicationGuaranteeKind {
return ReplicationGuaranteeKindNone
}
func (g ReplicationGuaranteeNone) SenderPreSend(ctx context.Context, jid JobID, sendArgs *zfs.ZFSSendArgsValidated) (keep []Abstraction, err error) {
return nil, nil
}
func (g ReplicationGuaranteeNone) ReceiverPostRecv(ctx context.Context, jid JobID, fs string, toRecvd zfs.FilesystemVersion) (keep []Abstraction, err error) {
return nil, nil
}
func (g ReplicationGuaranteeNone) SenderPostRecvConfirmed(ctx context.Context, jid JobID, fs string, to zfs.FilesystemVersion) (keep []Abstraction, err error) {
return nil, nil
}
type ReplicationGuaranteeIncremental struct{}
func (g ReplicationGuaranteeIncremental) Kind() ReplicationGuaranteeKind {
return ReplicationGuaranteeKindIncremental
}
func (g ReplicationGuaranteeIncremental) SenderPreSend(ctx context.Context, jid JobID, sendArgs *zfs.ZFSSendArgsValidated) (keep []Abstraction, err error) {
if sendArgs.FromVersion != nil {
from, err := CreateTentativeReplicationCursor(ctx, sendArgs.FS, *sendArgs.FromVersion, jid)
if err != nil {
if err == zfs.ErrBookmarkCloningNotSupported {
getLogger(ctx).WithField("replication_guarantee", g).
WithField("bookmark", sendArgs.From.FullPath(sendArgs.FS)).
Info("bookmark cloning is not supported, speculating that `from` will not be destroyed until step is done")
} else {
return nil, err
}
}
keep = append(keep, from)
}
to, err := CreateTentativeReplicationCursor(ctx, sendArgs.FS, sendArgs.ToVersion, jid)
if err != nil {
return nil, err
}
keep = append(keep, to)
return keep, nil
}
func (g ReplicationGuaranteeIncremental) ReceiverPostRecv(ctx context.Context, jid JobID, fs string, toRecvd zfs.FilesystemVersion) (keep []Abstraction, err error) {
return receiverPostRecvCommon(ctx, jid, fs, toRecvd)
}
func (g ReplicationGuaranteeIncremental) SenderPostRecvConfirmed(ctx context.Context, jid JobID, fs string, to zfs.FilesystemVersion) (keep []Abstraction, err error) {
return senderPostRecvConfirmedCommon(ctx, jid, fs, to)
}
type ReplicationGuaranteeResumability struct{}
func (g ReplicationGuaranteeResumability) Kind() ReplicationGuaranteeKind {
return ReplicationGuaranteeKindResumability
}
func (g ReplicationGuaranteeResumability) SenderPreSend(ctx context.Context, jid JobID, sendArgs *zfs.ZFSSendArgsValidated) (keep []Abstraction, err error) {
// try to hold the FromVersion
if sendArgs.FromVersion != nil {
if sendArgs.FromVersion.Type == zfs.Bookmark {
getLogger(ctx).WithField("replication_guarantee", g).WithField("fromVersion", sendArgs.FromVersion.FullPath(sendArgs.FS)).
Debug("cannot hold a bookmark, speculating that `from` will not be destroyed until step is done")
} else {
from, err := HoldStep(ctx, sendArgs.FS, *sendArgs.FromVersion, jid)
if err != nil {
return nil, err
}
keep = append(keep, from)
}
// fallthrough
}
to, err := HoldStep(ctx, sendArgs.FS, sendArgs.ToVersion, jid)
if err != nil {
return nil, err
}
keep = append(keep, to)
return keep, nil
}
func (g ReplicationGuaranteeResumability) ReceiverPostRecv(ctx context.Context, jid JobID, fs string, toRecvd zfs.FilesystemVersion) (keep []Abstraction, err error) {
return receiverPostRecvCommon(ctx, jid, fs, toRecvd)
}
func (g ReplicationGuaranteeResumability) SenderPostRecvConfirmed(ctx context.Context, jid JobID, fs string, to zfs.FilesystemVersion) (keep []Abstraction, err error) {
return senderPostRecvConfirmedCommon(ctx, jid, fs, to)
}
// helper function used by multiple strategies
func senderPostRecvConfirmedCommon(ctx context.Context, jid JobID, fs string, to zfs.FilesystemVersion) (keep []Abstraction, err error) {
log := getLogger(ctx).WithField("toVersion", to.FullPath(fs))
toReplicationCursor, err := CreateReplicationCursor(ctx, fs, to, jid)
if err != nil {
if err == zfs.ErrBookmarkCloningNotSupported {
log.Debug("not setting replication cursor, bookmark cloning not supported")
} else {
msg := "cannot move replication cursor, keeping hold on `to` until successful"
log.WithError(err).Error(msg)
err = errors.Wrap(err, msg)
return nil, err
}
} else {
log.WithField("to_cursor", toReplicationCursor.String()).Info("successfully created `to` replication cursor")
}
return []Abstraction{toReplicationCursor}, nil
}
// helper function used by multiple strategies
func receiverPostRecvCommon(ctx context.Context, jid JobID, fs string, toRecvd zfs.FilesystemVersion) (keep []Abstraction, err error) {
getLogger(ctx).Debug("create new last-received-hold")
lrh, err := CreateLastReceivedHold(ctx, fs, toRecvd, jid)
if err != nil {
return nil, err
}
return []Abstraction{lrh}, nil
}
+1 -1
View File
@@ -3,5 +3,5 @@ package endpoint
import "github.com/prometheus/client_golang/prometheus" import "github.com/prometheus/client_golang/prometheus"
func RegisterMetrics(r prometheus.Registerer) { func RegisterMetrics(r prometheus.Registerer) {
r.MustRegister(sendAbstractionsCacheMetrics.count) r.MustRegister(abstractionsCacheMetrics.count)
} }
+35 -22
View File
@@ -23,19 +23,19 @@ type AbstractionType string
// There are a lot of exhaustive switches on AbstractionType in the code base. // There are a lot of exhaustive switches on AbstractionType in the code base.
// When adding a new abstraction type, make sure to search and update them! // When adding a new abstraction type, make sure to search and update them!
const ( const (
AbstractionStepBookmark AbstractionType = "step-bookmark" AbstractionStepHold AbstractionType = "step-hold"
AbstractionStepHold AbstractionType = "step-hold" AbstractionLastReceivedHold AbstractionType = "last-received-hold"
AbstractionLastReceivedHold AbstractionType = "last-received-hold" AbstractionTentativeReplicationCursorBookmark AbstractionType = "tentative-replication-cursor-bookmark-v2"
AbstractionReplicationCursorBookmarkV1 AbstractionType = "replication-cursor-bookmark-v1" AbstractionReplicationCursorBookmarkV1 AbstractionType = "replication-cursor-bookmark-v1"
AbstractionReplicationCursorBookmarkV2 AbstractionType = "replication-cursor-bookmark-v2" AbstractionReplicationCursorBookmarkV2 AbstractionType = "replication-cursor-bookmark-v2"
) )
var AbstractionTypesAll = map[AbstractionType]bool{ var AbstractionTypesAll = map[AbstractionType]bool{
AbstractionStepBookmark: true, AbstractionStepHold: true,
AbstractionStepHold: true, AbstractionLastReceivedHold: true,
AbstractionLastReceivedHold: true, AbstractionTentativeReplicationCursorBookmark: true,
AbstractionReplicationCursorBookmarkV1: true, AbstractionReplicationCursorBookmarkV1: true,
AbstractionReplicationCursorBookmarkV2: true, AbstractionReplicationCursorBookmarkV2: true,
} }
// Implementation Note: // Implementation Note:
@@ -80,12 +80,12 @@ func AbstractionEquals(a, b Abstraction) bool {
func (t AbstractionType) Validate() error { func (t AbstractionType) Validate() error {
switch t { switch t {
case AbstractionStepBookmark:
return nil
case AbstractionStepHold: case AbstractionStepHold:
return nil return nil
case AbstractionLastReceivedHold: case AbstractionLastReceivedHold:
return nil return nil
case AbstractionTentativeReplicationCursorBookmark:
return nil
case AbstractionReplicationCursorBookmarkV1: case AbstractionReplicationCursorBookmarkV1:
return nil return nil
case AbstractionReplicationCursorBookmarkV2: case AbstractionReplicationCursorBookmarkV2:
@@ -185,8 +185,8 @@ type BookmarkExtractor func(fs *zfs.DatasetPath, v zfs.FilesystemVersion) Abstra
// returns nil if the abstraction type is not bookmark-based // returns nil if the abstraction type is not bookmark-based
func (t AbstractionType) BookmarkExtractor() BookmarkExtractor { func (t AbstractionType) BookmarkExtractor() BookmarkExtractor {
switch t { switch t {
case AbstractionStepBookmark: case AbstractionTentativeReplicationCursorBookmark:
return StepBookmarkExtractor return TentativeReplicationCursorExtractor
case AbstractionReplicationCursorBookmarkV1: case AbstractionReplicationCursorBookmarkV1:
return ReplicationCursorV1Extractor return ReplicationCursorV1Extractor
case AbstractionReplicationCursorBookmarkV2: case AbstractionReplicationCursorBookmarkV2:
@@ -205,7 +205,7 @@ type HoldExtractor = func(fs *zfs.DatasetPath, v zfs.FilesystemVersion, tag stri
// returns nil if the abstraction type is not hold-based // returns nil if the abstraction type is not hold-based
func (t AbstractionType) HoldExtractor() HoldExtractor { func (t AbstractionType) HoldExtractor() HoldExtractor {
switch t { switch t {
case AbstractionStepBookmark: case AbstractionTentativeReplicationCursorBookmark:
return nil return nil
case AbstractionReplicationCursorBookmarkV1: case AbstractionReplicationCursorBookmarkV1:
return nil return nil
@@ -220,6 +220,23 @@ func (t AbstractionType) HoldExtractor() HoldExtractor {
} }
} }
func (t AbstractionType) BookmarkNamer() func(fs string, guid uint64, jobId JobID) (string, error) {
switch t {
case AbstractionTentativeReplicationCursorBookmark:
return TentativeReplicationCursorBookmarkName
case AbstractionReplicationCursorBookmarkV1:
panic("shouldn't be creating new ones")
case AbstractionReplicationCursorBookmarkV2:
return ReplicationCursorBookmarkName
case AbstractionStepHold:
return nil
case AbstractionLastReceivedHold:
return nil
default:
panic(fmt.Sprintf("unimpl: %q", t))
}
}
type ListZFSHoldsAndBookmarksQuery struct { type ListZFSHoldsAndBookmarksQuery struct {
FS ListZFSHoldsAndBookmarksQueryFilesystemFilter FS ListZFSHoldsAndBookmarksQueryFilesystemFilter
// What abstraction types should match (any contained in the set) // What abstraction types should match (any contained in the set)
@@ -697,11 +714,9 @@ func ListStale(ctx context.Context, q ListZFSHoldsAndBookmarksQuery) (*Staleness
return nil, &ListStaleQueryError{errors.New("ListStale cannot have Until != nil set on query")} return nil, &ListStaleQueryError{errors.New("ListStale cannot have Until != nil set on query")}
} }
// if asking for step holds, must also as for step bookmarks (same kind of abstraction) // if asking for step holds must also ask for replication cursor bookmarks (for firstNotStale)
// as well as replication cursor bookmarks (for firstNotStale)
ifAnyThenAll := AbstractionTypeSet{ ifAnyThenAll := AbstractionTypeSet{
AbstractionStepHold: true, AbstractionStepHold: true,
AbstractionStepBookmark: true,
AbstractionReplicationCursorBookmarkV2: true, AbstractionReplicationCursorBookmarkV2: true,
} }
if q.What.ContainsAnyOf(ifAnyThenAll) && !q.What.ContainsAll(ifAnyThenAll) { if q.What.ContainsAnyOf(ifAnyThenAll) && !q.What.ContainsAll(ifAnyThenAll) {
@@ -730,7 +745,7 @@ type fsAjobAtype struct {
} }
// For step holds and bookmarks, only those older than the most recent replication cursor // For step holds and bookmarks, only those older than the most recent replication cursor
// of their (filesystem,job) is considered because younger ones cannot be stale by definition // of their (filesystem,job) are considered because younger ones cannot be stale by definition
// (if we destroy them, we might actually lose the hold on the `To` for an ongoing incremental replication) // (if we destroy them, we might actually lose the hold on the `To` for an ongoing incremental replication)
// //
// For replication cursors and last-received-holds, only the most recent one is kept. // For replication cursors and last-received-holds, only the most recent one is kept.
@@ -772,8 +787,6 @@ func listStaleFiltering(abs []Abstraction, sinceBound *CreateTXGRangeBound) *Sta
} }
// stepFirstNotStaleCandidate.step // stepFirstNotStaleCandidate.step
case AbstractionStepBookmark:
fallthrough
case AbstractionStepHold: case AbstractionStepHold:
if c.step == nil || (*c.step).GetCreateTXG() < a.GetCreateTXG() { if c.step == nil || (*c.step).GetCreateTXG() < a.GetCreateTXG() {
a := a a := a
@@ -797,7 +810,7 @@ func listStaleFiltering(abs []Abstraction, sinceBound *CreateTXGRangeBound) *Sta
for k := range by { for k := range by {
l := by[k] l := by[k]
if k.Type == AbstractionStepHold || k.Type == AbstractionStepBookmark { if k.Type == AbstractionStepHold {
// all older than the most recent cursor are stale, others are always live // all older than the most recent cursor are stale, others are always live
// if we don't have a replication cursor yet, use untilBound = nil // if we don't have a replication cursor yet, use untilBound = nil
@@ -0,0 +1,92 @@
package endpoint
import (
"context"
"fmt"
"regexp"
"github.com/pkg/errors"
"github.com/zrepl/zrepl/zfs"
)
const (
LastReceivedHoldTagNamePrefix = "zrepl_last_received_J_"
)
var lastReceivedHoldTagRE = regexp.MustCompile("^zrepl_last_received_J_(.+)$")
var _ HoldExtractor = LastReceivedHoldExtractor
func LastReceivedHoldExtractor(fs *zfs.DatasetPath, v zfs.FilesystemVersion, holdTag string) Abstraction {
var err error
if v.Type != zfs.Snapshot {
panic("impl error")
}
jobID, err := ParseLastReceivedHoldTag(holdTag)
if err == nil {
return &holdBasedAbstraction{
Type: AbstractionLastReceivedHold,
FS: fs.ToString(),
FilesystemVersion: v,
Tag: holdTag,
JobID: jobID,
}
}
return nil
}
// err != nil always means that the bookmark is not a step bookmark
func ParseLastReceivedHoldTag(tag string) (JobID, error) {
match := lastReceivedHoldTagRE.FindStringSubmatch(tag)
if match == nil {
return JobID{}, errors.Errorf("parse last-received-hold tag: does not match regex %s", lastReceivedHoldTagRE.String())
}
jobId, err := MakeJobID(match[1])
if err != nil {
return JobID{}, errors.Wrap(err, "parse last-received-hold tag: invalid job id field")
}
return jobId, nil
}
func LastReceivedHoldTag(jobID JobID) (string, error) {
return lastReceivedHoldImpl(jobID.String())
}
func lastReceivedHoldImpl(jobid string) (string, error) {
tag := fmt.Sprintf("%s%s", LastReceivedHoldTagNamePrefix, jobid)
if err := zfs.ValidHoldTag(tag); err != nil {
return "", err
}
return tag, nil
}
func CreateLastReceivedHold(ctx context.Context, fs string, to zfs.FilesystemVersion, jobID JobID) (Abstraction, error) {
if !to.IsSnapshot() {
return nil, errors.Errorf("last-received-hold: target must be a snapshot: %s", to.FullPath(fs))
}
tag, err := LastReceivedHoldTag(jobID)
if err != nil {
return nil, errors.Wrap(err, "last-received-hold: hold tag")
}
// we never want to be without a hold
// => hold new one before releasing old hold
err = zfs.ZFSHold(ctx, fs, to, tag)
if err != nil {
return nil, errors.Wrap(err, "last-received-hold: hold newly received")
}
return &holdBasedAbstraction{
Type: AbstractionLastReceivedHold,
FS: fs,
FilesystemVersion: to,
JobID: jobID,
Tag: tag,
}, nil
}
@@ -4,12 +4,10 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"regexp"
"sort" "sort"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/zrepl/zrepl/util/errorarray"
"github.com/zrepl/zrepl/zfs" "github.com/zrepl/zrepl/zfs"
) )
@@ -53,6 +51,28 @@ func ParseReplicationCursorBookmarkName(fullname string) (uint64, JobID, error)
return guid, jobID, err return guid, jobID, err
} }
const tentativeReplicationCursorBookmarkNamePrefix = "zrepl_CURSORTENTATIVE_"
// v must be validated by caller
func TentativeReplicationCursorBookmarkName(fs string, guid uint64, id JobID) (string, error) {
return tentativeReplicationCursorBookmarkNameImpl(fs, guid, id.String())
}
func tentativeReplicationCursorBookmarkNameImpl(fs string, guid uint64, jobid string) (string, error) {
return makeJobAndGuidBookmarkName(tentativeReplicationCursorBookmarkNamePrefix, fs, guid, jobid)
}
// name is the full bookmark name, including dataset path
//
// err != nil always means that the bookmark is not a step bookmark
func ParseTentativeReplicationCursorBookmarkName(fullname string) (guid uint64, jobID JobID, err error) {
guid, jobID, err = parseJobAndGuidBookmarkName(fullname, tentativeReplicationCursorBookmarkNamePrefix)
if err != nil {
err = errors.Wrap(err, "parse step bookmark name") // no shadow!
}
return guid, jobID, err
}
// may return nil for both values, indicating there is no cursor // may return nil for both values, indicating there is no cursor
func GetMostRecentReplicationCursorOfJob(ctx context.Context, fs string, jobID JobID) (*zfs.FilesystemVersion, error) { func GetMostRecentReplicationCursorOfJob(ctx context.Context, fs string, jobID JobID) (*zfs.FilesystemVersion, error) {
fsp, err := zfs.NewDatasetPath(fs) fsp, err := zfs.NewDatasetPath(fs)
@@ -122,23 +142,23 @@ func GetReplicationCursors(ctx context.Context, dp *zfs.DatasetPath, jobID JobID
// //
// returns ErrBookmarkCloningNotSupported if version is a bookmark and bookmarking bookmarks is not supported by ZFS // returns ErrBookmarkCloningNotSupported if version is a bookmark and bookmarking bookmarks is not supported by ZFS
func CreateReplicationCursor(ctx context.Context, fs string, target zfs.FilesystemVersion, jobID JobID) (a Abstraction, err error) { func CreateReplicationCursor(ctx context.Context, fs string, target zfs.FilesystemVersion, jobID JobID) (a Abstraction, err error) {
return createBookmarkAbstraction(ctx, AbstractionReplicationCursorBookmarkV2, fs, target, jobID)
}
bookmarkname, err := ReplicationCursorBookmarkName(fs, target.GetGuid(), jobID) func CreateTentativeReplicationCursor(ctx context.Context, fs string, target zfs.FilesystemVersion, jobID JobID) (a Abstraction, err error) {
return createBookmarkAbstraction(ctx, AbstractionTentativeReplicationCursorBookmark, fs, target, jobID)
}
func createBookmarkAbstraction(ctx context.Context, abstractionType AbstractionType, fs string, target zfs.FilesystemVersion, jobID JobID) (a Abstraction, err error) {
bookmarkNamer := abstractionType.BookmarkNamer()
if bookmarkNamer == nil {
panic(abstractionType)
}
bookmarkname, err := bookmarkNamer(fs, target.GetGuid(), jobID)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "determine replication cursor name") return nil, errors.Wrapf(err, "determine %s name", abstractionType)
}
if target.IsBookmark() && target.GetName() == bookmarkname {
return &bookmarkBasedAbstraction{
Type: AbstractionReplicationCursorBookmarkV2,
FS: fs,
FilesystemVersion: target,
JobID: jobID,
}, nil
}
if !target.IsSnapshot() {
return nil, zfs.ErrBookmarkCloningNotSupported
} }
// idempotently create bookmark (guid is encoded in it) // idempotently create bookmark (guid is encoded in it)
@@ -152,125 +172,13 @@ func CreateReplicationCursor(ctx context.Context, fs string, target zfs.Filesyst
} }
return &bookmarkBasedAbstraction{ return &bookmarkBasedAbstraction{
Type: AbstractionReplicationCursorBookmarkV2, Type: abstractionType,
FS: fs, FS: fs,
FilesystemVersion: cursorBookmark, FilesystemVersion: cursorBookmark,
JobID: jobID, JobID: jobID,
}, nil }, nil
} }
const (
ReplicationCursorBookmarkNamePrefix = "zrepl_last_received_J_"
)
var lastReceivedHoldTagRE = regexp.MustCompile("^zrepl_last_received_J_(.+)$")
// err != nil always means that the bookmark is not a step bookmark
func ParseLastReceivedHoldTag(tag string) (JobID, error) {
match := lastReceivedHoldTagRE.FindStringSubmatch(tag)
if match == nil {
return JobID{}, errors.Errorf("parse last-received-hold tag: does not match regex %s", lastReceivedHoldTagRE.String())
}
jobId, err := MakeJobID(match[1])
if err != nil {
return JobID{}, errors.Wrap(err, "parse last-received-hold tag: invalid job id field")
}
return jobId, nil
}
func LastReceivedHoldTag(jobID JobID) (string, error) {
return lastReceivedHoldImpl(jobID.String())
}
func lastReceivedHoldImpl(jobid string) (string, error) {
tag := fmt.Sprintf("%s%s", ReplicationCursorBookmarkNamePrefix, jobid)
if err := zfs.ValidHoldTag(tag); err != nil {
return "", err
}
return tag, nil
}
func CreateLastReceivedHold(ctx context.Context, fs string, to zfs.FilesystemVersion, jobID JobID) (Abstraction, error) {
if !to.IsSnapshot() {
return nil, errors.Errorf("last-received-hold: target must be a snapshot: %s", to.FullPath(fs))
}
tag, err := LastReceivedHoldTag(jobID)
if err != nil {
return nil, errors.Wrap(err, "last-received-hold: hold tag")
}
// we never want to be without a hold
// => hold new one before releasing old hold
err = zfs.ZFSHold(ctx, fs, to, tag)
if err != nil {
return nil, errors.Wrap(err, "last-received-hold: hold newly received")
}
return &holdBasedAbstraction{
Type: AbstractionLastReceivedHold,
FS: fs,
FilesystemVersion: to,
JobID: jobID,
Tag: tag,
}, nil
}
func MoveLastReceivedHold(ctx context.Context, fs string, to zfs.FilesystemVersion, jobID JobID) error {
_, err := CreateLastReceivedHold(ctx, fs, to, jobID)
if err != nil {
return err
}
q := ListZFSHoldsAndBookmarksQuery{
What: AbstractionTypeSet{
AbstractionLastReceivedHold: true,
},
FS: ListZFSHoldsAndBookmarksQueryFilesystemFilter{
FS: &fs,
},
JobID: &jobID,
CreateTXG: CreateTXGRange{
Since: nil,
Until: &CreateTXGRangeBound{
CreateTXG: to.GetCreateTXG(),
Inclusive: &zfs.NilBool{B: false},
},
},
Concurrency: 1,
}
abs, absErrs, err := ListAbstractions(ctx, q)
if err != nil {
return errors.Wrap(err, "last-received-hold: list")
}
if len(absErrs) > 0 {
return errors.Wrap(ListAbstractionsErrors(absErrs), "last-received-hold: list")
}
getLogger(ctx).WithField("last-received-holds", fmt.Sprintf("%s", abs)).Debug("releasing last-received-holds")
var errs []error
for res := range BatchDestroy(ctx, abs) {
log := getLogger(ctx).
WithField("last-received-hold", res.Abstraction)
if res.DestroyErr != nil {
errs = append(errs, res.DestroyErr)
log.WithError(err).
Error("cannot release last-received-hold")
} else {
log.Info("released last-received-hold")
}
}
if len(errs) == 0 {
return nil
} else {
return errorarray.Wrap(errs, "last-received-hold: release")
}
}
func ReplicationCursorV2Extractor(fs *zfs.DatasetPath, v zfs.FilesystemVersion) (_ Abstraction) { func ReplicationCursorV2Extractor(fs *zfs.DatasetPath, v zfs.FilesystemVersion) (_ Abstraction) {
if v.Type != zfs.Bookmark { if v.Type != zfs.Bookmark {
panic("impl error") panic("impl error")
@@ -308,24 +216,28 @@ func ReplicationCursorV1Extractor(fs *zfs.DatasetPath, v zfs.FilesystemVersion)
return nil return nil
} }
var _ HoldExtractor = LastReceivedHoldExtractor var _ BookmarkExtractor = TentativeReplicationCursorExtractor
func LastReceivedHoldExtractor(fs *zfs.DatasetPath, v zfs.FilesystemVersion, holdTag string) Abstraction { func TentativeReplicationCursorExtractor(fs *zfs.DatasetPath, v zfs.FilesystemVersion) (_ Abstraction) {
var err error if v.Type != zfs.Bookmark {
if v.Type != zfs.Snapshot {
panic("impl error") panic("impl error")
} }
jobID, err := ParseLastReceivedHoldTag(holdTag) fullname := v.ToAbsPath(fs)
guid, jobid, err := ParseTentativeReplicationCursorBookmarkName(fullname)
if guid != v.Guid {
// TODO log this possibly tinkered-with bookmark
return nil
}
if err == nil { if err == nil {
return &holdBasedAbstraction{ bm := &bookmarkBasedAbstraction{
Type: AbstractionLastReceivedHold, Type: AbstractionTentativeReplicationCursorBookmark,
FS: fs.ToString(), FS: fs.ToString(),
FilesystemVersion: v, FilesystemVersion: v,
Tag: holdTag, JobID: jobid,
JobID: jobID,
} }
return bm
} }
return nil return nil
} }
-159
View File
@@ -1,159 +0,0 @@
package endpoint
import (
"context"
"fmt"
"regexp"
"github.com/pkg/errors"
"github.com/zrepl/zrepl/zfs"
)
var stepHoldTagRE = regexp.MustCompile("^zrepl_STEP_J_(.+)")
func StepHoldTag(jobid JobID) (string, error) {
return stepHoldTagImpl(jobid.String())
}
func stepHoldTagImpl(jobid string) (string, error) {
t := fmt.Sprintf("zrepl_STEP_J_%s", jobid)
if err := zfs.ValidHoldTag(t); err != nil {
return "", err
}
return t, nil
}
// err != nil always means that the bookmark is not a step bookmark
func ParseStepHoldTag(tag string) (JobID, error) {
match := stepHoldTagRE.FindStringSubmatch(tag)
if match == nil {
return JobID{}, fmt.Errorf("parse hold tag: match regex %q", stepHoldTagRE)
}
jobID, err := MakeJobID(match[1])
if err != nil {
return JobID{}, errors.Wrap(err, "parse hold tag: invalid job id field")
}
return jobID, nil
}
const stepBookmarkNamePrefix = "zrepl_STEP"
// v must be validated by caller
func StepBookmarkName(fs string, guid uint64, id JobID) (string, error) {
return stepBookmarkNameImpl(fs, guid, id.String())
}
func stepBookmarkNameImpl(fs string, guid uint64, jobid string) (string, error) {
return makeJobAndGuidBookmarkName(stepBookmarkNamePrefix, fs, guid, jobid)
}
// name is the full bookmark name, including dataset path
//
// err != nil always means that the bookmark is not a step bookmark
func ParseStepBookmarkName(fullname string) (guid uint64, jobID JobID, err error) {
guid, jobID, err = parseJobAndGuidBookmarkName(fullname, stepBookmarkNamePrefix)
if err != nil {
err = errors.Wrap(err, "parse step bookmark name") // no shadow!
}
return guid, jobID, err
}
// idempotently hold / step-bookmark `version`
//
// returns ErrBookmarkCloningNotSupported if version is a bookmark and bookmarking bookmarks is not supported by ZFS
func HoldStep(ctx context.Context, fs string, v zfs.FilesystemVersion, jobID JobID) (Abstraction, error) {
if v.IsSnapshot() {
tag, err := StepHoldTag(jobID)
if err != nil {
return nil, errors.Wrap(err, "step hold tag")
}
if err := zfs.ZFSHold(ctx, fs, v, tag); err != nil {
return nil, errors.Wrap(err, "step hold: zfs")
}
return &holdBasedAbstraction{
Type: AbstractionStepHold,
FS: fs,
Tag: tag,
JobID: jobID,
FilesystemVersion: v,
}, nil
}
if !v.IsBookmark() {
panic(fmt.Sprintf("version must bei either snapshot or bookmark, got %#v", v))
}
bmname, err := StepBookmarkName(fs, v.Guid, jobID)
if err != nil {
return nil, errors.Wrap(err, "create step bookmark: determine bookmark name")
}
// idempotently create bookmark
stepBookmark, err := zfs.ZFSBookmark(ctx, fs, v, bmname)
if err != nil {
if err == zfs.ErrBookmarkCloningNotSupported {
// TODO we could actually try to find a local snapshot that has the requested GUID
// however, the replication algorithm prefers snapshots anyways, so this quest
// is most likely not going to be successful. Also, there's the possibility that
// the caller might want to filter what snapshots are eligibile, and this would
// complicate things even further.
return nil, err // TODO go1.13 use wrapping
}
return nil, errors.Wrap(err, "create step bookmark: zfs")
}
return &bookmarkBasedAbstraction{
Type: AbstractionStepBookmark,
FS: fs,
FilesystemVersion: stepBookmark,
JobID: jobID,
}, nil
}
var _ BookmarkExtractor = StepBookmarkExtractor
func StepBookmarkExtractor(fs *zfs.DatasetPath, v zfs.FilesystemVersion) (_ Abstraction) {
if v.Type != zfs.Bookmark {
panic("impl error")
}
fullname := v.ToAbsPath(fs)
guid, jobid, err := ParseStepBookmarkName(fullname)
if guid != v.Guid {
// TODO log this possibly tinkered-with bookmark
return nil
}
if err == nil {
bm := &bookmarkBasedAbstraction{
Type: AbstractionStepBookmark,
FS: fs.ToString(),
FilesystemVersion: v,
JobID: jobid,
}
return bm
}
return nil
}
var _ HoldExtractor = StepHoldExtractor
func StepHoldExtractor(fs *zfs.DatasetPath, v zfs.FilesystemVersion, holdTag string) Abstraction {
if v.Type != zfs.Snapshot {
panic("impl error")
}
jobID, err := ParseStepHoldTag(holdTag)
if err == nil {
return &holdBasedAbstraction{
Type: AbstractionStepHold,
FS: fs.ToString(),
Tag: holdTag,
FilesystemVersion: v,
JobID: jobID,
}
}
return nil
}
@@ -0,0 +1,83 @@
package endpoint
import (
"context"
"fmt"
"regexp"
"github.com/pkg/errors"
"github.com/zrepl/zrepl/zfs"
)
var stepHoldTagRE = regexp.MustCompile("^zrepl_STEP_J_(.+)")
func StepHoldTag(jobid JobID) (string, error) {
return stepHoldTagImpl(jobid.String())
}
func stepHoldTagImpl(jobid string) (string, error) {
t := fmt.Sprintf("zrepl_STEP_J_%s", jobid)
if err := zfs.ValidHoldTag(t); err != nil {
return "", err
}
return t, nil
}
// err != nil always means that the bookmark is not a step bookmark
func ParseStepHoldTag(tag string) (JobID, error) {
match := stepHoldTagRE.FindStringSubmatch(tag)
if match == nil {
return JobID{}, fmt.Errorf("parse hold tag: match regex %q", stepHoldTagRE)
}
jobID, err := MakeJobID(match[1])
if err != nil {
return JobID{}, errors.Wrap(err, "parse hold tag: invalid job id field")
}
return jobID, nil
}
// idempotently hold `version`
func HoldStep(ctx context.Context, fs string, v zfs.FilesystemVersion, jobID JobID) (Abstraction, error) {
if !v.IsSnapshot() {
panic(fmt.Sprintf("version must be a snapshot got %#v", v))
}
tag, err := StepHoldTag(jobID)
if err != nil {
return nil, errors.Wrap(err, "step hold tag")
}
if err := zfs.ZFSHold(ctx, fs, v, tag); err != nil {
return nil, errors.Wrap(err, "step hold: zfs")
}
return &holdBasedAbstraction{
Type: AbstractionStepHold,
FS: fs,
Tag: tag,
JobID: jobID,
FilesystemVersion: v,
}, nil
}
var _ HoldExtractor = StepHoldExtractor
func StepHoldExtractor(fs *zfs.DatasetPath, v zfs.FilesystemVersion, holdTag string) Abstraction {
if v.Type != zfs.Snapshot {
panic("impl error")
}
jobID, err := ParseStepHoldTag(holdTag)
if err == nil {
return &holdBasedAbstraction{
Type: AbstractionStepHold,
FS: fs.ToString(),
Tag: holdTag,
FilesystemVersion: v,
JobID: jobID,
}
}
return nil
}
+2 -2
View File
@@ -24,9 +24,9 @@ func MakeJobID(s string) (JobID, error) {
return JobID{}, errors.Wrap(err, "must be usable as a dataset path component") return JobID{}, errors.Wrap(err, "must be usable as a dataset path component")
} }
if _, err := stepBookmarkNameImpl("pool/ds", 0xface601d, s); err != nil { if _, err := tentativeReplicationCursorBookmarkNameImpl("pool/ds", 0xface601d, s); err != nil {
// note that this might still fail due to total maximum name length, but we can't enforce that // note that this might still fail due to total maximum name length, but we can't enforce that
return JobID{}, errors.Wrap(err, "must be usable for a step bookmark") return JobID{}, errors.Wrap(err, "must be usable for a tentative replication cursor bookmark")
} }
if _, err := stepHoldTagImpl(s); err != nil { if _, err := stepHoldTagImpl(s); err != nil {
@@ -0,0 +1,80 @@
// Code generated by "enumer -type=ReplicationGuaranteeKind -json -transform=snake -trimprefix=ReplicationGuaranteeKind"; DO NOT EDIT.
//
package endpoint
import (
"encoding/json"
"fmt"
)
const (
_ReplicationGuaranteeKindName_0 = "resumabilityincremental"
_ReplicationGuaranteeKindName_1 = "none"
)
var (
_ReplicationGuaranteeKindIndex_0 = [...]uint8{0, 12, 23}
_ReplicationGuaranteeKindIndex_1 = [...]uint8{0, 4}
)
func (i ReplicationGuaranteeKind) String() string {
switch {
case 1 <= i && i <= 2:
i -= 1
return _ReplicationGuaranteeKindName_0[_ReplicationGuaranteeKindIndex_0[i]:_ReplicationGuaranteeKindIndex_0[i+1]]
case i == 4:
return _ReplicationGuaranteeKindName_1
default:
return fmt.Sprintf("ReplicationGuaranteeKind(%d)", i)
}
}
var _ReplicationGuaranteeKindValues = []ReplicationGuaranteeKind{1, 2, 4}
var _ReplicationGuaranteeKindNameToValueMap = map[string]ReplicationGuaranteeKind{
_ReplicationGuaranteeKindName_0[0:12]: 1,
_ReplicationGuaranteeKindName_0[12:23]: 2,
_ReplicationGuaranteeKindName_1[0:4]: 4,
}
// ReplicationGuaranteeKindString retrieves an enum value from the enum constants string name.
// Throws an error if the param is not part of the enum.
func ReplicationGuaranteeKindString(s string) (ReplicationGuaranteeKind, error) {
if val, ok := _ReplicationGuaranteeKindNameToValueMap[s]; ok {
return val, nil
}
return 0, fmt.Errorf("%s does not belong to ReplicationGuaranteeKind values", s)
}
// ReplicationGuaranteeKindValues returns all values of the enum
func ReplicationGuaranteeKindValues() []ReplicationGuaranteeKind {
return _ReplicationGuaranteeKindValues
}
// IsAReplicationGuaranteeKind returns "true" if the value is listed in the enum definition. "false" otherwise
func (i ReplicationGuaranteeKind) IsAReplicationGuaranteeKind() bool {
for _, v := range _ReplicationGuaranteeKindValues {
if i == v {
return true
}
}
return false
}
// MarshalJSON implements the json.Marshaler interface for ReplicationGuaranteeKind
func (i ReplicationGuaranteeKind) MarshalJSON() ([]byte, error) {
return json.Marshal(i.String())
}
// UnmarshalJSON implements the json.Unmarshaler interface for ReplicationGuaranteeKind
func (i *ReplicationGuaranteeKind) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return fmt.Errorf("ReplicationGuaranteeKind should be a string, got %s", data)
}
var err error
*i, err = ReplicationGuaranteeKindString(s)
return err
}
+31 -16
View File
@@ -1,4 +1,4 @@
#!/bin/bash #!/usr/bin/env bash
set -e set -e
@@ -14,23 +14,31 @@ step() {
echo "${bold}$1${normal}" echo "${bold}$1${normal}"
} }
if ! type go >/dev/null; then
step "go binary not installed or not in \$PATH" 1>&2
exit 1
fi
if [ -z "$GOPATH" ]; then
step "Make sure you have your GOPATH configured correctly" 1>&2
exit 1
fi
CHECKOUTPATH="${GOPATH}/src/github.com/zrepl/zrepl"
godep() { godep() {
step "install build dependencies (versions pinned in build/go.mod and build/tools.go)" step "install build dependencies (versions pinned in build/go.mod and build/tools.go)"
if ! type go >/dev/null; then
step "go binary not installed or not in \$PATH" 1>&2
exit 1
fi
if [ -z "$GOPATH" ]; then
step "Your GOPATH is not configured correctly" 1>&2
exit 1
fi
if ! (echo "$PATH" | grep "${GOPATH}/bin" > /dev/null); then
step "GOPATH/bin is not in your PATH (it should be towards the start of it)"
exit 1
fi
pushd "$(dirname "${BASH_SOURCE[0]}")"/build pushd "$(dirname "${BASH_SOURCE[0]}")"/build
set -x set -x
export GO111MODULE=on # otherwise, a checkout of this repo in GOPATH will disable modules on Go 1.12 and earlier export GO111MODULE=on # otherwise, a checkout of this repo in GOPATH will disable modules on Go 1.12 and earlier
source <(go env)
export GOOS="$GOHOSTOS"
export GOARCH="$GOHOSTARCH"
# TODO GOARM=$GOHOSTARM?
go build -v -mod=readonly -o "$GOPATH/bin/stringer" golang.org/x/tools/cmd/stringer go build -v -mod=readonly -o "$GOPATH/bin/stringer" golang.org/x/tools/cmd/stringer
go build -v -mod=readonly -o "$GOPATH/bin/protoc-gen-go" github.com/golang/protobuf/protoc-gen-go go build -v -mod=readonly -o "$GOPATH/bin/protoc-gen-go" github.com/golang/protobuf/protoc-gen-go
go build -v -mod=readonly -o "$GOPATH/bin/enumer" github.com/alvaroloes/enumer go build -v -mod=readonly -o "$GOPATH/bin/enumer" github.com/alvaroloes/enumer
@@ -51,8 +59,9 @@ docdep() {
exit 1 exit 1
fi fi
step "Installing doc build dependencies" step "Installing doc build dependencies"
local reqpath="${CHECKOUTPATH}/docs/requirements.txt" # shellcheck disable=SC2155
if [ ! -z "$ZREPL_LAZY_DOCS_REQPATH" ]; then local reqpath="$(dirname "${BASH_SOURCE[0]}")/docs/requirements.txt"
if [ -n "$ZREPL_LAZY_DOCS_REQPATH" ]; then
reqpath="$ZREPL_LAZY_DOCS_REQPATH" reqpath="$ZREPL_LAZY_DOCS_REQPATH"
fi fi
pip3 install -r "$reqpath" pip3 install -r "$reqpath"
@@ -63,9 +72,15 @@ release() {
make release make release
} }
# shellcheck disable=SC2198
if [ -z "$@" ]; then
step "No command specified, exiting"
exit 1
fi
for cmd in "$@"; do for cmd in "$@"; do
case "$cmd" in case "$cmd" in
godep|docdep|release_bins|docs) godep|docdep|release|docs)
eval $cmd eval $cmd
continue continue
;; ;;
+1 -1
View File
@@ -82,7 +82,7 @@ func (l Level) String() string {
case Error: case Error:
return "error" return "error"
default: default:
return string(l) return fmt.Sprintf("unknown level %d", l)
} }
} }
+11
View File
@@ -0,0 +1,11 @@
FROM debian:latest
RUN apt-get update && apt-get install -y \
build-essential \
devscripts \
dh-exec
RUN mkdir -p /build/src && chmod -R 0777 /build
WORKDIR /build/src
+5
View File
@@ -0,0 +1,5 @@
zrepl (VERSION) unstable;
Check https://zrepl.github.io/changelog.html.
-- zrepl build process <me@cschwarz.com> DATE_DASH_R_OUTPUT
+1
View File
@@ -0,0 +1 @@
11
+8
View File
@@ -0,0 +1,8 @@
Source: zrepl
Maintainer: Christian Schwarz <me@cschwarz.com>
Build-Depends: dh-exec
Package: zrepl
Architecture: arm64 amd64 armhf i386
Description: ZFS replication
zrepl is a one-stop solution for ZFS filesystem replication
+50
View File
@@ -0,0 +1,50 @@
#!/usr/bin/make -f
%:
dh $@
override_dh_strip:
override_dh_auto_build:
override_dh_auto_clean:
rm -rf debian/renamedir
override_dh_auto_install:
# install the zrepl.service for
# dh_install_systemd and dh_systemd_{start,enable}
install dist/systemd/zrepl.service debian/zrepl.service
sed -i 's#ExecStart=/usr/local/bin/zrepl #ExecStart=/usr/bin/zrepl #' debian/zrepl.service
sed -i 's#ExecStartPre=/usr/local/bin/zrepl #ExecStartPre=/usr/bin/zrepl #' debian/zrepl.service
mkdir -p debian/renamedir
# install binary
stat artifacts/$(ZREPL_DPKG_ZREPL_BINARY_FILENAME)
cp --preserve=all artifacts/$(ZREPL_DPKG_ZREPL_BINARY_FILENAME) debian/renamedir/zrepl
dh_install debian/renamedir/zrepl usr/bin
# install bash completion
dh_install artifacts/bash_completion etc/bash_completion.d/zrepl
dh_install artifacts/_zrepl.zsh_completion usr/share/zsh/vendor-completions
# install docs
dh_install artifacts/docs/html usr/share/doc/zrepl/docs/
# install examples
dh_install config/samples/* usr/share/doc/zrepl/examples
# install default config
mkdir -p debian/tmp/etc/zrepl
chmod 0700 debian/tmp/etc/zrepl
sed 's#USR_SHARE_ZREPL#/usr/share/doc/zrepl#' packaging/systemd-default-zrepl.yml > debian/renamedir/zrepl.yml
dh_install debian/renamedir/zrepl.yml etc/zrepl
# save git revision of this packaging repo
echo $$(git rev-parse HEAD) > debian/packaging-repo-git-revision
dh_install debian/packaging-repo-git-revision usr/share/doc/zrepl
override_dh_auto_test:
# don't run tests at this point
+7
View File
@@ -0,0 +1,7 @@
FROM fedora:latest
RUN dnf install -y git make bash rpm-build 'dnf-command(builddep)'
ADD packaging/rpm/zrepl.spec /tmp/zrepl.spec
RUN dnf builddep -y /tmp/zrepl.spec
RUN mkdir -p /build/src && chmod -R 0777 /build
WORKDIR /build/src
+78
View File
@@ -0,0 +1,78 @@
# This Spec file packages pre-built artifacts in the artifacts directory
# into an RPM. This means that rpmbuild won't actually build any new artifacts
#
# Read the Makefile first, then come back here.
# Global meta data
Version: SUBSTITUTED_BY_MAKEFILE
%global common_description %{expand:
zrepl is a one-stop, integrated solution for ZFS replication.}
# use gzip to be compatible with centos7
%define _source_payload w9.gzdio
%define _binary_payload w9.gzdio
# don't strip pre-built binaries
%define __strip /usr/bin/true
Name: zrepl
Release: 1
Summary: One-stop, integrated solution for ZFS replication
License: MIT
URL: https://zrepl.github.io/
# Source: we use rpmbuild --build-in-tree => no source
BuildRequires: systemd
Requires(post): systemd
Requires(preun): systemd
Requires(postun): systemd
%description
%{common_description}
%prep
# we use rpmbuild --build-in-tree => no prep or setup
%build
# we don't actually need to build anything here, that has already been done by the Makefile
# Correct the path in the systemd unit file
sed s:/usr/local/bin/:%{_bindir}/:g dist/systemd/zrepl.service > artifacts/rpmbuild/zrepl.service
# Generate the default configuration file
sed 's#USR_SHARE_ZREPL#%{_datadir}/doc/zrepl#' packaging/systemd-default-zrepl.yml > artifacts/rpmbuild/zrepl.yml
%install
install -Dm 0755 artifacts/%{_zrepl_binary_filename} %{buildroot}%{_bindir}/zrepl
install -Dm 0644 artifacts/rpmbuild/zrepl.service %{buildroot}%{_unitdir}/zrepl.service
install -Dm 0644 artifacts/_zrepl.zsh_completion %{buildroot}%{_datadir}/zsh/site-functions/_zrepl
install -Dm 0644 artifacts/bash_completion %{buildroot}%{_datadir}/bash-completion/completions/zrepl
install -Dm 0644 artifacts/rpmbuild/zrepl.yml %{buildroot}%{_sysconfdir}/zrepl/zrepl.yml
install -d %{buildroot}%{_datadir}/doc/zrepl
cp -a artifacts/docs/html %{buildroot}%{_datadir}/doc/zrepl/html
cp -a config/samples %{buildroot}%{_datadir}/doc/zrepl/examples
%post
%systemd_post zrepl.service
%preun
%systemd_preun zrepl.service
%postun
%systemd_postun_with_restart zrepl.service
%files
%defattr(-,root,root)
%license LICENSE
%{_bindir}/zrepl
%config %{_unitdir}/zrepl.service
%dir %{_sysconfdir}/zrepl
%config %{_sysconfdir}/zrepl/zrepl.yml
%{_datadir}/zsh/site-functions/_zrepl
%{_datadir}/bash-completion/completions/zrepl
%{_datadir}/doc/zrepl
%changelog
# TODO: auto-fill changelog from git? -> need same solution for debian
+13
View File
@@ -0,0 +1,13 @@
global:
logging:
# use syslog instead of stdout because it makes journald happy
- type: syslog
format: human
level: warn
jobs:
# - name: foo
# type: bar
# see USR_SHARE_ZREPL/examples
# or https://zrepl.github.io/configuration/overview.html
+1
View File
@@ -11,6 +11,7 @@ import (
"github.com/fatih/color" "github.com/fatih/color"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/zrepl/zrepl/daemon/logging/trace" "github.com/zrepl/zrepl/daemon/logging/trace"
"github.com/zrepl/zrepl/config" "github.com/zrepl/zrepl/config"
+9 -3
View File
@@ -16,15 +16,21 @@ var Cases = []Case{BatchDestroy,
ListFilesystemsNoFilter, ListFilesystemsNoFilter,
ReceiveForceIntoEncryptedErr, ReceiveForceIntoEncryptedErr,
ReceiveForceRollbackWorksUnencrypted, ReceiveForceRollbackWorksUnencrypted,
ReplicationFailingInitialParentProhibitsChildReplication,
ReplicationIncrementalCleansUpStaleAbstractionsWithCacheOnSecondReplication, ReplicationIncrementalCleansUpStaleAbstractionsWithCacheOnSecondReplication,
ReplicationIncrementalCleansUpStaleAbstractionsWithoutCacheOnSecondReplication, ReplicationIncrementalCleansUpStaleAbstractionsWithoutCacheOnSecondReplication,
ReplicationIncrementalDestroysStepHoldsIffIncrementalStepHoldsAreDisabledButStepHoldsExist, ReplicationIncrementalDestroysStepHoldsIffIncrementalStepHoldsAreDisabledButStepHoldsExist,
ReplicationIncrementalIsPossibleIfCommonSnapshotIsDestroyed, ReplicationIncrementalIsPossibleIfCommonSnapshotIsDestroyed,
ReplicationIsResumableFullSend__DisableIncrementalStepHolds_False, ReplicationIsResumableFullSend__both_GuaranteeResumability,
ReplicationIsResumableFullSend__DisableIncrementalStepHolds_True, ReplicationIsResumableFullSend__initial_GuaranteeIncrementalReplication_incremental_GuaranteeIncrementalReplication,
ReplicationIsResumableFullSend__initial_GuaranteeResumability_incremental_GuaranteeIncrementalReplication,
ReplicationReceiverErrorWhileStillSending,
ReplicationStepCompletedLostBehavior__GuaranteeIncrementalReplication,
ReplicationStepCompletedLostBehavior__GuaranteeResumability,
ResumableRecvAndTokenHandling, ResumableRecvAndTokenHandling,
ResumeTokenParsing, ResumeTokenParsing,
SendArgsValidationEncryptedSendOfUnencryptedDatasetForbidden, SendArgsValidationEncryptedSendOfUnencryptedDatasetForbidden__EncryptionSupported_false,
SendArgsValidationEncryptedSendOfUnencryptedDatasetForbidden__EncryptionSupported_true,
SendArgsValidationResumeTokenDifferentFilesystemForbidden, SendArgsValidationResumeTokenDifferentFilesystemForbidden,
SendArgsValidationResumeTokenEncryptionMismatchForbidden, SendArgsValidationResumeTokenEncryptionMismatchForbidden,
UndestroyableSnapshotParsing, UndestroyableSnapshotParsing,
+1
View File
@@ -4,6 +4,7 @@ import (
"path" "path"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/zrepl/zrepl/platformtest" "github.com/zrepl/zrepl/platformtest"
"github.com/zrepl/zrepl/zfs" "github.com/zrepl/zrepl/zfs"
) )
+1
View File
@@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/zrepl/zrepl/platformtest" "github.com/zrepl/zrepl/platformtest"
"github.com/zrepl/zrepl/zfs" "github.com/zrepl/zrepl/zfs"
) )
@@ -4,11 +4,20 @@ import (
"fmt" "fmt"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/zrepl/zrepl/platformtest" "github.com/zrepl/zrepl/platformtest"
"github.com/zrepl/zrepl/zfs" "github.com/zrepl/zrepl/zfs"
) )
func ReceiveForceIntoEncryptedErr(ctx *platformtest.Context) { func ReceiveForceIntoEncryptedErr(ctx *platformtest.Context) {
supported, err := zfs.EncryptionCLISupported(ctx)
require.NoError(ctx, err, "encryption feature test failed")
if !supported {
ctx.SkipNow()
return
}
platformtest.Run(ctx, platformtest.PanicErr, ctx.RootDataset, ` platformtest.Run(ctx, platformtest.PanicErr, ctx.RootDataset, `
DESTROYROOT DESTROYROOT
CREATEROOT CREATEROOT
@@ -32,6 +41,7 @@ func ReceiveForceIntoEncryptedErr(ctx *platformtest.Context) {
sendStream, err := zfs.ZFSSend(ctx, sendArgs) sendStream, err := zfs.ZFSSend(ctx, sendArgs)
require.NoError(ctx, err) require.NoError(ctx, err)
defer sendStream.Close()
recvOpts := zfs.RecvOptions{ recvOpts := zfs.RecvOptions{
RollbackAndForceRecv: true, RollbackAndForceRecv: true,
+1
View File
@@ -5,6 +5,7 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/zrepl/zrepl/platformtest" "github.com/zrepl/zrepl/platformtest"
"github.com/zrepl/zrepl/zfs" "github.com/zrepl/zrepl/zfs"
) )
+419 -55
View File
@@ -4,11 +4,13 @@ import (
"context" "context"
"fmt" "fmt"
"io" "io"
"os"
"path" "path"
"sort" "sort"
"github.com/kr/pretty" "github.com/kr/pretty"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/zrepl/zrepl/daemon/filters" "github.com/zrepl/zrepl/daemon/filters"
"github.com/zrepl/zrepl/endpoint" "github.com/zrepl/zrepl/endpoint"
"github.com/zrepl/zrepl/platformtest" "github.com/zrepl/zrepl/platformtest"
@@ -27,11 +29,13 @@ import (
// of a new sender and receiver instance and one blocking invocation // of a new sender and receiver instance and one blocking invocation
// of the replication engine without encryption // of the replication engine without encryption
type replicationInvocation struct { type replicationInvocation struct {
sjid, rjid endpoint.JobID sjid, rjid endpoint.JobID
sfs string sfs string
rfsRoot string sfilter *filters.DatasetMapFilter
interceptSender func(e *endpoint.Sender) logic.Sender rfsRoot string
disableIncrementalStepHolds bool interceptSender func(e *endpoint.Sender) logic.Sender
interceptReceiver func(e *endpoint.Receiver) logic.Receiver
guarantee pdu.ReplicationConfigProtection
} }
func (i replicationInvocation) Do(ctx *platformtest.Context) *report.Report { func (i replicationInvocation) Do(ctx *platformtest.Context) *report.Report {
@@ -39,24 +43,33 @@ func (i replicationInvocation) Do(ctx *platformtest.Context) *report.Report {
if i.interceptSender == nil { if i.interceptSender == nil {
i.interceptSender = func(e *endpoint.Sender) logic.Sender { return e } i.interceptSender = func(e *endpoint.Sender) logic.Sender { return e }
} }
if i.interceptReceiver == nil {
i.interceptReceiver = func(e *endpoint.Receiver) logic.Receiver { return e }
}
sfilter := filters.NewDatasetMapFilter(1, true) if i.sfs != "" && i.sfilter != nil || i.sfs == "" && i.sfilter == nil {
err := sfilter.Add(i.sfs, "ok") panic("either sfs or sfilter must be set")
require.NoError(ctx, err) }
if i.sfilter == nil {
i.sfilter = filters.NewDatasetMapFilter(1, true)
err := i.sfilter.Add(i.sfs, "ok")
require.NoError(ctx, err)
}
sender := i.interceptSender(endpoint.NewSender(endpoint.SenderConfig{ sender := i.interceptSender(endpoint.NewSender(endpoint.SenderConfig{
FSF: sfilter.AsFilter(), FSF: i.sfilter.AsFilter(),
Encrypt: &zfs.NilBool{B: false}, Encrypt: &zfs.NilBool{B: false},
DisableIncrementalStepHolds: i.disableIncrementalStepHolds, JobID: i.sjid,
JobID: i.sjid,
})) }))
receiver := endpoint.NewReceiver(endpoint.ReceiverConfig{ receiver := i.interceptReceiver(endpoint.NewReceiver(endpoint.ReceiverConfig{
JobID: i.rjid, JobID: i.rjid,
AppendClientIdentity: false, AppendClientIdentity: false,
RootWithoutClientComponent: mustDatasetPath(i.rfsRoot), RootWithoutClientComponent: mustDatasetPath(i.rfsRoot),
UpdateLastReceivedHold: true, }))
})
plannerPolicy := logic.PlannerPolicy{ plannerPolicy := logic.PlannerPolicy{
EncryptedSend: logic.TriFromBool(false), EncryptedSend: logic.TriFromBool(false),
ReplicationConfig: pdu.ReplicationConfig{
Protection: &i.guarantee,
},
} }
report, wait := replication.Do( report, wait := replication.Do(
@@ -89,11 +102,11 @@ func ReplicationIncrementalIsPossibleIfCommonSnapshotIsDestroyed(ctx *platformte
snap1 := fsversion(ctx, sfs, "@1") snap1 := fsversion(ctx, sfs, "@1")
rep := replicationInvocation{ rep := replicationInvocation{
sjid: sjid, sjid: sjid,
rjid: rjid, rjid: rjid,
sfs: sfs, sfs: sfs,
rfsRoot: rfsRoot, rfsRoot: rfsRoot,
disableIncrementalStepHolds: false, guarantee: *pdu.ReplicationConfigProtectionWithKind(pdu.ReplicationGuaranteeKind_GuaranteeResumability),
} }
rfs := rep.ReceiveSideFilesystem() rfs := rep.ReceiveSideFilesystem()
@@ -153,11 +166,11 @@ func implReplicationIncrementalCleansUpStaleAbstractions(ctx *platformtest.Conte
rfsRoot := ctx.RootDataset + "/receiver" rfsRoot := ctx.RootDataset + "/receiver"
rep := replicationInvocation{ rep := replicationInvocation{
sjid: sjid, sjid: sjid,
rjid: rjid, rjid: rjid,
sfs: sfs, sfs: sfs,
rfsRoot: rfsRoot, rfsRoot: rfsRoot,
disableIncrementalStepHolds: false, guarantee: *pdu.ReplicationConfigProtectionWithKind(pdu.ReplicationGuaranteeKind_GuaranteeResumability),
} }
rfs := rep.ReceiveSideFilesystem() rfs := rep.ReceiveSideFilesystem()
@@ -207,7 +220,7 @@ func implReplicationIncrementalCleansUpStaleAbstractions(ctx *platformtest.Conte
snap5 := fsversion(ctx, sfs, "@5") snap5 := fsversion(ctx, sfs, "@5")
if invalidateCacheBeforeSecondReplication { if invalidateCacheBeforeSecondReplication {
endpoint.SendAbstractionsCacheInvalidate(sfs) endpoint.AbstractionsCacheInvalidate(sfs)
} }
// do another replication // do another replication
@@ -327,15 +340,59 @@ func (s *PartialSender) Send(ctx context.Context, r *pdu.SendReq) (r1 *pdu.SendR
return r1, r2, r3 return r1, r2, r3
} }
func ReplicationIsResumableFullSend__DisableIncrementalStepHolds_False(ctx *platformtest.Context) { func ReplicationIsResumableFullSend__both_GuaranteeResumability(ctx *platformtest.Context) {
implReplicationIsResumableFullSend(ctx, false)
setup := replicationIsResumableFullSendSetup{
protection: pdu.ReplicationConfigProtection{
Initial: pdu.ReplicationGuaranteeKind_GuaranteeResumability,
Incremental: pdu.ReplicationGuaranteeKind_GuaranteeResumability,
},
expectDatasetIsBusyErrorWhenDestroySnapshotWhilePartiallyReplicated: true,
expectAllThreeSnapshotsToThreeBePresentAfterLoop: true,
expectNoSnapshotsOnReceiverAfterLoop: false,
}
implReplicationIsResumableFullSend(ctx, setup)
} }
func ReplicationIsResumableFullSend__DisableIncrementalStepHolds_True(ctx *platformtest.Context) { func ReplicationIsResumableFullSend__initial_GuaranteeResumability_incremental_GuaranteeIncrementalReplication(ctx *platformtest.Context) {
implReplicationIsResumableFullSend(ctx, true)
setup := replicationIsResumableFullSendSetup{
protection: pdu.ReplicationConfigProtection{
Initial: pdu.ReplicationGuaranteeKind_GuaranteeResumability,
Incremental: pdu.ReplicationGuaranteeKind_GuaranteeIncrementalReplication,
},
expectDatasetIsBusyErrorWhenDestroySnapshotWhilePartiallyReplicated: true,
expectAllThreeSnapshotsToThreeBePresentAfterLoop: true,
expectNoSnapshotsOnReceiverAfterLoop: false,
}
implReplicationIsResumableFullSend(ctx, setup)
} }
func implReplicationIsResumableFullSend(ctx *platformtest.Context, disableIncrementalStepHolds bool) { func ReplicationIsResumableFullSend__initial_GuaranteeIncrementalReplication_incremental_GuaranteeIncrementalReplication(ctx *platformtest.Context) {
setup := replicationIsResumableFullSendSetup{
protection: pdu.ReplicationConfigProtection{
Initial: pdu.ReplicationGuaranteeKind_GuaranteeIncrementalReplication,
Incremental: pdu.ReplicationGuaranteeKind_GuaranteeIncrementalReplication,
},
expectDatasetIsBusyErrorWhenDestroySnapshotWhilePartiallyReplicated: false,
expectAllThreeSnapshotsToThreeBePresentAfterLoop: false,
expectNoSnapshotsOnReceiverAfterLoop: true,
}
implReplicationIsResumableFullSend(ctx, setup)
}
type replicationIsResumableFullSendSetup struct {
protection pdu.ReplicationConfigProtection
expectDatasetIsBusyErrorWhenDestroySnapshotWhilePartiallyReplicated bool
expectAllThreeSnapshotsToThreeBePresentAfterLoop bool
expectNoSnapshotsOnReceiverAfterLoop bool
}
func implReplicationIsResumableFullSend(ctx *platformtest.Context, setup replicationIsResumableFullSendSetup) {
platformtest.Run(ctx, platformtest.PanicErr, ctx.RootDataset, ` platformtest.Run(ctx, platformtest.PanicErr, ctx.RootDataset, `
CREATEROOT CREATEROOT
@@ -366,8 +423,9 @@ func implReplicationIsResumableFullSend(ctx *platformtest.Context, disableIncrem
interceptSender: func(e *endpoint.Sender) logic.Sender { interceptSender: func(e *endpoint.Sender) logic.Sender {
return &PartialSender{Sender: e, failAfterByteCount: 1 << 20} return &PartialSender{Sender: e, failAfterByteCount: 1 << 20}
}, },
disableIncrementalStepHolds: disableIncrementalStepHolds, guarantee: setup.protection,
} }
rfs := rep.ReceiveSideFilesystem() rfs := rep.ReceiveSideFilesystem()
for i := 2; i < 10; i++ { for i := 2; i < 10; i++ {
@@ -381,8 +439,11 @@ func implReplicationIsResumableFullSend(ctx *platformtest.Context, disableIncrem
// and we wrote dummy data 1<<22 bytes, thus at least // and we wrote dummy data 1<<22 bytes, thus at least
// for the first 4 times this should not be possible // for the first 4 times this should not be possible
// due to step holds // due to step holds
require.Error(ctx, err) if setup.expectDatasetIsBusyErrorWhenDestroySnapshotWhilePartiallyReplicated {
require.Contains(ctx, err.Error(), "dataset is busy") ctx.Logf("i=%v", i)
require.Error(ctx, err)
require.Contains(ctx, err.Error(), "dataset is busy")
}
} }
// and create some additional snapshots that could // and create some additional snapshots that could
@@ -401,11 +462,19 @@ func implReplicationIsResumableFullSend(ctx *platformtest.Context, disableIncrem
} }
} }
// make sure all the filesystem versions we created if setup.expectAllThreeSnapshotsToThreeBePresentAfterLoop {
// were replicated by the replication loop // make sure all the filesystem versions we created
_ = fsversion(ctx, rfs, "@1") // were replicated by the replication loop
_ = fsversion(ctx, rfs, "@2") _ = fsversion(ctx, rfs, "@1")
_ = fsversion(ctx, rfs, "@3") _ = fsversion(ctx, rfs, "@2")
_ = fsversion(ctx, rfs, "@3")
}
if setup.expectNoSnapshotsOnReceiverAfterLoop {
versions, err := zfs.ZFSListFilesystemVersions(ctx, mustDatasetPath(rfs), zfs.ListFilesystemVersionsOptions{})
require.NoError(ctx, err)
require.Empty(ctx, versions)
}
} }
@@ -428,11 +497,11 @@ func ReplicationIncrementalDestroysStepHoldsIffIncrementalStepHoldsAreDisabledBu
{ {
mustSnapshot(ctx, sfs+"@1") mustSnapshot(ctx, sfs+"@1")
rep := replicationInvocation{ rep := replicationInvocation{
sjid: sjid, sjid: sjid,
rjid: rjid, rjid: rjid,
sfs: sfs, sfs: sfs,
rfsRoot: rfsRoot, rfsRoot: rfsRoot,
disableIncrementalStepHolds: false, guarantee: *pdu.ReplicationConfigProtectionWithKind(pdu.ReplicationGuaranteeKind_GuaranteeResumability),
} }
rfs := rep.ReceiveSideFilesystem() rfs := rep.ReceiveSideFilesystem()
report := rep.Do(ctx) report := rep.Do(ctx)
@@ -455,11 +524,11 @@ func ReplicationIncrementalDestroysStepHoldsIffIncrementalStepHoldsAreDisabledBu
// to effect a step-holds situation // to effect a step-holds situation
{ {
rep := replicationInvocation{ rep := replicationInvocation{
sjid: sjid, sjid: sjid,
rjid: rjid, rjid: rjid,
sfs: sfs, sfs: sfs,
rfsRoot: rfsRoot, rfsRoot: rfsRoot,
disableIncrementalStepHolds: false, // ! guarantee: *pdu.ReplicationConfigProtectionWithKind(pdu.ReplicationGuaranteeKind_GuaranteeResumability), // !
interceptSender: func(e *endpoint.Sender) logic.Sender { interceptSender: func(e *endpoint.Sender) logic.Sender {
return &PartialSender{Sender: e, failAfterByteCount: 1 << 20} return &PartialSender{Sender: e, failAfterByteCount: 1 << 20}
}, },
@@ -495,17 +564,17 @@ func ReplicationIncrementalDestroysStepHoldsIffIncrementalStepHoldsAreDisabledBu
// end of test setup // end of test setup
// //
// retry replication with incremental step holds disabled // retry replication with incremental step holds disabled (set to bookmarks-only in this case)
// - replication should not fail due to holds-related stuff // - replication should not fail due to holds-related stuff
// - replication should fail intermittently due to partial sender being fully read // - replication should fail intermittently due to partial sender being fully read
// - the partial sender is 1/4th the length of the stream, thus expect // - the partial sender is 1/4th the length of the stream, thus expect
// successful replication after 5 more attempts // successful replication after 5 more attempts
rep := replicationInvocation{ rep := replicationInvocation{
sjid: sjid, sjid: sjid,
rjid: rjid, rjid: rjid,
sfs: sfs, sfs: sfs,
rfsRoot: rfsRoot, rfsRoot: rfsRoot,
disableIncrementalStepHolds: true, // ! guarantee: *pdu.ReplicationConfigProtectionWithKind(pdu.ReplicationGuaranteeKind_GuaranteeIncrementalReplication), // !
interceptSender: func(e *endpoint.Sender) logic.Sender { interceptSender: func(e *endpoint.Sender) logic.Sender {
return &PartialSender{Sender: e, failAfterByteCount: 1 << 20} return &PartialSender{Sender: e, failAfterByteCount: 1 << 20}
}, },
@@ -551,3 +620,298 @@ func ReplicationIncrementalDestroysStepHoldsIffIncrementalStepHoldsAreDisabledBu
require.Len(ctx, abs, 1) require.Len(ctx, abs, 1)
require.True(ctx, zfs.FilesystemVersionEqualIdentity(abs[0].GetFilesystemVersion(), snap2sfs)) require.True(ctx, zfs.FilesystemVersionEqualIdentity(abs[0].GetFilesystemVersion(), snap2sfs))
} }
func ReplicationStepCompletedLostBehavior__GuaranteeResumability(ctx *platformtest.Context) {
scenario := replicationStepCompletedLostBehavior_impl(ctx, pdu.ReplicationGuaranteeKind_GuaranteeResumability)
require.Error(ctx, scenario.deleteSfs1Err, "protected by holds")
require.Contains(ctx, scenario.deleteSfs1Err.Error(), "dataset is busy")
require.Error(ctx, scenario.deleteSfs2Err, "protected by holds")
require.Contains(ctx, scenario.deleteSfs2Err.Error(), "dataset is busy")
require.Nil(ctx, scenario.finalReport.Error())
_ = fsversion(ctx, scenario.rfs, "@3") // @3 ade it to the other side
}
func ReplicationStepCompletedLostBehavior__GuaranteeIncrementalReplication(ctx *platformtest.Context) {
scenario := replicationStepCompletedLostBehavior_impl(ctx, pdu.ReplicationGuaranteeKind_GuaranteeIncrementalReplication)
require.NoError(ctx, scenario.deleteSfs1Err, "not protected by holds")
require.NoError(ctx, scenario.deleteSfs2Err, "not protected by holds")
// step bookmarks should protect against loss of StepCompleted message
require.Nil(ctx, scenario.finalReport.Error())
_ = fsversion(ctx, scenario.rfs, "@3") // @3 ade it to the other side
}
type FailSendCompletedSender struct {
*endpoint.Sender
}
var _ logic.Sender = (*FailSendCompletedSender)(nil)
func (p *FailSendCompletedSender) SendCompleted(ctx context.Context, r *pdu.SendCompletedReq) (*pdu.SendCompletedRes, error) {
return nil, fmt.Errorf("[mock] SendCompleted not delivered to actual endpoint")
}
type replicationStepCompletedLost_scenario struct {
rfs string
deleteSfs1Err, deleteSfs2Err error
finalReport *report.FilesystemReport
}
func replicationStepCompletedLostBehavior_impl(ctx *platformtest.Context, guaranteeKind pdu.ReplicationGuaranteeKind) *replicationStepCompletedLost_scenario {
platformtest.Run(ctx, platformtest.PanicErr, ctx.RootDataset, `
CREATEROOT
+ "sender"
+ "receiver"
R zfs create -p "${ROOTDS}/receiver/${ROOTDS}"
`)
sjid := endpoint.MustMakeJobID("sender-job")
rjid := endpoint.MustMakeJobID("receiver-job")
sfs := ctx.RootDataset + "/sender"
rfsRoot := ctx.RootDataset + "/receiver"
// fully replicate snapshots @1
{
mustSnapshot(ctx, sfs+"@1")
rep := replicationInvocation{
sjid: sjid,
rjid: rjid,
sfs: sfs,
rfsRoot: rfsRoot,
guarantee: *pdu.ReplicationConfigProtectionWithKind(guaranteeKind),
}
rfs := rep.ReceiveSideFilesystem()
report := rep.Do(ctx)
ctx.Logf("\n%s", pretty.Sprint(report))
// assert this worked (not the main subject of the test)
_ = fsversion(ctx, rfs, "@1")
}
// create a second snapshot @2
mustSnapshot(ctx, sfs+"@2")
// fake loss of stepcompleted message
rep := replicationInvocation{
sjid: sjid,
rjid: rjid,
sfs: sfs,
rfsRoot: rfsRoot,
guarantee: *pdu.ReplicationConfigProtectionWithKind(guaranteeKind),
interceptSender: func(e *endpoint.Sender) logic.Sender {
return &FailSendCompletedSender{e}
},
}
rfs := rep.ReceiveSideFilesystem()
report := rep.Do(ctx)
ctx.Logf("\n%s", pretty.Sprint(report))
// assert the replication worked
_ = fsversion(ctx, rfs, "@2")
// and that we hold it using a last-received-hold
abs, absErrs, err := endpoint.ListAbstractions(ctx, endpoint.ListZFSHoldsAndBookmarksQuery{
FS: endpoint.ListZFSHoldsAndBookmarksQueryFilesystemFilter{
FS: &rfs,
},
Concurrency: 1,
JobID: &rjid,
What: endpoint.AbstractionTypeSet{endpoint.AbstractionLastReceivedHold: true},
})
require.NoError(ctx, err)
require.Empty(ctx, absErrs)
require.Len(ctx, abs, 1)
require.True(ctx, zfs.FilesystemVersionEqualIdentity(abs[0].GetFilesystemVersion(), fsversion(ctx, rfs, "@2")))
// now try to delete @2 on the sender, this should work because don't have step holds on it
deleteSfs2Err := zfs.ZFSDestroy(ctx, sfs+"@2")
// defer check to caller
// and create a new snapshot on the sender
mustSnapshot(ctx, sfs+"@3")
// now we have: sender @1, @3
// recver @1, @2
// delete @1 on both sides to demonstrate that, if we didn't have bookmarks, we would be out of sync
deleteSfs1Err := zfs.ZFSDestroy(ctx, sfs+"@1")
// defer check to caller
err = zfs.ZFSDestroy(ctx, rfs+"@1")
require.NoError(ctx, err)
// attempt replication and return the filesystem report report
{
rep := replicationInvocation{
sjid: sjid,
rjid: rjid,
sfs: sfs,
rfsRoot: rfsRoot,
guarantee: *pdu.ReplicationConfigProtectionWithKind(guaranteeKind),
}
report := rep.Do(ctx)
ctx.Logf("expecting failure:\n%s", pretty.Sprint(report))
require.Len(ctx, report.Attempts, 1)
require.Len(ctx, report.Attempts[0].Filesystems, 1)
return &replicationStepCompletedLost_scenario{
rfs: rfs,
deleteSfs1Err: deleteSfs1Err,
deleteSfs2Err: deleteSfs2Err,
finalReport: report.Attempts[0].Filesystems[0],
}
}
}
type ErroringReceiver struct {
recvErr error
*endpoint.Receiver
}
func (r *ErroringReceiver) Receive(ctx context.Context, req *pdu.ReceiveReq, stream io.ReadCloser) (*pdu.ReceiveRes, error) {
return nil, r.recvErr
}
type NeverEndingSender struct {
*endpoint.Sender
}
func (s *NeverEndingSender) Send(ctx context.Context, req *pdu.SendReq) (r *pdu.SendRes, stream io.ReadCloser, _ error) {
stream = nil
r = &pdu.SendRes{
UsedResumeToken: false,
ExpectedSize: 1 << 30,
}
if req.DryRun {
return r, stream, nil
}
dz, err := os.Open("/dev/zero")
if err != nil {
panic(err)
}
return r, dz, nil
}
func ReplicationReceiverErrorWhileStillSending(ctx *platformtest.Context) {
platformtest.Run(ctx, platformtest.PanicErr, ctx.RootDataset, `
CREATEROOT
+ "sender"
+ "sender@1"
+ "receiver"
R zfs create -p "${ROOTDS}/receiver/${ROOTDS}"
`)
sjid := endpoint.MustMakeJobID("sender-job")
rjid := endpoint.MustMakeJobID("receiver-job")
sfs := ctx.RootDataset + "/sender"
rfsRoot := ctx.RootDataset + "/receiver"
mockRecvErr := fmt.Errorf("YiezahK3thie8ahKiel5sah2uugei2ize1yi8feivuu7musoat")
rep := replicationInvocation{
sjid: sjid,
rjid: rjid,
sfs: sfs,
rfsRoot: rfsRoot,
guarantee: *pdu.ReplicationConfigProtectionWithKind(pdu.ReplicationGuaranteeKind_GuaranteeNothing),
interceptReceiver: func(r *endpoint.Receiver) logic.Receiver {
return &ErroringReceiver{recvErr: mockRecvErr, Receiver: r}
},
interceptSender: func(s *endpoint.Sender) logic.Sender {
return &NeverEndingSender{s}
},
}
// first replication
report := rep.Do(ctx)
ctx.Logf("\n%s", pretty.Sprint(report))
require.Len(ctx, report.Attempts, 1)
attempt := report.Attempts[0]
require.Nil(ctx, attempt.PlanError)
require.Len(ctx, attempt.Filesystems, 1)
afs := attempt.Filesystems[0]
require.Nil(ctx, afs.PlanError)
require.Len(ctx, afs.Steps, 1)
require.Nil(ctx, afs.PlanError)
require.NotNil(ctx, afs.StepError)
require.Contains(ctx, afs.StepError.Err, mockRecvErr.Error())
}
func ReplicationFailingInitialParentProhibitsChildReplication(ctx *platformtest.Context) {
platformtest.Run(ctx, platformtest.PanicErr, ctx.RootDataset, `
CREATEROOT
+ "sender"
+ "sender/a"
+ "sender/a/child"
+ "sender/aa"
+ "receiver"
R zfs create -p "${ROOTDS}/receiver/${ROOTDS}"
R zfs snapshot -r ${ROOTDS}/sender@initial
`)
sjid := endpoint.MustMakeJobID("sender-job")
rjid := endpoint.MustMakeJobID("receiver-job")
fsA := ctx.RootDataset + "/sender/a"
fsAChild := ctx.RootDataset + "/sender/a/child"
fsAA := ctx.RootDataset + "/sender/aa"
sfilter := filters.NewDatasetMapFilter(3, true)
mustAddToSFilter := func(fs string) {
err := sfilter.Add(fs, "ok")
require.NoError(ctx, err)
}
mustAddToSFilter(fsA)
mustAddToSFilter(fsAChild)
mustAddToSFilter(fsAA)
rfsRoot := ctx.RootDataset + "/receiver"
mockRecvErr := fmt.Errorf("yifae4ohPhaquaes0hohghiep9oufie4roo7quoWooluaj2ee8")
rep := replicationInvocation{
sjid: sjid,
rjid: rjid,
sfilter: sfilter,
rfsRoot: rfsRoot,
guarantee: *pdu.ReplicationConfigProtectionWithKind(pdu.ReplicationGuaranteeKind_GuaranteeNothing),
interceptReceiver: func(r *endpoint.Receiver) logic.Receiver {
return &ErroringReceiver{recvErr: mockRecvErr, Receiver: r}
},
}
r := rep.Do(ctx)
ctx.Logf("\n%s", pretty.Sprint(r))
require.Len(ctx, r.Attempts, 1)
attempt := r.Attempts[0]
require.Nil(ctx, attempt.PlanError)
require.Len(ctx, attempt.Filesystems, 3)
fsByName := make(map[string]*report.FilesystemReport, len(attempt.Filesystems))
for _, fs := range attempt.Filesystems {
fsByName[fs.Info.Name] = fs
}
require.Contains(ctx, fsByName, fsA)
require.Contains(ctx, fsByName, fsAA)
require.Contains(ctx, fsByName, fsAA)
checkFS := func(fs string, expectErrMsg string) {
rep := fsByName[fs]
require.Len(ctx, rep.Steps, 1)
require.Nil(ctx, rep.PlanError)
require.NotNil(ctx, rep.StepError)
require.Contains(ctx, rep.StepError.Err, expectErrMsg)
}
checkFS(fsA, mockRecvErr.Error())
checkFS(fsAChild, "parent(s) failed during initial replication")
checkFS(fsAA, mockRecvErr.Error()) // fsAA is not treated as a child of fsA
}
+6 -1
View File
@@ -22,6 +22,7 @@ func CreateReplicationCursor(ctx *platformtest.Context) {
R zfs bookmark "${ROOTDS}/foo bar@2 with space" "${ROOTDS}/foo bar#2 with space" R zfs bookmark "${ROOTDS}/foo bar@2 with space" "${ROOTDS}/foo bar#2 with space"
+ "foo bar@3 with space" + "foo bar@3 with space"
R zfs bookmark "${ROOTDS}/foo bar@3 with space" "${ROOTDS}/foo bar#3 with space" R zfs bookmark "${ROOTDS}/foo bar@3 with space" "${ROOTDS}/foo bar#3 with space"
- "foo bar@3 with space"
`) `)
jobid := endpoint.MustMakeJobID("zreplplatformtest") jobid := endpoint.MustMakeJobID("zreplplatformtest")
@@ -42,6 +43,7 @@ func CreateReplicationCursor(ctx *platformtest.Context) {
snap := fsversion(ctx, fs, "@1 with space") snap := fsversion(ctx, fs, "@1 with space")
book := fsversion(ctx, fs, "#1 with space") book := fsversion(ctx, fs, "#1 with space")
book3 := fsversion(ctx, fs, "#3 with space")
// create first cursor // create first cursor
cursorOfSnap, err := endpoint.CreateReplicationCursor(ctx, fs, snap, jobid) cursorOfSnap, err := endpoint.CreateReplicationCursor(ctx, fs, snap, jobid)
@@ -49,8 +51,11 @@ func CreateReplicationCursor(ctx *platformtest.Context) {
// check CreateReplicationCursor is idempotent (for snapshot target) // check CreateReplicationCursor is idempotent (for snapshot target)
cursorOfSnapIdemp, err := endpoint.CreateReplicationCursor(ctx, fs, snap, jobid) cursorOfSnapIdemp, err := endpoint.CreateReplicationCursor(ctx, fs, snap, jobid)
checkCreateCursor(err, cursorOfSnap, snap) checkCreateCursor(err, cursorOfSnap, snap)
// check CreateReplicationCursor is idempotent (for bookmark target of snapshot)
cursorOfBook, err := endpoint.CreateReplicationCursor(ctx, fs, book, jobid)
checkCreateCursor(err, cursorOfBook, snap)
// ... for target = non-cursor bookmark // ... for target = non-cursor bookmark
_, err = endpoint.CreateReplicationCursor(ctx, fs, book, jobid) _, err = endpoint.CreateReplicationCursor(ctx, fs, book3, jobid)
assert.Equal(ctx, zfs.ErrBookmarkCloningNotSupported, err) assert.Equal(ctx, zfs.ErrBookmarkCloningNotSupported, err)
// ... for target = replication cursor bookmark to be created // ... for target = replication cursor bookmark to be created
cursorOfCursor, err := endpoint.CreateReplicationCursor(ctx, fs, cursorOfSnapIdemp.GetFilesystemVersion(), jobid) cursorOfCursor, err := endpoint.CreateReplicationCursor(ctx, fs, cursorOfSnapIdemp.GetFilesystemVersion(), jobid)
+18 -11
View File
@@ -9,11 +9,22 @@ import (
"github.com/zrepl/zrepl/zfs" "github.com/zrepl/zrepl/zfs"
) )
func SendArgsValidationEncryptedSendOfUnencryptedDatasetForbidden(ctx *platformtest.Context) { func SendArgsValidationEncryptedSendOfUnencryptedDatasetForbidden__EncryptionSupported_true(ctx *platformtest.Context) {
sendArgsValidationEncryptedSendOfUnencryptedDatasetForbidden_impl(ctx, true)
}
func SendArgsValidationEncryptedSendOfUnencryptedDatasetForbidden__EncryptionSupported_false(ctx *platformtest.Context) {
sendArgsValidationEncryptedSendOfUnencryptedDatasetForbidden_impl(ctx, false)
}
func sendArgsValidationEncryptedSendOfUnencryptedDatasetForbidden_impl(ctx *platformtest.Context, testForEncryptionSupported bool) {
supported, err := zfs.EncryptionCLISupported(ctx) supported, err := zfs.EncryptionCLISupported(ctx)
check(err) check(err)
expectNotSupportedErr := !supported if supported != testForEncryptionSupported {
ctx.SkipNow()
}
noEncryptionCLISupport := !supported
platformtest.Run(ctx, platformtest.PanicErr, ctx.RootDataset, ` platformtest.Run(ctx, platformtest.PanicErr, ctx.RootDataset, `
DESTROYROOT DESTROYROOT
@@ -44,9 +55,11 @@ func SendArgsValidationEncryptedSendOfUnencryptedDatasetForbidden(ctx *platformt
// fallthrough // fallthrough
} }
if expectNotSupportedErr { if noEncryptionCLISupport {
require.Error(ctx, err) require.Error(ctx, err)
require.Equal(ctx, zfs.ErrEncryptedSendNotSupported, err) saverr, ok := err.(*zfs.ZFSSendArgsValidationError)
require.True(ctx, ok, "%T", err)
require.Equal(ctx, zfs.ZFSSendArgsEncryptedSendRequestedButFSUnencrypted, saverr.What)
return return
} }
require.Error(ctx, err) require.Error(ctx, err)
@@ -138,13 +151,7 @@ func SendArgsValidationResumeTokenEncryptionMismatchForbidden(ctx *platformtest.
} }
func SendArgsValidationResumeTokenDifferentFilesystemForbidden(ctx *platformtest.Context) { func SendArgsValidationResumeTokenDifferentFilesystemForbidden(ctx *platformtest.Context) {
supported, err := zfs.ResumeSendSupported(ctx)
supported, err := zfs.EncryptionCLISupported(ctx)
check(err)
if !supported {
ctx.SkipNow()
}
supported, err = zfs.ResumeSendSupported(ctx)
check(err) check(err)
if !supported { if !supported {
ctx.SkipNow() ctx.SkipNow()
+53 -41
View File
@@ -3,7 +3,6 @@ package pruning
import ( import (
"fmt" "fmt"
"regexp" "regexp"
"sort"
"time" "time"
"github.com/pkg/errors" "github.com/pkg/errors"
@@ -30,75 +29,88 @@ func NewKeepGrid(in *config.PruneGrid) (p *KeepGrid, err error) {
return nil, errors.Wrap(err, "Regex is invalid") return nil, errors.Wrap(err, "Regex is invalid")
} }
return newKeepGrid(re, in.Grid)
}
func MustNewKeepGrid(regex, gridspec string) *KeepGrid {
ris, err := config.ParseRetentionIntervalSpec(gridspec)
if err != nil {
panic(err)
}
re := regexp.MustCompile(regex)
grid, err := newKeepGrid(re, ris)
if err != nil {
panic(err)
}
return grid
}
func newKeepGrid(re *regexp.Regexp, configIntervals []config.RetentionInterval) (*KeepGrid, error) {
if re == nil {
panic("re must not be nil")
}
if len(configIntervals) == 0 {
return nil, errors.New("retention grid must specify at least one interval")
}
intervals := make([]retentiongrid.Interval, len(configIntervals))
for i := range configIntervals {
intervals[i] = &configIntervals[i]
}
// Assert intervals are of increasing length (not necessarily required, but indicates config mistake) // Assert intervals are of increasing length (not necessarily required, but indicates config mistake)
lastDuration := time.Duration(0) lastDuration := time.Duration(0)
for i := range in.Grid { for i := range intervals {
if in.Grid[i].Length() < lastDuration { if intervals[i].Length() < lastDuration {
// If all intervals before were keep=all, this is ok // If all intervals before were keep=all, this is ok
allPrevKeepCountAll := true allPrevKeepCountAll := true
for j := i - 1; allPrevKeepCountAll && j >= 0; j-- { for j := i - 1; allPrevKeepCountAll && j >= 0; j-- {
allPrevKeepCountAll = in.Grid[j].KeepCount() == config.RetentionGridKeepCountAll allPrevKeepCountAll = intervals[j].KeepCount() == config.RetentionGridKeepCountAll
} }
if allPrevKeepCountAll { if allPrevKeepCountAll {
goto isMonotonicIncrease goto isMonotonicIncrease
} }
err = errors.New("retention grid interval length must be monotonically increasing") return nil, errors.New("retention grid interval length must be monotonically increasing")
return
} }
isMonotonicIncrease: isMonotonicIncrease:
lastDuration = in.Grid[i].Length() lastDuration = intervals[i].Length()
}
retentionIntervals := make([]retentiongrid.Interval, len(in.Grid))
for i := range in.Grid {
retentionIntervals[i] = &in.Grid[i]
} }
return &KeepGrid{ return &KeepGrid{
retentiongrid.NewGrid(retentionIntervals), retentionGrid: retentiongrid.NewGrid(intervals),
re, re: re,
}, nil }, nil
} }
type retentionGridAdaptor struct {
Snapshot
}
func (a retentionGridAdaptor) LessThan(b retentiongrid.Entry) bool {
return a.Date().Before(b.Date())
}
// Prune filters snapshots with the retention grid. // Prune filters snapshots with the retention grid.
func (p *KeepGrid) KeepRule(snaps []Snapshot) (destroyList []Snapshot) { func (p *KeepGrid) KeepRule(snaps []Snapshot) (destroyList []Snapshot) {
snaps = filterSnapList(snaps, func(snapshot Snapshot) bool { matching, notMatching := partitionSnapList(snaps, func(snapshot Snapshot) bool {
return p.re.MatchString(snapshot.Name()) return p.re.MatchString(snapshot.Name())
}) })
if len(snaps) == 0 {
return nil
}
// Build adaptors for retention grid // snaps that don't match the regex are not kept by this rule
adaptors := make([]retentiongrid.Entry, 0) destroyList = append(destroyList, notMatching...)
for i := range snaps {
adaptors = append(adaptors, retentionGridAdaptor{snaps[i]})
}
// determine 'now' edge if len(matching) == 0 {
sort.SliceStable(adaptors, func(i, j int) bool { return destroyList
return adaptors[i].LessThan(adaptors[j]) }
})
now := adaptors[len(adaptors)-1].Date()
// Evaluate retention grid // Evaluate retention grid
_, removea := p.retentionGrid.FitEntries(now, adaptors) entrySlice := make([]retentiongrid.Entry, 0)
for i := range matching {
entrySlice = append(entrySlice, matching[i])
}
_, gridDestroyList := p.retentionGrid.FitEntries(entrySlice)
// Revert adaptors // Revert adaptors
destroyList = make([]Snapshot, len(removea)) for i := range gridDestroyList {
for i := range removea { destroyList = append(destroyList, gridDestroyList[i].(Snapshot))
destroyList[i] = removea[i].(retentionGridAdaptor).Snapshot
} }
return destroyList return destroyList
} }
+11
View File
@@ -10,6 +10,17 @@ func filterSnapList(snaps []Snapshot, predicate func(Snapshot) bool) []Snapshot
return r return r
} }
func partitionSnapList(snaps []Snapshot, predicate func(Snapshot) bool) (sTrue, sFalse []Snapshot) {
for i := range snaps {
if predicate(snaps[i]) {
sTrue = append(sTrue, snaps[i])
} else {
sFalse = append(sFalse, snaps[i])
}
}
return
}
func shallowCopySnapList(snaps []Snapshot) []Snapshot { func shallowCopySnapList(snaps []Snapshot) []Snapshot {
c := make([]Snapshot, len(snaps)) c := make([]Snapshot, len(snaps))
copy(c, snaps) copy(c, snaps)
+37 -8
View File
@@ -1,20 +1,35 @@
package pruning package pruning
import ( import (
"regexp"
"sort" "sort"
"strings"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
type KeepLastN struct { type KeepLastN struct {
n int n int
re *regexp.Regexp
} }
func NewKeepLastN(n int) (*KeepLastN, error) { func MustKeepLastN(n int, regex string) *KeepLastN {
k, err := NewKeepLastN(n, regex)
if err != nil {
panic(err)
}
return k
}
func NewKeepLastN(n int, regex string) (*KeepLastN, error) {
if n <= 0 { if n <= 0 {
return nil, errors.Errorf("must specify positive number as 'keep last count', got %d", n) return nil, errors.Errorf("must specify positive number as 'keep last count', got %d", n)
} }
return &KeepLastN{n}, nil re, err := regexp.Compile(regex)
if err != nil {
return nil, errors.Errorf("invalid regex %q: %s", regex, err)
}
return &KeepLastN{n, re}, nil
} }
func (k KeepLastN) KeepRule(snaps []Snapshot) (destroyList []Snapshot) { func (k KeepLastN) KeepRule(snaps []Snapshot) (destroyList []Snapshot) {
@@ -23,11 +38,25 @@ func (k KeepLastN) KeepRule(snaps []Snapshot) (destroyList []Snapshot) {
return []Snapshot{} return []Snapshot{}
} }
res := shallowCopySnapList(snaps) matching, notMatching := partitionSnapList(snaps, func(snapshot Snapshot) bool {
return k.re.MatchString(snapshot.Name())
sort.Slice(res, func(i, j int) bool {
return res[i].Date().After(res[j].Date())
}) })
// snaps that don't match the regex are not kept by this rule
destroyList = append(destroyList, notMatching...)
return res[k.n:] if len(matching) == 0 {
return destroyList
}
sort.Slice(matching, func(i, j int) bool {
// by date (youngest first)
id, jd := matching[i].Date(), matching[j].Date()
if !id.Equal(jd) {
return id.After(jd)
}
// then lexicographically descending (e.g. b, a)
return strings.Compare(matching[i].Name(), matching[j].Name()) == 1
})
destroyList = append(destroyList, matching[k.n:]...)
return destroyList
} }
+41 -11
View File
@@ -5,6 +5,7 @@ import (
"time" "time"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestKeepLastN(t *testing.T) { func TestKeepLastN(t *testing.T) {
@@ -28,7 +29,7 @@ func TestKeepLastN(t *testing.T) {
"keep2": { "keep2": {
inputs: inputs["s1"], inputs: inputs["s1"],
rules: []KeepRule{ rules: []KeepRule{
KeepLastN{2}, MustKeepLastN(2, ""),
}, },
expDestroy: map[string]bool{ expDestroy: map[string]bool{
"1": true, "2": true, "3": true, "1": true, "2": true, "3": true,
@@ -37,37 +38,66 @@ func TestKeepLastN(t *testing.T) {
"keep1OfTwoWithSameTime": { // Keep one of two with same time "keep1OfTwoWithSameTime": { // Keep one of two with same time
inputs: inputs["s1"], inputs: inputs["s1"],
rules: []KeepRule{ rules: []KeepRule{
KeepLastN{1}, MustKeepLastN(1, ""),
},
expDestroyAlternatives: []map[string]bool{
{"1": true, "2": true, "3": true, "4": true},
{"1": true, "2": true, "3": true, "5": true},
}, },
expDestroy: map[string]bool{"1": true, "2": true, "3": true, "4": true},
}, },
"keepMany": { "keepMany": {
inputs: inputs["s1"], inputs: inputs["s1"],
rules: []KeepRule{ rules: []KeepRule{
KeepLastN{100}, MustKeepLastN(100, ""),
}, },
expDestroy: map[string]bool{}, expDestroy: map[string]bool{},
}, },
"empty": { "empty_input": {
inputs: inputs["s2"], inputs: inputs["s2"],
rules: []KeepRule{ rules: []KeepRule{
KeepLastN{100}, MustKeepLastN(100, ""),
}, },
expDestroy: map[string]bool{}, expDestroy: map[string]bool{},
}, },
"empty_regex": {
inputs: inputs["s1"],
rules: []KeepRule{
MustKeepLastN(4, ""),
},
expDestroy: map[string]bool{
"1": true,
},
},
"multiple_regexes": {
inputs: []Snapshot{
stubSnap{"a1", false, o(10)},
stubSnap{"b1", false, o(11)},
stubSnap{"a2", false, o(20)},
stubSnap{"b2", false, o(21)},
stubSnap{"a3", false, o(30)},
stubSnap{"b3", false, o(31)},
},
rules: []KeepRule{
MustKeepLastN(2, "^a"),
MustKeepLastN(2, "^b"),
},
expDestroy: map[string]bool{
"a1": true,
"b1": true,
},
},
} }
testTable(tcs, t) testTable(tcs, t)
t.Run("mustBePositive", func(t *testing.T) { t.Run("mustBePositive", func(t *testing.T) {
var err error var err error
_, err = NewKeepLastN(0) _, err = NewKeepLastN(0, "foo")
assert.Error(t, err) assert.Error(t, err)
_, err = NewKeepLastN(-5) _, err = NewKeepLastN(-5, "foo")
assert.Error(t, err) assert.Error(t, err)
}) })
t.Run("emptyRegexAllowed", func(t *testing.T) {
_, err := NewKeepLastN(23, "")
require.NoError(t, err)
})
} }
+1 -1
View File
@@ -60,7 +60,7 @@ func RuleFromConfig(in config.PruningEnum) (KeepRule, error) {
case *config.PruneKeepNotReplicated: case *config.PruneKeepNotReplicated:
return NewKeepNotReplicated(), nil return NewKeepNotReplicated(), nil
case *config.PruneKeepLastN: case *config.PruneKeepLastN:
return NewKeepLastN(v.Count) return NewKeepLastN(v.Count, v.Regex)
case *config.PruneKeepRegex: case *config.PruneKeepRegex:
return NewKeepRegex(v.Regex, v.Negate) return NewKeepRegex(v.Regex, v.Negate)
case *config.PruneGrid: case *config.PruneGrid:
+38 -35
View File
@@ -3,6 +3,9 @@ package pruning
import ( import (
"testing" "testing"
"time" "time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
type stubSnap struct { type stubSnap struct {
@@ -18,10 +21,9 @@ func (s stubSnap) Replicated() bool { return s.replicated }
func (s stubSnap) Date() time.Time { return s.date } func (s stubSnap) Date() time.Time { return s.date }
type testCase struct { type testCase struct {
inputs []Snapshot inputs []Snapshot
rules []KeepRule rules []KeepRule
expDestroy, effDestroy map[string]bool expDestroy map[string]bool
expDestroyAlternatives []map[string]bool
} }
type snapshotList []Snapshot type snapshotList []Snapshot
@@ -44,41 +46,20 @@ func (l snapshotList) NameList() []string {
} }
func testTable(tcs map[string]testCase, t *testing.T) { func testTable(tcs map[string]testCase, t *testing.T) {
mapEqual := func(a, b map[string]bool) bool {
if len(a) != len(b) {
return false
}
for k, v := range a {
if w, ok := b[k]; !ok || v != w {
return false
}
}
return true
}
for name := range tcs { for name := range tcs {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
tc := tcs[name] tc := tcs[name]
remove := PruneSnapshots(tc.inputs, tc.rules) destroyList := PruneSnapshots(tc.inputs, tc.rules)
tc.effDestroy = make(map[string]bool) destroySet := make(map[string]bool, len(destroyList))
for _, s := range remove { for _, s := range destroyList {
tc.effDestroy[s.Name()] = true destroySet[s.Name()] = true
} }
if tc.expDestroyAlternatives == nil { t.Logf("destroySet:\n%#v", destroySet)
if tc.expDestroy == nil { t.Logf("expected:\n%#v", tc.expDestroy)
panic("must specify either expDestroyAlternatives or expDestroy")
} require.Equal(t, len(tc.expDestroy), len(destroySet))
tc.expDestroyAlternatives = []map[string]bool{tc.expDestroy} for name := range destroySet {
} assert.True(t, tc.expDestroy[name], "%q", name)
var okAlt map[string]bool = nil
for _, alt := range tc.expDestroyAlternatives {
t.Logf("testing possible result: %v", alt)
if mapEqual(alt, tc.effDestroy) {
okAlt = alt
}
}
if okAlt == nil {
t.Errorf("no alternatives matched result: %v", tc.effDestroy)
} }
}) })
} }
@@ -94,6 +75,10 @@ func TestPruneSnapshots(t *testing.T) {
}, },
} }
reltime := func(secs int64) time.Time {
return time.Unix(secs, 0)
}
tcs := map[string]testCase{ tcs := map[string]testCase{
"simple": { "simple": {
inputs: inputs["s1"], inputs: inputs["s1"],
@@ -140,6 +125,24 @@ func TestPruneSnapshots(t *testing.T) {
}, },
expDestroy: map[string]bool{}, expDestroy: map[string]bool{},
}, },
"multiple_grids_with_disjoint_regexes": {
inputs: []Snapshot{
stubSnap{"p1_a", false, reltime(4)},
stubSnap{"p2_a", false, reltime(5)},
stubSnap{"p1_b", false, reltime(14)},
stubSnap{"p2_b", false, reltime(15)},
stubSnap{"p1_c", false, reltime(29)},
stubSnap{"p2_c", false, reltime(30)},
},
rules: []KeepRule{
MustNewKeepGrid("^p1_", `1x10s | 1x10s`),
MustNewKeepGrid("^p2_", `1x10s | 1x10s`),
},
expDestroy: map[string]bool{
"p1_a": true,
"p2_a": true,
},
},
} }
testTable(tcs, t) testTable(tcs, t)
+96 -54
View File
@@ -16,85 +16,127 @@ type Grid struct {
intervals []Interval intervals []Interval
} }
//A point inside the grid, i.e. a thing the grid can decide to remove
type Entry interface { type Entry interface {
Date() time.Time Date() time.Time
LessThan(b Entry) bool
}
func dateInInterval(date, startDateInterval time.Time, i Interval) bool {
return date.After(startDateInterval) && date.Before(startDateInterval.Add(i.Length()))
} }
func NewGrid(l []Interval) *Grid { func NewGrid(l []Interval) *Grid {
if len(l) == 0 {
panic("must specify at least one interval")
}
// TODO Maybe check for ascending interval lengths here, although the algorithm // TODO Maybe check for ascending interval lengths here, although the algorithm
// itself doesn't care about that. // itself doesn't care about that.
return &Grid{l} return &Grid{l}
} }
// Partition a list of RetentionGridEntries into the Grid, func (g Grid) FitEntries(entries []Entry) (keep, remove []Entry) {
// relative to a given start date `now`.
//
// The `keepCount` oldest entries per `retentiongrid.Interval` are kept (`keep`),
// the others are removed (`remove`).
//
// Entries that are younger than `now` are always kept.
// Those that are older than the earliest beginning of an interval are removed.
func (g Grid) FitEntries(now time.Time, entries []Entry) (keep, remove []Entry) {
type bucket struct { if len(entries) == 0 {
entries []Entry return
} }
// determine 'now' based on youngest snapshot
// => sort youngest-to-oldest
sort.SliceStable(entries, func(i, j int) bool {
return entries[i].Date().After(entries[j].Date())
})
now := entries[0].Date()
return g.fitEntriesWithNow(now, entries)
}
type bucket struct {
keepCount int
youngerThan time.Time
olderThanOrEq time.Time
entries []Entry
}
func makeBucketFromInterval(olderThanOrEq time.Time, i Interval) bucket {
var b bucket
kc := i.KeepCount()
if kc == 0 {
panic("keep count 0 is not allowed")
}
if (kc < 0) && kc != RetentionGridKeepCountAll {
panic("negative keep counts are not allowed")
}
b.keepCount = kc
b.olderThanOrEq = olderThanOrEq
b.youngerThan = b.olderThanOrEq.Add(-i.Length())
return b
}
func (b *bucket) Contains(e Entry) bool {
d := e.Date()
olderThan := d.Before(b.olderThanOrEq)
eq := d.Equal(b.olderThanOrEq)
youngerThan := d.After(b.youngerThan)
return (olderThan || eq) && youngerThan
}
func (b *bucket) AddIfContains(e Entry) (added bool) {
added = b.Contains(e)
if added {
b.entries = append(b.entries, e)
}
return
}
func (b *bucket) RemoveYoungerSnapsExceedingKeepCount() (removed []Entry) {
if b.keepCount == RetentionGridKeepCountAll {
return nil
}
removeCount := len(b.entries) - b.keepCount
if removeCount <= 0 {
return nil
}
// sort youngest-to-oldest
sort.SliceStable(b.entries, func(i, j int) bool {
return b.entries[i].Date().After(b.entries[j].Date())
})
return b.entries[:removeCount]
}
func (g Grid) fitEntriesWithNow(now time.Time, entries []Entry) (keep, remove []Entry) {
buckets := make([]bucket, len(g.intervals)) buckets := make([]bucket, len(g.intervals))
buckets[0] = makeBucketFromInterval(now, g.intervals[0])
for i := 1; i < len(g.intervals); i++ {
buckets[i] = makeBucketFromInterval(buckets[i-1].youngerThan, g.intervals[i])
}
keep = make([]Entry, 0) keep = make([]Entry, 0)
remove = make([]Entry, 0) remove = make([]Entry, 0)
oldestIntervalStart := now assignEntriesToBuckets:
for i := range g.intervals {
oldestIntervalStart = oldestIntervalStart.Add(-g.intervals[i].Length())
}
for ei := 0; ei < len(entries); ei++ { for ei := 0; ei < len(entries); ei++ {
e := entries[ei] e := entries[ei]
// unconditionally keep entries that are in the future
date := e.Date() if now.Before(e.Date()) {
if date == now || date.After(now) {
keep = append(keep, e) keep = append(keep, e)
continue continue assignEntriesToBuckets
} else if date.Before(oldestIntervalStart) {
remove = append(remove, e)
continue
} }
// add to matching bucket, if any
iStartTime := now for bi := range buckets {
for i := 0; i < len(g.intervals); i++ { if buckets[bi].AddIfContains(e) {
iStartTime = iStartTime.Add(-g.intervals[i].Length()) continue assignEntriesToBuckets
if date == iStartTime || dateInInterval(date, iStartTime, g.intervals[i]) {
buckets[i].entries = append(buckets[i].entries, e)
} }
} }
// unconditionally remove entries older than the oldest bucket
remove = append(remove, e)
} }
for bi, b := range buckets { // now apply the `KeepCount` per bucket
for _, b := range buckets {
interval := g.intervals[bi] destroy := b.RemoveYoungerSnapsExceedingKeepCount()
remove = append(remove, destroy...)
sort.SliceStable(b.entries, func(i, j int) bool { keep = append(keep, b.entries...)
return b.entries[i].LessThan((b.entries[j]))
})
i := 0
for ; (interval.KeepCount() == RetentionGridKeepCountAll || i < interval.KeepCount()) && i < len(b.entries); i++ {
keep = append(keep, b.entries[i])
}
for ; i < len(b.entries); i++ {
remove = append(remove, b.entries[i])
}
} }
return return
} }
+81 -57
View File
@@ -10,25 +10,18 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
type retentionIntervalStub struct { type testInterval struct {
length time.Duration length time.Duration
keepCount int keepCount int
} }
func (i *retentionIntervalStub) Length() time.Duration { func (i *testInterval) Length() time.Duration { return i.length }
return i.length func (i *testInterval) KeepCount() int { return i.keepCount }
}
func (i *retentionIntervalStub) KeepCount() int {
return i.keepCount
}
func gridFromString(gs string) (g *Grid) { func gridFromString(gs string) (g *Grid) {
intervals := strings.Split(gs, "|") sintervals := strings.Split(gs, "|")
g = &Grid{ intervals := make([]Interval, len(sintervals))
intervals: make([]Interval, len(intervals)), for idx, i := range sintervals {
}
for idx, i := range intervals {
comps := strings.SplitN(i, ",", 2) comps := strings.SplitN(i, ",", 2)
var durationStr, numSnapsStr string var durationStr, numSnapsStr string
durationStr = comps[0] durationStr = comps[0]
@@ -39,7 +32,7 @@ func gridFromString(gs string) (g *Grid) {
} }
var err error var err error
var interval retentionIntervalStub var interval testInterval
if interval.keepCount, err = strconv.Atoi(numSnapsStr); err != nil { if interval.keepCount, err = strconv.Atoi(numSnapsStr); err != nil {
panic(err) panic(err)
@@ -48,44 +41,38 @@ func gridFromString(gs string) (g *Grid) {
panic(err) panic(err)
} }
g.intervals[idx] = &interval intervals[idx] = &interval
} }
return return NewGrid(intervals)
} }
type dummySnap struct { type testSnap struct {
Name string Name string
ShouldKeep bool ShouldKeep bool
date time.Time date time.Time
} }
func (ds dummySnap) Date() time.Time { func (ds testSnap) Date() time.Time { return ds.date }
return ds.date
}
func (ds dummySnap) LessThan(b Entry) bool {
return ds.date.Before(b.(dummySnap).date) // don't have a txg here
}
func validateRetentionGridFitEntries(t *testing.T, now time.Time, input, keep, remove []Entry) { func validateRetentionGridFitEntries(t *testing.T, now time.Time, input, keep, remove []Entry) {
snapDescr := func(d dummySnap) string { snapDescr := func(d testSnap) string {
return fmt.Sprintf("%s@%s", d.Name, d.date.Sub(now)) return fmt.Sprintf("%s@%s", d.Name, d.date.Sub(now))
} }
t.Logf("keep list:\n") t.Logf("keep list:\n")
for k := range keep { for k := range keep {
t.Logf("\t%s\n", snapDescr(keep[k].(dummySnap))) t.Logf("\t%s\n", snapDescr(keep[k].(testSnap)))
} }
t.Logf("remove list:\n") t.Logf("remove list:\n")
for k := range remove { for k := range remove {
t.Logf("\t%s\n", snapDescr(remove[k].(dummySnap))) t.Logf("\t%s\n", snapDescr(remove[k].(testSnap)))
} }
t.Logf("\n\n") t.Logf("\n\n")
for _, s := range input { for _, s := range input {
d := s.(dummySnap) d := s.(testSnap)
descr := snapDescr(d) descr := snapDescr(d)
t.Logf("testing %s\n", descr) t.Logf("testing %s\n", descr)
if d.ShouldKeep { if d.ShouldKeep {
@@ -97,21 +84,18 @@ func validateRetentionGridFitEntries(t *testing.T, now time.Time, input, keep, r
t.Logf("resulting list:\n") t.Logf("resulting list:\n")
for k := range keep { for k := range keep {
t.Logf("\t%s\n", snapDescr(keep[k].(dummySnap))) t.Logf("\t%s\n", snapDescr(keep[k].(testSnap)))
} }
} }
func TestRetentionGridFitEntriesEmptyInput(t *testing.T) { func TestEmptyInput(t *testing.T) {
g := gridFromString("10m|10m|10m|1h") g := gridFromString("10m|10m|10m|1h")
keep, remove := g.FitEntries(time.Now(), []Entry{}) keep, remove := g.FitEntries([]Entry{})
assert.Empty(t, keep) assert.Empty(t, keep)
assert.Empty(t, remove) assert.Empty(t, remove)
} }
func TestRetentionGridFitEntriesIntervalBoundariesAndAlignment(t *testing.T) { func TestIntervalBoundariesAndAlignment(t *testing.T) {
// Intervals are (duration], i.e. 10min is in the first interval, not in the second
g := gridFromString("10m|10m|10m") g := gridFromString("10m|10m|10m")
t.Logf("%#v\n", g) t.Logf("%#v\n", g)
@@ -119,20 +103,51 @@ func TestRetentionGridFitEntriesIntervalBoundariesAndAlignment(t *testing.T) {
now := time.Unix(0, 0) now := time.Unix(0, 0)
snaps := []Entry{ snaps := []Entry{
dummySnap{"0", true, now.Add(1 * time.Minute)}, // before now testSnap{"0", true, now.Add(1 * time.Minute)}, // before now => keep unconditionally
dummySnap{"1", true, now}, // before now testSnap{"1", true, now}, // 1st interval left edge => inclusive
dummySnap{"2", true, now.Add(-10 * time.Minute)}, // 1st interval testSnap{"2", true, now.Add(-10 * time.Minute)}, // 2nd interval left edge => inclusive
dummySnap{"3", true, now.Add(-20 * time.Minute)}, // 2nd interval testSnap{"3", true, now.Add(-20 * time.Minute)}, // 3rd interval left edge => inclusuive
dummySnap{"4", true, now.Add(-30 * time.Minute)}, // 3rd interval testSnap{"4", false, now.Add(-30 * time.Minute)}, // 3rd interval right edge => excludive
dummySnap{"5", false, now.Add(-40 * time.Minute)}, // after last interval testSnap{"5", false, now.Add(-40 * time.Minute)}, // after last interval => remove unconditionally
} }
keep, remove := g.FitEntries(now, snaps) keep, remove := g.fitEntriesWithNow(now, snaps)
validateRetentionGridFitEntries(t, now, snaps, keep, remove) validateRetentionGridFitEntries(t, now, snaps, keep, remove)
} }
func TestRetentionGridFitEntries(t *testing.T) { func TestKeepsOldestSnapsInABucket(t *testing.T) {
g := gridFromString("1m,2")
relt := func(secs int64) time.Time { return time.Unix(secs, 0) }
snaps := []Entry{
testSnap{"1", true, relt(1)},
testSnap{"2", true, relt(2)},
testSnap{"3", false, relt(3)},
testSnap{"4", false, relt(4)},
testSnap{"5", false, relt(5)},
}
now := relt(6)
keep, remove := g.FitEntries(snaps)
validateRetentionGridFitEntries(t, now, snaps, keep, remove)
}
func TestRespectsKeepCountAll(t *testing.T) {
g := gridFromString("1m,-1|1m,1")
relt := func(secs int64) time.Time { return time.Unix(secs, 0) }
snaps := []Entry{
testSnap{"a", true, relt(0)},
testSnap{"b", true, relt(-1)},
testSnap{"c", true, relt(-2)},
testSnap{"d", false, relt(-60)},
testSnap{"e", true, relt(-61)},
}
keep, remove := g.FitEntries(snaps)
validateRetentionGridFitEntries(t, relt(61), snaps, keep, remove)
}
func TestComplex(t *testing.T) {
g := gridFromString("10m,-1|10m|10m,2|1h") g := gridFromString("10m,-1|10m|10m,2|1h")
@@ -141,20 +156,29 @@ func TestRetentionGridFitEntries(t *testing.T) {
now := time.Unix(0, 0) now := time.Unix(0, 0)
snaps := []Entry{ snaps := []Entry{
dummySnap{"1", true, now.Add(3 * time.Minute)}, // pre-now must always be kept // pre-now must always be kept
dummySnap{"b1", true, now.Add(-6 * time.Minute)}, // 1st interval allows unlimited entries testSnap{"1", true, now.Add(3 * time.Minute)},
dummySnap{"b3", true, now.Add(-8 * time.Minute)}, // 1st interval allows unlimited entries // 1st interval allows unlimited entries
dummySnap{"b2", true, now.Add(-9 * time.Minute)}, // 1st interval allows unlimited entries testSnap{"b1", true, now.Add(-6 * time.Minute)},
dummySnap{"a", false, now.Add(-11 * time.Minute)}, testSnap{"b3", true, now.Add(-8 * time.Minute)},
dummySnap{"c", true, now.Add(-19 * time.Minute)}, // 2nd interval allows 1 entry testSnap{"b2", true, now.Add(-9 * time.Minute)},
dummySnap{"foo", false, now.Add(-25 * time.Minute)}, // 2nd interval allows 1 entry
dummySnap{"bar", true, now.Add(-26 * time.Minute)}, // 3rd interval allows 2 entries testSnap{"a", false, now.Add(-11 * time.Minute)},
dummySnap{"border", true, now.Add(-30 * time.Minute)}, testSnap{"c", true, now.Add(-19 * time.Minute)},
dummySnap{"d", true, now.Add(-1*time.Hour - 15*time.Minute)}, // 3rd interval allows 2 entries
dummySnap{"e", false, now.Add(-1*time.Hour - 31*time.Minute)}, // before earliest interval must always be deleted testSnap{"foo", true, now.Add(-25 * time.Minute)},
dummySnap{"f", false, now.Add(-2 * time.Hour)}, testSnap{"bar", true, now.Add(-26 * time.Minute)},
// this is at the left edge of the 4th interval
testSnap{"border", false, now.Add(-30 * time.Minute)},
// right in the 4th interval
testSnap{"d", true, now.Add(-1*time.Hour - 15*time.Minute)},
// on the right edge of 4th interval => not in it => delete
testSnap{"q", false, now.Add(-1*time.Hour - 30*time.Minute)},
// older then 4th interval => always delete
testSnap{"e", false, now.Add(-1*time.Hour - 31*time.Minute)},
testSnap{"f", false, now.Add(-2 * time.Hour)},
} }
keep, remove := g.FitEntries(now, snaps) keep, remove := g.fitEntriesWithNow(now, snaps)
validateRetentionGridFitEntries(t, now, snaps, keep, remove) validateRetentionGridFitEntries(t, now, snaps, keep, remove)
+40 -10
View File
@@ -2,7 +2,6 @@ package driver
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"net" "net"
"sort" "sort"
@@ -10,10 +9,14 @@ import (
"sync" "sync"
"time" "time"
"github.com/zrepl/zrepl/daemon/logging/trace" "github.com/kr/pretty"
"github.com/pkg/errors"
"google.golang.org/grpc/codes" "google.golang.org/grpc/codes"
"google.golang.org/grpc/status" "google.golang.org/grpc/status"
"github.com/zrepl/zrepl/daemon/logging/trace"
"github.com/zrepl/zrepl/zfs"
"github.com/zrepl/zrepl/replication/report" "github.com/zrepl/zrepl/replication/report"
"github.com/zrepl/zrepl/util/chainlock" "github.com/zrepl/zrepl/util/chainlock"
"github.com/zrepl/zrepl/util/envconst" "github.com/zrepl/zrepl/util/envconst"
@@ -227,7 +230,10 @@ func Do(ctx context.Context, planner Planner) (ReportFunc, WaitFunc) {
errRep := cur.errorReport() errRep := cur.errorReport()
if rep.State == report.AttemptDone { if rep.State == report.AttemptDone {
log.Debug("attempt completed successfully") if len(rep.Filesystems) == 0 {
log.Warn("no filesystems were considered for replication")
}
log.Debug("attempt completed")
break break
} }
@@ -235,7 +241,7 @@ func Do(ctx context.Context, planner Planner) (ReportFunc, WaitFunc) {
log.WithField("most_recent_err", mostRecentErr).WithField("most_recent_err_class", mostRecentErrClass).Debug("most recent error used for re-connect decision") log.WithField("most_recent_err", mostRecentErr).WithField("most_recent_err_class", mostRecentErrClass).Debug("most recent error used for re-connect decision")
if mostRecentErr == nil { if mostRecentErr == nil {
// inconsistent reporting, let's bail out // inconsistent reporting, let's bail out
log.Warn("attempt does not report done but error report does not report errors, aborting run") log.WithField("attempt_state", rep.State).Warn("attempt does not report done but error report does not report errors, aborting run")
break break
} }
log.WithError(mostRecentErr.Err).Error("most recent error in this attempt") log.WithError(mostRecentErr.Err).Error("most recent error in this attempt")
@@ -309,6 +315,9 @@ func (a *attempt) doGlobalPlanning(ctx context.Context, prev *attempt) map[*fs]*
return nil return nil
} }
// a.fss != nil indicates that there was no planning error (see doc comment)
a.fss = make([]*fs, 0)
for _, pfs := range pfss { for _, pfs := range pfss {
fs := &fs{ fs := &fs{
fs: pfs, fs: pfs,
@@ -374,11 +383,28 @@ func (a *attempt) doGlobalPlanning(ctx context.Context, prev *attempt) map[*fs]*
// invariant: prevs contains an entry for each unambiguous correspondence // invariant: prevs contains an entry for each unambiguous correspondence
// build up parent-child relationship (FIXME (O(n^2), but who's going to have that many filesystems...)) // build up parent-child relationship (FIXME (O(n^2), but who's going to have that many filesystems...))
mustDatasetPathOrPlanFail := func(fs string) *zfs.DatasetPath {
dp, err := zfs.NewDatasetPath(fs)
if err != nil {
now := time.Now()
a.planErr = newTimedError(errors.Wrapf(err, "%q", fs), now)
a.fss = nil
a.finishedAt = now
return nil
}
return dp
}
for _, f1 := range a.fss { for _, f1 := range a.fss {
fs1 := f1.fs.ReportInfo().Name fs1 := mustDatasetPathOrPlanFail(f1.fs.ReportInfo().Name)
if fs1 == nil {
return nil
}
for _, f2 := range a.fss { for _, f2 := range a.fss {
fs2 := f2.fs.ReportInfo().Name fs2 := mustDatasetPathOrPlanFail(f2.fs.ReportInfo().Name)
if strings.HasPrefix(fs1, fs2) && fs1 != fs2 { if fs2 == nil {
return nil
}
if fs1.HasPrefix(fs2) && !fs1.Equal(fs2) {
f1.initialRepOrd.parents = append(f1.initialRepOrd.parents, f2) f1.initialRepOrd.parents = append(f1.initialRepOrd.parents, f2)
f2.initialRepOrd.children = append(f2.initialRepOrd.children, f1) f2.initialRepOrd.children = append(f2.initialRepOrd.children, f1)
} }
@@ -657,10 +683,12 @@ func (a *attempt) report() *report.AttemptReport {
r.Filesystems[i] = a.fss[i].report() r.Filesystems[i] = a.fss[i].report()
} }
state := report.AttemptPlanning var state report.AttemptState
if a.planErr != nil { if a.planErr == nil && a.fss == nil {
state = report.AttemptPlanning
} else if a.planErr != nil && a.fss == nil {
state = report.AttemptPlanningError state = report.AttemptPlanningError
} else if a.fss != nil { } else if a.planErr == nil && a.fss != nil {
if a.finishedAt.IsZero() { if a.finishedAt.IsZero() {
state = report.AttemptFanOutFSs state = report.AttemptFanOutFSs
} else { } else {
@@ -673,6 +701,8 @@ func (a *attempt) report() *report.AttemptReport {
state = report.AttemptFanOutError state = report.AttemptFanOutError
} }
} }
} else {
panic(fmt.Sprintf("attempt.planErr and attempt.fss must not both be != nil:\n%s\n%s", pretty.Sprint(a.planErr), pretty.Sprint(a.fss)))
} }
r.State = state r.State = state
@@ -10,6 +10,7 @@ import (
"time" "time"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/zrepl/zrepl/daemon/logging/trace" "github.com/zrepl/zrepl/daemon/logging/trace"
"github.com/zrepl/zrepl/replication/report" "github.com/zrepl/zrepl/replication/report"
@@ -12,6 +12,7 @@ import (
"github.com/montanaflynn/stats" "github.com/montanaflynn/stats"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/zrepl/zrepl/daemon/logging/trace" "github.com/zrepl/zrepl/daemon/logging/trace"
) )
+25 -33
View File
@@ -77,56 +77,48 @@ func IncrementalPath(receiver, sender []*FilesystemVersion) (incPath []*Filesyst
receiver = SortVersionListByCreateTXGThenBookmarkLTSnapshot(receiver) receiver = SortVersionListByCreateTXGThenBookmarkLTSnapshot(receiver)
sender = SortVersionListByCreateTXGThenBookmarkLTSnapshot(sender) sender = SortVersionListByCreateTXGThenBookmarkLTSnapshot(sender)
// Find most recent common ancestor by name, preferring snapshots over bookmarks var mrcaCandidate struct {
found bool
guid uint64
r, s int
}
mrcaRcv := len(receiver) - 1 findCandidate:
mrcaSnd := len(sender) - 1 for r := len(receiver) - 1; r >= 0; r-- {
for s := len(sender) - 1; s >= 0; s-- {
for mrcaRcv >= 0 && mrcaSnd >= 0 { if sender[s].GetGuid() == receiver[r].GetGuid() {
if receiver[mrcaRcv].Guid == sender[mrcaSnd].Guid { mrcaCandidate.guid = sender[s].GetGuid()
// Since we arrive from the end of the array, and because we defined bookmark < snapshot, mrcaCandidate.s = s
// this condition will match snapshot first, which is what we want because it gives us mrcaCandidate.r = r
// size estimation mrcaCandidate.found = true
break break findCandidate
} }
receiverCreation, err := receiver[mrcaRcv].CreationAsTime()
if err != nil {
panic(err) // FIXME move this to a sorting phase before
}
senderCreation, err := sender[mrcaSnd].CreationAsTime()
if err != nil {
panic(err) // FIXME move this to the sorting phase before
}
if receiverCreation.Before(senderCreation) {
mrcaSnd--
} else {
mrcaRcv--
} }
} }
if mrcaRcv == -1 || mrcaSnd == -1 { // handle failure cases
if !mrcaCandidate.found {
return nil, &ConflictNoCommonAncestor{ return nil, &ConflictNoCommonAncestor{
SortedSenderVersions: sender, SortedSenderVersions: sender,
SortedReceiverVersions: receiver, SortedReceiverVersions: receiver,
} }
} } else if mrcaCandidate.r != len(receiver)-1 {
if mrcaRcv != len(receiver)-1 {
return nil, &ConflictDiverged{ return nil, &ConflictDiverged{
SortedSenderVersions: sender, SortedSenderVersions: sender,
SortedReceiverVersions: receiver, SortedReceiverVersions: receiver,
CommonAncestor: sender[mrcaSnd], CommonAncestor: sender[mrcaCandidate.s],
SenderOnly: sender[mrcaSnd+1:], SenderOnly: sender[mrcaCandidate.s+1:],
ReceiverOnly: receiver[mrcaRcv+1:], ReceiverOnly: receiver[mrcaCandidate.r+1:],
} }
} }
// incPath is possible
// incPath must not contain bookmarks except initial one, // incPath must not contain bookmarks except initial one,
incPath = make([]*FilesystemVersion, 0, len(sender)) incPath = make([]*FilesystemVersion, 0, len(sender))
incPath = append(incPath, sender[mrcaSnd]) incPath = append(incPath, sender[mrcaCandidate.s])
// it's ok if incPath[0] is a bookmark, but not the subsequent ones in the incPath // it's ok if incPath[0] is a bookmark, but not the subsequent ones in the incPath
for i := mrcaSnd + 1; i < len(sender); i++ { for i := mrcaCandidate.s + 1; i < len(sender); i++ {
if sender[i].Type == FilesystemVersion_Snapshot && incPath[len(incPath)-1].Guid != sender[i].Guid { if sender[i].Type == FilesystemVersion_Snapshot && incPath[len(incPath)-1].Guid != sender[i].Guid {
incPath = append(incPath, sender[i]) incPath = append(incPath, sender[i])
} }
+230 -88
View File
@@ -46,7 +46,36 @@ func (x Tri) String() string {
return proto.EnumName(Tri_name, int32(x)) return proto.EnumName(Tri_name, int32(x))
} }
func (Tri) EnumDescriptor() ([]byte, []int) { func (Tri) EnumDescriptor() ([]byte, []int) {
return fileDescriptor_pdu_483c6918b7b3d747, []int{0} return fileDescriptor_pdu_616c27178643eca4, []int{0}
}
type ReplicationGuaranteeKind int32
const (
ReplicationGuaranteeKind_GuaranteeInvalid ReplicationGuaranteeKind = 0
ReplicationGuaranteeKind_GuaranteeResumability ReplicationGuaranteeKind = 1
ReplicationGuaranteeKind_GuaranteeIncrementalReplication ReplicationGuaranteeKind = 2
ReplicationGuaranteeKind_GuaranteeNothing ReplicationGuaranteeKind = 3
)
var ReplicationGuaranteeKind_name = map[int32]string{
0: "GuaranteeInvalid",
1: "GuaranteeResumability",
2: "GuaranteeIncrementalReplication",
3: "GuaranteeNothing",
}
var ReplicationGuaranteeKind_value = map[string]int32{
"GuaranteeInvalid": 0,
"GuaranteeResumability": 1,
"GuaranteeIncrementalReplication": 2,
"GuaranteeNothing": 3,
}
func (x ReplicationGuaranteeKind) String() string {
return proto.EnumName(ReplicationGuaranteeKind_name, int32(x))
}
func (ReplicationGuaranteeKind) EnumDescriptor() ([]byte, []int) {
return fileDescriptor_pdu_616c27178643eca4, []int{1}
} }
type FilesystemVersion_VersionType int32 type FilesystemVersion_VersionType int32
@@ -69,7 +98,7 @@ func (x FilesystemVersion_VersionType) String() string {
return proto.EnumName(FilesystemVersion_VersionType_name, int32(x)) return proto.EnumName(FilesystemVersion_VersionType_name, int32(x))
} }
func (FilesystemVersion_VersionType) EnumDescriptor() ([]byte, []int) { func (FilesystemVersion_VersionType) EnumDescriptor() ([]byte, []int) {
return fileDescriptor_pdu_483c6918b7b3d747, []int{5, 0} return fileDescriptor_pdu_616c27178643eca4, []int{5, 0}
} }
type ListFilesystemReq struct { type ListFilesystemReq struct {
@@ -82,7 +111,7 @@ func (m *ListFilesystemReq) Reset() { *m = ListFilesystemReq{} }
func (m *ListFilesystemReq) String() string { return proto.CompactTextString(m) } func (m *ListFilesystemReq) String() string { return proto.CompactTextString(m) }
func (*ListFilesystemReq) ProtoMessage() {} func (*ListFilesystemReq) ProtoMessage() {}
func (*ListFilesystemReq) Descriptor() ([]byte, []int) { func (*ListFilesystemReq) Descriptor() ([]byte, []int) {
return fileDescriptor_pdu_483c6918b7b3d747, []int{0} return fileDescriptor_pdu_616c27178643eca4, []int{0}
} }
func (m *ListFilesystemReq) XXX_Unmarshal(b []byte) error { func (m *ListFilesystemReq) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_ListFilesystemReq.Unmarshal(m, b) return xxx_messageInfo_ListFilesystemReq.Unmarshal(m, b)
@@ -113,7 +142,7 @@ func (m *ListFilesystemRes) Reset() { *m = ListFilesystemRes{} }
func (m *ListFilesystemRes) String() string { return proto.CompactTextString(m) } func (m *ListFilesystemRes) String() string { return proto.CompactTextString(m) }
func (*ListFilesystemRes) ProtoMessage() {} func (*ListFilesystemRes) ProtoMessage() {}
func (*ListFilesystemRes) Descriptor() ([]byte, []int) { func (*ListFilesystemRes) Descriptor() ([]byte, []int) {
return fileDescriptor_pdu_483c6918b7b3d747, []int{1} return fileDescriptor_pdu_616c27178643eca4, []int{1}
} }
func (m *ListFilesystemRes) XXX_Unmarshal(b []byte) error { func (m *ListFilesystemRes) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_ListFilesystemRes.Unmarshal(m, b) return xxx_messageInfo_ListFilesystemRes.Unmarshal(m, b)
@@ -154,7 +183,7 @@ func (m *Filesystem) Reset() { *m = Filesystem{} }
func (m *Filesystem) String() string { return proto.CompactTextString(m) } func (m *Filesystem) String() string { return proto.CompactTextString(m) }
func (*Filesystem) ProtoMessage() {} func (*Filesystem) ProtoMessage() {}
func (*Filesystem) Descriptor() ([]byte, []int) { func (*Filesystem) Descriptor() ([]byte, []int) {
return fileDescriptor_pdu_483c6918b7b3d747, []int{2} return fileDescriptor_pdu_616c27178643eca4, []int{2}
} }
func (m *Filesystem) XXX_Unmarshal(b []byte) error { func (m *Filesystem) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_Filesystem.Unmarshal(m, b) return xxx_messageInfo_Filesystem.Unmarshal(m, b)
@@ -213,7 +242,7 @@ func (m *ListFilesystemVersionsReq) Reset() { *m = ListFilesystemVersion
func (m *ListFilesystemVersionsReq) String() string { return proto.CompactTextString(m) } func (m *ListFilesystemVersionsReq) String() string { return proto.CompactTextString(m) }
func (*ListFilesystemVersionsReq) ProtoMessage() {} func (*ListFilesystemVersionsReq) ProtoMessage() {}
func (*ListFilesystemVersionsReq) Descriptor() ([]byte, []int) { func (*ListFilesystemVersionsReq) Descriptor() ([]byte, []int) {
return fileDescriptor_pdu_483c6918b7b3d747, []int{3} return fileDescriptor_pdu_616c27178643eca4, []int{3}
} }
func (m *ListFilesystemVersionsReq) XXX_Unmarshal(b []byte) error { func (m *ListFilesystemVersionsReq) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_ListFilesystemVersionsReq.Unmarshal(m, b) return xxx_messageInfo_ListFilesystemVersionsReq.Unmarshal(m, b)
@@ -251,7 +280,7 @@ func (m *ListFilesystemVersionsRes) Reset() { *m = ListFilesystemVersion
func (m *ListFilesystemVersionsRes) String() string { return proto.CompactTextString(m) } func (m *ListFilesystemVersionsRes) String() string { return proto.CompactTextString(m) }
func (*ListFilesystemVersionsRes) ProtoMessage() {} func (*ListFilesystemVersionsRes) ProtoMessage() {}
func (*ListFilesystemVersionsRes) Descriptor() ([]byte, []int) { func (*ListFilesystemVersionsRes) Descriptor() ([]byte, []int) {
return fileDescriptor_pdu_483c6918b7b3d747, []int{4} return fileDescriptor_pdu_616c27178643eca4, []int{4}
} }
func (m *ListFilesystemVersionsRes) XXX_Unmarshal(b []byte) error { func (m *ListFilesystemVersionsRes) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_ListFilesystemVersionsRes.Unmarshal(m, b) return xxx_messageInfo_ListFilesystemVersionsRes.Unmarshal(m, b)
@@ -293,7 +322,7 @@ func (m *FilesystemVersion) Reset() { *m = FilesystemVersion{} }
func (m *FilesystemVersion) String() string { return proto.CompactTextString(m) } func (m *FilesystemVersion) String() string { return proto.CompactTextString(m) }
func (*FilesystemVersion) ProtoMessage() {} func (*FilesystemVersion) ProtoMessage() {}
func (*FilesystemVersion) Descriptor() ([]byte, []int) { func (*FilesystemVersion) Descriptor() ([]byte, []int) {
return fileDescriptor_pdu_483c6918b7b3d747, []int{5} return fileDescriptor_pdu_616c27178643eca4, []int{5}
} }
func (m *FilesystemVersion) XXX_Unmarshal(b []byte) error { func (m *FilesystemVersion) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_FilesystemVersion.Unmarshal(m, b) return xxx_messageInfo_FilesystemVersion.Unmarshal(m, b)
@@ -359,19 +388,20 @@ type SendReq struct {
// SHOULD clear the resume token on their side and use From and To instead If // SHOULD clear the resume token on their side and use From and To instead If
// ResumeToken is not empty, the GUIDs of From and To MUST correspond to those // ResumeToken is not empty, the GUIDs of From and To MUST correspond to those
// encoded in the ResumeToken. Otherwise, the Sender MUST return an error. // encoded in the ResumeToken. Otherwise, the Sender MUST return an error.
ResumeToken string `protobuf:"bytes,4,opt,name=ResumeToken,proto3" json:"ResumeToken,omitempty"` ResumeToken string `protobuf:"bytes,4,opt,name=ResumeToken,proto3" json:"ResumeToken,omitempty"`
Encrypted Tri `protobuf:"varint,5,opt,name=Encrypted,proto3,enum=Tri" json:"Encrypted,omitempty"` Encrypted Tri `protobuf:"varint,5,opt,name=Encrypted,proto3,enum=Tri" json:"Encrypted,omitempty"`
DryRun bool `protobuf:"varint,6,opt,name=DryRun,proto3" json:"DryRun,omitempty"` DryRun bool `protobuf:"varint,6,opt,name=DryRun,proto3" json:"DryRun,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"` ReplicationConfig *ReplicationConfig `protobuf:"bytes,7,opt,name=ReplicationConfig,proto3" json:"ReplicationConfig,omitempty"`
XXX_unrecognized []byte `json:"-"` XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_sizecache int32 `json:"-"` XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
} }
func (m *SendReq) Reset() { *m = SendReq{} } func (m *SendReq) Reset() { *m = SendReq{} }
func (m *SendReq) String() string { return proto.CompactTextString(m) } func (m *SendReq) String() string { return proto.CompactTextString(m) }
func (*SendReq) ProtoMessage() {} func (*SendReq) ProtoMessage() {}
func (*SendReq) Descriptor() ([]byte, []int) { func (*SendReq) Descriptor() ([]byte, []int) {
return fileDescriptor_pdu_483c6918b7b3d747, []int{6} return fileDescriptor_pdu_616c27178643eca4, []int{6}
} }
func (m *SendReq) XXX_Unmarshal(b []byte) error { func (m *SendReq) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_SendReq.Unmarshal(m, b) return xxx_messageInfo_SendReq.Unmarshal(m, b)
@@ -433,6 +463,97 @@ func (m *SendReq) GetDryRun() bool {
return false return false
} }
func (m *SendReq) GetReplicationConfig() *ReplicationConfig {
if m != nil {
return m.ReplicationConfig
}
return nil
}
type ReplicationConfig struct {
Protection *ReplicationConfigProtection `protobuf:"bytes,1,opt,name=protection,proto3" json:"protection,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *ReplicationConfig) Reset() { *m = ReplicationConfig{} }
func (m *ReplicationConfig) String() string { return proto.CompactTextString(m) }
func (*ReplicationConfig) ProtoMessage() {}
func (*ReplicationConfig) Descriptor() ([]byte, []int) {
return fileDescriptor_pdu_616c27178643eca4, []int{7}
}
func (m *ReplicationConfig) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_ReplicationConfig.Unmarshal(m, b)
}
func (m *ReplicationConfig) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_ReplicationConfig.Marshal(b, m, deterministic)
}
func (dst *ReplicationConfig) XXX_Merge(src proto.Message) {
xxx_messageInfo_ReplicationConfig.Merge(dst, src)
}
func (m *ReplicationConfig) XXX_Size() int {
return xxx_messageInfo_ReplicationConfig.Size(m)
}
func (m *ReplicationConfig) XXX_DiscardUnknown() {
xxx_messageInfo_ReplicationConfig.DiscardUnknown(m)
}
var xxx_messageInfo_ReplicationConfig proto.InternalMessageInfo
func (m *ReplicationConfig) GetProtection() *ReplicationConfigProtection {
if m != nil {
return m.Protection
}
return nil
}
type ReplicationConfigProtection struct {
Initial ReplicationGuaranteeKind `protobuf:"varint,1,opt,name=Initial,proto3,enum=ReplicationGuaranteeKind" json:"Initial,omitempty"`
Incremental ReplicationGuaranteeKind `protobuf:"varint,2,opt,name=Incremental,proto3,enum=ReplicationGuaranteeKind" json:"Incremental,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *ReplicationConfigProtection) Reset() { *m = ReplicationConfigProtection{} }
func (m *ReplicationConfigProtection) String() string { return proto.CompactTextString(m) }
func (*ReplicationConfigProtection) ProtoMessage() {}
func (*ReplicationConfigProtection) Descriptor() ([]byte, []int) {
return fileDescriptor_pdu_616c27178643eca4, []int{8}
}
func (m *ReplicationConfigProtection) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_ReplicationConfigProtection.Unmarshal(m, b)
}
func (m *ReplicationConfigProtection) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_ReplicationConfigProtection.Marshal(b, m, deterministic)
}
func (dst *ReplicationConfigProtection) XXX_Merge(src proto.Message) {
xxx_messageInfo_ReplicationConfigProtection.Merge(dst, src)
}
func (m *ReplicationConfigProtection) XXX_Size() int {
return xxx_messageInfo_ReplicationConfigProtection.Size(m)
}
func (m *ReplicationConfigProtection) XXX_DiscardUnknown() {
xxx_messageInfo_ReplicationConfigProtection.DiscardUnknown(m)
}
var xxx_messageInfo_ReplicationConfigProtection proto.InternalMessageInfo
func (m *ReplicationConfigProtection) GetInitial() ReplicationGuaranteeKind {
if m != nil {
return m.Initial
}
return ReplicationGuaranteeKind_GuaranteeInvalid
}
func (m *ReplicationConfigProtection) GetIncremental() ReplicationGuaranteeKind {
if m != nil {
return m.Incremental
}
return ReplicationGuaranteeKind_GuaranteeInvalid
}
type Property struct { type Property struct {
Name string `protobuf:"bytes,1,opt,name=Name,proto3" json:"Name,omitempty"` Name string `protobuf:"bytes,1,opt,name=Name,proto3" json:"Name,omitempty"`
Value string `protobuf:"bytes,2,opt,name=Value,proto3" json:"Value,omitempty"` Value string `protobuf:"bytes,2,opt,name=Value,proto3" json:"Value,omitempty"`
@@ -445,7 +566,7 @@ func (m *Property) Reset() { *m = Property{} }
func (m *Property) String() string { return proto.CompactTextString(m) } func (m *Property) String() string { return proto.CompactTextString(m) }
func (*Property) ProtoMessage() {} func (*Property) ProtoMessage() {}
func (*Property) Descriptor() ([]byte, []int) { func (*Property) Descriptor() ([]byte, []int) {
return fileDescriptor_pdu_483c6918b7b3d747, []int{7} return fileDescriptor_pdu_616c27178643eca4, []int{9}
} }
func (m *Property) XXX_Unmarshal(b []byte) error { func (m *Property) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_Property.Unmarshal(m, b) return xxx_messageInfo_Property.Unmarshal(m, b)
@@ -496,7 +617,7 @@ func (m *SendRes) Reset() { *m = SendRes{} }
func (m *SendRes) String() string { return proto.CompactTextString(m) } func (m *SendRes) String() string { return proto.CompactTextString(m) }
func (*SendRes) ProtoMessage() {} func (*SendRes) ProtoMessage() {}
func (*SendRes) Descriptor() ([]byte, []int) { func (*SendRes) Descriptor() ([]byte, []int) {
return fileDescriptor_pdu_483c6918b7b3d747, []int{8} return fileDescriptor_pdu_616c27178643eca4, []int{10}
} }
func (m *SendRes) XXX_Unmarshal(b []byte) error { func (m *SendRes) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_SendRes.Unmarshal(m, b) return xxx_messageInfo_SendRes.Unmarshal(m, b)
@@ -548,7 +669,7 @@ func (m *SendCompletedReq) Reset() { *m = SendCompletedReq{} }
func (m *SendCompletedReq) String() string { return proto.CompactTextString(m) } func (m *SendCompletedReq) String() string { return proto.CompactTextString(m) }
func (*SendCompletedReq) ProtoMessage() {} func (*SendCompletedReq) ProtoMessage() {}
func (*SendCompletedReq) Descriptor() ([]byte, []int) { func (*SendCompletedReq) Descriptor() ([]byte, []int) {
return fileDescriptor_pdu_483c6918b7b3d747, []int{9} return fileDescriptor_pdu_616c27178643eca4, []int{11}
} }
func (m *SendCompletedReq) XXX_Unmarshal(b []byte) error { func (m *SendCompletedReq) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_SendCompletedReq.Unmarshal(m, b) return xxx_messageInfo_SendCompletedReq.Unmarshal(m, b)
@@ -585,7 +706,7 @@ func (m *SendCompletedRes) Reset() { *m = SendCompletedRes{} }
func (m *SendCompletedRes) String() string { return proto.CompactTextString(m) } func (m *SendCompletedRes) String() string { return proto.CompactTextString(m) }
func (*SendCompletedRes) ProtoMessage() {} func (*SendCompletedRes) ProtoMessage() {}
func (*SendCompletedRes) Descriptor() ([]byte, []int) { func (*SendCompletedRes) Descriptor() ([]byte, []int) {
return fileDescriptor_pdu_483c6918b7b3d747, []int{10} return fileDescriptor_pdu_616c27178643eca4, []int{12}
} }
func (m *SendCompletedRes) XXX_Unmarshal(b []byte) error { func (m *SendCompletedRes) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_SendCompletedRes.Unmarshal(m, b) return xxx_messageInfo_SendCompletedRes.Unmarshal(m, b)
@@ -610,17 +731,18 @@ type ReceiveReq struct {
To *FilesystemVersion `protobuf:"bytes,2,opt,name=To,proto3" json:"To,omitempty"` To *FilesystemVersion `protobuf:"bytes,2,opt,name=To,proto3" json:"To,omitempty"`
// If true, the receiver should clear the resume token before performing the // If true, the receiver should clear the resume token before performing the
// zfs recv of the stream in the request // zfs recv of the stream in the request
ClearResumeToken bool `protobuf:"varint,3,opt,name=ClearResumeToken,proto3" json:"ClearResumeToken,omitempty"` ClearResumeToken bool `protobuf:"varint,3,opt,name=ClearResumeToken,proto3" json:"ClearResumeToken,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"` ReplicationConfig *ReplicationConfig `protobuf:"bytes,4,opt,name=ReplicationConfig,proto3" json:"ReplicationConfig,omitempty"`
XXX_unrecognized []byte `json:"-"` XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_sizecache int32 `json:"-"` XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
} }
func (m *ReceiveReq) Reset() { *m = ReceiveReq{} } func (m *ReceiveReq) Reset() { *m = ReceiveReq{} }
func (m *ReceiveReq) String() string { return proto.CompactTextString(m) } func (m *ReceiveReq) String() string { return proto.CompactTextString(m) }
func (*ReceiveReq) ProtoMessage() {} func (*ReceiveReq) ProtoMessage() {}
func (*ReceiveReq) Descriptor() ([]byte, []int) { func (*ReceiveReq) Descriptor() ([]byte, []int) {
return fileDescriptor_pdu_483c6918b7b3d747, []int{11} return fileDescriptor_pdu_616c27178643eca4, []int{13}
} }
func (m *ReceiveReq) XXX_Unmarshal(b []byte) error { func (m *ReceiveReq) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_ReceiveReq.Unmarshal(m, b) return xxx_messageInfo_ReceiveReq.Unmarshal(m, b)
@@ -661,6 +783,13 @@ func (m *ReceiveReq) GetClearResumeToken() bool {
return false return false
} }
func (m *ReceiveReq) GetReplicationConfig() *ReplicationConfig {
if m != nil {
return m.ReplicationConfig
}
return nil
}
type ReceiveRes struct { type ReceiveRes struct {
XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"` XXX_unrecognized []byte `json:"-"`
@@ -671,7 +800,7 @@ func (m *ReceiveRes) Reset() { *m = ReceiveRes{} }
func (m *ReceiveRes) String() string { return proto.CompactTextString(m) } func (m *ReceiveRes) String() string { return proto.CompactTextString(m) }
func (*ReceiveRes) ProtoMessage() {} func (*ReceiveRes) ProtoMessage() {}
func (*ReceiveRes) Descriptor() ([]byte, []int) { func (*ReceiveRes) Descriptor() ([]byte, []int) {
return fileDescriptor_pdu_483c6918b7b3d747, []int{12} return fileDescriptor_pdu_616c27178643eca4, []int{14}
} }
func (m *ReceiveRes) XXX_Unmarshal(b []byte) error { func (m *ReceiveRes) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_ReceiveRes.Unmarshal(m, b) return xxx_messageInfo_ReceiveRes.Unmarshal(m, b)
@@ -704,7 +833,7 @@ func (m *DestroySnapshotsReq) Reset() { *m = DestroySnapshotsReq{} }
func (m *DestroySnapshotsReq) String() string { return proto.CompactTextString(m) } func (m *DestroySnapshotsReq) String() string { return proto.CompactTextString(m) }
func (*DestroySnapshotsReq) ProtoMessage() {} func (*DestroySnapshotsReq) ProtoMessage() {}
func (*DestroySnapshotsReq) Descriptor() ([]byte, []int) { func (*DestroySnapshotsReq) Descriptor() ([]byte, []int) {
return fileDescriptor_pdu_483c6918b7b3d747, []int{13} return fileDescriptor_pdu_616c27178643eca4, []int{15}
} }
func (m *DestroySnapshotsReq) XXX_Unmarshal(b []byte) error { func (m *DestroySnapshotsReq) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_DestroySnapshotsReq.Unmarshal(m, b) return xxx_messageInfo_DestroySnapshotsReq.Unmarshal(m, b)
@@ -750,7 +879,7 @@ func (m *DestroySnapshotRes) Reset() { *m = DestroySnapshotRes{} }
func (m *DestroySnapshotRes) String() string { return proto.CompactTextString(m) } func (m *DestroySnapshotRes) String() string { return proto.CompactTextString(m) }
func (*DestroySnapshotRes) ProtoMessage() {} func (*DestroySnapshotRes) ProtoMessage() {}
func (*DestroySnapshotRes) Descriptor() ([]byte, []int) { func (*DestroySnapshotRes) Descriptor() ([]byte, []int) {
return fileDescriptor_pdu_483c6918b7b3d747, []int{14} return fileDescriptor_pdu_616c27178643eca4, []int{16}
} }
func (m *DestroySnapshotRes) XXX_Unmarshal(b []byte) error { func (m *DestroySnapshotRes) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_DestroySnapshotRes.Unmarshal(m, b) return xxx_messageInfo_DestroySnapshotRes.Unmarshal(m, b)
@@ -795,7 +924,7 @@ func (m *DestroySnapshotsRes) Reset() { *m = DestroySnapshotsRes{} }
func (m *DestroySnapshotsRes) String() string { return proto.CompactTextString(m) } func (m *DestroySnapshotsRes) String() string { return proto.CompactTextString(m) }
func (*DestroySnapshotsRes) ProtoMessage() {} func (*DestroySnapshotsRes) ProtoMessage() {}
func (*DestroySnapshotsRes) Descriptor() ([]byte, []int) { func (*DestroySnapshotsRes) Descriptor() ([]byte, []int) {
return fileDescriptor_pdu_483c6918b7b3d747, []int{15} return fileDescriptor_pdu_616c27178643eca4, []int{17}
} }
func (m *DestroySnapshotsRes) XXX_Unmarshal(b []byte) error { func (m *DestroySnapshotsRes) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_DestroySnapshotsRes.Unmarshal(m, b) return xxx_messageInfo_DestroySnapshotsRes.Unmarshal(m, b)
@@ -833,7 +962,7 @@ func (m *ReplicationCursorReq) Reset() { *m = ReplicationCursorReq{} }
func (m *ReplicationCursorReq) String() string { return proto.CompactTextString(m) } func (m *ReplicationCursorReq) String() string { return proto.CompactTextString(m) }
func (*ReplicationCursorReq) ProtoMessage() {} func (*ReplicationCursorReq) ProtoMessage() {}
func (*ReplicationCursorReq) Descriptor() ([]byte, []int) { func (*ReplicationCursorReq) Descriptor() ([]byte, []int) {
return fileDescriptor_pdu_483c6918b7b3d747, []int{16} return fileDescriptor_pdu_616c27178643eca4, []int{18}
} }
func (m *ReplicationCursorReq) XXX_Unmarshal(b []byte) error { func (m *ReplicationCursorReq) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_ReplicationCursorReq.Unmarshal(m, b) return xxx_messageInfo_ReplicationCursorReq.Unmarshal(m, b)
@@ -874,7 +1003,7 @@ func (m *ReplicationCursorRes) Reset() { *m = ReplicationCursorRes{} }
func (m *ReplicationCursorRes) String() string { return proto.CompactTextString(m) } func (m *ReplicationCursorRes) String() string { return proto.CompactTextString(m) }
func (*ReplicationCursorRes) ProtoMessage() {} func (*ReplicationCursorRes) ProtoMessage() {}
func (*ReplicationCursorRes) Descriptor() ([]byte, []int) { func (*ReplicationCursorRes) Descriptor() ([]byte, []int) {
return fileDescriptor_pdu_483c6918b7b3d747, []int{17} return fileDescriptor_pdu_616c27178643eca4, []int{19}
} }
func (m *ReplicationCursorRes) XXX_Unmarshal(b []byte) error { func (m *ReplicationCursorRes) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_ReplicationCursorRes.Unmarshal(m, b) return xxx_messageInfo_ReplicationCursorRes.Unmarshal(m, b)
@@ -1010,7 +1139,7 @@ func (m *PingReq) Reset() { *m = PingReq{} }
func (m *PingReq) String() string { return proto.CompactTextString(m) } func (m *PingReq) String() string { return proto.CompactTextString(m) }
func (*PingReq) ProtoMessage() {} func (*PingReq) ProtoMessage() {}
func (*PingReq) Descriptor() ([]byte, []int) { func (*PingReq) Descriptor() ([]byte, []int) {
return fileDescriptor_pdu_483c6918b7b3d747, []int{18} return fileDescriptor_pdu_616c27178643eca4, []int{20}
} }
func (m *PingReq) XXX_Unmarshal(b []byte) error { func (m *PingReq) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_PingReq.Unmarshal(m, b) return xxx_messageInfo_PingReq.Unmarshal(m, b)
@@ -1049,7 +1178,7 @@ func (m *PingRes) Reset() { *m = PingRes{} }
func (m *PingRes) String() string { return proto.CompactTextString(m) } func (m *PingRes) String() string { return proto.CompactTextString(m) }
func (*PingRes) ProtoMessage() {} func (*PingRes) ProtoMessage() {}
func (*PingRes) Descriptor() ([]byte, []int) { func (*PingRes) Descriptor() ([]byte, []int) {
return fileDescriptor_pdu_483c6918b7b3d747, []int{19} return fileDescriptor_pdu_616c27178643eca4, []int{21}
} }
func (m *PingRes) XXX_Unmarshal(b []byte) error { func (m *PingRes) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_PingRes.Unmarshal(m, b) return xxx_messageInfo_PingRes.Unmarshal(m, b)
@@ -1084,6 +1213,8 @@ func init() {
proto.RegisterType((*ListFilesystemVersionsRes)(nil), "ListFilesystemVersionsRes") proto.RegisterType((*ListFilesystemVersionsRes)(nil), "ListFilesystemVersionsRes")
proto.RegisterType((*FilesystemVersion)(nil), "FilesystemVersion") proto.RegisterType((*FilesystemVersion)(nil), "FilesystemVersion")
proto.RegisterType((*SendReq)(nil), "SendReq") proto.RegisterType((*SendReq)(nil), "SendReq")
proto.RegisterType((*ReplicationConfig)(nil), "ReplicationConfig")
proto.RegisterType((*ReplicationConfigProtection)(nil), "ReplicationConfigProtection")
proto.RegisterType((*Property)(nil), "Property") proto.RegisterType((*Property)(nil), "Property")
proto.RegisterType((*SendRes)(nil), "SendRes") proto.RegisterType((*SendRes)(nil), "SendRes")
proto.RegisterType((*SendCompletedReq)(nil), "SendCompletedReq") proto.RegisterType((*SendCompletedReq)(nil), "SendCompletedReq")
@@ -1098,6 +1229,7 @@ func init() {
proto.RegisterType((*PingReq)(nil), "PingReq") proto.RegisterType((*PingReq)(nil), "PingReq")
proto.RegisterType((*PingRes)(nil), "PingRes") proto.RegisterType((*PingRes)(nil), "PingRes")
proto.RegisterEnum("Tri", Tri_name, Tri_value) proto.RegisterEnum("Tri", Tri_name, Tri_value)
proto.RegisterEnum("ReplicationGuaranteeKind", ReplicationGuaranteeKind_name, ReplicationGuaranteeKind_value)
proto.RegisterEnum("FilesystemVersion_VersionType", FilesystemVersion_VersionType_name, FilesystemVersion_VersionType_value) proto.RegisterEnum("FilesystemVersion_VersionType", FilesystemVersion_VersionType_name, FilesystemVersion_VersionType_value)
} }
@@ -1338,61 +1470,71 @@ var _Replication_serviceDesc = grpc.ServiceDesc{
Metadata: "pdu.proto", Metadata: "pdu.proto",
} }
func init() { proto.RegisterFile("pdu.proto", fileDescriptor_pdu_483c6918b7b3d747) } func init() { proto.RegisterFile("pdu.proto", fileDescriptor_pdu_616c27178643eca4) }
var fileDescriptor_pdu_483c6918b7b3d747 = []byte{ var fileDescriptor_pdu_616c27178643eca4 = []byte{
// 833 bytes of a gzipped FileDescriptorProto // 995 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x56, 0x5f, 0x6f, 0xe3, 0x44, 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x56, 0xcf, 0x6e, 0xdb, 0xc6,
0x10, 0xaf, 0x13, 0xa7, 0x75, 0x26, 0x3d, 0x2e, 0x9d, 0x96, 0x93, 0xb1, 0xe0, 0x54, 0x2d, 0x08, 0x13, 0x36, 0x25, 0xda, 0xa2, 0x46, 0xce, 0x2f, 0xf4, 0xd8, 0x09, 0x68, 0xfd, 0xd2, 0xd4, 0xd8,
0xe5, 0x2a, 0x61, 0xa1, 0xf2, 0x47, 0x42, 0x48, 0x27, 0xd1, 0xb4, 0xbd, 0x3b, 0x01, 0x47, 0xb4, 0x14, 0x85, 0x63, 0xa0, 0x44, 0xe1, 0xb4, 0x05, 0x8a, 0x14, 0x41, 0xeb, 0xbf, 0x31, 0xd2, 0xba,
0x35, 0x27, 0x74, 0x6f, 0x26, 0x19, 0xb5, 0x56, 0x1d, 0xaf, 0xbb, 0xe3, 0xa0, 0x0b, 0xe2, 0x89, 0xea, 0x5a, 0x0d, 0x8a, 0xdc, 0x18, 0x69, 0x2a, 0x2f, 0x4c, 0x71, 0xe9, 0x5d, 0x2a, 0x88, 0x7a,
0x47, 0xbe, 0x1e, 0x7c, 0x10, 0x3e, 0x02, 0xf2, 0xc6, 0x4e, 0x9c, 0xd8, 0x41, 0x79, 0xca, 0xce, 0xec, 0xa1, 0x87, 0x5e, 0x7a, 0xea, 0xeb, 0xf4, 0x29, 0xfa, 0x20, 0x7d, 0x84, 0x82, 0x6b, 0x92,
0x6f, 0x66, 0x77, 0x67, 0x7f, 0xf3, 0x9b, 0x71, 0xa0, 0x9b, 0x4e, 0x66, 0x7e, 0xaa, 0x55, 0xa6, 0xa2, 0x44, 0xca, 0x70, 0x4f, 0xda, 0xf9, 0xe6, 0xdb, 0xd9, 0xd9, 0xd1, 0x37, 0xb3, 0x84, 0x76,
0xc4, 0x31, 0x1c, 0xfd, 0x10, 0x71, 0x76, 0x1d, 0xc5, 0xc4, 0x73, 0xce, 0x68, 0x2a, 0xe9, 0x41, 0x3c, 0x9c, 0xf8, 0xb1, 0x92, 0x89, 0x64, 0x9b, 0xb0, 0xf1, 0xad, 0xd0, 0xc9, 0x89, 0x08, 0x49,
0x5c, 0xd4, 0x41, 0xc6, 0xcf, 0xa0, 0xb7, 0x02, 0xd8, 0xb5, 0x4e, 0xdb, 0x83, 0xde, 0x79, 0xcf, 0x4f, 0x75, 0x42, 0x63, 0x4e, 0xd7, 0xec, 0xa0, 0x0a, 0x6a, 0xfc, 0x04, 0x3a, 0x33, 0x40, 0x7b,
0xaf, 0x04, 0x55, 0xfd, 0xe2, 0x2f, 0x0b, 0x60, 0x65, 0x23, 0x82, 0x3d, 0x0a, 0xb3, 0x3b, 0xd7, 0xd6, 0x4e, 0x73, 0xb7, 0xb3, 0xdf, 0xf1, 0x4b, 0xa4, 0xb2, 0x9f, 0xfd, 0x6e, 0x01, 0xcc, 0x6c,
0x3a, 0xb5, 0x06, 0x5d, 0x69, 0xd6, 0x78, 0x0a, 0x3d, 0x49, 0x3c, 0x9b, 0x52, 0xa0, 0xee, 0x29, 0x44, 0xb0, 0x7b, 0x41, 0x72, 0xe9, 0x59, 0x3b, 0xd6, 0x6e, 0x9b, 0x9b, 0x35, 0xee, 0x40, 0x87,
0x71, 0x5b, 0xc6, 0x55, 0x85, 0xf0, 0x13, 0x78, 0xf4, 0x8a, 0x47, 0x71, 0x38, 0xa6, 0x3b, 0x15, 0x93, 0x9e, 0x8c, 0xa9, 0x2f, 0xaf, 0x28, 0xf2, 0x1a, 0xc6, 0x55, 0x86, 0xf0, 0x23, 0xb8, 0x77,
0x4f, 0x48, 0xbb, 0xed, 0x53, 0x6b, 0xe0, 0xc8, 0x75, 0x30, 0x3f, 0xe7, 0x15, 0x5f, 0x25, 0x63, 0xa6, 0x7b, 0x61, 0x30, 0xa0, 0x4b, 0x19, 0x0e, 0x49, 0x79, 0xcd, 0x1d, 0x6b, 0xd7, 0xe1, 0xf3,
0x3d, 0x4f, 0x33, 0x9a, 0xb8, 0xb6, 0x89, 0xa9, 0x42, 0xe2, 0x5b, 0xf8, 0x60, 0xfd, 0x41, 0x6f, 0x60, 0x1a, 0xe7, 0x4c, 0x1f, 0x47, 0x03, 0x35, 0x8d, 0x13, 0x1a, 0x7a, 0xb6, 0xe1, 0x94, 0x21,
0x48, 0x73, 0xa4, 0x12, 0x96, 0xf4, 0x80, 0x4f, 0xab, 0x89, 0x16, 0x09, 0x56, 0x10, 0xf1, 0xfd, 0xf6, 0x1c, 0xb6, 0xe7, 0x2f, 0xf4, 0x9a, 0x94, 0x16, 0x32, 0xd2, 0x9c, 0xae, 0xf1, 0x71, 0x39,
0xf6, 0xcd, 0x8c, 0x3e, 0x38, 0xa5, 0x59, 0x50, 0x82, 0x7e, 0x2d, 0x52, 0x2e, 0x63, 0xc4, 0x3f, 0xd1, 0x2c, 0xc1, 0x12, 0xc2, 0x5e, 0x2d, 0xdf, 0xac, 0xd1, 0x07, 0x27, 0x37, 0xb3, 0x92, 0xa0,
0x16, 0x1c, 0xd5, 0xfc, 0x78, 0x0e, 0x76, 0x30, 0x4f, 0xc9, 0x5c, 0xfe, 0xde, 0xf9, 0xd3, 0xfa, 0x5f, 0x61, 0xf2, 0x82, 0xc3, 0xfe, 0xb6, 0x60, 0xa3, 0xe2, 0xc7, 0x7d, 0xb0, 0xfb, 0xd3, 0x98,
0x09, 0x7e, 0xf1, 0x9b, 0x47, 0x49, 0x13, 0x9b, 0x33, 0xfa, 0x3a, 0x9c, 0x52, 0x41, 0x9b, 0x59, 0xcc, 0xe1, 0xff, 0xdb, 0x7f, 0x5c, 0x8d, 0xe0, 0x67, 0xbf, 0x29, 0x8b, 0x1b, 0x6e, 0x5a, 0xd1,
0xe7, 0xd8, 0x8b, 0x59, 0x34, 0x31, 0x34, 0xd9, 0xd2, 0xac, 0xf1, 0x43, 0xe8, 0x0e, 0x35, 0x85, 0xf3, 0x60, 0x4c, 0x59, 0xd9, 0xcc, 0x3a, 0xc5, 0x4e, 0x27, 0x62, 0x68, 0xca, 0x64, 0x73, 0xb3,
0x19, 0x05, 0xbf, 0xbc, 0x30, 0xdc, 0xd8, 0x72, 0x05, 0xa0, 0x07, 0x8e, 0x31, 0x22, 0x95, 0xb8, 0xc6, 0x47, 0xd0, 0x3e, 0x54, 0x14, 0x24, 0xd4, 0xff, 0xe9, 0xd4, 0xd4, 0xc6, 0xe6, 0x33, 0x00,
0x1d, 0x73, 0xd2, 0xd2, 0x16, 0xcf, 0xa0, 0x57, 0xb9, 0x16, 0x0f, 0xc1, 0xb9, 0x49, 0xc2, 0x94, 0xbb, 0xe0, 0x18, 0x43, 0xc8, 0xc8, 0x5b, 0x35, 0x91, 0x0a, 0x9b, 0x3d, 0x85, 0x4e, 0xe9, 0x58,
0xef, 0x54, 0xd6, 0xdf, 0xcb, 0xad, 0x0b, 0xa5, 0xee, 0xa7, 0xa1, 0xbe, 0xef, 0x5b, 0xe2, 0x6f, 0x5c, 0x07, 0xe7, 0x22, 0x0a, 0x62, 0x7d, 0x29, 0x13, 0x77, 0x25, 0xb5, 0x0e, 0xa4, 0xbc, 0x1a,
0x0b, 0x0e, 0x6e, 0x28, 0x99, 0xec, 0xc0, 0x27, 0x7e, 0x0a, 0xf6, 0xb5, 0x56, 0x53, 0x93, 0x78, 0x07, 0xea, 0xca, 0xb5, 0xd8, 0x9f, 0x0d, 0x68, 0x5d, 0x50, 0x34, 0xbc, 0x43, 0x3d, 0xf1, 0x63,
0x33, 0x5d, 0xc6, 0x8f, 0x02, 0x5a, 0x81, 0x32, 0x4f, 0x69, 0x8e, 0x6a, 0x05, 0x6a, 0x53, 0x42, 0xb0, 0x4f, 0x94, 0x1c, 0x9b, 0xc4, 0xeb, 0xcb, 0x65, 0xfc, 0xc8, 0xa0, 0xd1, 0x97, 0xe6, 0x2a,
0x76, 0x5d, 0x42, 0x02, 0xba, 0x2b, 0x69, 0x74, 0x0c, 0xbf, 0xb6, 0x1f, 0xe8, 0x48, 0xae, 0x60, 0xf5, 0xac, 0x46, 0x5f, 0x2e, 0x4a, 0xc8, 0xae, 0x4a, 0x88, 0x41, 0x7b, 0x26, 0x8d, 0x55, 0x53,
0x7c, 0x02, 0xfb, 0x97, 0x7a, 0x2e, 0x67, 0x89, 0xbb, 0x6f, 0xb4, 0x53, 0x58, 0xe2, 0x4b, 0x70, 0x5f, 0xdb, 0xef, 0x2b, 0xc1, 0x67, 0x30, 0x3e, 0x84, 0xb5, 0x23, 0x35, 0xe5, 0x93, 0xc8, 0x5b,
0x46, 0x5a, 0xa5, 0xa4, 0xb3, 0xf9, 0x92, 0x6e, 0xab, 0x42, 0xf7, 0x09, 0x74, 0xde, 0x84, 0xf1, 0x33, 0xda, 0xc9, 0x2c, 0xfc, 0x1a, 0x36, 0x38, 0xc5, 0xa1, 0x18, 0x98, 0x7a, 0x1c, 0xca, 0xe8,
0xac, 0xac, 0xc1, 0xc2, 0x10, 0x7f, 0x2e, 0xb9, 0x60, 0x1c, 0xc0, 0xe3, 0x9f, 0x99, 0x26, 0x9b, 0x67, 0x31, 0xf2, 0x5a, 0x59, 0x42, 0x15, 0x0f, 0xaf, 0x92, 0xd9, 0x0f, 0x35, 0x11, 0xf0, 0x2b,
0x32, 0x77, 0xe4, 0x26, 0x8c, 0x02, 0x0e, 0xaf, 0xde, 0xa5, 0x34, 0xce, 0x68, 0x72, 0x13, 0xfd, 0x80, 0xb4, 0xf9, 0x68, 0x60, 0xaa, 0x6e, 0x99, 0x78, 0x8f, 0xaa, 0xf1, 0x7a, 0x05, 0x87, 0x97,
0x4e, 0xe6, 0xdd, 0x6d, 0xb9, 0x86, 0xe1, 0x33, 0x80, 0x22, 0x9f, 0x88, 0xd8, 0xb5, 0x8d, 0xdc, 0xf8, 0xec, 0x0f, 0x0b, 0xfe, 0x7f, 0x0b, 0x17, 0x9f, 0x41, 0xeb, 0x2c, 0x12, 0x89, 0x08, 0xc2,
0xba, 0x7e, 0x99, 0xa2, 0xac, 0x38, 0xc5, 0x73, 0xe8, 0xe7, 0x39, 0x0c, 0xd5, 0x34, 0x8d, 0x29, 0x4c, 0x4e, 0xdb, 0xe5, 0xd0, 0xa7, 0x93, 0x40, 0x05, 0x51, 0x42, 0xf4, 0x4a, 0x44, 0x43, 0x9e,
0x23, 0x53, 0x98, 0x33, 0xe8, 0xfd, 0xa4, 0xa3, 0xdb, 0x28, 0x09, 0x63, 0x49, 0x0f, 0x05, 0xff, 0x33, 0xf1, 0x39, 0x74, 0xce, 0xa2, 0x81, 0xa2, 0x31, 0x45, 0x49, 0x10, 0x9a, 0xbf, 0xe6, 0xd6,
0x8e, 0x5f, 0xd4, 0x4d, 0x56, 0x9d, 0x02, 0x6b, 0xfb, 0x59, 0xfc, 0x01, 0x20, 0x69, 0x4c, 0xd1, 0x8d, 0x65, 0x36, 0xfb, 0x0c, 0x9c, 0x9e, 0x92, 0x31, 0xa9, 0x64, 0x5a, 0xa8, 0xd2, 0x2a, 0xa9,
0x6f, 0xb4, 0x4b, 0x99, 0x17, 0xe5, 0x6b, 0xfd, 0x6f, 0xf9, 0xce, 0xa0, 0x3f, 0x8c, 0x29, 0xd4, 0x72, 0x0b, 0x56, 0x5f, 0x07, 0xe1, 0x24, 0x97, 0xea, 0x8d, 0xc1, 0x7e, 0xb5, 0x72, 0xc9, 0x68,
0x55, 0x7e, 0x16, 0x2d, 0x5e, 0xc3, 0xc5, 0x61, 0xe5, 0x76, 0x16, 0xb7, 0x70, 0x7c, 0x49, 0x9c, 0xdc, 0x85, 0xfb, 0x3f, 0x6a, 0x1a, 0x2e, 0x4e, 0x03, 0x87, 0x2f, 0xc2, 0xc8, 0x60, 0xfd, 0xf8,
0x69, 0x35, 0x2f, 0x35, 0xb9, 0x4b, 0x2f, 0xe3, 0xe7, 0xd0, 0x5d, 0xc6, 0xbb, 0xad, 0xad, 0xfd, 0x7d, 0x4c, 0x83, 0x84, 0x86, 0x17, 0xe2, 0x17, 0x32, 0xf2, 0x68, 0xf2, 0x39, 0x0c, 0x9f, 0x02,
0xba, 0x0a, 0x12, 0x6f, 0x01, 0x37, 0x2e, 0x2a, 0xda, 0xbe, 0x34, 0xcd, 0x2d, 0x5b, 0xda, 0xbe, 0x64, 0xf9, 0x08, 0xd2, 0x9e, 0x6d, 0xba, 0xb2, 0xed, 0xe7, 0x29, 0xf2, 0x92, 0x93, 0xbd, 0x00,
0x8c, 0xc9, 0x95, 0x72, 0xa5, 0xb5, 0xd2, 0xa5, 0x52, 0x8c, 0x21, 0x2e, 0x9b, 0x1e, 0x91, 0x4f, 0x37, 0xcd, 0xe1, 0x50, 0x8e, 0xe3, 0x90, 0x12, 0x32, 0xfa, 0xdd, 0x83, 0xce, 0xf7, 0x4a, 0x8c,
0xda, 0x83, 0xfc, 0xe1, 0x71, 0x56, 0x8e, 0x94, 0x63, 0xbf, 0x9e, 0x82, 0x2c, 0x63, 0xc4, 0xd7, 0x44, 0x14, 0x84, 0x9c, 0xae, 0x33, 0x99, 0x3a, 0x7e, 0x26, 0x6f, 0x5e, 0x76, 0x32, 0xac, 0xec,
0x70, 0x22, 0x29, 0x8d, 0xa3, 0xb1, 0xe9, 0xda, 0xe1, 0x4c, 0xb3, 0xd2, 0xbb, 0xcc, 0xb5, 0xa0, 0xd7, 0xec, 0x2f, 0x0b, 0x80, 0xd3, 0x80, 0xc4, 0x3b, 0xba, 0x4b, 0x3b, 0xdc, 0xc8, 0xbc, 0x71,
0x71, 0x1f, 0xe3, 0x49, 0x31, 0x44, 0xf2, 0x1d, 0xf6, 0xcb, 0xbd, 0xe5, 0x18, 0x71, 0x5e, 0xab, 0xab, 0xcc, 0xf7, 0xc0, 0x3d, 0x0c, 0x29, 0x50, 0xe5, 0x02, 0xdd, 0x8c, 0xc2, 0x0a, 0x5e, 0x2f,
0x8c, 0xde, 0x45, 0x9c, 0x2d, 0x24, 0xfc, 0x72, 0x4f, 0x2e, 0x91, 0x0b, 0x07, 0xf6, 0x17, 0xe9, 0x5a, 0xfb, 0xbf, 0x88, 0x76, 0xbd, 0x94, 0xbf, 0x66, 0x23, 0xd8, 0x3c, 0x22, 0x9d, 0x28, 0x39,
0x88, 0x8f, 0xe1, 0x60, 0x14, 0x25, 0xb7, 0x79, 0x02, 0x2e, 0x1c, 0xfc, 0x48, 0xcc, 0xe1, 0x6d, 0xcd, 0xbb, 0xff, 0x2e, 0x53, 0x13, 0x3f, 0x85, 0x76, 0xc1, 0xf7, 0x1a, 0x4b, 0x27, 0xe3, 0x8c,
0xd9, 0x35, 0xa5, 0x29, 0x3e, 0x2a, 0x83, 0x38, 0xef, 0xab, 0xab, 0xf1, 0x9d, 0x2a, 0xfb, 0x2a, 0xc4, 0xde, 0x00, 0x2e, 0x1c, 0x94, 0x0d, 0xd8, 0xdc, 0xcc, 0x5a, 0xa5, 0x76, 0xc0, 0xe6, 0x9c,
0x5f, 0x9f, 0x0d, 0xa0, 0x1d, 0xe8, 0x28, 0x1f, 0x31, 0x97, 0x2a, 0xc9, 0x86, 0xa1, 0xa6, 0xfe, 0x54, 0x6c, 0xc7, 0x4a, 0x49, 0x95, 0x8b, 0xcd, 0x18, 0xec, 0xa8, 0xee, 0x12, 0xe9, 0x9b, 0xd6,
0x1e, 0x76, 0xa1, 0x73, 0x1d, 0xc6, 0x4c, 0x7d, 0x0b, 0x1d, 0xb0, 0x03, 0x3d, 0xa3, 0x7e, 0xeb, 0x4a, 0x4b, 0x17, 0x26, 0xf9, 0xf0, 0xde, 0xf4, 0xab, 0x29, 0xf0, 0x9c, 0xc3, 0xbe, 0x80, 0xad,
0xfc, 0xdf, 0x56, 0x3e, 0x00, 0x96, 0x8f, 0x40, 0x0f, 0xec, 0xfc, 0x60, 0x74, 0xfc, 0x22, 0x09, 0x72, 0xb5, 0x26, 0x4a, 0x4b, 0x75, 0x97, 0x17, 0xa4, 0x5f, 0xbb, 0x4f, 0xe3, 0x56, 0x36, 0xae,
0xaf, 0x5c, 0x31, 0x7e, 0x03, 0x8f, 0xd7, 0xe7, 0x38, 0x23, 0xfa, 0xb5, 0x8f, 0x9f, 0x57, 0xc7, 0xd3, 0x1d, 0xf6, 0xcb, 0x95, 0x62, 0x60, 0x3b, 0xe7, 0x32, 0xa1, 0xf7, 0x42, 0x27, 0x37, 0x5d,
0x18, 0x47, 0xf0, 0xa4, 0xf9, 0x13, 0x80, 0x9e, 0xbf, 0xf5, 0xc3, 0xe2, 0x6d, 0xf7, 0x31, 0x3e, 0xf0, 0x72, 0x85, 0x17, 0xc8, 0x81, 0x03, 0x6b, 0x37, 0xe9, 0xb0, 0x27, 0xd0, 0xea, 0x89, 0x68,
0x87, 0xfe, 0x66, 0xe9, 0xf1, 0xc4, 0x6f, 0x90, 0xb4, 0xd7, 0x84, 0x32, 0x7e, 0x07, 0x47, 0xb5, 0x94, 0x26, 0xe0, 0x41, 0xeb, 0x3b, 0xd2, 0x3a, 0x18, 0xe5, 0x8d, 0x97, 0x9b, 0xec, 0x83, 0x9c,
0xe2, 0xe1, 0xfb, 0x7e, 0x93, 0x10, 0xbc, 0x46, 0x98, 0xf1, 0x2b, 0x78, 0xb4, 0xd6, 0xe2, 0x78, 0xa4, 0xd3, 0xd6, 0x3c, 0x1e, 0x5c, 0xca, 0xbc, 0x35, 0xd3, 0xf5, 0xde, 0x2e, 0x34, 0xfb, 0x4a,
0xe4, 0x6f, 0x8e, 0x0c, 0xaf, 0x06, 0xf1, 0x45, 0xe7, 0x6d, 0x3b, 0x9d, 0xcc, 0x7e, 0xdd, 0x37, 0xa4, 0xc3, 0xfc, 0x48, 0x46, 0xc9, 0x61, 0xa0, 0xc8, 0x5d, 0xc1, 0x36, 0xac, 0x9e, 0x04, 0xa1,
0xff, 0x1f, 0xbe, 0xf8, 0x2f, 0x00, 0x00, 0xff, 0xff, 0x27, 0x95, 0xc1, 0x78, 0x4c, 0x08, 0x00, 0x26, 0xd7, 0x42, 0x07, 0xec, 0xbe, 0x9a, 0x90, 0xdb, 0xd8, 0xfb, 0xcd, 0x02, 0x6f, 0xd9, 0x38,
0x00, 0xc0, 0x2d, 0x70, 0x0b, 0xe0, 0x2c, 0x7a, 0x17, 0x84, 0x62, 0xe8, 0xae, 0xe0, 0x36, 0x3c, 0x28,
0x50, 0xa3, 0xd0, 0xe0, 0xad, 0x08, 0x45, 0x32, 0x75, 0x2d, 0x7c, 0x02, 0x1f, 0x96, 0x36, 0x14,
0xa3, 0xa4, 0x74, 0x80, 0xdb, 0x98, 0x8b, 0x7a, 0x2e, 0x93, 0x4b, 0x11, 0x8d, 0xdc, 0xe6, 0xfe,
0x3f, 0x8d, 0x74, 0xe6, 0x17, 0x3c, 0xec, 0x82, 0x9d, 0xde, 0x10, 0x1d, 0x3f, 0xab, 0x46, 0x37,
0x5f, 0x69, 0xfc, 0x12, 0xee, 0xcf, 0x3f, 0xdd, 0x1a, 0xd1, 0xaf, 0x7c, 0xef, 0x74, 0xab, 0x98,
0xc6, 0x1e, 0x3c, 0xac, 0x7f, 0xf5, 0xb1, 0xeb, 0x2f, 0xfd, 0x96, 0xe8, 0x2e, 0xf7, 0x69, 0x7c,
0x01, 0xee, 0xa2, 0x06, 0x71, 0xcb, 0xaf, 0xe9, 0xad, 0x6e, 0x1d, 0xaa, 0xf1, 0x9b, 0xf9, 0xc6,
0x36, 0x2a, 0xc2, 0x07, 0x7e, 0x9d, 0x22, 0xbb, 0xb5, 0xb0, 0xc6, 0xcf, 0xe1, 0xde, 0xdc, 0xb8,
0xc2, 0x0d, 0x7f, 0x71, 0xfc, 0x75, 0x2b, 0x90, 0x3e, 0x58, 0x7d, 0xd3, 0x8c, 0x87, 0x93, 0xb7,
0x6b, 0xe6, 0x93, 0xf1, 0xd9, 0xbf, 0x01, 0x00, 0x00, 0xff, 0xff, 0x7e, 0xa4, 0xfb, 0xca, 0x3f,
0x0a, 0x00, 0x00,
} }
+21
View File
@@ -61,6 +61,25 @@ message SendReq {
Tri Encrypted = 5; Tri Encrypted = 5;
bool DryRun = 6; bool DryRun = 6;
ReplicationConfig ReplicationConfig = 7;
}
message ReplicationConfig {
ReplicationConfigProtection protection = 1;
}
message ReplicationConfigProtection {
ReplicationGuaranteeKind Initial = 1;
ReplicationGuaranteeKind Incremental = 2;
}
enum ReplicationGuaranteeKind {
GuaranteeInvalid = 0;
GuaranteeResumability = 1;
GuaranteeIncrementalReplication = 2;
GuaranteeNothing = 3;
} }
message Property { message Property {
@@ -93,6 +112,8 @@ message ReceiveReq {
// If true, the receiver should clear the resume token before performing the // If true, the receiver should clear the resume token before performing the
// zfs recv of the stream in the request // zfs recv of the stream in the request
bool ClearResumeToken = 3; bool ClearResumeToken = 3;
ReplicationConfig ReplicationConfig = 4;
} }
message ReceiveRes {} message ReceiveRes {}
+7
View File
@@ -83,3 +83,10 @@ func (v *FilesystemVersion) ZFSFilesystemVersion() (*zfs.FilesystemVersion, erro
Creation: ct, Creation: ct,
}, nil }, nil
} }
func ReplicationConfigProtectionWithKind(both ReplicationGuaranteeKind) *ReplicationConfigProtection {
return &ReplicationConfigProtection{
Initial: both,
Incremental: both,
}
}
+23 -14
View File
@@ -9,6 +9,7 @@ import (
"time" "time"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"github.com/zrepl/zrepl/daemon/logging/trace" "github.com/zrepl/zrepl/daemon/logging/trace"
"github.com/zrepl/zrepl/logger" "github.com/zrepl/zrepl/logger"
@@ -52,10 +53,6 @@ type Receiver interface {
Receive(ctx context.Context, req *pdu.ReceiveReq, receive io.ReadCloser) (*pdu.ReceiveRes, error) Receive(ctx context.Context, req *pdu.ReceiveReq, receive io.ReadCloser) (*pdu.ReceiveRes, error)
} }
type PlannerPolicy struct {
EncryptedSend tri // all sends must be encrypted (send -w, and encryption!=off)
}
type Planner struct { type Planner struct {
sender Sender sender Sender
receiver Receiver receiver Receiver
@@ -554,19 +551,25 @@ func (s *Step) updateSizeEstimate(ctx context.Context) error {
log.WithError(err).Error("dry run send request failed") log.WithError(err).Error("dry run send request failed")
return err return err
} }
s.expectedSize = sres.ExpectedSize if sres == nil {
err := fmt.Errorf("dry run send request returned nil send result")
log.Error(err.Error())
return err
}
s.expectedSize = sres.GetExpectedSize()
return nil return nil
} }
func (s *Step) buildSendRequest(dryRun bool) (sr *pdu.SendReq) { func (s *Step) buildSendRequest(dryRun bool) (sr *pdu.SendReq) {
fs := s.parent.Path fs := s.parent.Path
sr = &pdu.SendReq{ sr = &pdu.SendReq{
Filesystem: fs, Filesystem: fs,
From: s.from, // may be nil From: s.from, // may be nil
To: s.to, To: s.to,
Encrypted: s.encrypt.ToPDU(), Encrypted: s.encrypt.ToPDU(),
ResumeToken: s.resumeToken, ResumeToken: s.resumeToken,
DryRun: dryRun, DryRun: dryRun,
ReplicationConfig: &s.parent.policy.ReplicationConfig,
} }
return sr return sr
} }
@@ -584,6 +587,11 @@ func (s *Step) doReplication(ctx context.Context) error {
log.WithError(err).Error("send request failed") log.WithError(err).Error("send request failed")
return err return err
} }
if sres == nil {
err := fmt.Errorf("send request returned nil send result")
log.Error(err.Error())
return err
}
if stream == nil { if stream == nil {
err := errors.New("send request did not return a stream, broken endpoint implementation") err := errors.New("send request did not return a stream, broken endpoint implementation")
return err return err
@@ -603,9 +611,10 @@ func (s *Step) doReplication(ctx context.Context) error {
}() }()
rr := &pdu.ReceiveReq{ rr := &pdu.ReceiveReq{
Filesystem: fs, Filesystem: fs,
To: sr.GetTo(), To: sr.GetTo(),
ClearResumeToken: !sres.UsedResumeToken, ClearResumeToken: !sres.UsedResumeToken,
ReplicationConfig: &s.parent.policy.ReplicationConfig,
} }
log.Debug("initiate receive request") log.Debug("initiate receive request")
_, err = s.receiver.Receive(ctx, rr, byteCountingStream) _, err = s.receiver.Receive(ctx, rr, byteCountingStream)
@@ -0,0 +1,43 @@
package logic
import (
"github.com/pkg/errors"
"github.com/zrepl/zrepl/config"
"github.com/zrepl/zrepl/replication/logic/pdu"
)
type PlannerPolicy struct {
EncryptedSend tri // all sends must be encrypted (send -w, and encryption!=off)
ReplicationConfig pdu.ReplicationConfig
}
func ReplicationConfigFromConfig(in *config.Replication) (*pdu.ReplicationConfig, error) {
initial, err := pduReplicationGuaranteeKindFromConfig(in.Protection.Initial)
if err != nil {
return nil, errors.Wrap(err, "field 'initial'")
}
incremental, err := pduReplicationGuaranteeKindFromConfig(in.Protection.Incremental)
if err != nil {
return nil, errors.Wrap(err, "field 'incremental'")
}
return &pdu.ReplicationConfig{
Protection: &pdu.ReplicationConfigProtection{
Initial: initial,
Incremental: incremental,
},
}, nil
}
func pduReplicationGuaranteeKindFromConfig(in string) (k pdu.ReplicationGuaranteeKind, _ error) {
switch in {
case "guarantee_nothing":
return pdu.ReplicationGuaranteeKind_GuaranteeNothing, nil
case "guarantee_incremental":
return pdu.ReplicationGuaranteeKind_GuaranteeIncrementalReplication, nil
case "guarantee_resumability":
return pdu.ReplicationGuaranteeKind_GuaranteeResumability, nil
default:
return k, errors.Errorf("%q is not in guarantee_{nothing,incremental,resumability}", in)
}
}
+27
View File
@@ -162,3 +162,30 @@ func (f *FilesystemReport) NextStep() *StepReport {
func (f *StepReport) IsIncremental() bool { func (f *StepReport) IsIncremental() bool {
return f.Info.From != "" return f.Info.From != ""
} }
// Returns, for the latest replication attempt,
// 0 if there have not been any replication attempts,
// -1 if the replication failed while enumerating file systems
// N if N filesystems could not not be replicated successfully
func (r *Report) GetFailedFilesystemsCountInLatestAttempt() int {
if len(r.Attempts) == 0 {
return 0
}
a := r.Attempts[len(r.Attempts)-1]
switch a.State {
case AttemptPlanningError:
return -1
case AttemptFanOutError:
var count int
for _, f := range a.Filesystems {
if f.Error() != nil {
count++
}
}
return count
default:
return 0
}
}
+1 -1
View File
@@ -66,7 +66,7 @@ type ProtocolError struct {
} }
func (e *ProtocolError) Error() string { func (e *ProtocolError) Error() string {
return fmt.Sprintf("protocol error: %s", e) return fmt.Sprintf("protocol error: %s", e.cause)
} }
func (c *Client) recv(ctx context.Context, conn *stream.Conn, res proto.Message) error { func (c *Client) recv(ctx context.Context, conn *stream.Conn, res proto.Message) error {
+4
View File
@@ -239,6 +239,10 @@ func (s *Server) serveConnRequest(ctx context.Context, endpoint string, c *strea
if sendStream != nil { if sendStream != nil {
err := c.SendStream(ctx, sendStream, ZFSStream) err := c.SendStream(ctx, sendStream, ZFSStream)
closeErr := sendStream.Close()
if closeErr != nil {
s.log.WithError(err).Error("cannot close send stream")
}
if err != nil { if err != nil {
s.log.WithError(err).Error("cannot write send stream") s.log.WithError(err).Error("cannot write send stream")
} }
+2 -1
View File
@@ -10,9 +10,10 @@ import (
"sync/atomic" "sync/atomic"
"time" "time"
"github.com/zrepl/zrepl/daemon/logging/trace"
"google.golang.org/grpc" "google.golang.org/grpc"
"github.com/zrepl/zrepl/daemon/logging/trace"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/zrepl/zrepl/replication/logic" "github.com/zrepl/zrepl/replication/logic"
+1 -1
View File
@@ -152,7 +152,7 @@ func (m *HandshakeMessage) DecodeReader(r io.Reader, maxLen int) error {
func DoHandshakeCurrentVersion(conn net.Conn, deadline time.Time) *HandshakeError { func DoHandshakeCurrentVersion(conn net.Conn, deadline time.Time) *HandshakeError {
// current protocol version is hardcoded here // current protocol version is hardcoded here
return DoHandshakeVersion(conn, deadline, 4) return DoHandshakeVersion(conn, deadline, 5)
} }
const HandshakeMessageMaxLen = 16 * 4096 const HandshakeMessageMaxLen = 16 * 4096

Some files were not shown because too many files have changed in this diff Show More