Compare commits
59 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0ee7a49d31 | |||
| 02db5994fe | |||
| 1d7a84e8ae | |||
| a0e3dc7040 | |||
| 43495d70c7 | |||
| fa586b493c | |||
| 30cdc1430e | |||
| 27673a23e9 | |||
| 95fc299733 | |||
| 4e702eedc9 | |||
| 8ff83f2f1a | |||
| 4b8f0ad112 | |||
| dbc8bbeb6a | |||
| b3e856f40d | |||
| 8e1937fe75 | |||
| 073514fc21 | |||
| dab222d95f | |||
| a827894274 | |||
| 9a8d813d14 | |||
| e391fa94f9 | |||
| 509185dfbe | |||
| 4b1b7a8561 | |||
| b330ccca5d | |||
| 05f1237a6d | |||
| 1c270b7e39 | |||
| 1b39e9d03c | |||
| 655a2e5404 | |||
| 9c80eea045 | |||
| 175ad1dd0b | |||
| 728e97700f | |||
| 94a0fbf953 | |||
| b056e7b2b9 | |||
| 6e927f20f9 | |||
| 301f163a44 | |||
| 474652ea51 | |||
| 1bc731e782 | |||
| 292b85b5ef | |||
| dce98d50da | |||
| 10a14a8c50 | |||
| bcb5965617 | |||
| f772b3d39f | |||
| 27db8c0afe | |||
| 0e5c77d2be | |||
| 0280727985 | |||
| b4abebce00 | |||
| d89afe58d4 | |||
| c855546b9f | |||
| 7d6ee4c166 | |||
| 2fbd9d8f8c | |||
| 18e101a04e | |||
| e594421322 | |||
| 0d4bfda2fb | |||
| b83c026cdc | |||
| 46caf31075 | |||
| 2b9d696b49 | |||
| 70afff6e3b | |||
| 01bbda13e5 | |||
| c5a8f6635f | |||
| 6f441c55dc |
+15
-3
@@ -60,10 +60,10 @@ jobs:
|
||||
|
||||
- run: make zrepl-bin
|
||||
- run: make vet
|
||||
- run: make test
|
||||
- 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
|
||||
@@ -92,11 +92,23 @@ jobs:
|
||||
REPO="zrepl/zrepl"
|
||||
COMMIT="${CIRCLE_SHA1}"
|
||||
JOB_NAME="${CIRCLE_JOB}"
|
||||
curl "https://api.github.com/repos/$REPO/statuses/$COMMIT?access_token=$GITHUB_COMMIT_STATUS_TOKEN" \
|
||||
curl "https://api.github.com/repos/$REPO/statuses/$COMMIT" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: token $GITHUB_COMMIT_STATUS_TOKEN" \
|
||||
-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"'/"}'
|
||||
|
||||
# kick off binary packaging workflow
|
||||
- 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."
|
||||
exit 0
|
||||
fi
|
||||
set -u # from now on
|
||||
GITHUB_ACCESS_TOKEN="$ZREPL_DEBIAN_BINARYPACKAGIN_TRIGGER_BUILD_GITHUB_TOKEN" .circleci/trigger_debian_binary_packaging_workflow.bash "$CIRCLE_SHA1" "${CIRCLE_JOB##build-}"
|
||||
|
||||
build-1.11:
|
||||
<<: *build-latest
|
||||
docker:
|
||||
|
||||
+10
@@ -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"'" }}'
|
||||
@@ -1,3 +1,4 @@
|
||||
github: problame
|
||||
patreon: zrepl
|
||||
liberapay: zrepl
|
||||
custom: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=R5QSXJVYHGX96
|
||||
|
||||
@@ -27,6 +27,8 @@ GO_EXTRA_BUILDFLAGS :=
|
||||
GO_BUILDFLAGS := $(GO_MOD_READONLY) $(GO_EXTRA_BUILDFLAGS)
|
||||
GO_BUILD := $(GO_ENV_VARS) $(GO) build $(GO_BUILDFLAGS) -ldflags $(GO_LDFLAGS)
|
||||
GOLANGCI_LINT := golangci-lint
|
||||
GOCOVMERGE := gocovmerge
|
||||
|
||||
ifneq ($(GOARM),)
|
||||
ZREPL_TARGET_TUPLE := $(GOOS)-$(GOARCH)v$(GOARM)
|
||||
else
|
||||
@@ -45,7 +47,7 @@ printvars:
|
||||
|
||||
release: clean
|
||||
# no cross-platform support for target test
|
||||
$(MAKE) test
|
||||
$(MAKE) test-go
|
||||
$(MAKE) bins-all
|
||||
$(MAKE) noarch
|
||||
$(MAKE) wrapup-and-checksum
|
||||
@@ -99,9 +101,9 @@ clean: docs-clean
|
||||
rm -rf "$(ARTIFACTDIR)"
|
||||
|
||||
##################### BINARIES #####################
|
||||
.PHONY: bins-all lint test vet zrepl-bin platformtest-bin
|
||||
.PHONY: bins-all lint test-go test-platform cover-merge cover-html vet zrepl-bin test-platform-bin generate-platform-test-list
|
||||
|
||||
BINS_ALL_TARGETS := zrepl-bin platformtest-bin vet lint
|
||||
BINS_ALL_TARGETS := zrepl-bin test-platform-bin vet lint
|
||||
GO_SUPPORTS_ILLUMOS := $(shell $(GO) version | gawk -F '.' '/^go version /{split($$0, comps, " "); split(comps[3], v, "."); if (v[1] == "go1" && v[2] >= 13) { print "illumos"; } else { print "noillumos"; }}')
|
||||
bins-all:
|
||||
$(MAKE) $(BINS_ALL_TARGETS) GOOS=freebsd GOARCH=amd64
|
||||
@@ -122,27 +124,34 @@ endif
|
||||
|
||||
lint:
|
||||
$(GO_ENV_VARS) $(GOLANGCI_LINT) run ./...
|
||||
test:
|
||||
$(GO_ENV_VARS) $(GO) test $(GO_BUILDFLAGS) ./...
|
||||
|
||||
vet:
|
||||
$(GO_ENV_VARS) $(GO) vet $(GO_BUILDFLAGS) ./...
|
||||
|
||||
test-go: $(ARTIFACTDIR)
|
||||
rm -f "$(ARTIFACTDIR)/gotest.cover"
|
||||
ifeq ($(COVER),1)
|
||||
$(GO_ENV_VARS) $(GO) test $(GO_BUILDFLAGS) \
|
||||
-coverpkg github.com/zrepl/zrepl/... \
|
||||
-covermode atomic \
|
||||
-coverprofile "$(ARTIFACTDIR)/gotest.cover" \
|
||||
./...
|
||||
else
|
||||
$(GO_ENV_VARS) $(GO) test $(GO_BUILDFLAGS) \
|
||||
./...
|
||||
endif
|
||||
|
||||
zrepl-bin:
|
||||
$(GO_BUILD) -o "$(ARTIFACTDIR)/zrepl-$(ZREPL_TARGET_TUPLE)"
|
||||
|
||||
platformtest-bin:
|
||||
$(GO_BUILD) -o "$(ARTIFACTDIR)/platformtest-$(ZREPL_TARGET_TUPLE)" ./platformtest/harness
|
||||
generate-platform-test-list:
|
||||
$(GO_BUILD) -o $(ARTIFACTDIR)/generate-platform-test-list ./platformtest/tests/gen
|
||||
|
||||
##################### DEV TARGETS #####################
|
||||
# not part of the build, must do that manually
|
||||
.PHONY: generate format platformtest
|
||||
|
||||
generate:
|
||||
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 ./...
|
||||
|
||||
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')
|
||||
test-platform-bin:
|
||||
$(GO_ENV_VARS) $(GO) test $(GO_BUILDFLAGS) \
|
||||
-c -o "$(ARTIFACTDIR)/platformtest-$(ZREPL_TARGET_TUPLE)" \
|
||||
-covermode=atomic -cover -coverpkg github.com/zrepl/zrepl/... \
|
||||
./platformtest/harness
|
||||
|
||||
ZREPL_PLATFORMTEST_POOLNAME := zreplplatformtest
|
||||
ZREPL_PLATFORMTEST_IMAGEPATH := /tmp/zreplplatformtest.pool.img
|
||||
@@ -150,16 +159,42 @@ ZREPL_PLATFORMTEST_MOUNTPOINT := /tmp/zreplplatformtest.pool
|
||||
ZREPL_PLATFORMTEST_ZFS_LOG := /tmp/zreplplatformtest.zfs.log
|
||||
# ZREPL_PLATFORMTEST_STOP_AND_KEEP := -failure.stop-and-keep-pool
|
||||
ZREPL_PLATFORMTEST_ARGS :=
|
||||
platformtest: # do not track dependency on platformtest-bin to allow build of platformtest outside of test VM
|
||||
test-platform: $(ARTIFACTDIR) # do not track dependency on test-platform-bin to allow build of platformtest outside of test VM
|
||||
rm -f "$(ZREPL_PLATFORMTEST_ZFS_LOG)"
|
||||
rm -f "$(ARTIFACTDIR)/platformtest.cover"
|
||||
platformtest/logmockzfs/logzfsenv "$(ZREPL_PLATFORMTEST_ZFS_LOG)" `which zfs` \
|
||||
"$(ARTIFACTDIR)/platformtest-$(ZREPL_TARGET_TUPLE)" \
|
||||
-test.coverprofile "$(ARTIFACTDIR)/platformtest.cover" \
|
||||
-test.v \
|
||||
__DEVEL--i-heard-you-like-tests \
|
||||
-poolname "$(ZREPL_PLATFORMTEST_POOLNAME)" \
|
||||
-imagepath "$(ZREPL_PLATFORMTEST_IMAGEPATH)" \
|
||||
-mountpoint "$(ZREPL_PLATFORMTEST_MOUNTPOINT)" \
|
||||
$(ZREPL_PLATFORMTEST_STOP_AND_KEEP) \
|
||||
$(ZREPL_PLATFORMTEST_ARGS)
|
||||
|
||||
cover-merge: $(ARTIFACTDIR)
|
||||
$(GOCOVMERGE) $(ARTIFACTDIR)/platformtest.cover $(ARTIFACTDIR)/gotest.cover > $(ARTIFACTDIR)/merged.cover
|
||||
cover-html: cover-merge
|
||||
$(GO) tool cover -html "$(ARTIFACTDIR)/merged.cover" -o "$(ARTIFACTDIR)/merged.cover.html"
|
||||
|
||||
test-full:
|
||||
test "$$(id -u)" = "0" || echo "MUST RUN AS ROOT" 1>&2
|
||||
$(MAKE) test-go COVER=1
|
||||
$(MAKE) test-platform
|
||||
$(MAKE) cover-html
|
||||
|
||||
##################### DEV TARGETS #####################
|
||||
# not part of the build, must do that manually
|
||||
.PHONY: generate format
|
||||
|
||||
generate: generate-platform-test-list
|
||||
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 ./...
|
||||
|
||||
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')
|
||||
|
||||
##################### NOARCH #####################
|
||||
.PHONY: noarch $(ARTIFACTDIR)/bash_completion $(ARTIFACTDIR)/_zrepl.zsh_completion $(ARTIFACTDIR)/go_env.txt docs docs-clean
|
||||
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
[](https://golang.org/)
|
||||
[](https://zrepl.github.io)
|
||||
[](https://www.patreon.com/zrepl)
|
||||
[](https://liberapay.com/zrepl/donate)
|
||||
[](https://github.com/sponsors/problame)
|
||||
[](https://liberapay.com/zrepl/donate)
|
||||
[](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=R5QSXJVYHGX96)
|
||||
[](https://twitter.com/intent/tweet?text=Wow:&url=https%3A%2F%2Fgithub.com%2Fzrepl%2Fzrepl)
|
||||
|
||||
@@ -39,7 +40,7 @@ Check out the *Coding Workflow* section below for details.
|
||||
* 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 `make platformtest` **on a test system** to validate that zrepl's abstractions on top of ZFS work with the system ZFS.
|
||||
* 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
|
||||
@@ -47,10 +48,18 @@ Check out the *Coding Workflow* section below for details.
|
||||
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.
|
||||
|
||||
To get started, run `./lazy.sh devsetup` to easily install build dependencies and read `docs/installation.rst -> Compiling from Source`.
|
||||
Install **build dependencies** using `./lazy.sh devsetup`.
|
||||
`lazy.sh` uses `python3-pip` to fetch the build dependencies for the docs - you might want to use a [venv](https://docs.python.org/3/library/venv.html).
|
||||
If you just want to install the Go dependencies, run `./lazy.sh godep`.
|
||||
|
||||
The **test suite** is split into pure **Go tests** (`make test-go`) and **platform tests** that interact with ZFS and thus generally **require root privileges** (`sudo make test-platform`).
|
||||
Platform tests run on their own pool with the name `zreplplatformtest`, which is created using the file vdev in `/tmp`.
|
||||
|
||||
For a full **code coverage** profile, run `make test-go COVER=1 && sudo make test-platform && make cover-merge`.
|
||||
An HTML report can be generated using `make cover-html`.
|
||||
|
||||
**Code generation** is triggered by `make generate`. Generated code is committed to the source tree.
|
||||
|
||||
### Project Structure
|
||||
|
||||
```
|
||||
|
||||
@@ -28,6 +28,7 @@ require (
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/spf13/viper v1.3.2 // indirect
|
||||
github.com/stretchr/testify v1.4.0 // indirect
|
||||
github.com/wadey/gocovmerge v0.0.0-20160331181800-b5bfa59ec0ad // indirect
|
||||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980 // indirect
|
||||
golang.org/x/sys v0.0.0-20191010194322-b09406accb47 // indirect
|
||||
golang.org/x/tools v0.0.0-20190524210228-3d17549cdc6b
|
||||
|
||||
@@ -214,6 +214,8 @@ github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyC
|
||||
github.com/valyala/fasthttp v1.2.0/go.mod h1:4vX61m6KN+xDduDNwXrhIAVZaZaZiQ1luJk8LWSxF3s=
|
||||
github.com/valyala/quicktemplate v1.1.1/go.mod h1:EH+4AkTd43SvgIbQHYu59/cJyxDoOVRUAfrukLPuGJ4=
|
||||
github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
|
||||
github.com/wadey/gocovmerge v0.0.0-20160331181800-b5bfa59ec0ad h1:W0LEBv82YCGEtcmPA3uNZBI33/qF//HAAs3MawDjRa0=
|
||||
github.com/wadey/gocovmerge v0.0.0-20160331181800-b5bfa59ec0ad/go.mod h1:Hy8o65+MXnS6EwGElrSRjUzQDLXreJlzYLlWiHtt8hM=
|
||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
_ "github.com/alvaroloes/enumer"
|
||||
_ "github.com/golang/protobuf/protoc-gen-go"
|
||||
_ "github.com/golangci/golangci-lint/cmd/golangci-lint"
|
||||
_ "github.com/wadey/gocovmerge"
|
||||
_ "golang.org/x/tools/cmd/goimports"
|
||||
_ "golang.org/x/tools/cmd/stringer"
|
||||
)
|
||||
|
||||
@@ -63,7 +63,7 @@ func doZabsList(ctx context.Context, sc *cli.Subcommand, args []string) error {
|
||||
defer line.Lock().Unlock()
|
||||
if zabsListFlags.Json {
|
||||
enc.SetIndent("", " ")
|
||||
if err := enc.Encode(abstractions); err != nil {
|
||||
if err := enc.Encode(a); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Println()
|
||||
|
||||
+27
-15
@@ -52,11 +52,12 @@ func (j JobEnum) Name() string {
|
||||
}
|
||||
|
||||
type ActiveJob struct {
|
||||
Type string `yaml:"type"`
|
||||
Name string `yaml:"name"`
|
||||
Connect ConnectEnum `yaml:"connect"`
|
||||
Pruning PruningSenderReceiver `yaml:"pruning"`
|
||||
Debug JobDebugSettings `yaml:"debug,optional"`
|
||||
Type string `yaml:"type"`
|
||||
Name string `yaml:"name"`
|
||||
Connect ConnectEnum `yaml:"connect"`
|
||||
Pruning PruningSenderReceiver `yaml:"pruning"`
|
||||
Debug JobDebugSettings `yaml:"debug,optional"`
|
||||
Replication *Replication `yaml:"replication,optional,fromdefaults"`
|
||||
}
|
||||
|
||||
type PassiveJob struct {
|
||||
@@ -76,13 +77,7 @@ type SnapJob struct {
|
||||
}
|
||||
|
||||
type SendOptions struct {
|
||||
Encrypted bool `yaml:"encrypted"`
|
||||
}
|
||||
|
||||
var _ yaml.Defaulter = (*SendOptions)(nil)
|
||||
|
||||
func (l *SendOptions) SetDefault() {
|
||||
*l = SendOptions{Encrypted: false}
|
||||
Encrypted bool `yaml:"encrypted,optional,default=false"`
|
||||
}
|
||||
|
||||
type RecvOptions struct {
|
||||
@@ -93,10 +88,13 @@ type RecvOptions struct {
|
||||
// Reencrypt bool `yaml:"reencrypt"`
|
||||
}
|
||||
|
||||
var _ yaml.Defaulter = (*RecvOptions)(nil)
|
||||
type Replication struct {
|
||||
Protection *ReplicationOptionsProtection `yaml:"protection,optional,fromdefaults"`
|
||||
}
|
||||
|
||||
func (l *RecvOptions) SetDefault() {
|
||||
*l = RecvOptions{}
|
||||
type ReplicationOptionsProtection struct {
|
||||
Initial string `yaml:"initial,optional,default=guarantee_resumability"`
|
||||
Incremental string `yaml:"incremental,optional,default=guarantee_resumability"`
|
||||
}
|
||||
|
||||
type PushJob struct {
|
||||
@@ -106,6 +104,9 @@ type PushJob struct {
|
||||
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 {
|
||||
ActiveJob `yaml:",inline"`
|
||||
RootFS string `yaml:"root_fs"`
|
||||
@@ -113,6 +114,10 @@ type PullJob struct {
|
||||
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 {
|
||||
Interval time.Duration
|
||||
Manual bool
|
||||
@@ -150,6 +155,10 @@ type SinkJob struct {
|
||||
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 {
|
||||
PassiveJob `yaml:",inline"`
|
||||
Snapshotting SnapshottingEnum `yaml:"snapshotting"`
|
||||
@@ -157,6 +166,9 @@ type SourceJob struct {
|
||||
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 SnapshottingEnum struct {
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
@@ -60,9 +60,9 @@ jobs:
|
||||
})
|
||||
|
||||
t.Run("encrypted_unspecified", func(t *testing.T) {
|
||||
c, err := testConfig(t, fill(encrypted_unspecified))
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, c)
|
||||
c = testValidConfig(t, fill(encrypted_unspecified))
|
||||
encrypted := c.Jobs[0].Ret.(*PushJob).Send.Encrypted
|
||||
assert.Equal(t, false, encrypted)
|
||||
})
|
||||
|
||||
t.Run("send_not_specified", func(t *testing.T) {
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
# This config serves as an example for a local zrepl installation that
|
||||
# backups the entire zpool `system` to `backuppool/zrepl/sink`
|
||||
#
|
||||
# The requirements covered by this setup are described in the zrepl documentation's
|
||||
# quick start section which inlines this example.
|
||||
#
|
||||
# CUSTOMIZATIONS YOU WILL LIKELY WANT TO APPLY:
|
||||
# - adjust the name of the production pool `system` in the `filesystems` filter of jobs `snapjob` and `push_to_derive`
|
||||
# - adjust the name of the backup pool `backuppool` in the `backuppool_sink` job
|
||||
# - adjust the occurences of `myhostname` to the name of the system you are backing up (cannot be easily changed once you start replicating)
|
||||
# - make sure the `zrepl_` prefix is not being used by any other zfs tools you might have installed (it likely isn't)
|
||||
|
||||
jobs:
|
||||
|
||||
# this job takes care of snapshot creation + pruning
|
||||
- name: snapjob
|
||||
type: snap
|
||||
filesystems: {
|
||||
"system<": true,
|
||||
}
|
||||
# create snapshots with prefix `zrepl_` every 15 minutes
|
||||
snapshotting:
|
||||
type: periodic
|
||||
interval: 15m
|
||||
prefix: zrepl_
|
||||
pruning:
|
||||
keep:
|
||||
# fade-out scheme for snapshots starting with `zrepl_`
|
||||
# - keep all created in the last hour
|
||||
# - then destroy snapshots such that we keep 24 each 1 hour apart
|
||||
# - then destroy snapshots such that we keep 14 each 1 day apart
|
||||
# - then destroy all older snapshots
|
||||
- type: grid
|
||||
grid: 1x1h(keep=all) | 24x1h | 14x1d
|
||||
regex: "^zrepl_.*"
|
||||
# keep all snapshots that don't have the `zrepl_` prefix
|
||||
- type: regex
|
||||
negate: true
|
||||
regex: "^zrepl_.*"
|
||||
|
||||
# This job pushes to the local sink defined in job `backuppool_sink`.
|
||||
# We trigger replication manually from the command line / udev rules using
|
||||
# `zrepl signal wakeup push_to_drive`
|
||||
- type: push
|
||||
name: push_to_drive
|
||||
connect:
|
||||
type: local
|
||||
listener_name: backuppool_sink
|
||||
client_identity: myhostname
|
||||
filesystems: {
|
||||
"system<": true
|
||||
}
|
||||
send:
|
||||
encrypted: true
|
||||
replication:
|
||||
protection:
|
||||
initial: guarantee_resumability
|
||||
# Downgrade protection to guarantee_incremental which uses zfs bookmarks instead of zfs holds.
|
||||
# Thus, when we yank out the backup drive during replication
|
||||
# - 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:
|
||||
type: manual
|
||||
pruning:
|
||||
# no-op prune rule on sender (keep all snapshots), job `snapshot` takes care of this
|
||||
keep_sender:
|
||||
- type: regex
|
||||
regex: ".*"
|
||||
# retain
|
||||
keep_receiver:
|
||||
# longer retention on the backup drive, we have more space there
|
||||
- type: grid
|
||||
grid: 1x1h(keep=all) | 24x1h | 360x1d
|
||||
regex: "^zrepl_.*"
|
||||
# retain all non-zrepl snapshots on the backup drive
|
||||
- type: regex
|
||||
negate: true
|
||||
regex: "^zrepl_.*"
|
||||
|
||||
# This job receives from job `push_to_drive` into `backuppool/zrepl/sink/myhostname`
|
||||
- type: sink
|
||||
name: backuppool_sink
|
||||
root_fs: "backuppool/zrepl/sink"
|
||||
serve:
|
||||
type: local
|
||||
listener_name: backuppool_sink
|
||||
@@ -0,0 +1,12 @@
|
||||
jobs:
|
||||
- name: sink
|
||||
type: sink
|
||||
serve:
|
||||
type: tls
|
||||
listen: ":8888"
|
||||
ca: "/etc/zrepl/prod.crt"
|
||||
cert: "/etc/zrepl/backups.crt"
|
||||
key: "/etc/zrepl/backups.key"
|
||||
client_cns:
|
||||
- "prod"
|
||||
root_fs: "storage/zrepl/sink"
|
||||
@@ -0,0 +1,28 @@
|
||||
jobs:
|
||||
- name: prod_to_backups
|
||||
type: push
|
||||
connect:
|
||||
type: tls
|
||||
address: "backups.example.com:8888"
|
||||
ca: /etc/zrepl/backups.crt
|
||||
cert: /etc/zrepl/prod.crt
|
||||
key: /etc/zrepl/prod.key
|
||||
server_cn: "backups"
|
||||
filesystems: {
|
||||
"zroot/var/db": true,
|
||||
"zroot/usr/home<": true,
|
||||
"zroot/usr/home/paranoid": false
|
||||
}
|
||||
snapshotting:
|
||||
type: periodic
|
||||
prefix: zrepl_
|
||||
interval: 10m
|
||||
pruning:
|
||||
keep_sender:
|
||||
- type: not_replicated
|
||||
- type: last_n
|
||||
count: 10
|
||||
keep_receiver:
|
||||
- type: grid
|
||||
grid: 1x1h(keep=all) | 24x1h | 30x1d | 6x30d
|
||||
regex: "^zrepl_"
|
||||
@@ -5,7 +5,11 @@ jobs:
|
||||
type: tcp
|
||||
listen: "0.0.0.0:8888"
|
||||
clients: {
|
||||
"192.168.122.123" : "client1"
|
||||
"192.168.122.123" : "mysql01",
|
||||
"192.168.122.42" : "mx01",
|
||||
"2001:0db8:85a3::8a2e:0370:7334": "gateway",
|
||||
"10.23.42.0/24": "cluster-*",
|
||||
"fde4:8dba:82e1::/64": "san-*",
|
||||
}
|
||||
filesystems: {
|
||||
"<": true,
|
||||
|
||||
+7
-1
@@ -120,7 +120,13 @@ func (j *controlJob) Run(ctx context.Context) {
|
||||
jsonResponder{log, func() (interface{}, error) {
|
||||
jobs := j.jobs.status()
|
||||
globalZFS := zfscmd.GetReport()
|
||||
s := Status{Jobs: jobs, Global: GlobalStatus{ZFSCmds: globalZFS}}
|
||||
envconstReport := envconst.GetReport()
|
||||
s := Status{
|
||||
Jobs: jobs,
|
||||
Global: GlobalStatus{
|
||||
ZFSCmds: globalZFS,
|
||||
Envconst: envconstReport,
|
||||
}}
|
||||
return s, nil
|
||||
}})
|
||||
|
||||
|
||||
+15
-1
@@ -13,6 +13,8 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/zrepl/zrepl/daemon/logging/trace"
|
||||
"github.com/zrepl/zrepl/endpoint"
|
||||
"github.com/zrepl/zrepl/util/envconst"
|
||||
|
||||
"github.com/zrepl/zrepl/config"
|
||||
"github.com/zrepl/zrepl/daemon/job"
|
||||
@@ -50,6 +52,15 @@ func Run(ctx context.Context, conf *config.Config) error {
|
||||
log.Info(version.NewZreplVersionInformation().String())
|
||||
|
||||
ctx = logging.WithLoggers(ctx, logging.SubsystemLoggersWithUniversalLogger(log))
|
||||
trace.RegisterCallback(trace.Callback{
|
||||
OnBegin: func(ctx context.Context) { logging.GetLogger(ctx, logging.SubsysTraceData).Debug("begin span") },
|
||||
OnEnd: func(ctx context.Context, spanInfo trace.SpanInfo) {
|
||||
logging.
|
||||
GetLogger(ctx, logging.SubsysTraceData).
|
||||
WithField("duration_s", spanInfo.EndedAt().Sub(spanInfo.StartedAt()).Seconds()).
|
||||
Debug("finished span " + spanInfo.TaskAndSpanStack(trace.SpanStackKindAnnotation))
|
||||
},
|
||||
})
|
||||
|
||||
for _, job := range confJobs {
|
||||
if IsInternalJobName(job.Name()) {
|
||||
@@ -84,8 +95,10 @@ func Run(ctx context.Context, conf *config.Config) error {
|
||||
}
|
||||
|
||||
// register global (=non job-local) metrics
|
||||
version.PrometheusRegister(prometheus.DefaultRegisterer)
|
||||
zfscmd.RegisterMetrics(prometheus.DefaultRegisterer)
|
||||
trace.RegisterMetrics(prometheus.DefaultRegisterer)
|
||||
endpoint.RegisterMetrics(prometheus.DefaultRegisterer)
|
||||
|
||||
log.Info("starting daemon")
|
||||
|
||||
@@ -139,7 +152,8 @@ type Status struct {
|
||||
}
|
||||
|
||||
type GlobalStatus struct {
|
||||
ZFSCmds *zfscmd.Report
|
||||
ZFSCmds *zfscmd.Report
|
||||
Envconst *envconst.Report
|
||||
}
|
||||
|
||||
func (s *jobs) status() map[string]*job.Status {
|
||||
|
||||
+30
-25
@@ -12,7 +12,6 @@ import (
|
||||
"github.com/zrepl/zrepl/daemon/logging/trace"
|
||||
|
||||
"github.com/zrepl/zrepl/config"
|
||||
"github.com/zrepl/zrepl/daemon/filters"
|
||||
"github.com/zrepl/zrepl/daemon/job/reset"
|
||||
"github.com/zrepl/zrepl/daemon/job/wakeup"
|
||||
"github.com/zrepl/zrepl/daemon/pruner"
|
||||
@@ -145,22 +144,24 @@ func (m *modePush) ResetConnectBackoff() {
|
||||
|
||||
func modePushFromConfig(g *config.Global, in *config.PushJob, jobID endpoint.JobID) (*modePush, error) {
|
||||
m := &modePush{}
|
||||
var err error
|
||||
|
||||
fsf, err := filters.DatasetMapFilterFromConfig(in.Filesystems)
|
||||
m.senderConfig, err = buildSenderConfig(in, jobID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "cannot build filesystem filter")
|
||||
return nil, errors.Wrap(err, "sender config")
|
||||
}
|
||||
|
||||
m.senderConfig = &endpoint.SenderConfig{
|
||||
FSF: fsf,
|
||||
Encrypt: &zfs.NilBool{B: in.Send.Encrypted},
|
||||
JobID: jobID,
|
||||
replicationConfig, err := logic.ReplicationConfigFromConfig(in.Replication)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "field `replication`")
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
@@ -172,7 +173,6 @@ type modePull struct {
|
||||
receiver *endpoint.Receiver
|
||||
receiverConfig endpoint.ReceiverConfig
|
||||
sender *rpc.Client
|
||||
rootFS *zfs.DatasetPath
|
||||
plannerPolicy *logic.PlannerPolicy
|
||||
interval config.PositiveDurationOrManual
|
||||
}
|
||||
@@ -246,26 +246,19 @@ func modePullFromConfig(g *config.Global, in *config.PullJob, jobID endpoint.Job
|
||||
m = &modePull{}
|
||||
m.interval = in.Interval
|
||||
|
||||
m.rootFS, err = zfs.NewDatasetPath(in.RootFS)
|
||||
replicationConfig, err := logic.ReplicationConfigFromConfig(in.Replication)
|
||||
if err != nil {
|
||||
return nil, errors.New("RootFS is not a valid zfs filesystem path")
|
||||
}
|
||||
if m.rootFS.Length() <= 0 {
|
||||
return nil, errors.New("RootFS must not be empty") // duplicates error check of receiver
|
||||
return nil, errors.Wrap(err, "field `replication`")
|
||||
}
|
||||
|
||||
m.plannerPolicy = &logic.PlannerPolicy{
|
||||
EncryptedSend: logic.DontCare,
|
||||
EncryptedSend: logic.DontCare,
|
||||
ReplicationConfig: *replicationConfig,
|
||||
}
|
||||
|
||||
m.receiverConfig = endpoint.ReceiverConfig{
|
||||
JobID: jobID,
|
||||
RootWithoutClientComponent: m.rootFS,
|
||||
AppendClientIdentity: false, // !
|
||||
UpdateLastReceivedHold: true,
|
||||
}
|
||||
if err := m.receiverConfig.Validate(); err != nil {
|
||||
return nil, errors.Wrap(err, "cannot build receiver config")
|
||||
m.receiverConfig, err = buildReceiverConfig(in, jobID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return m, nil
|
||||
@@ -364,7 +357,7 @@ func (j *ActiveSide) OwnedDatasetSubtreeRoot() (rfs *zfs.DatasetPath, ok bool) {
|
||||
_ = j.mode.(*modePush) // make sure we didn't introduce a new job type
|
||||
return nil, false
|
||||
}
|
||||
return pull.rootFS.Copy(), true
|
||||
return pull.receiverConfig.RootWithoutClientComponent.Copy(), true
|
||||
}
|
||||
|
||||
func (j *ActiveSide) SenderConfig() *endpoint.SenderConfig {
|
||||
@@ -376,9 +369,21 @@ func (j *ActiveSide) SenderConfig() *endpoint.SenderConfig {
|
||||
return push.senderConfig
|
||||
}
|
||||
|
||||
// The active side of a replication uses one end (sender or receiver)
|
||||
// directly by method invocation, without going through a transport that
|
||||
// provides a client identity.
|
||||
// However, in order to avoid the need to distinguish between direct-method-invocating
|
||||
// clients and RPC client, we use an invalid client identity as a sentinel value.
|
||||
func FakeActiveSideDirectMethodInvocationClientIdentity(jobId endpoint.JobID) string {
|
||||
return fmt.Sprintf("<local><active><job><client><identity><job=%q>", jobId.String())
|
||||
}
|
||||
|
||||
func (j *ActiveSide) Run(ctx context.Context) {
|
||||
ctx, endTask := trace.WithTaskAndSpan(ctx, "active-side-job", j.Name())
|
||||
defer endTask()
|
||||
|
||||
ctx = context.WithValue(ctx, endpoint.ClientIdentityKey, FakeActiveSideDirectMethodInvocationClientIdentity(j.name))
|
||||
|
||||
log := GetLogger(ctx)
|
||||
|
||||
defer log.Info("job exiting")
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package job
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/zrepl/zrepl/endpoint"
|
||||
"github.com/zrepl/zrepl/transport"
|
||||
)
|
||||
|
||||
func TestFakeActiveSideDirectMethodInvocationClientIdentityDoesNotPassValidityTest(t *testing.T) {
|
||||
jobid, err := endpoint.MakeJobID("validjobname")
|
||||
require.NoError(t, err)
|
||||
clientIdentity := FakeActiveSideDirectMethodInvocationClientIdentity(jobid)
|
||||
t.Logf("%v", clientIdentity)
|
||||
err = transport.ValidateClientIdentity(clientIdentity)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
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
|
||||
}
|
||||
@@ -2,12 +2,16 @@ package job
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/kr/pretty"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/zrepl/zrepl/config"
|
||||
"github.com/zrepl/zrepl/transport/tls"
|
||||
)
|
||||
|
||||
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)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+6
-21
@@ -9,7 +9,6 @@ import (
|
||||
"github.com/zrepl/zrepl/daemon/logging/trace"
|
||||
|
||||
"github.com/zrepl/zrepl/config"
|
||||
"github.com/zrepl/zrepl/daemon/filters"
|
||||
"github.com/zrepl/zrepl/daemon/logging"
|
||||
"github.com/zrepl/zrepl/daemon/snapper"
|
||||
"github.com/zrepl/zrepl/endpoint"
|
||||
@@ -48,19 +47,9 @@ func (m *modeSink) SnapperReport() *snapper.Report { return nil }
|
||||
func modeSinkFromConfig(g *config.Global, in *config.SinkJob, jobID endpoint.JobID) (m *modeSink, err error) {
|
||||
m = &modeSink{}
|
||||
|
||||
rootDataset, err := zfs.NewDatasetPath(in.RootFS)
|
||||
m.receiverConfig, err = buildReceiverConfig(in, jobID)
|
||||
if err != nil {
|
||||
return nil, errors.New("root dataset is not a valid zfs filesystem path")
|
||||
}
|
||||
|
||||
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 nil, err
|
||||
}
|
||||
|
||||
return m, nil
|
||||
@@ -74,17 +63,13 @@ type modeSource struct {
|
||||
func modeSourceFromConfig(g *config.Global, in *config.SourceJob, jobID endpoint.JobID) (m *modeSource, err error) {
|
||||
// FIXME exact dedup of modePush
|
||||
m = &modeSource{}
|
||||
fsf, err := filters.DatasetMapFilterFromConfig(in.Filesystems)
|
||||
|
||||
m.senderConfig, err = buildSenderConfig(in, jobID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "cannot build filesystem filter")
|
||||
}
|
||||
m.senderConfig = &endpoint.SenderConfig{
|
||||
FSF: fsf,
|
||||
Encrypt: &zfs.NilBool{B: in.Send.Encrypted},
|
||||
JobID: jobID,
|
||||
return nil, errors.Wrap(err, "send options")
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
|
||||
@@ -73,6 +73,8 @@ const (
|
||||
SubsysRPCControl Subsystem = "rpc.ctrl"
|
||||
SubsysRPCData Subsystem = "rpc.data"
|
||||
SubsysZFSCmd Subsystem = "zfs.cmd"
|
||||
SubsysTraceData Subsystem = "trace.data"
|
||||
SubsysPlatformtest Subsystem = "platformtest"
|
||||
)
|
||||
|
||||
var AllSubsystems = []Subsystem{
|
||||
@@ -89,6 +91,8 @@ var AllSubsystems = []Subsystem{
|
||||
SubsysRPCControl,
|
||||
SubsysRPCData,
|
||||
SubsysZFSCmd,
|
||||
SubsysTraceData,
|
||||
SubsysPlatformtest,
|
||||
}
|
||||
|
||||
type injectedField struct {
|
||||
@@ -157,7 +161,7 @@ func getLoggerImpl(ctx context.Context, subsys Subsystem, panicIfEnded bool) log
|
||||
|
||||
l = l.WithField(SubsysField, subsys)
|
||||
|
||||
l = l.WithField(SpanField, trace.GetSpanStackOrDefault(ctx, "NOSPAN"))
|
||||
l = l.WithField(SpanField, trace.GetSpanStackOrDefault(ctx, *trace.StackKindId, "NOSPAN"))
|
||||
|
||||
fields := make(logger.Fields)
|
||||
iterInjectedFields(ctx, func(field string, value interface{}) {
|
||||
|
||||
@@ -102,9 +102,9 @@ import (
|
||||
)
|
||||
|
||||
var metrics struct {
|
||||
activeTasks prometheus.Gauge
|
||||
uniqueConcurrentTaskNameBitvecLength *prometheus.GaugeVec
|
||||
activeTasks prometheus.Gauge
|
||||
}
|
||||
var taskNamer *uniqueConcurrentTaskNamer = newUniqueTaskNamer()
|
||||
|
||||
func init() {
|
||||
metrics.activeTasks = prometheus.NewGauge(prometheus.GaugeOpts{
|
||||
@@ -113,21 +113,12 @@ func init() {
|
||||
Name: "active_tasks",
|
||||
Help: "number of active (tracing-level) tasks in the daemon",
|
||||
})
|
||||
metrics.uniqueConcurrentTaskNameBitvecLength = prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: "zrepl",
|
||||
Subsystem: "trace",
|
||||
Name: "unique_concurrent_task_name_bitvec_length",
|
||||
Help: "length of the bitvec used to find unique names for concurrent tasks",
|
||||
}, []string{"task_name"})
|
||||
}
|
||||
|
||||
func RegisterMetrics(r prometheus.Registerer) {
|
||||
r.MustRegister(metrics.activeTasks)
|
||||
r.MustRegister(metrics.uniqueConcurrentTaskNameBitvecLength)
|
||||
}
|
||||
|
||||
var taskNamer = newUniqueTaskNamer(metrics.uniqueConcurrentTaskNameBitvecLength)
|
||||
|
||||
type traceNode struct {
|
||||
id string
|
||||
annotation string
|
||||
@@ -143,6 +134,9 @@ type traceNode struct {
|
||||
endedAt time.Time
|
||||
}
|
||||
|
||||
func (s *traceNode) StartedAt() time.Time { return s.startedAt }
|
||||
func (s *traceNode) EndedAt() time.Time { return s.endedAt }
|
||||
|
||||
// Returned from WithTask or WithSpan.
|
||||
// Must be called once the task or span ends.
|
||||
// See package-level docs for nesting rules.
|
||||
@@ -150,7 +144,6 @@ type traceNode struct {
|
||||
type DoneFunc func()
|
||||
|
||||
var ErrTaskStillHasActiveChildTasks = fmt.Errorf("end task: task still has active child tasks")
|
||||
var ErrParentTaskAlreadyEnded = fmt.Errorf("create task: parent task already ended")
|
||||
|
||||
// Start a new root task or create a child task of an existing task.
|
||||
//
|
||||
@@ -174,6 +167,20 @@ func WithTask(ctx context.Context, taskName string) (context.Context, DoneFunc)
|
||||
parentTask = node
|
||||
}
|
||||
}
|
||||
// find the first ancestor that hasn't ended yet (nil if need be)
|
||||
if parentTask != nil {
|
||||
parentTask.mtx.Lock()
|
||||
}
|
||||
for parentTask != nil && !parentTask.endedAt.IsZero() {
|
||||
thisParent := parentTask
|
||||
parentTask = parentTask.parentTask
|
||||
// lock parent first such that it isn't modified by other callers to thisParent
|
||||
if parentTask != nil {
|
||||
parentTask.mtx.Lock()
|
||||
}
|
||||
thisParent.mtx.Unlock()
|
||||
}
|
||||
// invariant: either parentTask != nil and we hold the lock on parentTask, or parentTask is nil
|
||||
|
||||
taskName, taskNameDone := taskNamer.UniqueConcurrentTaskName(taskName)
|
||||
|
||||
@@ -189,13 +196,9 @@ func WithTask(ctx context.Context, taskName string) (context.Context, DoneFunc)
|
||||
endedAt: time.Time{},
|
||||
}
|
||||
|
||||
if this.parentTask != nil {
|
||||
this.parentTask.mtx.HoldWhile(func() {
|
||||
if !this.parentTask.endedAt.IsZero() {
|
||||
panic(ErrParentTaskAlreadyEnded)
|
||||
}
|
||||
this.parentTask.activeChildTasks++
|
||||
})
|
||||
if parentTask != nil {
|
||||
this.parentTask.activeChildTasks++
|
||||
parentTask.mtx.Unlock()
|
||||
}
|
||||
|
||||
ctx = context.WithValue(ctx, contextKeyTraceNode, this)
|
||||
@@ -215,7 +218,7 @@ func WithTask(ctx context.Context, taskName string) (context.Context, DoneFunc)
|
||||
defer this.mtx.Lock().Unlock()
|
||||
|
||||
if this.activeChildTasks != 0 {
|
||||
panic(errors.Wrapf(ErrTaskStillHasActiveChildTasks, "end task: %v active child tasks", this.activeChildSpan))
|
||||
panic(errors.Wrapf(ErrTaskStillHasActiveChildTasks, "end task: %v active child tasks", this.activeChildTasks))
|
||||
}
|
||||
|
||||
// support idempotent task ends
|
||||
@@ -285,6 +288,7 @@ func WithSpan(ctx context.Context, annotation string) (context.Context, DoneFunc
|
||||
|
||||
ctx = context.WithValue(ctx, contextKeyTraceNode, this)
|
||||
chrometraceBeginSpan(this)
|
||||
callbackEndSpan := callbackBeginSpan(ctx)
|
||||
|
||||
endTaskFunc := func() {
|
||||
|
||||
@@ -306,17 +310,47 @@ func WithSpan(ctx context.Context, annotation string) (context.Context, DoneFunc
|
||||
this.endedAt = time.Now()
|
||||
|
||||
chrometraceEndSpan(this)
|
||||
callbackEndSpan(this)
|
||||
}
|
||||
|
||||
return ctx, endTaskFunc
|
||||
}
|
||||
|
||||
func currentTaskNameAndSpanStack(this *traceNode) (taskName string, spanIdStack string) {
|
||||
type StackKind struct {
|
||||
symbolizeTask func(t *traceNode) string
|
||||
symbolizeSpan func(s *traceNode) string
|
||||
}
|
||||
|
||||
task := this.parentTask
|
||||
if this.parentSpan == nil {
|
||||
task = this
|
||||
var (
|
||||
StackKindId = &StackKind{
|
||||
symbolizeTask: func(t *traceNode) string { return t.id },
|
||||
symbolizeSpan: func(s *traceNode) string { return s.id },
|
||||
}
|
||||
SpanStackKindCombined = &StackKind{
|
||||
symbolizeTask: func(t *traceNode) string { return fmt.Sprintf("(%s %q)", t.id, t.annotation) },
|
||||
symbolizeSpan: func(s *traceNode) string { return fmt.Sprintf("(%s %q)", s.id, s.annotation) },
|
||||
}
|
||||
SpanStackKindAnnotation = &StackKind{
|
||||
symbolizeTask: func(t *traceNode) string { return t.annotation },
|
||||
symbolizeSpan: func(s *traceNode) string { return s.annotation },
|
||||
}
|
||||
)
|
||||
|
||||
func (n *traceNode) task() *traceNode {
|
||||
task := n.parentTask
|
||||
if n.parentSpan == nil {
|
||||
task = n
|
||||
}
|
||||
return task
|
||||
}
|
||||
|
||||
func (n *traceNode) TaskName() string {
|
||||
task := n.task()
|
||||
return task.annotation
|
||||
}
|
||||
|
||||
func (this *traceNode) TaskAndSpanStack(kind *StackKind) (spanIdStack string) {
|
||||
task := this.task()
|
||||
|
||||
var spansInTask []*traceNode
|
||||
for s := this; s != nil; s = s.parentSpan {
|
||||
@@ -330,25 +364,22 @@ func currentTaskNameAndSpanStack(this *traceNode) (taskName string, spanIdStack
|
||||
|
||||
var taskIdsRev []string
|
||||
for i := len(tasks) - 1; i >= 0; i-- {
|
||||
taskIdsRev = append(taskIdsRev, tasks[i].id)
|
||||
taskIdsRev = append(taskIdsRev, kind.symbolizeTask(tasks[i]))
|
||||
}
|
||||
|
||||
var spanIdsRev []string
|
||||
for i := len(spansInTask) - 1; i >= 0; i-- {
|
||||
spanIdsRev = append(spanIdsRev, spansInTask[i].id)
|
||||
spanIdsRev = append(spanIdsRev, kind.symbolizeSpan(spansInTask[i]))
|
||||
}
|
||||
|
||||
taskStack := strings.Join(taskIdsRev, "$")
|
||||
spanIdStack = fmt.Sprintf("%s$%s", taskStack, strings.Join(spanIdsRev, "."))
|
||||
|
||||
return task.annotation, spanIdStack
|
||||
return fmt.Sprintf("%s$%s", taskStack, strings.Join(spanIdsRev, "."))
|
||||
}
|
||||
|
||||
func GetSpanStackOrDefault(ctx context.Context, def string) string {
|
||||
func GetSpanStackOrDefault(ctx context.Context, kind StackKind, def string) string {
|
||||
if nI := ctx.Value(contextKeyTraceNode); nI != nil {
|
||||
n := nI.(*traceNode)
|
||||
_, spanStack := currentTaskNameAndSpanStack(n)
|
||||
return spanStack
|
||||
return n.TaskAndSpanStack(StackKindId)
|
||||
} else {
|
||||
return def
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
package trace
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/zrepl/zrepl/util/chainlock"
|
||||
)
|
||||
|
||||
type SpanInfo interface {
|
||||
StartedAt() time.Time
|
||||
EndedAt() time.Time
|
||||
TaskAndSpanStack(kind *StackKind) string
|
||||
}
|
||||
|
||||
type Callback struct {
|
||||
OnBegin func(ctx context.Context)
|
||||
OnEnd func(ctx context.Context, spanInfo SpanInfo)
|
||||
}
|
||||
|
||||
var callbacks struct {
|
||||
mtx chainlock.L
|
||||
cs []Callback
|
||||
}
|
||||
|
||||
func RegisterCallback(c Callback) {
|
||||
callbacks.mtx.HoldWhile(func() {
|
||||
callbacks.cs = append(callbacks.cs, c)
|
||||
})
|
||||
}
|
||||
|
||||
func callbackBeginSpan(ctx context.Context) func(SpanInfo) {
|
||||
// capture the current state of callbacks into a local variable
|
||||
// this is safe because the slice is append-only and immutable
|
||||
|
||||
// (it is important that a callback registered _after_ callbackBeginSpin is called does not get called on OnEnd)
|
||||
|
||||
var cbs []Callback
|
||||
callbacks.mtx.HoldWhile(func() {
|
||||
cbs = callbacks.cs
|
||||
})
|
||||
for _, cb := range cbs {
|
||||
if cb.OnBegin != nil {
|
||||
cb.OnBegin(ctx)
|
||||
}
|
||||
}
|
||||
return func(spanInfo SpanInfo) {
|
||||
for _, cb := range cbs {
|
||||
if cb.OnEnd != nil {
|
||||
cb.OnEnd(ctx, spanInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -51,7 +51,7 @@ type chrometraceEvent struct {
|
||||
}
|
||||
|
||||
func chrometraceBeginSpan(s *traceNode) {
|
||||
taskName, _ := currentTaskNameAndSpanStack(s)
|
||||
taskName := s.TaskName()
|
||||
chrometraceWrite(chrometraceEvent{
|
||||
Name: s.annotation,
|
||||
Phase: "B",
|
||||
@@ -62,7 +62,7 @@ func chrometraceBeginSpan(s *traceNode) {
|
||||
}
|
||||
|
||||
func chrometraceEndSpan(s *traceNode) {
|
||||
taskName, _ := currentTaskNameAndSpanStack(s)
|
||||
taskName := s.TaskName()
|
||||
chrometraceWrite(chrometraceEvent{
|
||||
Name: s.annotation,
|
||||
Phase: "E",
|
||||
@@ -86,7 +86,7 @@ func chrometraceBeginTask(s *traceNode) {
|
||||
flowId := atomic.AddUint64(&chrometraceFlowId, 1)
|
||||
flowIdStr := fmt.Sprintf("%x", flowId)
|
||||
|
||||
parentTask, _ := currentTaskNameAndSpanStack(s.parentTask)
|
||||
parentTask := s.parentTask.TaskName()
|
||||
chrometraceWrite(chrometraceEvent{
|
||||
Cat: "task", // seems to be necessary, otherwise the GUI shows some `indexOf` JS error
|
||||
Name: "child-task",
|
||||
@@ -97,7 +97,7 @@ func chrometraceBeginTask(s *traceNode) {
|
||||
Id: flowIdStr,
|
||||
})
|
||||
|
||||
childTask, _ := currentTaskNameAndSpanStack(s)
|
||||
childTask := s.TaskName()
|
||||
if parentTask == childTask {
|
||||
panic(parentTask)
|
||||
}
|
||||
|
||||
@@ -112,12 +112,6 @@ func TestIdempotentEndTask(t *testing.T) {
|
||||
require.NotPanics(t, func() { end() })
|
||||
}
|
||||
|
||||
func TestCannotReuseEndedTask(t *testing.T) {
|
||||
root, end := WithTask(context.Background(), "root")
|
||||
end()
|
||||
require.PanicsWithValue(t, ErrParentTaskAlreadyEnded, func() { WithTask(root, "child-after-parent-ended") })
|
||||
}
|
||||
|
||||
func TestSpansPanicIfNoParentTask(t *testing.T) {
|
||||
require.Panics(t, func() { WithSpan(context.Background(), "taskless-span") })
|
||||
}
|
||||
@@ -170,3 +164,42 @@ func TestWhiteboxHierachy(t *testing.T) {
|
||||
e4()
|
||||
e1()
|
||||
}
|
||||
|
||||
func TestOrphanTasksBecomeNewRootWhenAllAncestorsAreDead(t *testing.T) {
|
||||
parent, e1 := WithTask(context.Background(), "parent")
|
||||
_ = logAndGetTraceNode(t, "parent-task", parent)
|
||||
e1()
|
||||
|
||||
var child context.Context
|
||||
var e2 DoneFunc
|
||||
require.NotPanics(t, func() {
|
||||
child, e2 = WithTask(parent, "child")
|
||||
})
|
||||
childN := logAndGetTraceNode(t, "child-task", child)
|
||||
assert.Nil(t, childN.parentTask)
|
||||
|
||||
grandchild, _ := WithTask(child, "grandchild")
|
||||
grandchildN := logAndGetTraceNode(t, "grandchild-task", grandchild)
|
||||
assert.Equal(t, childN, grandchildN.parentTask)
|
||||
|
||||
require.Panics(t, func() { e2() }, "if the parent was alive at creation of child task, wrong termination order remains a panicable offense though")
|
||||
}
|
||||
|
||||
func TestOrphanTaskBecomesChildToNearestLiveAncestor(t *testing.T) {
|
||||
parent, e1 := WithTask(context.Background(), "parent")
|
||||
parentN := logAndGetTraceNode(t, "parent-task", parent)
|
||||
|
||||
child, e2 := WithTask(parent, "child")
|
||||
childN := logAndGetTraceNode(t, "child-task", child)
|
||||
assert.Equal(t, parentN, childN.parentTask)
|
||||
e2()
|
||||
|
||||
grandchild, e3 := WithTask(child, "grandchild")
|
||||
grandchildN := logAndGetTraceNode(t, "grandchild-task", grandchild)
|
||||
assert.Equal(t, parentN, grandchildN.parentTask) // child is already dead
|
||||
|
||||
require.NotPanics(t, func() {
|
||||
e3()
|
||||
e1()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5,22 +5,18 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/willf/bitset"
|
||||
)
|
||||
|
||||
type uniqueConcurrentTaskNamer struct {
|
||||
mtx sync.Mutex
|
||||
active map[string]*bitset.BitSet
|
||||
|
||||
bitvecLengthGauge *prometheus.GaugeVec
|
||||
}
|
||||
|
||||
// bitvecLengthGauge may be nil
|
||||
func newUniqueTaskNamer(bitvecLengthGauge *prometheus.GaugeVec) *uniqueConcurrentTaskNamer {
|
||||
func newUniqueTaskNamer() *uniqueConcurrentTaskNamer {
|
||||
return &uniqueConcurrentTaskNamer{
|
||||
active: make(map[string]*bitset.BitSet),
|
||||
bitvecLengthGauge: bitvecLengthGauge,
|
||||
active: make(map[string]*bitset.BitSet),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,10 +41,6 @@ func (namer *uniqueConcurrentTaskNamer) UniqueConcurrentTaskName(name string) (u
|
||||
act.Set(id)
|
||||
namer.mtx.Unlock()
|
||||
|
||||
if namer.bitvecLengthGauge != nil {
|
||||
namer.bitvecLengthGauge.WithLabelValues(name).Set(float64(act.Len()))
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s#%d", name, id), func() {
|
||||
namer.mtx.Lock()
|
||||
defer namer.mtx.Unlock()
|
||||
|
||||
@@ -29,7 +29,7 @@ func TestBitsetFeaturesForUniqueConcurrentTaskNamer(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestUniqueConcurrentTaskNamer(t *testing.T) {
|
||||
namer := newUniqueTaskNamer(nil)
|
||||
namer := newUniqueTaskNamer()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
|
||||
+3
-1
@@ -8,10 +8,11 @@ import (
|
||||
"net"
|
||||
"net/http/pprof"
|
||||
|
||||
"github.com/zrepl/zrepl/daemon/logging/trace"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"golang.org/x/net/websocket"
|
||||
|
||||
"github.com/zrepl/zrepl/daemon/job"
|
||||
"github.com/zrepl/zrepl/daemon/logging/trace"
|
||||
)
|
||||
|
||||
type pprofServer struct {
|
||||
@@ -67,6 +68,7 @@ outer:
|
||||
mux.Handle("/debug/pprof/profile", http.HandlerFunc(pprof.Profile))
|
||||
mux.Handle("/debug/pprof/symbol", http.HandlerFunc(pprof.Symbol))
|
||||
mux.Handle("/debug/pprof/trace", http.HandlerFunc(pprof.Trace))
|
||||
mux.Handle("/metrics", promhttp.Handler())
|
||||
mux.Handle("/debug/zrepl/activity-trace", websocket.Handler(trace.ChrometraceClientWebsocketHandler))
|
||||
go func() {
|
||||
err := http.Serve(s.listener, mux)
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"github.com/zrepl/zrepl/daemon/logging/trace"
|
||||
|
||||
"github.com/zrepl/zrepl/config"
|
||||
"github.com/zrepl/zrepl/daemon/filters"
|
||||
"github.com/zrepl/zrepl/daemon/hooks"
|
||||
"github.com/zrepl/zrepl/daemon/logging"
|
||||
"github.com/zrepl/zrepl/logger"
|
||||
@@ -49,7 +48,7 @@ type args struct {
|
||||
ctx context.Context
|
||||
prefix string
|
||||
interval time.Duration
|
||||
fsf *filters.DatasetMapFilter
|
||||
fsf zfs.DatasetFilter
|
||||
snapshotsTaken chan<- struct{}
|
||||
hooks *hooks.List
|
||||
dryRun bool
|
||||
@@ -109,7 +108,7 @@ func getLogger(ctx context.Context) Logger {
|
||||
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 == "" {
|
||||
return nil, errors.New("prefix must not be empty")
|
||||
}
|
||||
@@ -383,7 +382,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)
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/zrepl/zrepl/config"
|
||||
"github.com/zrepl/zrepl/daemon/filters"
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
|
||||
// FIXME: properly abstract snapshotting:
|
||||
@@ -32,7 +32,7 @@ func (s *PeriodicOrManual) Report() *Report {
|
||||
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) {
|
||||
case *config.SnapshottingPeriodic:
|
||||
snapper, err := PeriodicFromConfig(g, fsf, v)
|
||||
|
||||
-595
@@ -1,595 +0,0 @@
|
||||
{
|
||||
"annotations": {
|
||||
"list": [
|
||||
{
|
||||
"$$hashKey": "object:3351",
|
||||
"builtIn": 1,
|
||||
"datasource": "-- Grafana --",
|
||||
"enable": true,
|
||||
"hide": true,
|
||||
"iconColor": "rgba(0, 211, 255, 1)",
|
||||
"name": "Annotations & Alerts",
|
||||
"type": "dashboard"
|
||||
}
|
||||
]
|
||||
},
|
||||
"editable": true,
|
||||
"gnetId": null,
|
||||
"graphTooltip": 1,
|
||||
"id": 7,
|
||||
"iteration": 1552746418826,
|
||||
"links": [],
|
||||
"panels": [
|
||||
{
|
||||
"content": "# zrepl Prometheus Metrics\n\nzrepl exposes Prometheus metrics and ships with this Grafana dashboard.\nThe exported metrics are suitable for health checks:\n\n* The log should generally be warning & error-free\n * The `Log Messages that require attention` graph visualizes log message counts indicating problems.\n* The number of goroutines should not grow unboundedly over time.\n * During replication, the number of goroutines can be way higher than during idle time.\n * If the goroutine count grows with each replication, there is clearly a goroutine leak. Please open a bug report.\n* The sys memory consumption should not grow unboundedly over time.\n * Note that the Go runtime pre-allocates some of its heap from the OS.\n * zrepl actually uses much less memory than allocated from the OS.\n * Since Go 1.11, Go pre-allocates more aggressively.\n* Monitor that some data is replicated, although that metric does not guarantee that replication was successful.\n\n**In general, note that the exported metrics are not stable unless declared otherwise.**",
|
||||
"gridPos": {
|
||||
"h": 9,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"id": 35,
|
||||
"mode": "markdown",
|
||||
"title": "Panel Title",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"aliasColors": {},
|
||||
"bars": false,
|
||||
"dashLength": 10,
|
||||
"dashes": false,
|
||||
"datasource": null,
|
||||
"fill": 1,
|
||||
"gridPos": {
|
||||
"h": 9,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 9
|
||||
},
|
||||
"id": 15,
|
||||
"legend": {
|
||||
"avg": false,
|
||||
"current": false,
|
||||
"max": false,
|
||||
"min": false,
|
||||
"show": true,
|
||||
"total": false,
|
||||
"values": false
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [],
|
||||
"spaceLength": 10,
|
||||
"stack": true,
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"$$hashKey": "object:3436",
|
||||
"expr": "up{job='$prom_job_name'}",
|
||||
"format": "time_series",
|
||||
"intervalFactor": 1,
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"thresholds": [],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "zrepl Instances Up",
|
||||
"tooltip": {
|
||||
"shared": true,
|
||||
"sort": 0,
|
||||
"value_type": "individual"
|
||||
},
|
||||
"type": "graph",
|
||||
"xaxis": {
|
||||
"buckets": null,
|
||||
"mode": "time",
|
||||
"name": null,
|
||||
"show": true,
|
||||
"values": []
|
||||
},
|
||||
"yaxes": [
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": "5",
|
||||
"min": "0",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"aliasColors": {},
|
||||
"bars": false,
|
||||
"dashLength": 10,
|
||||
"dashes": false,
|
||||
"datasource": null,
|
||||
"fill": 1,
|
||||
"gridPos": {
|
||||
"h": 9,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 9
|
||||
},
|
||||
"id": 22,
|
||||
"legend": {
|
||||
"avg": false,
|
||||
"current": false,
|
||||
"max": false,
|
||||
"min": false,
|
||||
"show": true,
|
||||
"total": false,
|
||||
"values": false
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [],
|
||||
"spaceLength": 10,
|
||||
"stack": false,
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"expr": "increase(zrepl_daemon_log_entries{job='$prom_job_name',level=~'warn|error'}[$__interval])",
|
||||
"format": "time_series",
|
||||
"intervalFactor": 1,
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"thresholds": [],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Log Messages that require attention",
|
||||
"tooltip": {
|
||||
"shared": true,
|
||||
"sort": 0,
|
||||
"value_type": "individual"
|
||||
},
|
||||
"type": "graph",
|
||||
"xaxis": {
|
||||
"buckets": null,
|
||||
"mode": "time",
|
||||
"name": null,
|
||||
"show": true,
|
||||
"values": []
|
||||
},
|
||||
"yaxes": [
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": "0",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"aliasColors": {},
|
||||
"bars": false,
|
||||
"dashLength": 10,
|
||||
"dashes": false,
|
||||
"datasource": null,
|
||||
"fill": 1,
|
||||
"gridPos": {
|
||||
"h": 9,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 18
|
||||
},
|
||||
"id": 33,
|
||||
"legend": {
|
||||
"avg": false,
|
||||
"current": false,
|
||||
"max": false,
|
||||
"min": false,
|
||||
"show": true,
|
||||
"total": false,
|
||||
"values": false
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [
|
||||
{
|
||||
"alias": "/replicated bytes in last.*/",
|
||||
"yaxis": 2
|
||||
}
|
||||
],
|
||||
"spaceLength": 10,
|
||||
"stack": false,
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"expr": "sum(rate(zrepl_replication_bytes_replicated{job='$prom_job_name'}[$__interval])) by (zrepl_job)",
|
||||
"format": "time_series",
|
||||
"hide": false,
|
||||
"interval": "",
|
||||
"intervalFactor": 1,
|
||||
"legendFormat": "replication data rate zrepl_job={{zrepl_job}}",
|
||||
"refId": "A"
|
||||
},
|
||||
{
|
||||
"expr": "sum(increase(zrepl_replication_bytes_replicated{job='$prom_job_name'}[10m])) by (zrepl_job)",
|
||||
"format": "time_series",
|
||||
"hide": false,
|
||||
"interval": "",
|
||||
"intervalFactor": 1,
|
||||
"legendFormat": "replicated bytes in last 10min zrepl_job={{zrepl_job}}",
|
||||
"refId": "B"
|
||||
}
|
||||
],
|
||||
"thresholds": [],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Replication Data Rate and Volume(integrates last 10min)",
|
||||
"tooltip": {
|
||||
"shared": true,
|
||||
"sort": 0,
|
||||
"value_type": "individual"
|
||||
},
|
||||
"type": "graph",
|
||||
"xaxis": {
|
||||
"buckets": null,
|
||||
"mode": "time",
|
||||
"name": null,
|
||||
"show": true,
|
||||
"values": []
|
||||
},
|
||||
"yaxes": [
|
||||
{
|
||||
"format": "Bps",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"format": "bytes",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"aliasColors": {},
|
||||
"bars": false,
|
||||
"dashLength": 10,
|
||||
"dashes": false,
|
||||
"datasource": null,
|
||||
"fill": 1,
|
||||
"gridPos": {
|
||||
"h": 9,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 18
|
||||
},
|
||||
"id": 23,
|
||||
"legend": {
|
||||
"avg": false,
|
||||
"current": false,
|
||||
"max": false,
|
||||
"min": false,
|
||||
"show": true,
|
||||
"total": false,
|
||||
"values": false
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [],
|
||||
"spaceLength": 10,
|
||||
"stack": false,
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"expr": "sum(increase(zrepl_daemon_log_entries{job='$prom_job_name',zrepl_job=~\"^[^_].*\"}[$__interval])) by (instance,zrepl_job)",
|
||||
"format": "time_series",
|
||||
"intervalFactor": 1,
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"thresholds": [],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Log Activity (without internal jobs)",
|
||||
"tooltip": {
|
||||
"shared": true,
|
||||
"sort": 0,
|
||||
"value_type": "individual"
|
||||
},
|
||||
"type": "graph",
|
||||
"xaxis": {
|
||||
"buckets": null,
|
||||
"mode": "time",
|
||||
"name": null,
|
||||
"show": true,
|
||||
"values": []
|
||||
},
|
||||
"yaxes": [
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": "0",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"aliasColors": {},
|
||||
"bars": false,
|
||||
"dashLength": 10,
|
||||
"dashes": false,
|
||||
"datasource": null,
|
||||
"fill": 1,
|
||||
"gridPos": {
|
||||
"h": 9,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 27
|
||||
},
|
||||
"id": 17,
|
||||
"legend": {
|
||||
"avg": false,
|
||||
"current": false,
|
||||
"max": false,
|
||||
"min": false,
|
||||
"show": true,
|
||||
"total": false,
|
||||
"values": false
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [],
|
||||
"spaceLength": 10,
|
||||
"stack": false,
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"$$hashKey": "object:3535",
|
||||
"expr": "go_memstats_sys_bytes{job='$prom_job_name'}",
|
||||
"format": "time_series",
|
||||
"hide": false,
|
||||
"intervalFactor": 1,
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"thresholds": [],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Memory Allocated by the Go runtime from the OS (should not grow unboundedly)",
|
||||
"tooltip": {
|
||||
"shared": true,
|
||||
"sort": 0,
|
||||
"value_type": "individual"
|
||||
},
|
||||
"type": "graph",
|
||||
"xaxis": {
|
||||
"buckets": null,
|
||||
"mode": "time",
|
||||
"name": null,
|
||||
"show": true,
|
||||
"values": []
|
||||
},
|
||||
"yaxes": [
|
||||
{
|
||||
"format": "bytes",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": "0",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"aliasColors": {},
|
||||
"bars": false,
|
||||
"dashLength": 10,
|
||||
"dashes": false,
|
||||
"datasource": null,
|
||||
"fill": 1,
|
||||
"gridPos": {
|
||||
"h": 9,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 27
|
||||
},
|
||||
"id": 19,
|
||||
"legend": {
|
||||
"avg": false,
|
||||
"current": false,
|
||||
"max": false,
|
||||
"min": false,
|
||||
"show": true,
|
||||
"total": false,
|
||||
"values": false
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [],
|
||||
"spaceLength": 10,
|
||||
"stack": false,
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"expr": "go_goroutines{job='$prom_job_name'}",
|
||||
"format": "time_series",
|
||||
"intervalFactor": 1,
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"thresholds": [],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "number of goroutines (should not grow unboundedly)",
|
||||
"tooltip": {
|
||||
"shared": true,
|
||||
"sort": 0,
|
||||
"value_type": "individual"
|
||||
},
|
||||
"type": "graph",
|
||||
"xaxis": {
|
||||
"buckets": null,
|
||||
"mode": "time",
|
||||
"name": null,
|
||||
"show": true,
|
||||
"values": []
|
||||
},
|
||||
"yaxes": [
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": "0",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"refresh": "1m",
|
||||
"schemaVersion": 16,
|
||||
"style": "dark",
|
||||
"tags": [],
|
||||
"templating": {
|
||||
"list": [
|
||||
{
|
||||
"allValue": null,
|
||||
"current": {
|
||||
"text": "zrepl",
|
||||
"value": "zrepl"
|
||||
},
|
||||
"datasource": "prometheus",
|
||||
"hide": 0,
|
||||
"includeAll": false,
|
||||
"label": "Prometheus Job Name",
|
||||
"multi": false,
|
||||
"name": "prom_job_name",
|
||||
"options": [],
|
||||
"query": "label_values(up, job)",
|
||||
"refresh": 1,
|
||||
"regex": "",
|
||||
"sort": 1,
|
||||
"tagValuesQuery": "",
|
||||
"tags": [],
|
||||
"tagsQuery": "",
|
||||
"type": "query",
|
||||
"useTags": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"time": {
|
||||
"from": "now-2d",
|
||||
"to": "now"
|
||||
},
|
||||
"timepicker": {
|
||||
"refresh_intervals": [
|
||||
"5s",
|
||||
"10s",
|
||||
"30s",
|
||||
"1m",
|
||||
"5m",
|
||||
"15m",
|
||||
"30m",
|
||||
"1h",
|
||||
"2h",
|
||||
"1d"
|
||||
],
|
||||
"time_options": [
|
||||
"5m",
|
||||
"15m",
|
||||
"1h",
|
||||
"6h",
|
||||
"12h",
|
||||
"24h",
|
||||
"2d",
|
||||
"7d",
|
||||
"30d"
|
||||
]
|
||||
},
|
||||
"timezone": "",
|
||||
"title": "zrepl 0.1",
|
||||
"uid": "xTljn4qmk",
|
||||
"version": 6
|
||||
}
|
||||
+1081
File diff suppressed because it is too large
Load Diff
+34
-21
@@ -34,38 +34,59 @@ This is a big one! Headlining features:
|
||||
|
||||
* **Resumable Send & Recv Support**
|
||||
No knobs required, automatically used where supported.
|
||||
* **Hold-Protected Send & Recv**
|
||||
Automatic ZFS holds to ensure that we can always resume a replication step.
|
||||
* **Encrypted Send & Recv Support** for OpenZFS native encryption.
|
||||
: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**
|
||||
The counterpart to the replication cursor bookmark on the send-side.
|
||||
Ensures that incremental replication will always be possible between a sender and receiver.
|
||||
* **Encrypted Send & Recv Support** for OpenZFS native encryption,
|
||||
:ref:`configurable <job-send-options>` at the job level, i.e., for all filesystems a job is responsible for.
|
||||
* **Replication Guarantees**
|
||||
Automatic use of ZFS holds and bookmarks to protect a replicated filesystem from losing synchronization between sender and receiver.
|
||||
By default, zrepl guarantees that incremental replication will always be possible and interrupted steps will always be resumable.
|
||||
|
||||
Actual changelog:
|
||||
.. TIP::
|
||||
|
||||
* |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.
|
||||
We highly recommend studying the updated :ref:`overview section of the configuration chapter <overview-how-replication-works>` to understand how replication works.
|
||||
|
||||
Quick-start guides:
|
||||
|
||||
* We have added :ref:`another quick-start guide for a typical workstation use case for zrepl <quickstart-backup-to-external-disk>`.
|
||||
Check it out to learn how you can use zrepl to back up your workstation's OpenZFS natively-encrypted root filesystem to an external disk.
|
||||
|
||||
Additional changelog:
|
||||
|
||||
* |break| |break_config| **more restrictive job names than in prior zrepl versions**
|
||||
Starting with this version, job names are going to be embedded into ZFS holds and bookmark names (see :ref:`this section for details <zrepl-zfs-abstractions>`).
|
||||
Therefore you might need to adjust your job names.
|
||||
**Note that jobs** cannot be renamed easily **once you start using zrepl 0.3.**
|
||||
* |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.
|
||||
* ZFS does not allow bookmark renaming, thus we cannot migrate the old replication cursors.
|
||||
* zrepl 0.3 will automatically create cursors in the new format for new replications, and warn if it still finds ones in the old format.
|
||||
* Run ``zrepl migrate replication-cursor:v1-v2`` to safely destroy old-format cursors.
|
||||
The migration will ensure that only those old-format cursors are destroyed that have been superseeded by new-format cursors.
|
||||
|
||||
* |feature| New option ``listen_freebind`` (tcp, tls, prometheus listener)
|
||||
* |feature| :issue:`265` transport/tcp: support for CIDR masks in client IP whitelist
|
||||
* |feature| documented subcommand to generate ``bash`` and ``zsh`` completions
|
||||
* |feature| :issue:`307` ``chrome://trace`` -compatible activity tracing of zrepl daemon activity
|
||||
* |feature| logging: trace IDs for better log entry correlation with concurrent replication jobs
|
||||
* |feature| experimental environment variable for parallel replication (see :issue:`306` )
|
||||
* |bugfix| missing logger context vars in control connection handlers
|
||||
* |bugfix| improved error messages on ``zfs send`` errors
|
||||
* |feature| New option ``listen_freebind`` (tcp, tls, prometheus listener)
|
||||
* |bugfix| |docs| snapshotting: clarify sync-up behavior and warn about filesystems
|
||||
* |bugfix| transport/ssh: do not leak zombie ssh process on connection failures
|
||||
that will not be snapshotted until the sync-up phase is over
|
||||
* |docs| Installation: :ref:`FreeBSD jail with iocage <installation-freebsd-jail-with-iocage>`
|
||||
* |docs| Document new replication features in the :ref:`config overview <overview-how-replication-works>` and :repomasterlink:`replication/design.md`.
|
||||
* |feature| documented subcommand to generate ``bash`` and ``zsh`` completions
|
||||
* **[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.
|
||||
|
||||
.. 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
|
||||
-----
|
||||
|
||||
@@ -102,14 +123,6 @@ Actual changelog:
|
||||
The pool name ``zreplplatformtest`` is reserved for this use case.
|
||||
Only run ``make platformtest`` on test systems, e.g. a FreeBSD VM image.
|
||||
|
||||
.. 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 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.1.1
|
||||
-----
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ Configuration
|
||||
configuration/transports
|
||||
configuration/filter_syntax
|
||||
configuration/sendrecvoptions
|
||||
configuration/replication
|
||||
configuration/snapshotting
|
||||
configuration/prune
|
||||
configuration/logging
|
||||
|
||||
@@ -19,7 +19,7 @@ Job Type ``push``
|
||||
* - ``type``
|
||||
- = ``push``
|
||||
* - ``name``
|
||||
- unique name of the job
|
||||
- unique name of the job :issue:`(must not change)<327>`
|
||||
* - ``connect``
|
||||
- |connect-transport|
|
||||
* - ``filesystems``
|
||||
@@ -47,7 +47,7 @@ Job Type ``sink``
|
||||
* - ``type``
|
||||
- = ``sink``
|
||||
* - ``name``
|
||||
- unique name of the job
|
||||
- unique name of the job :issue:`(must not change)<327>`
|
||||
* - ``serve``
|
||||
- |serve-transport|
|
||||
* - ``root_fs``
|
||||
@@ -70,7 +70,7 @@ Job Type ``pull``
|
||||
* - ``type``
|
||||
- = ``pull``
|
||||
* - ``name``
|
||||
- unique name of the job
|
||||
- unique name of the job :issue:`(must not change)<327>`
|
||||
* - ``connect``
|
||||
- |connect-transport|
|
||||
* - ``root_fs``
|
||||
@@ -98,7 +98,7 @@ Job Type ``source``
|
||||
* - ``type``
|
||||
- = ``source``
|
||||
* - ``name``
|
||||
- unique name of the job
|
||||
- unique name of the job :issue:`(must not change)<327>`
|
||||
* - ``serve``
|
||||
- |serve-transport|
|
||||
* - ``filesystems``
|
||||
@@ -137,7 +137,7 @@ Job type that only takes snapshots and performs pruning on the local machine.
|
||||
* - ``type``
|
||||
- = ``snap``
|
||||
* - ``name``
|
||||
- unique name of the job
|
||||
- unique name of the job :issue:`(must not change)<327>`
|
||||
* - ``filesystems``
|
||||
- |filter-spec| for filesystems to be snapshotted
|
||||
* - ``snapshotting``
|
||||
|
||||
@@ -12,7 +12,7 @@ The following paths are considered:
|
||||
The ``zrepl configcheck`` subcommand can be used to validate the configuration.
|
||||
The command will output nothing and exit with zero status code if the configuration is valid.
|
||||
The error messages vary in quality and usefulness: please report confusing config errors to the tracking :issue:`155`.
|
||||
Full example configs such as in the :ref:`tutorial` or the :sampleconf:`/` directory might also be helpful.
|
||||
Full example configs such as in the :ref:`quick-start guides <quickstart-toc>` or the :sampleconf:`/` directory might also be helpful.
|
||||
However, copy-pasting examples is no substitute for reading documentation!
|
||||
|
||||
Config File Structure
|
||||
@@ -39,6 +39,10 @@ A *job* is the unit of activity tracked by the zrepl daemon.
|
||||
The ``type`` of a job determines its role in a replication setup and in snapshot management.
|
||||
Jobs are identified by their ``name``, both in log files and the ``zrepl status`` command.
|
||||
|
||||
.. NOTE::
|
||||
The job name is persisted in several places on disk and thus :issue:`cannot be changed easily<327>`.
|
||||
|
||||
|
||||
Replication always happens between a pair of jobs: one is the **active side**, and one the **passive side**.
|
||||
The active side connects to the passive side using a :ref:`transport <transport>` and starts executing the replication logic.
|
||||
The passive side responds to requests from the active side after checking its permissions.
|
||||
@@ -99,7 +103,7 @@ One of the major design goals of the replication module is to avoid any duplicat
|
||||
As such, the code works on abstract senders and receiver **endpoints**, where typically one will be implemented by a local program object and the other is an RPC client instance.
|
||||
Regardless of push- or pull-style setup, the logic executes on the active side, i.e. in the ``push`` or ``pull`` job.
|
||||
|
||||
The following steps take place during replication and can be monitored using the ``zrepl status`` subcommand:
|
||||
The following high-level steps take place during replication and can be monitored using the ``zrepl status`` subcommand:
|
||||
|
||||
* Plan the replication:
|
||||
|
||||
@@ -107,9 +111,9 @@ The following steps take place during replication and can be monitored using the
|
||||
* Build the **replication plan**
|
||||
|
||||
* Per filesystem, compute a diff between sender and receiver snapshots
|
||||
* Build a list of replication steps
|
||||
* Build a list of **replication steps**
|
||||
|
||||
* If possible, use incremental and resumable sends (``zfs send -i``)
|
||||
* If possible, use incremental and resumable sends
|
||||
* Otherwise, use full send of most recent snapshot on sender
|
||||
|
||||
* Retry on errors that are likely temporary (i.e. network failures).
|
||||
@@ -128,15 +132,35 @@ The following steps take place during replication and can be monitored using the
|
||||
|
||||
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.
|
||||
|
||||
.. _replication-cursor-and-last-received-hold:
|
||||
ZFS Background Knowledge
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
This section gives some background knowledge about ZFS features that zrepl uses to provide guarantees for a replication filesystem.
|
||||
Specifically, zrepl guarantees by default that **incremental replication is always possible and that started replication steps can always be resumed if they are interrupted.**
|
||||
|
||||
**Replication cursor** bookmark and **last-received-hold** are managed by zrepl to ensure that future replications can always be done incrementally:
|
||||
the replication cursor is a send-side bookmark of the most recent successfully replicated snapshot,
|
||||
and the last-received-hold is a hold of that snapshot on the receiving side.
|
||||
The replication cursor has the format ``#zrepl_CUSOR_G_<GUID>_J_<JOBNAME>``.
|
||||
The last-received-hold tag has the format ``#zrepl_last_received_J_<JOBNAME>``.
|
||||
Encoding the job name in the names ensures that multiple sending jobs can replicate the same filesystem to different receivers without interference.
|
||||
The ``zrepl holds list`` provides a listing of all bookmarks and holds managed by zrepl.
|
||||
**ZFS Send Modes & Bookmarks**
|
||||
ZFS supports full sends (``zfs send fs@to``) and incremental sends (``zfs send -i @from fs@to``).
|
||||
Full sends are used to create a new filesystem on the receiver with the send-side state of ``fs@to``.
|
||||
Incremental sends only transfer the delta between ``@from`` and ``@to``.
|
||||
Incremental sends require that ``@from`` be present on the receiving side when receiving the incremental stream.
|
||||
Incremental sends can also use a ZFS bookmark as *from* on the sending side (``zfs send -i #bm_from fs@to``), where ``#bm_from`` was created using ``zfs bookmark fs@from fs#bm_from``.
|
||||
The receiving side must always have the actual snapshot ``@from``, regardless of whether the sending side uses ``@from`` or a bookmark of it.
|
||||
|
||||
**Resumable Send & Recv**
|
||||
The ``-s`` flag for ``zfs recv`` tells zfs to save the partially received send stream in case it is interrupted.
|
||||
To resume the replication, the receiving side filesystem's ``receive_resume_token`` must be passed to a new ``zfs send -t <value> | zfs recv`` command.
|
||||
A full send can only be resumed if ``@to`` still exists.
|
||||
An incremental send can only be resumed if ``@to`` still exists *and* either ``@from`` still exists *or* a bookmark ``#fbm`` of ``@from`` still exists.
|
||||
|
||||
**ZFS Holds**
|
||||
ZFS holds prevent a snapshot from being deleted through ``zfs destroy``, letting the destroy fail with a ``datset is busy`` error.
|
||||
Holds are created and referred to by a *tag*. They can be thought of as a named, persistent lock on the snapshot.
|
||||
|
||||
|
||||
.. _zrepl-zfs-abstractions:
|
||||
|
||||
ZFS Abstractions Managed By zrepl
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
With the background knowledge from the previous paragraph, we now summarize the different on-disk ZFS objects that zrepl manages to provide its functionality.
|
||||
|
||||
.. _replication-placeholder-property:
|
||||
|
||||
@@ -148,16 +172,57 @@ Thus, zrepl creates the parent filesystems as placeholders on the receiving side
|
||||
If at some point ``S/H`` and ``S`` shall be replicated, the receiving side invalidates the placeholder flag automatically.
|
||||
The ``zrepl test placeholder`` command can be used to check whether a filesystem is a placeholder.
|
||||
|
||||
.. _replication-cursor-and-last-received-hold:
|
||||
|
||||
The **replication cursor** bookmark and **last-received-hold** are managed by zrepl to ensure that future replications can always be done incrementally.
|
||||
The replication cursor is a send-side bookmark of the most recent successfully replicated snapshot,
|
||||
and the last-received-hold is a hold of that snapshot on the receiving side.
|
||||
Both are moved atomically after the receiving side has confirmed that a replication step is complete.
|
||||
|
||||
The replication cursor has the format ``#zrepl_CUSOR_G_<GUID>_J_<JOBNAME>``.
|
||||
The last-received-hold tag has the format ``zrepl_last_received_J_<JOBNAME>``.
|
||||
Encoding the job name in the names ensures that multiple sending jobs can replicate the same filesystem to different receivers without interference.
|
||||
|
||||
.. _tentative-replication-cursor-bookmarks:
|
||||
|
||||
**Tentative replication cursor bookmarks** are short-lived 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.
|
||||
|
||||
.. _step-holds:
|
||||
|
||||
**Step holds** are zfs holds managed by zrepl to ensure that a replication step can always be resumed if it is interrupted, e.g., due to network outage.
|
||||
zrepl creates step holds before it attempts a replication step and releases them after the receiver confirms that the replication step is complete.
|
||||
For an initial replication ``full @initial_snap``, zrepl puts a zfs hold on ``@initial_snap``.
|
||||
For an incremental send ``@from -> @to``, zrepl puts a zfs hold on both ``@from`` and ``@to``.
|
||||
Note that ``@from`` is not strictly necessary for resumability -- a bookmark on the sending side would be sufficient --, but size-estimation in currently used OpenZFS versions only works if ``@from`` is a snapshot.
|
||||
The hold tag has the format ``zrepl_STEP_J_<JOBNAME>``.
|
||||
A job only ever has one active send per filesystem.
|
||||
Thus, there are never more than two step holds for a given pair of ``(job,filesystem)``.
|
||||
|
||||
**Step bookmarks** are zrepl's equivalent for holds on bookmarks (ZFS does not support putting holds on bookmarks).
|
||||
They are intended for a situation where a replication step uses a bookmark ``#bm`` as incremental ``from`` where ``#bm`` is not managed by zrepl.
|
||||
To ensure resumability, zrepl copies ``#bm`` to step bookmark ``#zrepl_STEP_G_<GUID>_J_<JOBNAME>``.
|
||||
If the replication is interrupted and ``#bm`` is deleted by the user, the step bookmark remains as an incremental source for the resumable send.
|
||||
Note that zrepl does not yet support creating step bookmarks because the `corresponding ZFS feature for copying bookmarks <https://github.com/openzfs/zfs/pull/9571>`_ is not yet widely available .
|
||||
Subscribe to zrepl :issue:`326` for details.
|
||||
|
||||
The ``zrepl zfs-abstraction list`` command provides a listing of all bookmarks and holds managed by zrepl.
|
||||
|
||||
.. NOTE::
|
||||
|
||||
More details can be found in the design document :repomasterlink:`replication/design.md`.
|
||||
|
||||
Limitations
|
||||
^^^^^^^^^^^
|
||||
|
||||
.. ATTENTION::
|
||||
|
||||
Currently, zrepl does not replicate filesystem properties.
|
||||
When receiving a filesystem, it is never mounted (`-u` flag) and `mountpoint=none` is set.
|
||||
This is temporary and being worked on :issue:`24`.
|
||||
|
||||
.. NOTE::
|
||||
|
||||
More details can be found in the design document :repomasterlink:`replication/design.md`.
|
||||
|
||||
|
||||
.. _jobs-multiple-jobs:
|
||||
|
||||
@@ -177,7 +242,7 @@ Multiple Jobs & More than 2 Machines
|
||||
If you would like to see improvements to multi-job setups, please `open an issue on GitHub <https://github.com/zrepl/zrepl/issues/new>`_.
|
||||
|
||||
No Overlapping
|
||||
~~~~~~~~~~~~~~
|
||||
^^^^^^^^^^^^^^
|
||||
|
||||
Jobs run independently of each other.
|
||||
If two jobs match the same filesystem with their ``filesystems`` filter, they will operate on that filesystem independently and potentially in parallel.
|
||||
@@ -185,14 +250,14 @@ For example, if job A prunes snapshots that job B is planning to replicate, the
|
||||
However, the next replication attempt will re-examine the situation from scratch and should work.
|
||||
|
||||
N push jobs to 1 sink
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
The :ref:`sink job <job-sink>` namespaces by client identity.
|
||||
It is thus safe to push to one sink job with different client identities.
|
||||
If the push jobs have the same client identity, the filesystems matched by the push jobs must be disjoint to avoid races.
|
||||
|
||||
N pull jobs from 1 source
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Multiple pull jobs pulling from the same source have potential for race conditions during pruning:
|
||||
each pull job prunes the source side independently, causing replication-prune and prune-prune races.
|
||||
|
||||
@@ -46,6 +46,8 @@ Example Configuration:
|
||||
grid: 1x1h(keep=all) | 24x1h | 35x1d | 6x30d
|
||||
regex: "^zrepl_.*"
|
||||
# manually created snapshots will be kept forever on receiver
|
||||
- type: regex
|
||||
regex: "^manual_.*"
|
||||
|
||||
.. DANGER::
|
||||
You might have **existing snapshots** of filesystems affected by pruning which you want to keep, i.e. not be destroyed by zrepl.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -52,8 +52,15 @@ Serve
|
||||
listen: ":8888"
|
||||
listen_freebind: true # optional, default false
|
||||
clients: {
|
||||
"192.168.122.123" : "mysql01"
|
||||
"192.168.122.123" : "mx01"
|
||||
"192.168.122.123" : "mysql01",
|
||||
"192.168.122.42" : "mx01",
|
||||
"2001:0db8:85a3::8a2e:0370:7334": "gateway",
|
||||
|
||||
# CIDR masks require a '*' in the client identity string
|
||||
# that is expanded to the client's IP address
|
||||
|
||||
"10.23.42.0/24": "cluster-*"
|
||||
"fde4:8dba:82e1::/64": "san-*"
|
||||
}
|
||||
...
|
||||
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import subprocess
|
||||
import re
|
||||
|
||||
output = subprocess.run(["git", "tag", "-l"], capture_output=True, check=True, text=True)
|
||||
tagRE = re.compile(r"^v(?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(-rc(?P<rc>\d+))?$")
|
||||
class Tag:
|
||||
orig: str
|
||||
major: int
|
||||
minor: int
|
||||
patch: int
|
||||
rc: int
|
||||
|
||||
def __str__(self):
|
||||
return self.orig
|
||||
|
||||
def __repr__(self):
|
||||
return self.orig
|
||||
|
||||
tags = []
|
||||
for line in output.stdout.split("\n"):
|
||||
m = tagRE.match(line)
|
||||
if not m:
|
||||
continue
|
||||
m = m.groupdict()
|
||||
|
||||
t = Tag()
|
||||
t.orig = line
|
||||
t.major = int(m["major"])
|
||||
t.minor = int(m["minor"])
|
||||
t.patch = int(m["patch"])
|
||||
t.rc = int(m["rc"] if m["rc"] is not None else 0)
|
||||
|
||||
tags.append(t)
|
||||
|
||||
by_major_minor = {}
|
||||
|
||||
for tag in tags:
|
||||
key = (tag.major, tag.minor)
|
||||
l = by_major_minor.get(key, [])
|
||||
l.append(tag)
|
||||
by_major_minor[key] = l
|
||||
|
||||
latest_by_major_minor = []
|
||||
for (mm, l) in by_major_minor.items():
|
||||
# sort ascending by patch-level (rc's weigh less)
|
||||
l.sort(key=lambda tag: (tag.patch, int(tag.rc == 0), tag.rc))
|
||||
latest_by_major_minor.append(l[-1])
|
||||
latest_by_major_minor.sort(key=lambda tag: (tag.major, tag.minor))
|
||||
|
||||
# print(by_major_minor)
|
||||
# print(latest_by_major_minor)
|
||||
|
||||
cmdline = []
|
||||
|
||||
for latest_patch in latest_by_major_minor:
|
||||
cmdline.append("--whitelist-tags")
|
||||
cmdline.append(f"^{re.escape(latest_patch.orig)}$")
|
||||
|
||||
# we want to render the latest non-rc version as the default page
|
||||
# (latest_by_major_minor is already sorted)
|
||||
default_version = latest_by_major_minor[-1]
|
||||
for tag in reversed(latest_by_major_minor):
|
||||
if tag.rc == 0:
|
||||
default_version = tag
|
||||
break
|
||||
|
||||
cmdline.extend(["--root-ref", f"{default_version}"])
|
||||
cmdline.extend(["--banner-main-ref", f"{default_version}"])
|
||||
cmdline.extend(["--show-banner"])
|
||||
cmdline.extend(["--sort", "semver"])
|
||||
|
||||
cmdline.extend(["--whitelist-branches", "master"])
|
||||
|
||||
print(" ".join(cmdline))
|
||||
+8
-2
@@ -11,10 +11,12 @@
|
||||
:target: https://zrepl.github.io
|
||||
.. |Donate via PayPal| image:: https://img.shields.io/badge/donate-paypal-yellow.svg
|
||||
:target: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=R5QSXJVYHGX96
|
||||
.. |Donate via Liberapay| image:: https://img.shields.io/liberapay/receives/zrepl.svg?logo=liberapay
|
||||
.. |Donate via Liberapay| image:: https://img.shields.io/liberapay/patrons/zrepl.svg?logo=liberapay
|
||||
: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
|
||||
: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
|
||||
:target: https://github.com/sponsors/problame
|
||||
.. |Twitter| image:: https://img.shields.io/twitter/url/https/github.com/zrepl/zrepl.svg?style=social
|
||||
:target: https://twitter.com/intent/tweet?text=Wow:&url=https%3A%2F%2Fgithub.com%2Fzrepl%2Fzrepl
|
||||
|
||||
@@ -25,4 +27,8 @@
|
||||
.. |recv-options| replace:: :ref:`recv options<job-recv-options>`
|
||||
.. |snapshotting-spec| replace:: :ref:`snapshotting specification <job-snapshotting-spec>`
|
||||
.. |pruning-spec| replace:: :ref:`pruning specification <prune>`
|
||||
.. |filter-spec| replace:: :ref:`filter specification<pattern-filter>`
|
||||
.. |filter-spec| replace:: :ref:`filter specification<pattern-filter>`
|
||||
|
||||
.. |br| raw:: html
|
||||
|
||||
<br/>
|
||||
+3
-3
@@ -5,7 +5,7 @@
|
||||
|
||||
.. include:: global.rst.inc
|
||||
|
||||
|GitHub license| |Language: Go| |Twitter| |Donate via Patreon| |Donate via Liberapay| |Donate via PayPal|
|
||||
|GitHub license| |Language: Go| |Twitter| |Donate via Patreon| |Donate via GitHub Sponsors| |Donate via Liberapay| |Donate via PayPal|
|
||||
|
||||
|
||||
zrepl - ZFS replication
|
||||
@@ -44,7 +44,7 @@ zrepl - ZFS replication
|
||||
Getting started
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
The :ref:`10 minutes tutorial setup <tutorial>` gives you a first impression.
|
||||
The :ref:`10 minute quick-start guides <quickstart-toc>` give you a first impression.
|
||||
|
||||
Main Features
|
||||
~~~~~~~~~~~~~
|
||||
@@ -125,7 +125,7 @@ Table of Contents
|
||||
:maxdepth: 2
|
||||
:caption: Contents:
|
||||
|
||||
tutorial
|
||||
quickstart
|
||||
installation
|
||||
configuration
|
||||
usage
|
||||
|
||||
+11
-130
@@ -1,138 +1,19 @@
|
||||
.. _binary releases: https://github.com/zrepl/zrepl/releases
|
||||
|
||||
.. _installation:
|
||||
.. _installation_toc:
|
||||
|
||||
************
|
||||
Installation
|
||||
============
|
||||
************
|
||||
|
||||
.. TIP::
|
||||
|
||||
Note: check out the :ref:`tutorial` if you want a first impression of zrepl.
|
||||
Note: check out the :ref:`quick-start guides <quickstart-toc>` if you want a first impression of zrepl.
|
||||
|
||||
User Privileges
|
||||
---------------
|
||||
.. toctree::
|
||||
|
||||
It is possible to run zrepl as an unprivileged user in combination with
|
||||
`ZFS delegation <https://www.freebsd.org/doc/handbook/zfs-zfs-allow.html>`_.
|
||||
Also, there is the possibility to run it in a jail on FreeBSD by delegating a dataset to the jail.
|
||||
However, until we get around documenting those setups, you will have to run zrepl as root or experiment yourself :)
|
||||
|
||||
Packages
|
||||
--------
|
||||
|
||||
zrepl source releases are signed & tagged by the author in the git repository.
|
||||
Your OS vendor may provide binary packages of zrepl through the package manager.
|
||||
Additionally, `binary releases`_ are provided on GitHub.
|
||||
The following list may be incomplete, feel free to submit a PR with an update:
|
||||
|
||||
.. list-table::
|
||||
:header-rows: 1
|
||||
|
||||
* - OS / Distro
|
||||
- Install Command
|
||||
- Link
|
||||
* - FreeBSD
|
||||
- ``pkg install zrepl``
|
||||
- `<https://www.freshports.org/sysutils/zrepl/>`_
|
||||
* - MacOS
|
||||
- ``brew install zrepl``
|
||||
- Available on `homebrew <https://brew.sh>`_
|
||||
* - Arch Linux
|
||||
- ``yay install zrepl``
|
||||
- Available on `AUR <https://aur.archlinux.org/packages/zrepl>`_
|
||||
* - Fedora
|
||||
- ``dnf install zrepl``
|
||||
- Available on `COPR <https://copr.fedorainfracloud.org/coprs/poettlerric/zrepl/>`_
|
||||
* - CentOS/RHEL
|
||||
- ``yum install zrepl``
|
||||
- Available on `COPR <https://copr.fedorainfracloud.org/coprs/poettlerric/zrepl/>`_
|
||||
* - Debian + Ubuntu
|
||||
- ``apt install zrepl``
|
||||
- APT repository config :ref:`see below <installation-apt-repos>`
|
||||
* - OmniOS
|
||||
- ``pkg install zrepl``
|
||||
- Available since `r151030 <https://pkg.omniosce.org/r151030/extra/en/search.shtml?token=zrepl&action=Search>`_
|
||||
* - Void Linux
|
||||
- ``xbps-install zrepl``
|
||||
- Available since `a88a2a4 <https://github.com/void-linux/void-packages/commit/a88a2a4d7bf56072dadf61ab56b8424e39155890>`_
|
||||
* - Others
|
||||
-
|
||||
- Use `binary releases`_ or build from source.
|
||||
|
||||
.. _installation-apt-repos:
|
||||
|
||||
Debian / Ubuntu APT repositories
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
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``.
|
||||
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.
|
||||
|
||||
The following snippet configure the repository for your Debian or Ubuntu release:
|
||||
|
||||
::
|
||||
|
||||
apt update && apt install curl gnupg lsb-release; \
|
||||
ARCH="$(dpkg --print-architecture)"; \
|
||||
CODENAME="$(lsb_release -i -s | tr '[:upper:]' '[:lower:]') $(lsb_release -c -s | tr '[:upper:]' '[:lower:]')"; \
|
||||
echo "Using Distro and Codename: $CODENAME"; \
|
||||
(curl https://zrepl.cschwarz.com/apt/apt-key.asc | apt-key add -) && \
|
||||
(echo "deb [arch=$ARCH] https://zrepl.cschwarz.com/apt/$CODENAME main" > /etc/apt/sources.list.d/zrepl.list) && \
|
||||
apt update
|
||||
|
||||
|
||||
.. NOTE::
|
||||
|
||||
Until zrepl reaches 1.0, all APT repositories will be updated to the latest zrepl release immediately.
|
||||
This includes breaking changes between zrepl versions.
|
||||
Use ``apt-mark hold zrepl`` to prevent upgrades of zrepl.
|
||||
|
||||
Compile From Source
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Producing a release requires **Go 1.11** or newer and **Python 3** + **pip3** + ``docs/requirements.txt`` for the Sphinx documentation.
|
||||
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.
|
||||
|
||||
Alternatively, you can use the Docker build process:
|
||||
it is used to produce the official zrepl `binary releases`_
|
||||
and serves as a reference for build dependencies and procedure:
|
||||
|
||||
::
|
||||
|
||||
git clone https://github.com/zrepl/zrepl.git && \
|
||||
cd zrepl && \
|
||||
sudo docker build -t zrepl_build -f build.Dockerfile . && \
|
||||
sudo docker run -it --rm \
|
||||
-v "${PWD}:/src" \
|
||||
--user "$(id -u):$(id -g)" \
|
||||
zrepl_build make release
|
||||
|
||||
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::
|
||||
|
||||
It is your job to install the appropriate binary in the zrepl users's ``$PATH``, e.g. ``/usr/local/bin/zrepl``.
|
||||
Otherwise, the examples in the :ref:`tutorial` may need to be adjusted.
|
||||
|
||||
What next?
|
||||
----------
|
||||
|
||||
Read the :ref:`configuration chapter<configuration_toc>` and then continue with the :ref:`usage chapter<usage>`.
|
||||
|
||||
**Reminder**: If you want a quick introduction, please read the :ref:`tutorial`.
|
||||
installation/user-privileges
|
||||
installation/packages
|
||||
installation/apt-repos
|
||||
installation/compile-from-source
|
||||
installation/freebsd-jail-with-iocage
|
||||
installation/what-next
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
|
||||
.. _installation-apt-repos:
|
||||
|
||||
Debian / Ubuntu APT repositories
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
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``.
|
||||
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.
|
||||
|
||||
The following snippet configure the repository for your Debian or Ubuntu release:
|
||||
|
||||
::
|
||||
|
||||
apt update && apt install curl gnupg lsb-release; \
|
||||
ARCH="$(dpkg --print-architecture)"; \
|
||||
CODENAME="$(lsb_release -i -s | tr '[:upper:]' '[:lower:]') $(lsb_release -c -s | tr '[:upper:]' '[:lower:]')"; \
|
||||
echo "Using Distro and Codename: $CODENAME"; \
|
||||
(curl https://zrepl.cschwarz.com/apt/apt-key.asc | apt-key add -) && \
|
||||
(echo "deb [arch=$ARCH] https://zrepl.cschwarz.com/apt/$CODENAME main" > /etc/apt/sources.list.d/zrepl.list) && \
|
||||
apt update
|
||||
|
||||
|
||||
.. NOTE::
|
||||
|
||||
Until zrepl reaches 1.0, all APT repositories will be updated to the latest zrepl release immediately.
|
||||
This includes breaking changes between zrepl versions.
|
||||
Use ``apt-mark hold zrepl`` to prevent upgrades of zrepl.
|
||||
@@ -0,0 +1,45 @@
|
||||
.. _binary releases: https://github.com/zrepl/zrepl/releases
|
||||
|
||||
.. _installation-compile-from-source:
|
||||
|
||||
Compile From Source
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Producing a release requires **Go 1.11** or newer and **Python 3** + **pip3** + ``docs/requirements.txt`` for the Sphinx documentation.
|
||||
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.
|
||||
|
||||
Alternatively, you can use the Docker build process:
|
||||
it is used to produce the official zrepl `binary releases`_
|
||||
and serves as a reference for build dependencies and procedure:
|
||||
|
||||
::
|
||||
|
||||
git clone https://github.com/zrepl/zrepl.git && \
|
||||
cd zrepl && \
|
||||
sudo docker build -t zrepl_build -f build.Dockerfile . && \
|
||||
sudo docker run -it --rm \
|
||||
-v "${PWD}:/src" \
|
||||
--user "$(id -u):$(id -g)" \
|
||||
zrepl_build make release
|
||||
|
||||
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::
|
||||
|
||||
It is your job to install the appropriate 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.
|
||||
@@ -0,0 +1,140 @@
|
||||
.. include:: ../global.rst.inc
|
||||
|
||||
.. _installation-freebsd-jail-with-iocage:
|
||||
|
||||
FreeBSD Jail With iocage
|
||||
========================
|
||||
|
||||
|
||||
This tutorial shows how zrepl can be installed on FreeBSD, or FreeNAS in a jail using iocage.
|
||||
While this tutorial focuses on using iocage, much of the setup would be similar
|
||||
using a different jail manager.
|
||||
|
||||
.. NOTE::
|
||||
|
||||
From a security perspective, just keep in mind that ``zfs send``/``recv`` was never designed with
|
||||
jails in mind, an attacker could probably crash the receive-side kernel or worse induce stateful
|
||||
damage to the receive-side pool if they were able to get access to the jail.
|
||||
|
||||
The jail doesn't provide security benefits, but only management ones.
|
||||
|
||||
Requirements
|
||||
------------
|
||||
|
||||
A dataset that will be delegated to the jail needs to be created if one does not already exist.
|
||||
For the tutorial ``tank/zrepl`` will be used.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
zfs create -o mountpoint=none tank/zrepl
|
||||
|
||||
The only software requirements on the host system are ``iocage``, which can be installed
|
||||
from ports or packages.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pkg install py37-iocage
|
||||
|
||||
.. NOTE::
|
||||
|
||||
By default ``iocage`` will "activate" on first use which will set up some defaults such as
|
||||
which pool will be used. To activate ``iocage`` manually the ``iocage activate`` command can be used.
|
||||
|
||||
Jail Creation
|
||||
-------------
|
||||
|
||||
There are two options for jail creation using FreeBSD.
|
||||
|
||||
1. Manually set up the jail from scratch
|
||||
2. Create the jail using the ``zrepl`` plugin. On FreeNAS this is possible from the user interface using the community index.
|
||||
|
||||
Manual Jail
|
||||
###########
|
||||
|
||||
Create a jail, using the same release as the host, called ``zrepl`` that will be automatically started at boot.
|
||||
The jail will have ``tank/zrepl`` delegated into it.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
iocage create --release "$(freebsd-version -k | cut -d '-' -f '1,2')" --name zrepl \
|
||||
boot=on nat=1 \
|
||||
jail_zfs=on \
|
||||
jail_zfs_dataset=zrepl \
|
||||
jail_zfs_mountpoint='none'
|
||||
|
||||
Enter the jail:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
iocage console zrepl
|
||||
|
||||
Install ``zrepl``
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pkg update && pkg upgrade
|
||||
pkg install zrepl
|
||||
|
||||
Create the log file ``/var/log/zrepl.log``
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
touch /var/log/zrepl.log && service newsyslog restart
|
||||
|
||||
Tell syslogd to redirect facility local0 to the ``zrepl.log`` file:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
service syslogd reload
|
||||
|
||||
Enable the zrepl daemon to start automatically at boot:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
sysrc zrepl_enable="YES"
|
||||
|
||||
|
||||
Plugin
|
||||
######
|
||||
|
||||
When using the plugin, ``zrepl`` will be installed for you in a jail using the following ``iocage`` properties.
|
||||
|
||||
* ``nat=1``
|
||||
* ``jail_zfs=on``
|
||||
* ``jail_zfs_mountpoint=none``
|
||||
|
||||
Additionally the delegated dataset should be specified upon creation, and optionally start on boot can be set.
|
||||
This can also be done from the FreeNAS webui.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
fetch https://raw.githubusercontent.com/ix-plugin-hub/iocage-plugin-index/master/zrepl.json -o /tmp/zrepl.json
|
||||
iocage fetch -P /tmp/zrepl.json --name zrepl jail_zfs_dataset=zrepl boot=on
|
||||
|
||||
Configuration
|
||||
-------------
|
||||
|
||||
Now ``zrepl`` can be configured.
|
||||
|
||||
Enter the jail.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
iocage console zrepl
|
||||
|
||||
Modify the ``/usr/local/etc/zrepl/zrepl.yml`` configuration file.
|
||||
|
||||
.. TIP::
|
||||
|
||||
Note: check out the :ref:`quick-start guides <quickstart-toc>` for examples of a ``sink`` job.
|
||||
|
||||
Now ``zrepl`` can be started.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
service zrepl start
|
||||
|
||||
Summary
|
||||
-------
|
||||
|
||||
Congratulations, you have a working jail!
|
||||
@@ -0,0 +1,50 @@
|
||||
.. _installation-packages:
|
||||
|
||||
.. _binary releases: https://github.com/zrepl/zrepl/releases
|
||||
|
||||
Packages
|
||||
--------
|
||||
|
||||
zrepl source releases are signed & tagged by the author in the git repository.
|
||||
Your OS vendor may provide binary packages of zrepl through the package manager.
|
||||
Additionally, `binary releases`_ are provided on GitHub.
|
||||
The following list may be incomplete, feel free to submit a PR with an update:
|
||||
|
||||
.. list-table::
|
||||
:header-rows: 1
|
||||
|
||||
* - OS / Distro
|
||||
- Install Command
|
||||
- Link
|
||||
* - FreeBSD
|
||||
- ``pkg install zrepl``
|
||||
- `<https://www.freshports.org/sysutils/zrepl/>`_
|
||||
|
||||
:ref:`installation-freebsd-jail-with-iocage`
|
||||
* - FreeNAS
|
||||
-
|
||||
- :ref:`installation-freebsd-jail-with-iocage`
|
||||
* - MacOS
|
||||
- ``brew install zrepl``
|
||||
- Available on `homebrew <https://brew.sh>`_
|
||||
* - Arch Linux
|
||||
- ``yay install zrepl``
|
||||
- Available on `AUR <https://aur.archlinux.org/packages/zrepl>`_
|
||||
* - Fedora
|
||||
- ``dnf install zrepl``
|
||||
- Available on `COPR <https://copr.fedorainfracloud.org/coprs/poettlerric/zrepl/>`_
|
||||
* - CentOS/RHEL
|
||||
- ``yum install zrepl``
|
||||
- Available on `COPR <https://copr.fedorainfracloud.org/coprs/poettlerric/zrepl/>`_
|
||||
* - Debian + Ubuntu
|
||||
- ``apt install zrepl``
|
||||
- APT repository config :ref:`see below <installation-apt-repos>`
|
||||
* - OmniOS
|
||||
- ``pkg install zrepl``
|
||||
- Available since `r151030 <https://pkg.omniosce.org/r151030/extra/en/search.shtml?token=zrepl&action=Search>`_
|
||||
* - Void Linux
|
||||
- ``xbps-install zrepl``
|
||||
- Available since `a88a2a4 <https://github.com/void-linux/void-packages/commit/a88a2a4d7bf56072dadf61ab56b8424e39155890>`_
|
||||
* - Others
|
||||
-
|
||||
- Use `binary releases`_ or build from source.
|
||||
@@ -0,0 +1,12 @@
|
||||
.. _installation-user-privileges:
|
||||
|
||||
User Privileges
|
||||
---------------
|
||||
|
||||
It is possible to run zrepl as an unprivileged user in combination with
|
||||
`ZFS delegation <https://www.freebsd.org/doc/handbook/zfs-zfs-allow.html>`_.
|
||||
Also, there is the possibility to run it in a jail on FreeBSD by delegating a dataset to the jail.
|
||||
|
||||
.. TIP::
|
||||
|
||||
Note: check out the :ref:`installation-freebsd-jail-with-iocage` for FreeBSD jail setup instructions.
|
||||
@@ -0,0 +1,8 @@
|
||||
.. _installation-what-next:
|
||||
|
||||
What next?
|
||||
----------
|
||||
|
||||
Read the :ref:`configuration chapter<configuration_toc>` and then continue with the :ref:`usage chapter<usage>`.
|
||||
|
||||
**Reminder**: If you want a quick introduction, please read the :ref:`quick-start guides <quickstart-toc>`.
|
||||
+2
-3
@@ -51,11 +51,10 @@ git rm -rf .
|
||||
popd
|
||||
|
||||
echo "building site"
|
||||
|
||||
set -e
|
||||
sphinx-versioning build \
|
||||
--show-banner \
|
||||
--whitelist-branches '^master$' \
|
||||
--whitelist-tags '(v)?\d+\.[1-9][0-9]*.\d+$' \
|
||||
$(python3 gen-sphinx-versioning-flags.py) \
|
||||
docs ./public_git \
|
||||
-- -c sphinxconf # older conf.py throw errors because they used
|
||||
# version = subprocess.show_output(["git", "describe"])
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
.. include:: global.rst.inc
|
||||
|
||||
.. _quickstart-toc:
|
||||
|
||||
***********************
|
||||
Quick Start by Use Case
|
||||
***********************
|
||||
|
||||
The goal of this quick-start guide is to give you an impression of how zrepl can accomodate your use case.
|
||||
|
||||
Install zrepl
|
||||
=============
|
||||
|
||||
Follow the :ref:`OS-specific installation instructions <installation_toc>` and come back here.
|
||||
|
||||
Overview Of How zrepl Works
|
||||
============================
|
||||
|
||||
Check out the :ref:`overview section <job-overview>` to get a rough idea of what you are going to configure in the next step, then come back here.
|
||||
|
||||
Configuration Examples
|
||||
======================
|
||||
|
||||
zrepl is configured through a YAML configuration file in ``/etc/zrepl/zrepl.yml``.
|
||||
We have prepared example use cases that show-case typical deployments and different functionality of zrepl.
|
||||
We encourage you to read through all of the examples to get an idea of what zrepl has to offer, and how you can mix-and-match configurations for your use case.
|
||||
Keep the :ref:`full config documentation <configuration_toc>` handy if a config snippet is unclear.
|
||||
|
||||
**Example Use Cases**
|
||||
|
||||
.. toctree::
|
||||
:titlesonly:
|
||||
|
||||
quickstart/continuous_server_backup
|
||||
quickstart/backup_to_external_disk
|
||||
|
||||
Use ``zrepl configcheck`` to validate your configuration.
|
||||
No output indicates that everything is fine.
|
||||
|
||||
.. NOTE::
|
||||
|
||||
Please open an issue on GitHub if your use case for zrepl is significantly different from those listed above.
|
||||
Or even better, write it up in the same style as above and open a PR!
|
||||
|
||||
.. _quickstart-apply-config:
|
||||
|
||||
Apply Configuration Changes
|
||||
===========================
|
||||
|
||||
We hope that you have found a configuration that fits your use case.
|
||||
Use ``zrepl configcheck`` once again to make sure the config is correct (output indicates that everything is fine).
|
||||
Then restart the zrepl daemon on all systems involved in the replication, likely using ``service zrepl restart`` or ``systemctl restart zrepl``.
|
||||
|
||||
Watch it Work
|
||||
=============
|
||||
|
||||
Run ``zrepl status`` on the active side of the replication setup to monitor snaphotting, replication and pruning activity.
|
||||
To re-trigger replication (snapshots are separate!), use ``zrepl signal wakeup JOBNAME``.
|
||||
(refer to the example use case document if you are uncertain which job you want to wake up).
|
||||
|
||||
You can also use basic UNIX tools to inspect see what's going on.
|
||||
If you like tmux, here is a handy script that works on FreeBSD: ::
|
||||
|
||||
pkg install gnu-watch tmux
|
||||
tmux new -s zrepl -d
|
||||
tmux split-window -t zrepl "tail -f /var/log/messages"
|
||||
tmux split-window -t zrepl "gnu-watch 'zfs list -t snapshot -o name,creation -s creation'"
|
||||
tmux split-window -t zrepl "zrepl status"
|
||||
tmux select-layout -t zrepl tiled
|
||||
tmux attach -t zrepl
|
||||
|
||||
The Linux equivalent might look like this: ::
|
||||
|
||||
# make sure tmux is installed & let's assume you use systemd + journald
|
||||
tmux new -s zrepl -d
|
||||
tmux split-window -t zrepl "journalctl -f -u zrepl.service"
|
||||
tmux split-window -t zrepl "watch 'zfs list -t snapshot -o name,creation -s creation'"
|
||||
tmux split-window -t zrepl "zrepl status"
|
||||
tmux select-layout -t zrepl tiled
|
||||
tmux attach -t zrepl
|
||||
|
||||
What Next?
|
||||
==========
|
||||
|
||||
* Read more about :ref:`configuration format, options & job types <configuration_toc>`
|
||||
* Configure :ref:`logging <logging>` \& :ref:`monitoring <monitoring>`.
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
.. include:: ../global.rst.inc
|
||||
|
||||
.. _quickstart-backup-to-external-disk:
|
||||
|
||||
Local Snapshots + Offline Backup to an External Disk
|
||||
====================================================
|
||||
|
||||
This config example shows how we can use zrepl to make periodic snapshots of our local workstation and back it up to a zpool on an external disk which we occassionally connect.
|
||||
|
||||
The local snapshots should be taken every 15 minutes for pain-free recovery from CLI disasters (``rm -rf /`` and the like).
|
||||
However, we do not want to keep the snapshots around for very long because our workstation is a little tight on disk space.
|
||||
Thus, we only keep one hour worth of high-resolution snapshots, then fade them out to one per hour for a day (24 hours), then one per day for 14 days.
|
||||
|
||||
At the end of each work day, we connect our external disk that serves as our workstation's local offline backup.
|
||||
We want zrepl to inspect the filesystems and snapshots on the external pool, figure out which snapshots were created since the last time we connected the external disk, and use incremental replication to efficiently mirror our workstation to our backup disk.
|
||||
Afterwards, we want to clean up old snapshots on the backup pool: we want to keep all snapshots younger than one hour, 24 for each hor of the first day, then 360 daily backups.
|
||||
|
||||
A few additional requirements:
|
||||
|
||||
* Snapshot creation and pruning on our workstation should happen in the background, without interaction from our side.
|
||||
* However, we want to explicitly trigger replication via the command line.
|
||||
* We want to use OpenZFS native encryption to protect our data on the external disk.
|
||||
It is absolutely critical that only encrypted data leaves our workstation.
|
||||
**zrepl should provide an easy config knob for this and prevent replication of unencrypted datasets to the external disk.**
|
||||
* 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**.
|
||||
* 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 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**.
|
||||
|
||||
The following config snippet implements the setup described above.
|
||||
You will likely want to customize some aspects mentioned in the top comment in the file.
|
||||
|
||||
.. literalinclude:: ../../config/samples/quickstart_backup_to_external_disk.yml
|
||||
|
||||
:ref:`Click here <quickstart-apply-config>` to go back to the quickstart guide.
|
||||
@@ -0,0 +1,97 @@
|
||||
.. include:: ../global.rst.inc
|
||||
|
||||
.. _quickstart-continuous-replication:
|
||||
|
||||
Continuous Backup of a Server
|
||||
=============================
|
||||
|
||||
This config example shows how we can backup our ZFS-based server to another machine using a zrepl push job.
|
||||
|
||||
* Production server ``prod`` with filesystems to back up:
|
||||
|
||||
* ``zroot/var/db``
|
||||
* ``zroot/usr/home`` and all its child filesystems
|
||||
* **except** ``zroot/usr/home/paranoid`` belonging to a user doing backups themselves
|
||||
|
||||
* Backup server ``backups`` with
|
||||
|
||||
* Filesystem ``storage/zrepl/sink/prod`` + children dedicated to backups of ``prod``
|
||||
|
||||
Our backup solution should fulfill the following requirements:
|
||||
|
||||
* Periodically snapshot the filesystems on ``prod`` *every 10 minutes*
|
||||
* Incrementally replicate these snapshots to ``storage/zrepl/sink/prod/*`` on ``backups``
|
||||
* Keep only very few snapshots on ``prod`` to save disk space
|
||||
* Keep a fading history (24 hourly, 30 daily, 6 monthly) of snapshots on ``backups``
|
||||
* The network is untrusted - zrepl should use TLS to protect its communication and our data.
|
||||
|
||||
Analysis
|
||||
--------
|
||||
|
||||
We can model this situation as two jobs:
|
||||
|
||||
* A **push job** on ``prod``
|
||||
|
||||
* Creates the snapshots
|
||||
* Keeps a short history of local snapshots to enable incremental replication to ``backups``
|
||||
* Connects to the ``zrepl daemon`` process on ``backups``
|
||||
* Pushes snapshots ``backups``
|
||||
* Prunes snapshots on ``backups`` after replication is complete
|
||||
|
||||
* A **sink job** on ``backups``
|
||||
|
||||
* Accepts connections & responds to requests from ``prod``
|
||||
* Limits client ``prod`` access to filesystem sub-tree ``storage/zrepl/sink/prod``
|
||||
|
||||
Generate TLS Certificates
|
||||
-------------------------
|
||||
|
||||
We use the :ref:`TLS client authentication transport <transport-tcp+tlsclientauth>` to protect our data on the wire.
|
||||
To get things going quickly, we skip setting up a CA and generate two self-signed certificates as described :ref:`here <transport-tcp+tlsclientauth-2machineopenssl>`.
|
||||
For convenience, we generate the key pairs on our local machine and distribute them using ssh:
|
||||
|
||||
.. code-block:: bash
|
||||
:emphasize-lines: 6,13
|
||||
|
||||
openssl req -x509 -sha256 -nodes \
|
||||
-newkey rsa:4096 \
|
||||
-days 365 \
|
||||
-keyout backups.key \
|
||||
-out backups.crt
|
||||
# ... and use "backups" as Common Name (CN)
|
||||
|
||||
openssl req -x509 -sha256 -nodes \
|
||||
-newkey rsa:4096 \
|
||||
-days 365 \
|
||||
-keyout prod.key \
|
||||
-out prod.crt
|
||||
# ... and use "prod" as Common Name (CN)
|
||||
|
||||
ssh root@backups "mkdir /etc/zrepl"
|
||||
scp backups.key backups.crt prod.crt root@backups:/etc/zrepl
|
||||
|
||||
ssh root@prod "mkdir /etc/zrepl"
|
||||
scp prod.key prod.crt backups.crt root@prod:/etc/zrepl
|
||||
|
||||
Note that alternative transports exist, e.g. via :ref:`TCP without TLS <transport-tcp>` or :ref:`ssh <transport-ssh+stdinserver>`.
|
||||
|
||||
Configure server ``prod``
|
||||
-------------------------
|
||||
|
||||
We define a **push job** named ``prod_to_backups`` in ``/etc/zrepl/zrepl.yml`` on host ``prod`` :
|
||||
|
||||
.. literalinclude:: ../../config/samples/quickstart_continuous_server_backup_sender.yml
|
||||
|
||||
.. _tutorial-configure-prod:
|
||||
|
||||
Configure server ``backups``
|
||||
----------------------------
|
||||
|
||||
We define a corresponding **sink job** named ``sink`` in ``/etc/zrepl/zrepl.yml`` on host ``backups`` :
|
||||
|
||||
.. literalinclude:: ../../config/samples/quickstart_continuous_server_backup_receiver.yml
|
||||
|
||||
Go Back To Quickstart Guide
|
||||
---------------------------
|
||||
|
||||
:ref:`Click here <quickstart-apply-config>` to go back to the quickstart guide.
|
||||
+5
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
.. _supporters:
|
||||
|
||||
|Donate via Patreon| |Donate via Liberapay| |Donate via PayPal|
|
||||
|Donate via Patreon| |Donate via GitHub Sponsors| |Donate via Liberapay| |Donate via PayPal|
|
||||
|
||||
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 services listed above.
|
||||
@@ -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>
|
||||
|
||||
* |supporter-std| `Hans Schulz <https://github.com/schulzh>`_
|
||||
* |supporter-std| Henning Kessler
|
||||
* |supporter-std| `John Ramsden <https://github.com/johnramsden>`_
|
||||
* |supporter-std| `DrLuke <https://github.com/drluke>`_
|
||||
* |supporter-gold| `Mateusz Kwiatkowski (runhyve.app) <https://runhyve.app>`_
|
||||
* |supporter-std| `Gaelan D'costa <https://www.robot-disco.net>`_
|
||||
* |supporter-std| `Tenzin Lhakhang <https://confluence.tenzin.io>`_
|
||||
|
||||
@@ -1,177 +0,0 @@
|
||||
.. include:: global.rst.inc
|
||||
|
||||
.. _tutorial:
|
||||
|
||||
Tutorial
|
||||
========
|
||||
|
||||
|
||||
This tutorial shows how zrepl can be used to implement a ZFS-based push backup.
|
||||
We assume the following scenario:
|
||||
|
||||
* Production server ``prod`` with filesystems to back up:
|
||||
|
||||
* ``zroot/var/db``
|
||||
* ``zroot/usr/home`` and all its child filesystems
|
||||
* **except** ``zroot/usr/home/paranoid`` belonging to a user doing backups themselves
|
||||
|
||||
* Backup server ``backups`` with
|
||||
|
||||
* Filesystem ``storage/zrepl/sink/prod`` + children dedicated to backups of ``prod``
|
||||
|
||||
Our backup solution should fulfill the following requirements:
|
||||
|
||||
* Periodically snapshot the filesystems on ``prod`` *every 10 minutes*
|
||||
* Incrementally replicate these snapshots to ``storage/zrepl/sink/prod/*`` on ``backups``
|
||||
* Keep only very few snapshots on ``prod`` to save disk space
|
||||
* Keep a fading history (24 hourly, 30 daily, 6 monthly) of snapshots on ``backups``
|
||||
|
||||
Analysis
|
||||
--------
|
||||
|
||||
We can model this situation as two jobs:
|
||||
|
||||
* A **push job** on ``prod``
|
||||
|
||||
* Creates the snapshots
|
||||
* Keeps a short history of local snapshots to enable incremental replication to ``backups``
|
||||
* Connects to the ``zrepl daemon`` process on ``backups``
|
||||
* Pushes snapshots ``backups``
|
||||
* Prunes snapshots on ``backups`` after replication is complete
|
||||
|
||||
* A **sink job** on ``backups``
|
||||
|
||||
* Accepts connections & responds to requests from ``prod``
|
||||
* Limits client ``prod`` access to filesystem sub-tree ``storage/zrepl/sink/prod``
|
||||
|
||||
Install zrepl
|
||||
-------------
|
||||
|
||||
Follow the :ref:`OS-specific installation instructions <installation>` and come back here.
|
||||
|
||||
Generate TLS Certificates
|
||||
-------------------------
|
||||
|
||||
We use the :ref:`TLS client authentication transport <transport-tcp+tlsclientauth>` to protect our data on the wire.
|
||||
To get things going quickly, we skip setting up a CA and generate two self-signed certificates as described :ref:`here <transport-tcp+tlsclientauth-2machineopenssl>`.
|
||||
For convenience, we generate the key pairs on our local machine and distribute them using ssh:
|
||||
|
||||
.. code-block:: bash
|
||||
:emphasize-lines: 6,13
|
||||
|
||||
openssl req -x509 -sha256 -nodes \
|
||||
-newkey rsa:4096 \
|
||||
-days 365 \
|
||||
-keyout backups.key \
|
||||
-out backups.crt
|
||||
# ... and use "backups" as Common Name (CN)
|
||||
|
||||
openssl req -x509 -sha256 -nodes \
|
||||
-newkey rsa:4096 \
|
||||
-days 365 \
|
||||
-keyout prod.key \
|
||||
-out prod.crt
|
||||
# ... and use "prod" as Common Name (CN)
|
||||
|
||||
ssh root@backups "mkdir /etc/zrepl"
|
||||
scp backups.key backups.crt prod.crt root@backups:/etc/zrepl
|
||||
|
||||
ssh root@prod "mkdir /etc/zrepl"
|
||||
scp prod.key prod.crt backups.crt root@prod:/etc/zrepl
|
||||
|
||||
|
||||
Configure server ``prod``
|
||||
-------------------------
|
||||
|
||||
We define a **push job** named ``prod_to_backups`` in ``/etc/zrepl/zrepl.yml`` on host ``prod`` : ::
|
||||
|
||||
jobs:
|
||||
- name: prod_to_backups
|
||||
type: push
|
||||
connect:
|
||||
type: tls
|
||||
address: "backups.example.com:8888"
|
||||
ca: /etc/zrepl/backups.crt
|
||||
cert: /etc/zrepl/prod.crt
|
||||
key: /etc/zrepl/prod.key
|
||||
server_cn: "backups"
|
||||
filesystems: {
|
||||
"zroot/var/db": true,
|
||||
"zroot/usr/home<": true,
|
||||
"zroot/usr/home/paranoid": false
|
||||
}
|
||||
snapshotting:
|
||||
type: periodic
|
||||
prefix: zrepl_
|
||||
interval: 10m
|
||||
pruning:
|
||||
keep_sender:
|
||||
- type: not_replicated
|
||||
- type: last_n
|
||||
count: 10
|
||||
keep_receiver:
|
||||
- type: grid
|
||||
grid: 1x1h(keep=all) | 24x1h | 30x1d | 6x30d
|
||||
regex: "^zrepl_"
|
||||
|
||||
.. _tutorial-configure-prod:
|
||||
|
||||
Configure server ``backups``
|
||||
----------------------------
|
||||
|
||||
We define a corresponding **sink job** named ``sink`` in ``/etc/zrepl/zrepl.yml`` on host ``backups`` : ::
|
||||
|
||||
jobs:
|
||||
- name: sink
|
||||
type: sink
|
||||
serve:
|
||||
type: tls
|
||||
listen: ":8888"
|
||||
ca: "/etc/zrepl/prod.crt"
|
||||
cert: "/etc/zrepl/backups.crt"
|
||||
key: "/etc/zrepl/backups.key"
|
||||
client_cns:
|
||||
- "prod"
|
||||
root_fs: "storage/zrepl/sink"
|
||||
|
||||
|
||||
Apply Configuration Changes
|
||||
---------------------------
|
||||
|
||||
We use ``zrepl configcheck`` to catch any configuration errors: no output indicates that everything is fine.
|
||||
If that is the case, restart the zrepl daemon on **both** ``prod`` and ``backups`` using ``service zrepl restart`` or ``systemctl restart zrepl``.
|
||||
|
||||
|
||||
Watch it Work
|
||||
-------------
|
||||
|
||||
Run ``zrepl status`` on ``prod`` to monitor the replication and pruning activity.
|
||||
To re-trigger replication (snapshots are separate!), use ``zrepl signal wakeup prod_to_backups`` on ``prod``.
|
||||
|
||||
If you like tmux, here is a handy script that works on FreeBSD: ::
|
||||
|
||||
pkg install gnu-watch tmux
|
||||
tmux new -s zrepl -d
|
||||
tmux split-window -t zrepl "tail -f /var/log/messages"
|
||||
tmux split-window -t zrepl "gnu-watch 'zfs list -t snapshot -o name,creation -s creation | grep zrepl_'"
|
||||
tmux split-window -t zrepl "zrepl status"
|
||||
tmux select-layout -t zrepl tiled
|
||||
tmux attach -t zrepl
|
||||
|
||||
The Linux equivalent might look like this: ::
|
||||
|
||||
# make sure tmux is installed & let's assume you use systemd + journald
|
||||
tmux new -s zrepl -d
|
||||
tmux split-window -t zrepl "journalctl -f -u zrepl.service"
|
||||
tmux split-window -t zrepl "watch 'zfs list -t snapshot -o name,creation -s creation | grep zrepl_'"
|
||||
tmux split-window -t zrepl "zrepl status"
|
||||
tmux select-layout -t zrepl tiled
|
||||
tmux attach -t zrepl
|
||||
|
||||
Summary
|
||||
-------
|
||||
|
||||
Congratulations, you have a working push backup. Where to go next?
|
||||
|
||||
* Read more about :ref:`configuration format, options & job types <configuration_toc>`
|
||||
* Configure :ref:`logging <logging>` \& :ref:`monitoring <monitoring>`.
|
||||
+1
-1
@@ -38,7 +38,7 @@ CLI Overview
|
||||
* - ``zrepl migrate``
|
||||
- | perform on-disk state / ZFS property migrations
|
||||
| (see :ref:`changelog <changelog>` for details)
|
||||
* - ``zrepl zfs-abstractions``
|
||||
* - ``zrepl zfs-abstraction``
|
||||
- list and remove zrepl's abstractions on top of ZFS, e.g. holds and step bookmarks (see :ref:`overview <replication-cursor-and-last-received-hold>` )
|
||||
|
||||
.. _usage-zrepl-daemon:
|
||||
|
||||
+142
-229
@@ -7,8 +7,8 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"path"
|
||||
"sync"
|
||||
|
||||
"github.com/kr/pretty"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/zrepl/zrepl/daemon/logging/trace"
|
||||
|
||||
@@ -117,119 +117,8 @@ func (s *Sender) ListFilesystemVersions(ctx context.Context, r *pdu.ListFilesyst
|
||||
|
||||
}
|
||||
|
||||
func (p *Sender) HintMostRecentCommonAncestor(ctx context.Context, r *pdu.HintMostRecentCommonAncestorReq) (*pdu.HintMostRecentCommonAncestorRes, error) {
|
||||
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
|
||||
|
||||
fsp, err := p.filterCheckFS(r.GetFilesystem())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fs := fsp.ToString()
|
||||
|
||||
log := getLogger(ctx).WithField("fs", fs).WithField("hinted_most_recent", fmt.Sprintf("%#v", r.GetSenderVersion()))
|
||||
|
||||
log.WithField("full_hint", r).Debug("full hint")
|
||||
|
||||
if r.GetSenderVersion() == nil {
|
||||
// no common ancestor found, likely due to failed prior replication attempt
|
||||
// => release stale step holds to prevent them from accumulating
|
||||
// (they can accumulate on initial replication because each inital replication step might hold a different `to`)
|
||||
// => replication cursors cannot accumulate because we always _move_ the replication cursor
|
||||
log.Debug("releasing all step holds on the filesystem")
|
||||
TryReleaseStepStaleFS(ctx, fs, p.jobId)
|
||||
return &pdu.HintMostRecentCommonAncestorRes{}, nil
|
||||
}
|
||||
|
||||
// we were hinted a specific common ancestor
|
||||
|
||||
mostRecentVersion, err := sendArgsFromPDUAndValidateExistsAndGetVersion(ctx, fs, r.GetSenderVersion())
|
||||
if err != nil {
|
||||
msg := "HintMostRecentCommonAncestor rpc with nonexistent most recent version"
|
||||
log.Warn(msg)
|
||||
return nil, errors.Wrap(err, msg)
|
||||
}
|
||||
|
||||
// move replication cursor to this position
|
||||
destroyedCursors, err := MoveReplicationCursor(ctx, fs, mostRecentVersion, p.jobId)
|
||||
if err == zfs.ErrBookmarkCloningNotSupported {
|
||||
log.Debug("not creating replication cursor from bookmark because ZFS does not support it")
|
||||
// fallthrough
|
||||
} else if err != nil {
|
||||
return nil, errors.Wrap(err, "cannot set replication cursor to hinted version")
|
||||
}
|
||||
|
||||
// take care of stale step holds
|
||||
log.WithField("step-holds-cleanup-mode", senderHintMostRecentCommonAncestorStepCleanupMode).
|
||||
Debug("taking care of possibly stale step holds")
|
||||
doStepCleanup := false
|
||||
var stepCleanupSince *CreateTXGRangeBound
|
||||
switch senderHintMostRecentCommonAncestorStepCleanupMode {
|
||||
case StepCleanupNoCleanup:
|
||||
doStepCleanup = false
|
||||
case StepCleanupRangeSinceUnbounded:
|
||||
doStepCleanup = true
|
||||
stepCleanupSince = nil
|
||||
case StepCleanupRangeSinceReplicationCursor:
|
||||
doStepCleanup = true
|
||||
// Use the destroyed replication cursors as indicator how far the previous replication got.
|
||||
// To be precise: We limit the amount of visisted snapshots to exactly those snapshots
|
||||
// created since the last successful replication cursor movement (i.e. last successful replication step)
|
||||
//
|
||||
// If we crash now, we'll leak the step we are about to release, but the performance gain
|
||||
// of limiting the amount of snapshots we visit makes up for that.
|
||||
// Users have the `zrepl holds release-stale` command to cleanup leaked step holds.
|
||||
for _, destroyed := range destroyedCursors {
|
||||
if stepCleanupSince == nil {
|
||||
stepCleanupSince = &CreateTXGRangeBound{
|
||||
CreateTXG: destroyed.GetCreateTXG(),
|
||||
Inclusive: &zfs.NilBool{B: true},
|
||||
}
|
||||
} else if destroyed.GetCreateTXG() < stepCleanupSince.CreateTXG {
|
||||
stepCleanupSince.CreateTXG = destroyed.GetCreateTXG()
|
||||
}
|
||||
}
|
||||
default:
|
||||
panic(senderHintMostRecentCommonAncestorStepCleanupMode)
|
||||
}
|
||||
if !doStepCleanup {
|
||||
log.Info("skipping cleanup of prior invocations' step holds due to environment variable setting")
|
||||
} else {
|
||||
if err := ReleaseStepCummulativeInclusive(ctx, fs, stepCleanupSince, mostRecentVersion, p.jobId); err != nil {
|
||||
return nil, errors.Wrap(err, "cannot cleanup prior invocation's step holds and bookmarks")
|
||||
} else {
|
||||
log.Info("step hold cleanup done")
|
||||
}
|
||||
}
|
||||
|
||||
return &pdu.HintMostRecentCommonAncestorRes{}, nil
|
||||
}
|
||||
|
||||
type HintMostRecentCommonAncestorStepCleanupMode struct{ string }
|
||||
|
||||
var (
|
||||
StepCleanupRangeSinceReplicationCursor = HintMostRecentCommonAncestorStepCleanupMode{"range-since-replication-cursor"}
|
||||
StepCleanupRangeSinceUnbounded = HintMostRecentCommonAncestorStepCleanupMode{"range-since-unbounded"}
|
||||
StepCleanupNoCleanup = HintMostRecentCommonAncestorStepCleanupMode{"no-cleanup"}
|
||||
)
|
||||
|
||||
func (m HintMostRecentCommonAncestorStepCleanupMode) String() string { return string(m.string) }
|
||||
func (m *HintMostRecentCommonAncestorStepCleanupMode) Set(s string) error {
|
||||
switch s {
|
||||
case StepCleanupRangeSinceReplicationCursor.String():
|
||||
*m = StepCleanupRangeSinceReplicationCursor
|
||||
case StepCleanupRangeSinceUnbounded.String():
|
||||
*m = StepCleanupRangeSinceUnbounded
|
||||
case StepCleanupNoCleanup.String():
|
||||
*m = StepCleanupNoCleanup
|
||||
default:
|
||||
return fmt.Errorf("unknown step cleanup mode %q", s)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var senderHintMostRecentCommonAncestorStepCleanupMode = *envconst.Var("ZREPL_ENDPOINT_SENDER_HINT_MOST_RECENT_STEP_HOLD_CLEANUP_MODE", &StepCleanupRangeSinceReplicationCursor).(*HintMostRecentCommonAncestorStepCleanupMode)
|
||||
|
||||
var maxConcurrentZFSSendSemaphore = semaphore.New(envconst.Int64("ZREPL_ENDPOINT_MAX_CONCURRENT_SEND", 10))
|
||||
var maxConcurrentZFSSend = envconst.Int64("ZREPL_ENDPOINT_MAX_CONCURRENT_SEND", 10)
|
||||
var maxConcurrentZFSSendSemaphore = semaphore.New(maxConcurrentZFSSend)
|
||||
|
||||
func uncheckedSendArgsFromPDU(fsv *pdu.FilesystemVersion) *zfs.ZFSSendArgVersion {
|
||||
if fsv == nil {
|
||||
@@ -319,40 +208,97 @@ func (s *Sender) Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, io.Rea
|
||||
return res, nil, nil
|
||||
}
|
||||
|
||||
// update replication cursor
|
||||
if sendArgs.From != nil {
|
||||
// For all but the first replication, this should always be a no-op because SendCompleted already moved the cursor
|
||||
_, err = MoveReplicationCursor(ctx, sendArgs.FS, sendArgs.FromVersion, s.jobId)
|
||||
if err == zfs.ErrBookmarkCloningNotSupported {
|
||||
getLogger(ctx).Debug("not creating replication cursor from bookmark because ZFS does not support it")
|
||||
// fallthrough
|
||||
} else if err != nil {
|
||||
return nil, nil, errors.Wrap(err, "cannot set replication cursor to `from` version before starting send")
|
||||
}
|
||||
}
|
||||
|
||||
// make sure `From` doesn't go away in order to make this step resumable
|
||||
if sendArgs.From != nil {
|
||||
_, err := HoldStep(ctx, sendArgs.FS, *sendArgs.FromVersion, s.jobId)
|
||||
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)
|
||||
}
|
||||
}
|
||||
// make sure `To` doesn't go away in order to make this step resumable
|
||||
_, err = HoldStep(ctx, sendArgs.FS, sendArgs.ToVersion, s.jobId)
|
||||
// create holds or bookmarks of `From` and `To` to guarantee one of the following:
|
||||
// - that the replication step can always be resumed (`holds`),
|
||||
// - that the replication step can be interrupted and a future replication
|
||||
// step with same or different `To` but same `From` is still possible (`bookmarks`)
|
||||
// - nothing (`none`)
|
||||
//
|
||||
// ...
|
||||
//
|
||||
// ... actually create the abstractions
|
||||
replicationGuaranteeOptions, err := replicationGuaranteeOptionsFromPDU(r.GetReplicationConfig().Protection)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrapf(err, "cannot hold `to` version %q before starting send", sendArgs.ToVersion)
|
||||
return nil, nil, err
|
||||
}
|
||||
replicationGuaranteeStrategy := replicationGuaranteeOptions.Strategy(sendArgs.From != nil)
|
||||
liveAbs, err := replicationGuaranteeStrategy.SenderPreSend(ctx, s.jobId, &sendArgs)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
for _, a := range liveAbs {
|
||||
if a != nil {
|
||||
abstractionsCacheSingleton.Put(a)
|
||||
}
|
||||
}
|
||||
|
||||
// step holds & replication cursor released / moved forward in s.SendCompleted => s.moveCursorAndReleaseSendHolds
|
||||
// cleanup the mess that _this function_ might have created in prior failed attempts:
|
||||
//
|
||||
// In summary, we delete every endpoint ZFS abstraction created on this filesystem for this job id,
|
||||
// except for the ones we just created above.
|
||||
//
|
||||
// This is the most robust approach to avoid leaking (= forgetting to clean up) endpoint ZFS abstractions,
|
||||
// all under the assumption that there will only ever be one send for a (jobId,fs) combination at any given time.
|
||||
//
|
||||
// Note that the SendCompleted rpc can't be relied upon for this purpose:
|
||||
// - it might be lost due to network errors,
|
||||
// - or never be sent by a potentially malicious or buggy client,
|
||||
// - or never be send because the replication step failed at some point
|
||||
// (potentially leaving a resumable state on the receiver, which is the case where we really do not want to blow away the step holds too soon.)
|
||||
//
|
||||
// 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.
|
||||
func() {
|
||||
ctx, endSpan := trace.WithSpan(ctx, "cleanup-stale-abstractions")
|
||||
defer endSpan()
|
||||
|
||||
keep := func(a Abstraction) (keep bool) {
|
||||
keep = false
|
||||
for _, k := range liveAbs {
|
||||
keep = keep || AbstractionEquals(a, k)
|
||||
}
|
||||
return keep
|
||||
}
|
||||
check := func(obsoleteAbs []Abstraction) {
|
||||
// last line of defense: check that we don't destroy the incremental `from` and `to`
|
||||
// if we did that, we might be about to blow away the last common filesystem version between sender and receiver
|
||||
mustLiveVersions := []zfs.FilesystemVersion{sendArgs.ToVersion}
|
||||
if sendArgs.FromVersion != nil {
|
||||
mustLiveVersions = append(mustLiveVersions, *sendArgs.FromVersion)
|
||||
}
|
||||
for _, staleVersion := range obsoleteAbs {
|
||||
for _, mustLiveVersion := range mustLiveVersions {
|
||||
isSendArg := zfs.FilesystemVersionEqualIdentity(mustLiveVersion, staleVersion.GetFilesystemVersion())
|
||||
stepHoldBasedGuaranteeStrategy := false
|
||||
k := replicationGuaranteeStrategy.Kind()
|
||||
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)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
destroyTypes := AbstractionTypeSet{
|
||||
AbstractionStepHold: true,
|
||||
AbstractionTentativeReplicationCursorBookmark: true,
|
||||
}
|
||||
abstractionsCacheSingleton.TryBatchDestroy(ctx, s.jobId, sendArgs.FS, destroyTypes, keep, check)
|
||||
}()
|
||||
|
||||
sendStream, err := zfs.ZFSSend(ctx, sendArgs)
|
||||
if err != nil {
|
||||
// it's ok to not destroy the abstractions we just created here, a new send attempt will take care of it
|
||||
return nil, nil, errors.Wrap(err, "zfs send failed")
|
||||
}
|
||||
|
||||
return res, sendStream, nil
|
||||
}
|
||||
|
||||
@@ -379,82 +325,35 @@ func (p *Sender) SendCompleted(ctx context.Context, r *pdu.SendCompletedReq) (*p
|
||||
return nil, errors.Wrap(err, "validate `to` exists")
|
||||
}
|
||||
|
||||
log := func(ctx context.Context) Logger {
|
||||
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
|
||||
}
|
||||
|
||||
log(ctx).Debug("move replication cursor to most recent common version")
|
||||
destroyedCursors, err := MoveReplicationCursor(ctx, fs, to, p.jobId)
|
||||
replicationGuaranteeOptions, err := replicationGuaranteeOptionsFromPDU(orig.GetReplicationConfig().Protection)
|
||||
if err != nil {
|
||||
if err == zfs.ErrBookmarkCloningNotSupported {
|
||||
log(ctx).Debug("not setting replication cursor, bookmark cloning not supported")
|
||||
} else {
|
||||
msg := "cannot move replication cursor, keeping hold on `to` until successful"
|
||||
log(ctx).WithError(err).Error(msg)
|
||||
err = errors.Wrap(err, msg)
|
||||
// it is correct to not release the hold if we can't move the cursor!
|
||||
return &pdu.SendCompletedRes{}, err
|
||||
}
|
||||
} else {
|
||||
log(ctx).Info("successfully moved replication cursor")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// kick off releasing of step holds / bookmarks
|
||||
// if we fail to release them, don't bother the caller:
|
||||
// they are merely an implementation detail on the sender for better resumability
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
ctx, endTask := trace.WithTask(ctx, "release-step-hold-to")
|
||||
defer endTask()
|
||||
|
||||
log(ctx).Debug("release step-hold of or step-bookmark on `to`")
|
||||
err = ReleaseStep(ctx, fs, to, p.jobId)
|
||||
if err != nil {
|
||||
log(ctx).WithError(err).Error("cannot release step-holds on or destroy step-bookmark of `to`")
|
||||
} else {
|
||||
log(ctx).Info("successfully released step-holds on or destroyed step-bookmark of `to`")
|
||||
liveAbs, err := replicationGuaranteeOptions.Strategy(from != nil).SenderPostRecvConfirmed(ctx, p.jobId, fs, to)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, a := range liveAbs {
|
||||
if a != nil {
|
||||
abstractionsCacheSingleton.Put(a)
|
||||
}
|
||||
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
ctx, endTask := trace.WithTask(ctx, "release-step-hold-from")
|
||||
defer endTask()
|
||||
|
||||
if from == nil {
|
||||
return
|
||||
}
|
||||
keep := func(a Abstraction) (keep bool) {
|
||||
keep = false
|
||||
for _, k := range liveAbs {
|
||||
keep = keep || AbstractionEquals(a, k)
|
||||
}
|
||||
log(ctx).Debug("release step-hold of or step-bookmark on `from`")
|
||||
err := ReleaseStep(ctx, fs, *from, p.jobId)
|
||||
if err != nil {
|
||||
if dne, ok := err.(*zfs.DatasetDoesNotExist); ok {
|
||||
// If bookmark cloning is not supported, `from` might be the old replication cursor
|
||||
// and thus have already been destroyed by MoveReplicationCursor above
|
||||
// In that case, nonexistence of `from` is not an error, otherwise it is.
|
||||
for _, c := range destroyedCursors {
|
||||
if c.GetFullPath() == dne.Path {
|
||||
log(ctx).Info("`from` was a replication cursor and has already been destroyed")
|
||||
return
|
||||
}
|
||||
}
|
||||
// fallthrough
|
||||
}
|
||||
log(ctx).WithError(err).Error("cannot release step-holds on or destroy step-bookmark of `from`")
|
||||
} else {
|
||||
log(ctx).Info("successfully released step-holds on or destroyed step-bookmark of `from`")
|
||||
}
|
||||
}()
|
||||
wg.Wait()
|
||||
return keep
|
||||
}
|
||||
destroyTypes := AbstractionTypeSet{
|
||||
AbstractionStepHold: true,
|
||||
AbstractionTentativeReplicationCursorBookmark: true,
|
||||
AbstractionReplicationCursorBookmarkV2: true,
|
||||
}
|
||||
abstractionsCacheSingleton.TryBatchDestroy(ctx, p.jobId, fs, destroyTypes, keep, nil)
|
||||
|
||||
return &pdu.SendCompletedRes{}, nil
|
||||
|
||||
}
|
||||
|
||||
func (p *Sender) DestroySnapshots(ctx context.Context, req *pdu.DestroySnapshotsReq) (*pdu.DestroySnapshotsRes, error) {
|
||||
@@ -527,8 +426,6 @@ type ReceiverConfig struct {
|
||||
|
||||
RootWithoutClientComponent *zfs.DatasetPath // TODO use
|
||||
AppendClientIdentity bool
|
||||
|
||||
UpdateLastReceivedHold bool
|
||||
}
|
||||
|
||||
func (c *ReceiverConfig) copyIn() {
|
||||
@@ -769,7 +666,7 @@ func (s *Receiver) Receive(ctx context.Context, req *pdu.ReceiveReq, receive io.
|
||||
f := zfs.NewDatasetPathForest()
|
||||
f.Add(lp)
|
||||
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) {
|
||||
return false
|
||||
}
|
||||
@@ -797,7 +694,7 @@ func (s *Receiver) Receive(ctx context.Context, req *pdu.ReceiveReq, receive io.
|
||||
}
|
||||
l := getLogger(ctx).WithField("placeholder_fs", v.Path)
|
||||
l.Debug("create placeholder filesystem")
|
||||
err := zfs.ZFSCreatePlaceholderFilesystem(ctx, v.Path)
|
||||
err := zfs.ZFSCreatePlaceholderFilesystem(ctx, v.Path, v.Parent.Path)
|
||||
if err != nil {
|
||||
l.WithError(err).Error("cannot create placeholder filesystem")
|
||||
visitErr = err
|
||||
@@ -858,9 +755,9 @@ func (s *Receiver) Receive(ctx context.Context, req *pdu.ReceiveReq, receive io.
|
||||
}
|
||||
defer guard.Release()
|
||||
|
||||
log.Info("peeking 1M ahead")
|
||||
var peek bytes.Buffer
|
||||
var MaxPeek = envconst.Int64("ZREPL_ENDPOINT_RECV_PEEK_SIZE", 1<<20)
|
||||
log.WithField("max_peek_bytes", MaxPeek).Info("peeking incoming stream")
|
||||
if _, err := io.Copy(&peek, io.LimitReader(receive, MaxPeek)); err != nil {
|
||||
log.WithError(err).Error("cannot read peek-buffer from send stream")
|
||||
}
|
||||
@@ -953,12 +850,38 @@ func (s *Receiver) Receive(ctx context.Context, req *pdu.ReceiveReq, receive io.
|
||||
return nil, errors.Wrap(err, msg)
|
||||
}
|
||||
|
||||
if s.conf.UpdateLastReceivedHold {
|
||||
log.Debug("move last-received-hold")
|
||||
if err := MoveLastReceivedHold(ctx, lp.ToString(), toRecvd, s.conf.JobID); err != nil {
|
||||
return nil, errors.Wrap(err, "cannot move last-received-hold")
|
||||
replicationGuaranteeOptions, err := replicationGuaranteeOptionsFromPDU(req.GetReplicationConfig().Protection)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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
|
||||
}
|
||||
@@ -974,16 +897,6 @@ func (s *Receiver) DestroySnapshots(ctx context.Context, req *pdu.DestroySnapsho
|
||||
return doDestroySnapshots(ctx, lp, req.Snapshots)
|
||||
}
|
||||
|
||||
func (p *Receiver) HintMostRecentCommonAncestor(ctx context.Context, r *pdu.HintMostRecentCommonAncestorReq) (*pdu.HintMostRecentCommonAncestorRes, error) {
|
||||
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
|
||||
// we don't move last-received-hold as part of this hint
|
||||
// because that wouldn't give us any benefit wrt resumability.
|
||||
//
|
||||
// Other reason: the replication logic that issues this RPC would require refactoring
|
||||
// to include the receiver's FilesystemVersion in the request)
|
||||
return &pdu.HintMostRecentCommonAncestorRes{}, nil
|
||||
}
|
||||
|
||||
func (p *Receiver) SendCompleted(ctx context.Context, _ *pdu.SendCompletedReq) (*pdu.SendCompletedRes, error) {
|
||||
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
|
||||
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
package endpoint
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/zrepl/zrepl/daemon/logging/trace"
|
||||
"github.com/zrepl/zrepl/util/chainlock"
|
||||
)
|
||||
|
||||
var abstractionsCacheMetrics struct {
|
||||
count prometheus.Gauge
|
||||
}
|
||||
|
||||
func init() {
|
||||
abstractionsCacheMetrics.count = prometheus.NewGauge(prometheus.GaugeOpts{
|
||||
Namespace: "zrepl",
|
||||
Subsystem: "endpoint",
|
||||
Name: "abstractions_cache_entry_count",
|
||||
Help: "number of abstractions tracked in the abstractionsCache data structure",
|
||||
})
|
||||
}
|
||||
|
||||
var abstractionsCacheSingleton = newAbstractionsCache()
|
||||
|
||||
func AbstractionsCacheInvalidate(fs string) {
|
||||
abstractionsCacheSingleton.InvalidateFSCache(fs)
|
||||
}
|
||||
|
||||
type abstractionsCacheDidLoadFSState int
|
||||
|
||||
const (
|
||||
abstractionsCacheDidLoadFSStateNo abstractionsCacheDidLoadFSState = iota // 0-value has meaning
|
||||
abstractionsCacheDidLoadFSStateInProgress
|
||||
abstractionsCacheDidLoadFSStateDone
|
||||
)
|
||||
|
||||
type abstractionsCache struct {
|
||||
mtx chainlock.L
|
||||
abstractions []Abstraction
|
||||
didLoadFS map[string]abstractionsCacheDidLoadFSState
|
||||
didLoadFSChanged *sync.Cond
|
||||
}
|
||||
|
||||
func newAbstractionsCache() *abstractionsCache {
|
||||
c := &abstractionsCache{
|
||||
didLoadFS: make(map[string]abstractionsCacheDidLoadFSState),
|
||||
}
|
||||
c.didLoadFSChanged = c.mtx.NewCond()
|
||||
return c
|
||||
}
|
||||
|
||||
func (s *abstractionsCache) Put(a Abstraction) {
|
||||
defer s.mtx.Lock().Unlock()
|
||||
|
||||
var zeroJobId JobID
|
||||
if a.GetJobID() == nil {
|
||||
panic("abstraction must not have nil job id")
|
||||
} else if *a.GetJobID() == zeroJobId {
|
||||
panic(fmt.Sprintf("abstraction must not have zero-value job id: %s", a))
|
||||
}
|
||||
|
||||
s.abstractions = append(s.abstractions, a)
|
||||
abstractionsCacheMetrics.count.Set(float64(len(s.abstractions)))
|
||||
}
|
||||
|
||||
func (s *abstractionsCache) InvalidateFSCache(fs string) {
|
||||
// FIXME: O(n)
|
||||
newAbs := make([]Abstraction, 0, len(s.abstractions))
|
||||
for _, a := range s.abstractions {
|
||||
if a.GetFS() != fs {
|
||||
newAbs = append(newAbs, a)
|
||||
}
|
||||
}
|
||||
s.abstractions = newAbs
|
||||
abstractionsCacheMetrics.count.Set(float64(len(s.abstractions)))
|
||||
|
||||
s.didLoadFS[fs] = abstractionsCacheDidLoadFSStateNo
|
||||
s.didLoadFSChanged.Broadcast()
|
||||
|
||||
}
|
||||
|
||||
// - logs errors in getting on-disk abstractions
|
||||
// - 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.
|
||||
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 trace.WithSpanFromStackUpdateCtx(&ctx)()
|
||||
var zeroJobId JobID
|
||||
if jobID == zeroJobId {
|
||||
panic("must not pass zero-value job id")
|
||||
}
|
||||
if fs == "" {
|
||||
panic("must not pass zero-value fs")
|
||||
}
|
||||
|
||||
s.tryLoadOnDiskAbstractions(ctx, fs)
|
||||
|
||||
// FIXME O(n)
|
||||
var remaining []Abstraction
|
||||
for _, a := range s.abstractions {
|
||||
aJobId := *a.GetJobID()
|
||||
aFS := a.GetFS()
|
||||
if aJobId == jobID && aFS == fs && types[a.GetType()] && !keep(a) {
|
||||
ret = append(ret, a)
|
||||
} else {
|
||||
remaining = append(remaining, a)
|
||||
}
|
||||
}
|
||||
s.abstractions = remaining
|
||||
abstractionsCacheMetrics.count.Set(float64(len(s.abstractions)))
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
// caller must hold s.mtx
|
||||
func (s *abstractionsCache) tryLoadOnDiskAbstractions(ctx context.Context, fs string) {
|
||||
for s.didLoadFS[fs] != abstractionsCacheDidLoadFSStateDone {
|
||||
if s.didLoadFS[fs] == abstractionsCacheDidLoadFSStateInProgress {
|
||||
s.didLoadFSChanged.Wait()
|
||||
continue
|
||||
}
|
||||
if s.didLoadFS[fs] != abstractionsCacheDidLoadFSStateNo {
|
||||
panic(fmt.Sprintf("unreachable: %v", s.didLoadFS[fs]))
|
||||
}
|
||||
|
||||
s.didLoadFS[fs] = abstractionsCacheDidLoadFSStateInProgress
|
||||
defer s.didLoadFSChanged.Broadcast()
|
||||
|
||||
var onDiskAbs []Abstraction
|
||||
var err error
|
||||
s.mtx.DropWhile(func() {
|
||||
onDiskAbs, err = s.tryLoadOnDiskAbstractionsImpl(ctx, fs) // no shadow
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
s.didLoadFS[fs] = abstractionsCacheDidLoadFSStateNo
|
||||
getLogger(ctx).WithField("fs", fs).WithError(err).Error("cannot list abstractions for filesystem")
|
||||
} else {
|
||||
s.didLoadFS[fs] = abstractionsCacheDidLoadFSStateDone
|
||||
s.abstractions = append(s.abstractions, onDiskAbs...)
|
||||
getLogger(ctx).WithField("fs", fs).WithField("abstractions", onDiskAbs).Debug("loaded step abstractions for filesystem")
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// caller should _not hold s.mtx
|
||||
func (s *abstractionsCache) tryLoadOnDiskAbstractionsImpl(ctx context.Context, fs string) ([]Abstraction, error) {
|
||||
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
|
||||
|
||||
q := ListZFSHoldsAndBookmarksQuery{
|
||||
FS: ListZFSHoldsAndBookmarksQueryFilesystemFilter{
|
||||
FS: &fs,
|
||||
},
|
||||
JobID: nil,
|
||||
What: AbstractionTypeSet{
|
||||
AbstractionStepHold: true,
|
||||
AbstractionTentativeReplicationCursorBookmark: true,
|
||||
AbstractionReplicationCursorBookmarkV2: true,
|
||||
AbstractionLastReceivedHold: true,
|
||||
},
|
||||
Concurrency: 1,
|
||||
}
|
||||
abs, absErrs, err := ListAbstractions(ctx, q)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// safe to ignore absErrs here, this is best-effort cleanup
|
||||
if len(absErrs) > 0 {
|
||||
return nil, ListAbstractionsErrors(absErrs)
|
||||
}
|
||||
return abs, nil
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
|
||||
|
||||
obsoleteAbs := s.GetAndDeleteByJobIDAndFS(ctx, jobId, fs, types, keep)
|
||||
|
||||
if check != nil {
|
||||
check(obsoleteAbs)
|
||||
}
|
||||
|
||||
hadErr := false
|
||||
for res := range BatchDestroy(ctx, obsoleteAbs) {
|
||||
if res.DestroyErr != nil {
|
||||
hadErr = true
|
||||
getLogger(ctx).
|
||||
WithField("abstraction", res.Abstraction).
|
||||
WithError(res.DestroyErr).
|
||||
Error("cannot destroy abstraction")
|
||||
} else {
|
||||
getLogger(ctx).
|
||||
WithField("abstraction", res.Abstraction).
|
||||
Info("destroyed abstraction")
|
||||
}
|
||||
}
|
||||
if hadErr {
|
||||
s.InvalidateFSCache(fs)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package endpoint
|
||||
|
||||
import "github.com/prometheus/client_golang/prometheus"
|
||||
|
||||
func RegisterMetrics(r prometheus.Registerer) {
|
||||
r.MustRegister(abstractionsCacheMetrics.count)
|
||||
}
|
||||
@@ -23,19 +23,19 @@ type AbstractionType string
|
||||
// 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!
|
||||
const (
|
||||
AbstractionStepBookmark AbstractionType = "step-bookmark"
|
||||
AbstractionStepHold AbstractionType = "step-hold"
|
||||
AbstractionLastReceivedHold AbstractionType = "last-received-hold"
|
||||
AbstractionReplicationCursorBookmarkV1 AbstractionType = "replication-cursor-bookmark-v1"
|
||||
AbstractionReplicationCursorBookmarkV2 AbstractionType = "replication-cursor-bookmark-v2"
|
||||
AbstractionStepHold AbstractionType = "step-hold"
|
||||
AbstractionLastReceivedHold AbstractionType = "last-received-hold"
|
||||
AbstractionTentativeReplicationCursorBookmark AbstractionType = "tentative-replication-cursor-bookmark-v2"
|
||||
AbstractionReplicationCursorBookmarkV1 AbstractionType = "replication-cursor-bookmark-v1"
|
||||
AbstractionReplicationCursorBookmarkV2 AbstractionType = "replication-cursor-bookmark-v2"
|
||||
)
|
||||
|
||||
var AbstractionTypesAll = map[AbstractionType]bool{
|
||||
AbstractionStepBookmark: true,
|
||||
AbstractionStepHold: true,
|
||||
AbstractionLastReceivedHold: true,
|
||||
AbstractionReplicationCursorBookmarkV1: true,
|
||||
AbstractionReplicationCursorBookmarkV2: true,
|
||||
AbstractionStepHold: true,
|
||||
AbstractionLastReceivedHold: true,
|
||||
AbstractionTentativeReplicationCursorBookmark: true,
|
||||
AbstractionReplicationCursorBookmarkV1: true,
|
||||
AbstractionReplicationCursorBookmarkV2: true,
|
||||
}
|
||||
|
||||
// Implementation Note:
|
||||
@@ -54,14 +54,38 @@ type Abstraction interface {
|
||||
json.Marshaler
|
||||
}
|
||||
|
||||
func AbstractionEquals(a, b Abstraction) bool {
|
||||
if (a != nil) != (b != nil) {
|
||||
return false
|
||||
}
|
||||
if a == nil && b == nil {
|
||||
return true
|
||||
}
|
||||
var aJobId, bJobId JobID
|
||||
if aJid := a.GetJobID(); aJid != nil {
|
||||
aJobId = *aJid
|
||||
}
|
||||
if bJid := b.GetJobID(); bJid != nil {
|
||||
bJobId = *bJid
|
||||
}
|
||||
return a.GetType() == b.GetType() &&
|
||||
a.GetFS() == b.GetFS() &&
|
||||
a.GetName() == b.GetName() &&
|
||||
a.GetFullPath() == b.GetFullPath() &&
|
||||
aJobId == bJobId &&
|
||||
a.GetCreateTXG() == b.GetCreateTXG() &&
|
||||
zfs.FilesystemVersionEqualIdentity(a.GetFilesystemVersion(), b.GetFilesystemVersion()) &&
|
||||
a.String() == b.String()
|
||||
}
|
||||
|
||||
func (t AbstractionType) Validate() error {
|
||||
switch t {
|
||||
case AbstractionStepBookmark:
|
||||
return nil
|
||||
case AbstractionStepHold:
|
||||
return nil
|
||||
case AbstractionLastReceivedHold:
|
||||
return nil
|
||||
case AbstractionTentativeReplicationCursorBookmark:
|
||||
return nil
|
||||
case AbstractionReplicationCursorBookmarkV1:
|
||||
return nil
|
||||
case AbstractionReplicationCursorBookmarkV2:
|
||||
@@ -161,8 +185,8 @@ type BookmarkExtractor func(fs *zfs.DatasetPath, v zfs.FilesystemVersion) Abstra
|
||||
// returns nil if the abstraction type is not bookmark-based
|
||||
func (t AbstractionType) BookmarkExtractor() BookmarkExtractor {
|
||||
switch t {
|
||||
case AbstractionStepBookmark:
|
||||
return StepBookmarkExtractor
|
||||
case AbstractionTentativeReplicationCursorBookmark:
|
||||
return TentativeReplicationCursorExtractor
|
||||
case AbstractionReplicationCursorBookmarkV1:
|
||||
return ReplicationCursorV1Extractor
|
||||
case AbstractionReplicationCursorBookmarkV2:
|
||||
@@ -181,7 +205,7 @@ type HoldExtractor = func(fs *zfs.DatasetPath, v zfs.FilesystemVersion, tag stri
|
||||
// returns nil if the abstraction type is not hold-based
|
||||
func (t AbstractionType) HoldExtractor() HoldExtractor {
|
||||
switch t {
|
||||
case AbstractionStepBookmark:
|
||||
case AbstractionTentativeReplicationCursorBookmark:
|
||||
return nil
|
||||
case AbstractionReplicationCursorBookmarkV1:
|
||||
return nil
|
||||
@@ -196,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 {
|
||||
FS ListZFSHoldsAndBookmarksQueryFilesystemFilter
|
||||
// What abstraction types should match (any contained in the set)
|
||||
@@ -590,23 +631,23 @@ func listAbstractionsImplFS(ctx context.Context, fs string, query *ListZFSHoldsA
|
||||
panic("implementation error: extractors misconfigured for " + at)
|
||||
}
|
||||
for _, v := range fsvs {
|
||||
var a Abstraction
|
||||
if v.Type == zfs.Bookmark && bmE != nil {
|
||||
a = bmE(fsp, v)
|
||||
if a := bmE(fsp, v); a != nil {
|
||||
emitCandidate(a)
|
||||
}
|
||||
}
|
||||
if v.Type == zfs.Snapshot && holdE != nil && query.CreateTXG.Contains(v.GetCreateTXG()) && (!v.UserRefs.Valid || v.UserRefs.Value > 0) {
|
||||
if v.Type == zfs.Snapshot && holdE != nil && query.CreateTXG.Contains(v.GetCreateTXG()) && (!v.UserRefs.Valid || v.UserRefs.Value > 0) { // FIXME review v.UserRefsValid
|
||||
holds, err := zfs.ZFSHolds(ctx, fsp.ToString(), v.Name)
|
||||
if err != nil {
|
||||
errCb(err, v.ToAbsPath(fsp), "get hold on snap")
|
||||
continue
|
||||
}
|
||||
for _, tag := range holds {
|
||||
a = holdE(fsp, v, tag)
|
||||
if a := holdE(fsp, v, tag); a != nil {
|
||||
emitCandidate(a)
|
||||
}
|
||||
}
|
||||
}
|
||||
if a != nil {
|
||||
emitCandidate(a)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -673,11 +714,9 @@ func ListStale(ctx context.Context, q ListZFSHoldsAndBookmarksQuery) (*Staleness
|
||||
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)
|
||||
// as well as replication cursor bookmarks (for firstNotStale)
|
||||
// if asking for step holds must also ask for replication cursor bookmarks (for firstNotStale)
|
||||
ifAnyThenAll := AbstractionTypeSet{
|
||||
AbstractionStepHold: true,
|
||||
AbstractionStepBookmark: true,
|
||||
AbstractionReplicationCursorBookmarkV2: true,
|
||||
}
|
||||
if q.What.ContainsAnyOf(ifAnyThenAll) && !q.What.ContainsAll(ifAnyThenAll) {
|
||||
@@ -706,7 +745,7 @@ type fsAjobAtype struct {
|
||||
}
|
||||
|
||||
// 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)
|
||||
//
|
||||
// For replication cursors and last-received-holds, only the most recent one is kept.
|
||||
@@ -748,8 +787,6 @@ func listStaleFiltering(abs []Abstraction, sinceBound *CreateTXGRangeBound) *Sta
|
||||
}
|
||||
|
||||
// stepFirstNotStaleCandidate.step
|
||||
case AbstractionStepBookmark:
|
||||
fallthrough
|
||||
case AbstractionStepHold:
|
||||
if c.step == nil || (*c.step).GetCreateTXG() < a.GetCreateTXG() {
|
||||
a := a
|
||||
@@ -773,7 +810,7 @@ func listStaleFiltering(abs []Abstraction, sinceBound *CreateTXGRangeBound) *Sta
|
||||
for k := range by {
|
||||
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
|
||||
|
||||
// if we don't have a replication cursor yet, use untilBound = nil
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
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
|
||||
}
|
||||
+59
-179
@@ -4,12 +4,9 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sort"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/zrepl/zrepl/util/errorarray"
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
|
||||
@@ -53,6 +50,28 @@ func ParseReplicationCursorBookmarkName(fullname string) (uint64, JobID, error)
|
||||
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
|
||||
func GetMostRecentReplicationCursorOfJob(ctx context.Context, fs string, jobID JobID) (*zfs.FilesystemVersion, error) {
|
||||
fsp, err := zfs.NewDatasetPath(fs)
|
||||
@@ -118,31 +137,32 @@ func GetReplicationCursors(ctx context.Context, dp *zfs.DatasetPath, jobID JobID
|
||||
return candidates, nil
|
||||
}
|
||||
|
||||
type ReplicationCursorTarget interface {
|
||||
IsSnapshot() bool
|
||||
GetGuid() uint64
|
||||
GetCreateTXG() uint64
|
||||
ToSendArgVersion() zfs.ZFSSendArgVersion
|
||||
}
|
||||
|
||||
// `target` is validated before replication cursor is set. if validation fails, the cursor is not moved.
|
||||
// idempotently create a replication cursor targeting `target`
|
||||
//
|
||||
// returns ErrBookmarkCloningNotSupported if version is a bookmark and bookmarking bookmarks is not supported by ZFS
|
||||
func MoveReplicationCursor(ctx context.Context, fs string, target ReplicationCursorTarget, jobID JobID) (destroyedCursors []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)
|
||||
}
|
||||
|
||||
if !target.IsSnapshot() {
|
||||
return nil, zfs.ErrBookmarkCloningNotSupported
|
||||
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 := ReplicationCursorBookmarkName(fs, target.GetGuid(), jobID)
|
||||
bookmarkname, err := bookmarkNamer(fs, target.GetGuid(), jobID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "determine replication cursor name")
|
||||
return nil, errors.Wrapf(err, "determine %s name", abstractionType)
|
||||
}
|
||||
|
||||
// idempotently create bookmark (guid is encoded in it, hence we'll most likely add a new one
|
||||
// cleanup the old one afterwards
|
||||
// idempotently create bookmark (guid is encoded in it)
|
||||
|
||||
err = zfs.ZFSBookmark(ctx, fs, target.ToSendArgVersion(), bookmarkname)
|
||||
cursorBookmark, err := zfs.ZFSBookmark(ctx, fs, target, bookmarkname)
|
||||
if err != nil {
|
||||
if err == zfs.ErrBookmarkCloningNotSupported {
|
||||
return nil, err // TODO go1.13 use wrapping
|
||||
@@ -150,156 +170,12 @@ func MoveReplicationCursor(ctx context.Context, fs string, target ReplicationCur
|
||||
return nil, errors.Wrapf(err, "cannot create bookmark")
|
||||
}
|
||||
|
||||
destroyedCursors, err = DestroyObsoleteReplicationCursors(ctx, fs, target, jobID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "destroy obsolete replication cursors")
|
||||
}
|
||||
|
||||
return destroyedCursors, nil
|
||||
}
|
||||
|
||||
type ReplicationCursor interface {
|
||||
GetCreateTXG() uint64
|
||||
}
|
||||
|
||||
func DestroyObsoleteReplicationCursors(ctx context.Context, fs string, current ReplicationCursor, jobID JobID) (_ []Abstraction, err error) {
|
||||
|
||||
q := ListZFSHoldsAndBookmarksQuery{
|
||||
FS: ListZFSHoldsAndBookmarksQueryFilesystemFilter{
|
||||
FS: &fs,
|
||||
},
|
||||
What: AbstractionTypeSet{
|
||||
AbstractionReplicationCursorBookmarkV2: true,
|
||||
},
|
||||
JobID: &jobID,
|
||||
CreateTXG: CreateTXGRange{
|
||||
Since: nil,
|
||||
Until: &CreateTXGRangeBound{
|
||||
CreateTXG: current.GetCreateTXG(),
|
||||
Inclusive: &zfs.NilBool{B: false},
|
||||
},
|
||||
},
|
||||
Concurrency: 1,
|
||||
}
|
||||
abs, absErr, err := ListAbstractions(ctx, q)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "list abstractions")
|
||||
}
|
||||
if len(absErr) > 0 {
|
||||
return nil, errors.Wrap(ListAbstractionsErrors(absErr), "list abstractions")
|
||||
}
|
||||
|
||||
var destroyed []Abstraction
|
||||
var errs []error
|
||||
for res := range BatchDestroy(ctx, abs) {
|
||||
log := getLogger(ctx).
|
||||
WithField("replication_cursor_bookmark", res.Abstraction)
|
||||
if res.DestroyErr != nil {
|
||||
errs = append(errs, res.DestroyErr)
|
||||
log.WithError(err).
|
||||
Error("cannot destroy obsolete replication cursor bookmark")
|
||||
} else {
|
||||
destroyed = append(destroyed, res.Abstraction)
|
||||
log.Info("destroyed obsolete replication cursor bookmark")
|
||||
}
|
||||
}
|
||||
if len(errs) == 0 {
|
||||
return destroyed, nil
|
||||
} else {
|
||||
return destroyed, errorarray.Wrap(errs, "destroy obsolete replication cursor")
|
||||
}
|
||||
}
|
||||
|
||||
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("zrepl_last_received_J_%s", jobid)
|
||||
if err := zfs.ValidHoldTag(tag); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return tag, nil
|
||||
}
|
||||
|
||||
func MoveLastReceivedHold(ctx context.Context, fs string, to zfs.FilesystemVersion, jobID JobID) error {
|
||||
|
||||
if !to.IsSnapshot() {
|
||||
return errors.Errorf("last-received-hold: target must be a snapshot: %s", to.FullPath(fs))
|
||||
}
|
||||
|
||||
tag, err := LastReceivedHoldTag(jobID)
|
||||
if err != nil {
|
||||
return 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 errors.Wrap(err, "last-received-hold: hold newly received")
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
return &bookmarkBasedAbstraction{
|
||||
Type: abstractionType,
|
||||
FS: fs,
|
||||
FilesystemVersion: cursorBookmark,
|
||||
JobID: jobID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func ReplicationCursorV2Extractor(fs *zfs.DatasetPath, v zfs.FilesystemVersion) (_ Abstraction) {
|
||||
@@ -339,24 +215,28 @@ func ReplicationCursorV1Extractor(fs *zfs.DatasetPath, v zfs.FilesystemVersion)
|
||||
return nil
|
||||
}
|
||||
|
||||
var _ HoldExtractor = LastReceivedHoldExtractor
|
||||
var _ BookmarkExtractor = TentativeReplicationCursorExtractor
|
||||
|
||||
func LastReceivedHoldExtractor(fs *zfs.DatasetPath, v zfs.FilesystemVersion, holdTag string) Abstraction {
|
||||
var err error
|
||||
|
||||
if v.Type != zfs.Snapshot {
|
||||
func TentativeReplicationCursorExtractor(fs *zfs.DatasetPath, v zfs.FilesystemVersion) (_ Abstraction) {
|
||||
if v.Type != zfs.Bookmark {
|
||||
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 {
|
||||
return &holdBasedAbstraction{
|
||||
Type: AbstractionLastReceivedHold,
|
||||
bm := &bookmarkBasedAbstraction{
|
||||
Type: AbstractionTentativeReplicationCursorBookmark,
|
||||
FS: fs.ToString(),
|
||||
FilesystemVersion: v,
|
||||
Tag: holdTag,
|
||||
JobID: jobID,
|
||||
JobID: jobid,
|
||||
}
|
||||
return bm
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,286 +0,0 @@
|
||||
package endpoint
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/zrepl/zrepl/util/errorarray"
|
||||
"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
|
||||
err = zfs.ZFSBookmark(ctx, fs, v.ToSendArgVersion(), 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: v,
|
||||
JobID: jobID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// idempotently release the step-hold on v if v is a snapshot
|
||||
// or idempotently destroy the step-bookmark of v if v is a bookmark
|
||||
//
|
||||
// note that this operation leaves v itself untouched, unless v is the step-bookmark itself, in which case v is destroyed
|
||||
//
|
||||
// returns an instance of *zfs.DatasetDoesNotExist if `v` does not exist
|
||||
func ReleaseStep(ctx context.Context, fs string, v zfs.FilesystemVersion, jobID JobID) error {
|
||||
|
||||
if v.IsSnapshot() {
|
||||
tag, err := StepHoldTag(jobID)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "step release tag")
|
||||
}
|
||||
|
||||
if err := zfs.ZFSRelease(ctx, tag, v.FullPath(fs)); err != nil {
|
||||
return errors.Wrap(err, "step release: zfs")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
if !v.IsBookmark() {
|
||||
panic(fmt.Sprintf("impl error: expecting version to be a bookmark, got %#v", v))
|
||||
}
|
||||
|
||||
bmname, err := StepBookmarkName(fs, v.Guid, jobID)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "step release: determine bookmark name")
|
||||
}
|
||||
// idempotently destroy bookmark
|
||||
|
||||
if err := zfs.ZFSDestroyIdempotent(ctx, bmname); err != nil {
|
||||
return errors.Wrap(err, "step release: bookmark destroy: zfs")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// release {step holds, step bookmarks} earlier and including `mostRecent`
|
||||
func ReleaseStepCummulativeInclusive(ctx context.Context, fs string, since *CreateTXGRangeBound, mostRecent zfs.FilesystemVersion, jobID JobID) error {
|
||||
q := ListZFSHoldsAndBookmarksQuery{
|
||||
What: AbstractionTypeSet{
|
||||
AbstractionStepHold: true,
|
||||
AbstractionStepBookmark: true,
|
||||
},
|
||||
FS: ListZFSHoldsAndBookmarksQueryFilesystemFilter{
|
||||
FS: &fs,
|
||||
},
|
||||
JobID: &jobID,
|
||||
CreateTXG: CreateTXGRange{
|
||||
Since: since,
|
||||
Until: &CreateTXGRangeBound{
|
||||
CreateTXG: mostRecent.CreateTXG,
|
||||
Inclusive: &zfs.NilBool{B: true},
|
||||
},
|
||||
},
|
||||
Concurrency: 1,
|
||||
}
|
||||
abs, absErrs, err := ListAbstractions(ctx, q)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "step release cummulative: list")
|
||||
}
|
||||
if len(absErrs) > 0 {
|
||||
return errors.Wrap(ListAbstractionsErrors(absErrs), "step release cummulative: list")
|
||||
}
|
||||
|
||||
getLogger(ctx).WithField("step_holds_and_bookmarks", fmt.Sprintf("%s", abs)).Debug("releasing step holds and bookmarks")
|
||||
|
||||
var errs []error
|
||||
for res := range BatchDestroy(ctx, abs) {
|
||||
log := getLogger(ctx).
|
||||
WithField("step_hold_or_bookmark", res.Abstraction)
|
||||
if res.DestroyErr != nil {
|
||||
errs = append(errs, res.DestroyErr)
|
||||
log.WithError(err).
|
||||
Error("cannot release step hold or bookmark")
|
||||
} else {
|
||||
log.Info("released step hold or bookmark")
|
||||
}
|
||||
}
|
||||
if len(errs) == 0 {
|
||||
return nil
|
||||
} else {
|
||||
return errorarray.Wrap(errs, "step release cummulative: release")
|
||||
}
|
||||
}
|
||||
|
||||
func TryReleaseStepStaleFS(ctx context.Context, fs string, jobID JobID) {
|
||||
|
||||
q := ListZFSHoldsAndBookmarksQuery{
|
||||
FS: ListZFSHoldsAndBookmarksQueryFilesystemFilter{
|
||||
FS: &fs,
|
||||
},
|
||||
JobID: &jobID,
|
||||
What: AbstractionTypeSet{
|
||||
AbstractionStepHold: true,
|
||||
AbstractionStepBookmark: true,
|
||||
AbstractionReplicationCursorBookmarkV2: true,
|
||||
},
|
||||
Concurrency: 1,
|
||||
}
|
||||
staleness, err := ListStale(ctx, q)
|
||||
if _, ok := err.(*ListStaleQueryError); ok {
|
||||
panic(err)
|
||||
} else if err != nil {
|
||||
getLogger(ctx).WithError(err).Error("cannot list stale step holds and bookmarks")
|
||||
return
|
||||
}
|
||||
for _, s := range staleness.Stale {
|
||||
getLogger(ctx).WithField("stale_step_hold_or_bookmark", s).Info("batch-destroying stale step hold or bookmark")
|
||||
}
|
||||
for res := range BatchDestroy(ctx, staleness.Stale) {
|
||||
if res.DestroyErr != nil {
|
||||
getLogger(ctx).
|
||||
WithField("stale_step_hold_or_bookmark", res.Abstraction).
|
||||
WithError(res.DestroyErr).
|
||||
Error("cannot destroy stale step-hold or bookmark")
|
||||
} else {
|
||||
getLogger(ctx).
|
||||
WithField("stale_step_hold_or_bookmark", res.Abstraction).
|
||||
WithError(res.DestroyErr).
|
||||
Info("destroyed stale step-hold or bookmark")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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
@@ -24,9 +24,9 @@ func MakeJobID(s string) (JobID, error) {
|
||||
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
|
||||
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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -22,7 +22,7 @@ require (
|
||||
github.com/onsi/gomega v1.7.0 // indirect
|
||||
github.com/pkg/errors v0.8.1
|
||||
github.com/pkg/profile v1.2.1
|
||||
github.com/problame/go-netssh v0.0.0-20191209123953-18d8aa6923c7
|
||||
github.com/problame/go-netssh v0.0.0-20200601114649-26439f9f0dc5
|
||||
github.com/prometheus/client_golang v1.2.1
|
||||
github.com/prometheus/common v0.7.0
|
||||
github.com/sergi/go-diff v1.0.1-0.20180205163309-da645544ed44 // go1.12 thinks it needs this
|
||||
@@ -36,6 +36,7 @@ require (
|
||||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037
|
||||
golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e
|
||||
gonum.org/v1/gonum v0.7.0 // indirect
|
||||
google.golang.org/grpc v1.17.0
|
||||
)
|
||||
|
||||
@@ -212,6 +212,8 @@ github.com/problame/go-netssh v0.0.0-20191026123024-f34099f4f6b1 h1:HH8yzlaZq/A8
|
||||
github.com/problame/go-netssh v0.0.0-20191026123024-f34099f4f6b1/go.mod h1:JRs3nyBjvOv/9YHC+Vlw9z1Jfzl5s5t0BNnTzmclj1c=
|
||||
github.com/problame/go-netssh v0.0.0-20191209123953-18d8aa6923c7 h1:Oik8tLrLhoMq4fduDRuNNOAQ+q0M0dXkjAYLUf4+mAc=
|
||||
github.com/problame/go-netssh v0.0.0-20191209123953-18d8aa6923c7/go.mod h1:Ba6RaFpK1+IHWs1wLZ6sCBuXkt4iAVLeOG5GinXHusM=
|
||||
github.com/problame/go-netssh v0.0.0-20200601114649-26439f9f0dc5 h1:1eSrO2WAeSXjMol8WRKW9LekL1252VIMaKQwWkmEdvs=
|
||||
github.com/problame/go-netssh v0.0.0-20200601114649-26439f9f0dc5/go.mod h1:Ba6RaFpK1+IHWs1wLZ6sCBuXkt4iAVLeOG5GinXHusM=
|
||||
github.com/prometheus/client_golang v0.0.0-20180410130117-e11c6ff8170b/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||
@@ -268,6 +270,7 @@ github.com/spf13/viper v1.0.2/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7Sr
|
||||
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
@@ -350,6 +353,7 @@ golang.org/x/tools v0.0.0-20181117154741-2ddaf7f79a09/go.mod h1:n7NCudcB/nEzxVGm
|
||||
golang.org/x/tools v0.0.0-20181205014116-22934f0fdb62/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190110163146-51295c7ec13a/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190121143147-24cd39ecf745/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e h1:Io7mpb+aUAGF0MKxbyQ7HQl1VgB+cL6ZJZUFaFNqVV4=
|
||||
golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190213192042-740235f6c0d8/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
|
||||
@@ -36,9 +36,10 @@ godep() {
|
||||
go build -v -mod=readonly -o "$GOPATH/bin/enumer" github.com/alvaroloes/enumer
|
||||
go build -v -mod=readonly -o "$GOPATH/bin/goimports" golang.org/x/tools/cmd/goimports
|
||||
go build -v -mod=readonly -o "$GOPATH/bin/golangci-lint" github.com/golangci/golangci-lint/cmd/golangci-lint
|
||||
go build -v -mod=readonly -o "$GOPATH/bin/gocovmerge" github.com/wadey/gocovmerge
|
||||
set +x
|
||||
popd
|
||||
if ! type stringer || ! type protoc-gen-go || ! type enumer || ! type goimports || ! type golangci-lint; then
|
||||
if ! type stringer || ! type protoc-gen-go || ! type enumer || ! type goimports || ! type golangci-lint || ! type gocovmerge; then
|
||||
echo "Installed dependencies but can't find them in \$PATH, adjust it to contain \$GOPATH/bin" 1>&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -24,33 +24,36 @@ var bold = color.New(color.Bold)
|
||||
var boldRed = color.New(color.Bold, color.FgHiRed)
|
||||
var boldGreen = color.New(color.Bold, color.FgHiGreen)
|
||||
|
||||
var args struct {
|
||||
createArgs platformtest.ZpoolCreateArgs
|
||||
stopAndKeepPoolOnFail bool
|
||||
|
||||
run string
|
||||
runRE *regexp.Regexp
|
||||
}
|
||||
const DefaultPoolImageSize = 200 * (1 << 20)
|
||||
|
||||
func main() {
|
||||
if err := doMain(); err != nil {
|
||||
|
||||
var args HarnessArgs
|
||||
|
||||
flag.StringVar(&args.CreateArgs.PoolName, "poolname", "", "")
|
||||
flag.StringVar(&args.CreateArgs.ImagePath, "imagepath", "", "")
|
||||
flag.Int64Var(&args.CreateArgs.ImageSize, "imagesize", DefaultPoolImageSize, "")
|
||||
flag.StringVar(&args.CreateArgs.Mountpoint, "mountpoint", "", "")
|
||||
flag.BoolVar(&args.StopAndKeepPoolOnFail, "failure.stop-and-keep-pool", false, "if a test case fails, stop test execution and keep pool as it was when the test failed")
|
||||
flag.StringVar(&args.Run, "run", "", "")
|
||||
flag.Parse()
|
||||
|
||||
if err := HarnessRun(args); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
var exitWithErr = fmt.Errorf("exit with error")
|
||||
|
||||
func doMain() error {
|
||||
type HarnessArgs struct {
|
||||
CreateArgs platformtest.ZpoolCreateArgs
|
||||
StopAndKeepPoolOnFail bool
|
||||
Run string
|
||||
}
|
||||
|
||||
flag.StringVar(&args.createArgs.PoolName, "poolname", "", "")
|
||||
flag.StringVar(&args.createArgs.ImagePath, "imagepath", "", "")
|
||||
flag.Int64Var(&args.createArgs.ImageSize, "imagesize", 200*(1<<20), "")
|
||||
flag.StringVar(&args.createArgs.Mountpoint, "mountpoint", "", "")
|
||||
flag.BoolVar(&args.stopAndKeepPoolOnFail, "failure.stop-and-keep-pool", false, "if a test case fails, stop test execution and keep pool as it was when the test failed")
|
||||
flag.StringVar(&args.run, "run", "", "")
|
||||
flag.Parse()
|
||||
func HarnessRun(args HarnessArgs) error {
|
||||
|
||||
args.runRE = regexp.MustCompile(args.run)
|
||||
runRE := regexp.MustCompile(args.Run)
|
||||
|
||||
outlets := logger.NewOutlets()
|
||||
outlet, level, err := logging.ParseOutlet(config.LoggingOutletEnum{Ret: &config.StdoutLoggingOutlet{
|
||||
@@ -65,13 +68,13 @@ func doMain() error {
|
||||
outlets.Add(outlet, level)
|
||||
logger := logger.NewLogger(outlets, 1*time.Second)
|
||||
|
||||
if err := args.createArgs.Validate(); err != nil {
|
||||
if err := args.CreateArgs.Validate(); err != nil {
|
||||
logger.Error(err.Error())
|
||||
panic(err)
|
||||
}
|
||||
ctx := context.Background()
|
||||
defer trace.WithTaskFromStackUpdateCtx(&ctx)()
|
||||
ctx = platformtest.WithLogger(ctx, logger)
|
||||
ctx = logging.WithLoggers(ctx, logging.SubsystemLoggersWithUniversalLogger(logger))
|
||||
ex := platformtest.NewEx(logger)
|
||||
|
||||
type invocation struct {
|
||||
@@ -81,7 +84,7 @@ func doMain() error {
|
||||
|
||||
invocations := make([]*invocation, 0, len(tests.Cases))
|
||||
for _, c := range tests.Cases {
|
||||
if args.runRE.MatchString(c.String()) {
|
||||
if runRE.MatchString(c.String()) {
|
||||
invocations = append(invocations, &invocation{runFunc: c})
|
||||
}
|
||||
}
|
||||
@@ -90,7 +93,7 @@ func doMain() error {
|
||||
|
||||
bold.Printf("BEGIN TEST CASE %s\n", inv.runFunc.String())
|
||||
|
||||
pool, err := platformtest.CreateOrReplaceZpool(ctx, ex, args.createArgs)
|
||||
pool, err := platformtest.CreateOrReplaceZpool(ctx, ex, args.CreateArgs)
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "create test pool"))
|
||||
}
|
||||
@@ -106,7 +109,7 @@ func doMain() error {
|
||||
fmt.Printf("%+v\n", res.failedStack) // print with stack trace
|
||||
}
|
||||
|
||||
if res.failed && args.stopAndKeepPoolOnFail {
|
||||
if res.failed && args.StopAndKeepPoolOnFail {
|
||||
boldRed.Printf("STOPPING TEST RUN AT FAILING TEST PER USER REQUEST\n")
|
||||
return exitWithErr
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -21,22 +22,29 @@ import (
|
||||
// https://github.com/wadey/gocovmerge
|
||||
|
||||
func TestMain(t *testing.T) {
|
||||
fmt.Println("incoming args: ", os.Args)
|
||||
|
||||
var (
|
||||
args []string
|
||||
run bool
|
||||
args []string
|
||||
run bool
|
||||
startCaptureArgs bool
|
||||
)
|
||||
|
||||
for _, arg := range os.Args {
|
||||
for i, arg := range os.Args {
|
||||
switch {
|
||||
case arg == "__DEVEL--i-heard-you-like-tests":
|
||||
run = true
|
||||
startCaptureArgs = true
|
||||
case strings.HasPrefix(arg, "-test"):
|
||||
case strings.HasPrefix(arg, "__DEVEL"):
|
||||
default:
|
||||
case i == 0:
|
||||
args = append(args, arg)
|
||||
case startCaptureArgs:
|
||||
args = append(args, arg)
|
||||
}
|
||||
}
|
||||
os.Args = args
|
||||
fmt.Println("using args: ", os.Args)
|
||||
|
||||
if run {
|
||||
main()
|
||||
|
||||
@@ -27,6 +27,7 @@ func (c *Context) Logf(format string, args ...interface{}) {
|
||||
|
||||
func (c *Context) Errorf(format string, args ...interface{}) {
|
||||
GetLog(c).Printf(format, args...)
|
||||
c.FailNow()
|
||||
}
|
||||
|
||||
func (c *Context) FailNow() {
|
||||
|
||||
@@ -3,22 +3,12 @@ package platformtest
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zrepl/zrepl/daemon/logging"
|
||||
"github.com/zrepl/zrepl/logger"
|
||||
)
|
||||
|
||||
type Logger = logger.Logger
|
||||
|
||||
type contextKey int
|
||||
|
||||
const (
|
||||
contextKeyLogger contextKey = iota
|
||||
)
|
||||
|
||||
func WithLogger(ctx context.Context, logger Logger) context.Context {
|
||||
ctx = context.WithValue(ctx, contextKeyLogger, logger)
|
||||
return ctx
|
||||
}
|
||||
|
||||
func GetLog(ctx context.Context) Logger {
|
||||
return ctx.Value(contextKeyLogger).(Logger)
|
||||
return logging.GetLogger(ctx, logging.SubsysPlatformtest)
|
||||
}
|
||||
|
||||
@@ -58,6 +58,14 @@ func CreateOrReplaceZpool(ctx context.Context, e Execer, args ZpoolCreateArgs) (
|
||||
}
|
||||
}
|
||||
|
||||
// clear the mountpoint dir
|
||||
if err := os.RemoveAll(args.Mountpoint); err != nil {
|
||||
return nil, errors.Wrapf(err, "remove mountpoint dir %q", args.Mountpoint)
|
||||
}
|
||||
if err := os.Mkdir(args.Mountpoint, 0700); err != nil {
|
||||
return nil, errors.Wrapf(err, "create mountpoint dir %q", args.Mountpoint)
|
||||
}
|
||||
|
||||
// idempotently (re)create the pool image
|
||||
image, err := os.OpenFile(args.ImagePath, os.O_CREATE|os.O_RDWR, 0600)
|
||||
if err != nil {
|
||||
@@ -93,5 +101,9 @@ func (p *Zpool) Destroy(ctx context.Context, e Execer) error {
|
||||
return errors.Wrapf(err, "remove pool image")
|
||||
}
|
||||
|
||||
if err := os.RemoveAll(p.args.Mountpoint); err != nil {
|
||||
return errors.Wrapf(err, "remove mountpoint dir %q", p.args.Mountpoint)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"go/ast"
|
||||
"go/format"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"golang.org/x/tools/go/packages"
|
||||
)
|
||||
|
||||
func check(err error) {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
type platformtestFuncDeclFinder struct {
|
||||
pkg *packages.Package
|
||||
testFuncs []*ast.FuncDecl
|
||||
}
|
||||
|
||||
func isPlatformtestFunc(n *ast.FuncDecl) bool {
|
||||
if !n.Name.IsExported() {
|
||||
return false
|
||||
}
|
||||
if n.Recv != nil {
|
||||
return false
|
||||
}
|
||||
if n.Type.Results.NumFields() != 0 {
|
||||
return false
|
||||
}
|
||||
if n.Type.Params.NumFields() != 1 {
|
||||
return false
|
||||
}
|
||||
se, ok := n.Type.Params.List[0].Type.(*ast.StarExpr)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
sel, ok := se.X.(*ast.SelectorExpr)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
x, ok := sel.X.(*ast.Ident)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if x.Name != "platformtest" || sel.Sel.Name != "Context" {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (e *platformtestFuncDeclFinder) Visit(n2 ast.Node) ast.Visitor {
|
||||
switch n := n2.(type) {
|
||||
case *ast.File:
|
||||
return e
|
||||
case *ast.FuncDecl:
|
||||
if isPlatformtestFunc(n) {
|
||||
e.testFuncs = append(e.testFuncs, n)
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
||||
// TODO safeguards that prevent us from deleting non-generated generated_cases.go
|
||||
os.Remove("generated_cases.go")
|
||||
// (no error handling to easily cover the case where the file doesn't exist)
|
||||
|
||||
pkgs, err := packages.Load(
|
||||
&packages.Config{
|
||||
Mode: packages.LoadFiles,
|
||||
Tests: false,
|
||||
},
|
||||
os.Args[1],
|
||||
)
|
||||
check(err)
|
||||
|
||||
if len(pkgs) != 1 {
|
||||
panic(pkgs)
|
||||
}
|
||||
|
||||
p := pkgs[0]
|
||||
|
||||
var tests []*ast.FuncDecl
|
||||
|
||||
for _, f := range p.GoFiles {
|
||||
s := token.NewFileSet()
|
||||
a, err := parser.ParseFile(s, f, nil, parser.AllErrors)
|
||||
check(err)
|
||||
finder := &platformtestFuncDeclFinder{
|
||||
pkg: p,
|
||||
}
|
||||
ast.Walk(finder, a)
|
||||
tests = append(tests, finder.testFuncs...)
|
||||
}
|
||||
|
||||
sort.Slice(tests, func(i, j int) bool {
|
||||
return strings.Compare(tests[i].Name.Name, tests[j].Name.Name) < 0
|
||||
})
|
||||
|
||||
{
|
||||
casesTemplate := `
|
||||
// Code generated by zrepl tooling; DO NOT EDIT.
|
||||
|
||||
package tests
|
||||
|
||||
var Cases = []Case {
|
||||
{{- range . -}}
|
||||
{{ .Name }},
|
||||
{{ end -}}
|
||||
}
|
||||
|
||||
`
|
||||
t, err := template.New("CaseFunc").Parse(casesTemplate)
|
||||
check(err)
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = t.Execute(&buf, tests)
|
||||
check(err)
|
||||
|
||||
formatted, err := format.Source(buf.Bytes())
|
||||
check(err)
|
||||
|
||||
err = ioutil.WriteFile("generated_cases.go", formatted, 0664)
|
||||
check(err)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// Code generated by zrepl tooling; DO NOT EDIT.
|
||||
|
||||
package tests
|
||||
|
||||
var Cases = []Case{BatchDestroy,
|
||||
CreateReplicationCursor,
|
||||
GetNonexistent,
|
||||
HoldsWork,
|
||||
IdempotentBookmark,
|
||||
IdempotentDestroy,
|
||||
IdempotentHold,
|
||||
ListFilesystemVersionsFilesystemNotExist,
|
||||
ListFilesystemVersionsTypeFilteringAndPrefix,
|
||||
ListFilesystemVersionsUserrefs,
|
||||
ListFilesystemVersionsZeroExistIsNotAnError,
|
||||
ListFilesystemsNoFilter,
|
||||
ReceiveForceIntoEncryptedErr,
|
||||
ReceiveForceRollbackWorksUnencrypted,
|
||||
ReplicationFailingInitialParentProhibitsChildReplication,
|
||||
ReplicationIncrementalCleansUpStaleAbstractionsWithCacheOnSecondReplication,
|
||||
ReplicationIncrementalCleansUpStaleAbstractionsWithoutCacheOnSecondReplication,
|
||||
ReplicationIncrementalDestroysStepHoldsIffIncrementalStepHoldsAreDisabledButStepHoldsExist,
|
||||
ReplicationIncrementalIsPossibleIfCommonSnapshotIsDestroyed,
|
||||
ReplicationIsResumableFullSend__both_GuaranteeResumability,
|
||||
ReplicationIsResumableFullSend__initial_GuaranteeIncrementalReplication_incremental_GuaranteeIncrementalReplication,
|
||||
ReplicationIsResumableFullSend__initial_GuaranteeResumability_incremental_GuaranteeIncrementalReplication,
|
||||
ReplicationReceiverErrorWhileStillSending,
|
||||
ReplicationStepCompletedLostBehavior__GuaranteeIncrementalReplication,
|
||||
ReplicationStepCompletedLostBehavior__GuaranteeResumability,
|
||||
ResumableRecvAndTokenHandling,
|
||||
ResumeTokenParsing,
|
||||
SendArgsValidationEncryptedSendOfUnencryptedDatasetForbidden,
|
||||
SendArgsValidationResumeTokenDifferentFilesystemForbidden,
|
||||
SendArgsValidationResumeTokenEncryptionMismatchForbidden,
|
||||
UndestroyableSnapshotParsing,
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"path"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/zrepl/zrepl/platformtest"
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
|
||||
func HoldsWork(ctx *platformtest.Context) {
|
||||
platformtest.Run(ctx, platformtest.PanicErr, ctx.RootDataset, `
|
||||
DESTROYROOT
|
||||
CREATEROOT
|
||||
+ "foo bar"
|
||||
+ "foo bar@snap name"
|
||||
`)
|
||||
|
||||
fs := path.Join(ctx.RootDataset, "foo bar")
|
||||
|
||||
err := zfs.ZFSHold(ctx, fs, fsversion(ctx, fs, "@snap name"), "tag 1")
|
||||
require.NoError(ctx, err)
|
||||
|
||||
err = zfs.ZFSHold(ctx, fs, fsversion(ctx, fs, "@snap name"), "tag 2")
|
||||
require.NoError(ctx, err)
|
||||
|
||||
holds, err := zfs.ZFSHolds(ctx, fs, "snap name")
|
||||
require.NoError(ctx, err)
|
||||
require.Len(ctx, holds, 2)
|
||||
require.Contains(ctx, holds, "tag 1")
|
||||
require.Contains(ctx, holds, "tag 2")
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package tests
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/zrepl/zrepl/platformtest"
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
@@ -19,22 +20,23 @@ func IdempotentBookmark(ctx *platformtest.Context) {
|
||||
|
||||
fs := fmt.Sprintf("%s/foo bar", ctx.RootDataset)
|
||||
|
||||
asnap := sendArgVersion(ctx, fs, "@a snap")
|
||||
anotherSnap := sendArgVersion(ctx, fs, "@another snap")
|
||||
asnap := fsversion(ctx, fs, "@a snap")
|
||||
anotherSnap := fsversion(ctx, fs, "@another snap")
|
||||
|
||||
err := zfs.ZFSBookmark(ctx, fs, asnap, "a bookmark")
|
||||
aBookmark, err := zfs.ZFSBookmark(ctx, fs, asnap, "a bookmark")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// do it again, should be idempotent
|
||||
err = zfs.ZFSBookmark(ctx, fs, asnap, "a bookmark")
|
||||
aBookmarkIdemp, err := zfs.ZFSBookmark(ctx, fs, asnap, "a bookmark")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
assert.Equal(ctx, aBookmark, aBookmarkIdemp)
|
||||
|
||||
// should fail for another snapshot
|
||||
err = zfs.ZFSBookmark(ctx, fs, anotherSnap, "a bookmark")
|
||||
_, err = zfs.ZFSBookmark(ctx, fs, anotherSnap, "a bookmark")
|
||||
if err == nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -48,7 +50,7 @@ func IdempotentBookmark(ctx *platformtest.Context) {
|
||||
}
|
||||
|
||||
// do it again, should fail with special error type
|
||||
err = zfs.ZFSBookmark(ctx, fs, asnap, "a bookmark")
|
||||
_, err = zfs.ZFSBookmark(ctx, fs, asnap, "a bookmark")
|
||||
if err == nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
@@ -18,8 +18,8 @@ func IdempotentDestroy(ctx *platformtest.Context) {
|
||||
`)
|
||||
|
||||
fs := fmt.Sprintf("%s/foo bar", ctx.RootDataset)
|
||||
asnap := sendArgVersion(ctx, fs, "@a snap")
|
||||
err := zfs.ZFSBookmark(ctx, fs, asnap, "a bookmark")
|
||||
asnap := fsversion(ctx, fs, "@a snap")
|
||||
_, err := zfs.ZFSBookmark(ctx, fs, asnap, "a bookmark")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
@@ -85,9 +85,6 @@ func ListFilesystemVersionsZeroExistIsNotAnError(t *platformtest.Context) {
|
||||
vs, err := zfs.ZFSListFilesystemVersions(t, mustDatasetPath(fs), zfs.ListFilesystemVersionsOptions{})
|
||||
require.Empty(t, vs)
|
||||
require.NoError(t, err)
|
||||
dsne, ok := err.(*zfs.DatasetDoesNotExist)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, fs, dsne.Path)
|
||||
}
|
||||
|
||||
func ListFilesystemVersionsFilesystemNotExist(t *platformtest.Context) {
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/zrepl/zrepl/platformtest"
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
|
||||
func ReceiveForceIntoEncryptedErr(ctx *platformtest.Context) {
|
||||
platformtest.Run(ctx, platformtest.PanicErr, ctx.RootDataset, `
|
||||
DESTROYROOT
|
||||
CREATEROOT
|
||||
+ "foo bar" encrypted
|
||||
+ "sender" encrypted
|
||||
+ "sender@1"
|
||||
`)
|
||||
|
||||
rfs := fmt.Sprintf("%s/foo bar", ctx.RootDataset)
|
||||
sfs := fmt.Sprintf("%s/sender", ctx.RootDataset)
|
||||
sfsSnap1 := sendArgVersion(ctx, sfs, "@1")
|
||||
|
||||
sendArgs, err := zfs.ZFSSendArgsUnvalidated{
|
||||
FS: sfs,
|
||||
Encrypted: &zfs.NilBool{B: false},
|
||||
From: nil,
|
||||
To: &sfsSnap1,
|
||||
ResumeToken: "",
|
||||
}.Validate(ctx)
|
||||
require.NoError(ctx, err)
|
||||
|
||||
sendStream, err := zfs.ZFSSend(ctx, sendArgs)
|
||||
require.NoError(ctx, err)
|
||||
|
||||
recvOpts := zfs.RecvOptions{
|
||||
RollbackAndForceRecv: true,
|
||||
SavePartialRecvState: false,
|
||||
}
|
||||
err = zfs.ZFSRecv(ctx, rfs, &zfs.ZFSSendArgVersion{RelName: "@1", GUID: sfsSnap1.GUID}, sendStream, recvOpts)
|
||||
require.Error(ctx, err)
|
||||
re, ok := err.(*zfs.RecvDestroyOrOverwriteEncryptedErr)
|
||||
require.True(ctx, ok)
|
||||
require.Contains(ctx, re.Error(), "zfs receive -F cannot be used to destroy an encrypted filesystem or overwrite an unencrypted one with an encrypted on")
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/zrepl/zrepl/platformtest"
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
|
||||
func ReceiveForceRollbackWorksUnencrypted(ctx *platformtest.Context) {
|
||||
platformtest.Run(ctx, platformtest.PanicErr, ctx.RootDataset, `
|
||||
DESTROYROOT
|
||||
CREATEROOT
|
||||
+ "foo bar"
|
||||
+ "foo bar@a snap"
|
||||
+ "foo bar@another snap"
|
||||
+ "foo bar@snap3"
|
||||
+ "sender"
|
||||
+ "sender@1"
|
||||
`)
|
||||
|
||||
rfs := fmt.Sprintf("%s/foo bar", ctx.RootDataset)
|
||||
sfs := fmt.Sprintf("%s/sender", ctx.RootDataset)
|
||||
sfsSnap1 := sendArgVersion(ctx, sfs, "@1")
|
||||
|
||||
sendArgs, err := zfs.ZFSSendArgsUnvalidated{
|
||||
FS: sfs,
|
||||
Encrypted: &zfs.NilBool{B: false},
|
||||
From: nil,
|
||||
To: &sfsSnap1,
|
||||
ResumeToken: "",
|
||||
}.Validate(ctx)
|
||||
require.NoError(ctx, err)
|
||||
|
||||
sendStream, err := zfs.ZFSSend(ctx, sendArgs)
|
||||
require.NoError(ctx, err)
|
||||
|
||||
recvOpts := zfs.RecvOptions{
|
||||
RollbackAndForceRecv: true,
|
||||
SavePartialRecvState: false,
|
||||
}
|
||||
err = zfs.ZFSRecv(ctx, rfs, &zfs.ZFSSendArgVersion{RelName: "@1", GUID: sfsSnap1.GUID}, sendStream, recvOpts)
|
||||
require.NoError(ctx, err)
|
||||
|
||||
// assert exists on receiver
|
||||
rfsSnap1 := fsversion(ctx, rfs, "@1")
|
||||
// assert it's the only one (rollback and force-recv should be blowing away the other filesystems)
|
||||
rfsVersions, err := zfs.ZFSListFilesystemVersions(ctx, mustDatasetPath(rfs), zfs.ListFilesystemVersionsOptions{})
|
||||
require.NoError(ctx, err)
|
||||
assert.Len(ctx, rfsVersions, 1)
|
||||
assert.Equal(ctx, rfsVersions[0], rfsSnap1)
|
||||
}
|
||||
@@ -0,0 +1,916 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"sort"
|
||||
|
||||
"github.com/kr/pretty"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/zrepl/zrepl/daemon/filters"
|
||||
"github.com/zrepl/zrepl/endpoint"
|
||||
"github.com/zrepl/zrepl/platformtest"
|
||||
"github.com/zrepl/zrepl/replication"
|
||||
"github.com/zrepl/zrepl/replication/logic"
|
||||
"github.com/zrepl/zrepl/replication/logic/pdu"
|
||||
"github.com/zrepl/zrepl/replication/report"
|
||||
"github.com/zrepl/zrepl/util/limitio"
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
|
||||
// mimics the replication invocations of an active-side job
|
||||
// for a single sender-receiver filesystem pair
|
||||
//
|
||||
// each invocation of method Do results in the construction
|
||||
// of a new sender and receiver instance and one blocking invocation
|
||||
// of the replication engine without encryption
|
||||
type replicationInvocation struct {
|
||||
sjid, rjid endpoint.JobID
|
||||
sfs string
|
||||
sfilter *filters.DatasetMapFilter
|
||||
rfsRoot string
|
||||
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 {
|
||||
|
||||
if i.interceptSender == nil {
|
||||
i.interceptSender = func(e *endpoint.Sender) logic.Sender { return e }
|
||||
}
|
||||
if i.interceptReceiver == nil {
|
||||
i.interceptReceiver = func(e *endpoint.Receiver) logic.Receiver { return e }
|
||||
}
|
||||
|
||||
if i.sfs != "" && i.sfilter != nil || i.sfs == "" && i.sfilter == nil {
|
||||
panic("either sfs or sfilter must be set")
|
||||
}
|
||||
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{
|
||||
FSF: i.sfilter.AsFilter(),
|
||||
Encrypt: &zfs.NilBool{B: false},
|
||||
JobID: i.sjid,
|
||||
}))
|
||||
receiver := i.interceptReceiver(endpoint.NewReceiver(endpoint.ReceiverConfig{
|
||||
JobID: i.rjid,
|
||||
AppendClientIdentity: false,
|
||||
RootWithoutClientComponent: mustDatasetPath(i.rfsRoot),
|
||||
}))
|
||||
plannerPolicy := logic.PlannerPolicy{
|
||||
EncryptedSend: logic.TriFromBool(false),
|
||||
ReplicationConfig: pdu.ReplicationConfig{
|
||||
Protection: &i.guarantee,
|
||||
},
|
||||
}
|
||||
|
||||
report, wait := replication.Do(
|
||||
ctx,
|
||||
logic.NewPlanner(nil, nil, sender, receiver, plannerPolicy),
|
||||
)
|
||||
wait(true)
|
||||
return report()
|
||||
}
|
||||
|
||||
func (i replicationInvocation) ReceiveSideFilesystem() string {
|
||||
return path.Join(i.rfsRoot, i.sfs)
|
||||
}
|
||||
|
||||
func ReplicationIncrementalIsPossibleIfCommonSnapshotIsDestroyed(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"
|
||||
snap1 := fsversion(ctx, sfs, "@1")
|
||||
|
||||
rep := replicationInvocation{
|
||||
sjid: sjid,
|
||||
rjid: rjid,
|
||||
sfs: sfs,
|
||||
rfsRoot: rfsRoot,
|
||||
guarantee: *pdu.ReplicationConfigProtectionWithKind(pdu.ReplicationGuaranteeKind_GuaranteeResumability),
|
||||
}
|
||||
rfs := rep.ReceiveSideFilesystem()
|
||||
|
||||
// first replication
|
||||
report := rep.Do(ctx)
|
||||
ctx.Logf("\n%s", pretty.Sprint(report))
|
||||
|
||||
// assert @1 exists on receiver
|
||||
_ = fsversion(ctx, rfs, "@1")
|
||||
|
||||
// cut off the common base between sender and receiver
|
||||
// (replication engine guarantees resumability through bookmarks)
|
||||
err := zfs.ZFSDestroy(ctx, snap1.FullPath(sfs))
|
||||
require.NoError(ctx, err)
|
||||
|
||||
// assert that the replication cursor has been created
|
||||
snap1CursorName, err := endpoint.ReplicationCursorBookmarkName(sfs, snap1.Guid, sjid)
|
||||
require.NoError(ctx, err)
|
||||
snap1CursorInfo, err := zfs.ZFSGetFilesystemVersion(ctx, sfs+"#"+snap1CursorName)
|
||||
require.NoError(ctx, err)
|
||||
require.True(ctx, snap1CursorInfo.IsBookmark())
|
||||
|
||||
// second replication of a new snapshot, should use the cursor
|
||||
mustSnapshot(ctx, sfs+"@2")
|
||||
report = rep.Do(ctx)
|
||||
ctx.Logf("\n%s", pretty.Sprint(report))
|
||||
_ = fsversion(ctx, rfs, "@2")
|
||||
|
||||
}
|
||||
|
||||
func ReplicationIncrementalCleansUpStaleAbstractionsWithCacheOnSecondReplication(ctx *platformtest.Context) {
|
||||
implReplicationIncrementalCleansUpStaleAbstractions(ctx, true)
|
||||
}
|
||||
|
||||
func ReplicationIncrementalCleansUpStaleAbstractionsWithoutCacheOnSecondReplication(ctx *platformtest.Context) {
|
||||
implReplicationIncrementalCleansUpStaleAbstractions(ctx, false)
|
||||
}
|
||||
|
||||
func implReplicationIncrementalCleansUpStaleAbstractions(ctx *platformtest.Context, invalidateCacheBeforeSecondReplication bool) {
|
||||
|
||||
platformtest.Run(ctx, platformtest.PanicErr, ctx.RootDataset, `
|
||||
CREATEROOT
|
||||
+ "sender"
|
||||
+ "sender@1"
|
||||
+ "sender@2"
|
||||
+ "sender#2" "sender@2"
|
||||
+ "sender@3"
|
||||
+ "receiver"
|
||||
R zfs create -p "${ROOTDS}/receiver/${ROOTDS}"
|
||||
`)
|
||||
|
||||
sjid := endpoint.MustMakeJobID("sender-job")
|
||||
ojid := endpoint.MustMakeJobID("other-job")
|
||||
rjid := endpoint.MustMakeJobID("receiver-job")
|
||||
|
||||
sfs := ctx.RootDataset + "/sender"
|
||||
rfsRoot := ctx.RootDataset + "/receiver"
|
||||
|
||||
rep := replicationInvocation{
|
||||
sjid: sjid,
|
||||
rjid: rjid,
|
||||
sfs: sfs,
|
||||
rfsRoot: rfsRoot,
|
||||
guarantee: *pdu.ReplicationConfigProtectionWithKind(pdu.ReplicationGuaranteeKind_GuaranteeResumability),
|
||||
}
|
||||
rfs := rep.ReceiveSideFilesystem()
|
||||
|
||||
// first replication
|
||||
report := rep.Do(ctx)
|
||||
ctx.Logf("\n%s", pretty.Sprint(report))
|
||||
|
||||
// assert most recent send-side version @3 exists on receiver (=replication succeeded)
|
||||
rSnap3 := fsversion(ctx, rfs, "@3")
|
||||
// assert the source-side versions not managed by zrepl still exist
|
||||
snap1 := fsversion(ctx, sfs, "@1")
|
||||
snap2 := fsversion(ctx, sfs, "@2")
|
||||
_ = fsversion(ctx, sfs, "#2") // non-replicationc-cursor bookmarks should not be affected
|
||||
snap3 := fsversion(ctx, sfs, "@3")
|
||||
// assert a replication cursor is in place
|
||||
snap3CursorName, err := endpoint.ReplicationCursorBookmarkName(sfs, snap3.Guid, sjid)
|
||||
require.NoError(ctx, err)
|
||||
_ = fsversion(ctx, sfs, "#"+snap3CursorName)
|
||||
// assert a last-received hold is in place
|
||||
expectRjidHoldTag, err := endpoint.LastReceivedHoldTag(rjid)
|
||||
require.NoError(ctx, err)
|
||||
holds, err := zfs.ZFSHolds(ctx, rfs, rSnap3.Name)
|
||||
require.NoError(ctx, err)
|
||||
require.Contains(ctx, holds, expectRjidHoldTag)
|
||||
|
||||
// create artifical stale replication cursors & step holds
|
||||
createArtificalStaleAbstractions := func(jobId endpoint.JobID) []endpoint.Abstraction {
|
||||
snap2Cursor, err := endpoint.CreateReplicationCursor(ctx, sfs, snap2, jobId) // no shadow
|
||||
require.NoError(ctx, err)
|
||||
// create artifical stale step holds jobId
|
||||
snap1Hold, err := endpoint.HoldStep(ctx, sfs, snap1, jobId) // no shadow
|
||||
require.NoError(ctx, err)
|
||||
snap2Hold, err := endpoint.HoldStep(ctx, sfs, snap2, jobId) // no shadow
|
||||
require.NoError(ctx, err)
|
||||
return []endpoint.Abstraction{snap2Cursor, snap1Hold, snap2Hold}
|
||||
}
|
||||
createArtificalStaleAbstractions(sjid)
|
||||
ojidSendAbstractions := createArtificalStaleAbstractions(ojid)
|
||||
|
||||
snap3ojidLastReceivedHold, err := endpoint.CreateLastReceivedHold(ctx, rfs, fsversion(ctx, rfs, "@3"), ojid)
|
||||
require.NoError(ctx, err)
|
||||
require.True(ctx, zfs.FilesystemVersionEqualIdentity(fsversion(ctx, rfs, "@3"), snap3ojidLastReceivedHold.GetFilesystemVersion()))
|
||||
|
||||
// take another 2 snapshots
|
||||
mustSnapshot(ctx, sfs+"@4")
|
||||
mustSnapshot(ctx, sfs+"@5")
|
||||
snap5 := fsversion(ctx, sfs, "@5")
|
||||
|
||||
if invalidateCacheBeforeSecondReplication {
|
||||
endpoint.AbstractionsCacheInvalidate(sfs)
|
||||
}
|
||||
|
||||
// do another replication
|
||||
// - ojid's abstractions should not be affected on either side
|
||||
// - stale abstractions of sjid and rjid should be cleaned up
|
||||
// - 1 replication cursors and 1 last-received hold should be present
|
||||
|
||||
checkOjidAbstractionsExist := func() {
|
||||
var expectedOjidAbstractions []endpoint.Abstraction
|
||||
expectedOjidAbstractions = append(expectedOjidAbstractions, ojidSendAbstractions...)
|
||||
expectedOjidAbstractions = append(expectedOjidAbstractions, snap3ojidLastReceivedHold)
|
||||
|
||||
sfsAndRfsFilter := filters.NewDatasetMapFilter(2, true)
|
||||
require.NoError(ctx, sfsAndRfsFilter.Add(sfs, "ok"))
|
||||
require.NoError(ctx, sfsAndRfsFilter.Add(rfs, "ok"))
|
||||
rAbs, rAbsErrs, err := endpoint.ListAbstractions(ctx, endpoint.ListZFSHoldsAndBookmarksQuery{
|
||||
FS: endpoint.ListZFSHoldsAndBookmarksQueryFilesystemFilter{Filter: sfsAndRfsFilter},
|
||||
JobID: &ojid,
|
||||
What: endpoint.AbstractionTypesAll,
|
||||
Concurrency: 1,
|
||||
})
|
||||
require.NoError(ctx, err)
|
||||
require.Len(ctx, rAbsErrs, 0)
|
||||
ctx.Logf("rAbs=%s", rAbs)
|
||||
ctx.Logf("expectedOjidAbstractions=%s", expectedOjidAbstractions)
|
||||
require.Equal(ctx, len(expectedOjidAbstractions), len(rAbs))
|
||||
for _, ea := range expectedOjidAbstractions {
|
||||
ctx.Logf("looking for %s %#v", ea, ea.GetFilesystemVersion())
|
||||
found := false
|
||||
for _, a := range rAbs {
|
||||
eq := endpoint.AbstractionEquals(ea, a)
|
||||
ctx.Logf("comp=%v for %s %#v", eq, a, a.GetFilesystemVersion())
|
||||
found = found || eq
|
||||
}
|
||||
require.True(ctx, found, "%s", ea)
|
||||
}
|
||||
}
|
||||
checkOjidAbstractionsExist()
|
||||
|
||||
report = rep.Do(ctx)
|
||||
ctx.Logf("\n%s", pretty.Sprint(report))
|
||||
|
||||
checkOjidAbstractionsExist()
|
||||
|
||||
_ = fsversion(ctx, sfs, "@1")
|
||||
_ = fsversion(ctx, sfs, "@2")
|
||||
_ = fsversion(ctx, sfs, "#2")
|
||||
_ = fsversion(ctx, sfs, "@3")
|
||||
_ = fsversion(ctx, sfs, "@4")
|
||||
_ = fsversion(ctx, sfs, "@5")
|
||||
|
||||
_ = fsversion(ctx, rfs, "@3")
|
||||
_ = fsversion(ctx, rfs, "@4")
|
||||
_ = fsversion(ctx, rfs, "@5")
|
||||
|
||||
// check bookmark situation
|
||||
{
|
||||
sBms, err := zfs.ZFSListFilesystemVersions(ctx, mustDatasetPath(sfs), zfs.ListFilesystemVersionsOptions{
|
||||
Types: zfs.Bookmarks,
|
||||
})
|
||||
ctx.Logf("sbms=%s", sBms)
|
||||
require.NoError(ctx, err)
|
||||
|
||||
snap5SjidCursorName, err := endpoint.ReplicationCursorBookmarkName(sfs, snap5.Guid, sjid)
|
||||
require.NoError(ctx, err)
|
||||
snap2SjidCursorName, err := endpoint.ReplicationCursorBookmarkName(sfs, snap2.Guid, sjid)
|
||||
require.NoError(ctx, err)
|
||||
snap2OjidCursorName, err := endpoint.ReplicationCursorBookmarkName(sfs, snap2.Guid, ojid)
|
||||
require.NoError(ctx, err)
|
||||
var bmNames []string
|
||||
for _, bm := range sBms {
|
||||
bmNames = append(bmNames, bm.Name)
|
||||
}
|
||||
|
||||
if invalidateCacheBeforeSecondReplication {
|
||||
require.Len(ctx, sBms, 3)
|
||||
require.Contains(ctx, bmNames, snap5SjidCursorName)
|
||||
require.Contains(ctx, bmNames, snap2OjidCursorName)
|
||||
require.Contains(ctx, bmNames, "2")
|
||||
} else {
|
||||
require.Len(ctx, sBms, 4)
|
||||
require.Contains(ctx, bmNames, snap5SjidCursorName)
|
||||
require.Contains(ctx, bmNames, snap2SjidCursorName)
|
||||
require.Contains(ctx, bmNames, snap2OjidCursorName)
|
||||
require.Contains(ctx, bmNames, "2")
|
||||
}
|
||||
}
|
||||
|
||||
// check last-received hold moved
|
||||
{
|
||||
rAbs, rAbsErrs, err := endpoint.ListAbstractions(ctx, endpoint.ListZFSHoldsAndBookmarksQuery{
|
||||
FS: endpoint.ListZFSHoldsAndBookmarksQueryFilesystemFilter{FS: &rfs},
|
||||
JobID: &rjid,
|
||||
What: endpoint.AbstractionTypesAll,
|
||||
Concurrency: 1,
|
||||
})
|
||||
require.NoError(ctx, err)
|
||||
require.Len(ctx, rAbsErrs, 0)
|
||||
require.Len(ctx, rAbs, 1)
|
||||
require.Equal(ctx, rAbs[0].GetType(), endpoint.AbstractionLastReceivedHold)
|
||||
require.Equal(ctx, *rAbs[0].GetJobID(), rjid)
|
||||
require.Equal(ctx, rAbs[0].GetFilesystemVersion().GetGuid(), snap5.GetGuid())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
type PartialSender struct {
|
||||
*endpoint.Sender
|
||||
failAfterByteCount int64
|
||||
}
|
||||
|
||||
var _ logic.Sender = (*PartialSender)(nil)
|
||||
|
||||
func (s *PartialSender) Send(ctx context.Context, r *pdu.SendReq) (r1 *pdu.SendRes, r2 io.ReadCloser, r3 error) {
|
||||
r1, r2, r3 = s.Sender.Send(ctx, r)
|
||||
r2 = limitio.ReadCloser(r2, s.failAfterByteCount)
|
||||
return r1, r2, r3
|
||||
}
|
||||
|
||||
func ReplicationIsResumableFullSend__both_GuaranteeResumability(ctx *platformtest.Context) {
|
||||
|
||||
setup := replicationIsResumableFullSendSetup{
|
||||
protection: pdu.ReplicationConfigProtection{
|
||||
Initial: pdu.ReplicationGuaranteeKind_GuaranteeResumability,
|
||||
Incremental: pdu.ReplicationGuaranteeKind_GuaranteeResumability,
|
||||
},
|
||||
expectDatasetIsBusyErrorWhenDestroySnapshotWhilePartiallyReplicated: true,
|
||||
expectAllThreeSnapshotsToThreeBePresentAfterLoop: true,
|
||||
expectNoSnapshotsOnReceiverAfterLoop: false,
|
||||
}
|
||||
|
||||
implReplicationIsResumableFullSend(ctx, setup)
|
||||
}
|
||||
|
||||
func ReplicationIsResumableFullSend__initial_GuaranteeResumability_incremental_GuaranteeIncrementalReplication(ctx *platformtest.Context) {
|
||||
|
||||
setup := replicationIsResumableFullSendSetup{
|
||||
protection: pdu.ReplicationConfigProtection{
|
||||
Initial: pdu.ReplicationGuaranteeKind_GuaranteeResumability,
|
||||
Incremental: pdu.ReplicationGuaranteeKind_GuaranteeIncrementalReplication,
|
||||
},
|
||||
expectDatasetIsBusyErrorWhenDestroySnapshotWhilePartiallyReplicated: true,
|
||||
expectAllThreeSnapshotsToThreeBePresentAfterLoop: true,
|
||||
expectNoSnapshotsOnReceiverAfterLoop: false,
|
||||
}
|
||||
|
||||
implReplicationIsResumableFullSend(ctx, setup)
|
||||
}
|
||||
|
||||
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, `
|
||||
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"
|
||||
|
||||
sfsmp, err := zfs.ZFSGetMountpoint(ctx, sfs)
|
||||
require.NoError(ctx, err)
|
||||
require.True(ctx, sfsmp.Mounted)
|
||||
|
||||
writeDummyData(path.Join(sfsmp.Mountpoint, "dummy.data"), 1<<22)
|
||||
mustSnapshot(ctx, sfs+"@1")
|
||||
snap1 := fsversion(ctx, sfs, "@1")
|
||||
|
||||
rep := replicationInvocation{
|
||||
sjid: sjid,
|
||||
rjid: rjid,
|
||||
sfs: sfs,
|
||||
rfsRoot: rfsRoot,
|
||||
interceptSender: func(e *endpoint.Sender) logic.Sender {
|
||||
return &PartialSender{Sender: e, failAfterByteCount: 1 << 20}
|
||||
},
|
||||
guarantee: setup.protection,
|
||||
}
|
||||
|
||||
rfs := rep.ReceiveSideFilesystem()
|
||||
|
||||
for i := 2; i < 10; i++ {
|
||||
report := rep.Do(ctx)
|
||||
ctx.Logf("\n%s", pretty.Sprint(report))
|
||||
|
||||
// always attempt to destroy the incremental source
|
||||
err := zfs.ZFSDestroy(ctx, snap1.FullPath(sfs))
|
||||
if i < 4 {
|
||||
// we configured the PartialSender to fail after 1<<20 bytes
|
||||
// and we wrote dummy data 1<<22 bytes, thus at least
|
||||
// for the first 4 times this should not be possible
|
||||
// due to step holds
|
||||
if setup.expectDatasetIsBusyErrorWhenDestroySnapshotWhilePartiallyReplicated {
|
||||
ctx.Logf("i=%v", i)
|
||||
require.Error(ctx, err)
|
||||
require.Contains(ctx, err.Error(), "dataset is busy")
|
||||
}
|
||||
}
|
||||
|
||||
// and create some additional snapshots that could
|
||||
// confuse a naive implementation that doesn't take into
|
||||
// account resume state when planning replication
|
||||
if i == 2 || i == 3 {
|
||||
// no significant size to avoid making this test run longer than necessary
|
||||
mustSnapshot(ctx, fmt.Sprintf("%s@%d", sfs, i))
|
||||
}
|
||||
|
||||
require.Len(ctx, report.Attempts, 1)
|
||||
require.Nil(ctx, report.Attempts[0].PlanError)
|
||||
require.Len(ctx, report.Attempts[0].Filesystems, 1)
|
||||
if len(report.Attempts[0].Filesystems[0].Steps) == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if setup.expectAllThreeSnapshotsToThreeBePresentAfterLoop {
|
||||
// make sure all the filesystem versions we created
|
||||
// were replicated by the replication loop
|
||||
_ = fsversion(ctx, rfs, "@1")
|
||||
_ = 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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func ReplicationIncrementalDestroysStepHoldsIffIncrementalStepHoldsAreDisabledButStepHoldsExist(ctx *platformtest.Context) {
|
||||
|
||||
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(pdu.ReplicationGuaranteeKind_GuaranteeResumability),
|
||||
}
|
||||
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 large snapshot @2
|
||||
{
|
||||
sfsmp, err := zfs.ZFSGetMountpoint(ctx, sfs)
|
||||
require.NoError(ctx, err)
|
||||
require.True(ctx, sfsmp.Mounted)
|
||||
writeDummyData(path.Join(sfsmp.Mountpoint, "dummy.data"), 1<<22)
|
||||
mustSnapshot(ctx, sfs+"@2")
|
||||
}
|
||||
snap2sfs := fsversion(ctx, sfs, "@2")
|
||||
|
||||
// partially replicate snapshots @2 with step holds enabled
|
||||
// to effect a step-holds situation
|
||||
{
|
||||
rep := replicationInvocation{
|
||||
sjid: sjid,
|
||||
rjid: rjid,
|
||||
sfs: sfs,
|
||||
rfsRoot: rfsRoot,
|
||||
guarantee: *pdu.ReplicationConfigProtectionWithKind(pdu.ReplicationGuaranteeKind_GuaranteeResumability), // !
|
||||
interceptSender: func(e *endpoint.Sender) logic.Sender {
|
||||
return &PartialSender{Sender: e, failAfterByteCount: 1 << 20}
|
||||
},
|
||||
}
|
||||
rfs := rep.ReceiveSideFilesystem()
|
||||
report := rep.Do(ctx)
|
||||
ctx.Logf("\n%s", pretty.Sprint(report))
|
||||
// assert this partial receive worked
|
||||
_, err := zfs.ZFSGetFilesystemVersion(ctx, rfs+"@2")
|
||||
ctx.Logf("%T %s", err, err)
|
||||
_, notFullyReceived := err.(*zfs.DatasetDoesNotExist)
|
||||
require.True(ctx, notFullyReceived)
|
||||
// assert step holds are in place
|
||||
abs, absErrs, err := endpoint.ListAbstractions(ctx, endpoint.ListZFSHoldsAndBookmarksQuery{
|
||||
FS: endpoint.ListZFSHoldsAndBookmarksQueryFilesystemFilter{
|
||||
FS: &sfs,
|
||||
},
|
||||
Concurrency: 1,
|
||||
JobID: &sjid,
|
||||
What: endpoint.AbstractionTypeSet{endpoint.AbstractionStepHold: true},
|
||||
})
|
||||
require.NoError(ctx, err)
|
||||
require.Empty(ctx, absErrs)
|
||||
require.Len(ctx, abs, 2)
|
||||
sort.Slice(abs, func(i, j int) bool {
|
||||
return abs[i].GetCreateTXG() < abs[j].GetCreateTXG()
|
||||
})
|
||||
require.True(ctx, zfs.FilesystemVersionEqualIdentity(abs[0].GetFilesystemVersion(), fsversion(ctx, sfs, "@1")))
|
||||
require.True(ctx, zfs.FilesystemVersionEqualIdentity(abs[1].GetFilesystemVersion(), fsversion(ctx, sfs, "@2")))
|
||||
}
|
||||
|
||||
//
|
||||
// end of test setup
|
||||
//
|
||||
|
||||
// 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 fail intermittently due to partial sender being fully read
|
||||
// - the partial sender is 1/4th the length of the stream, thus expect
|
||||
// successful replication after 5 more attempts
|
||||
rep := replicationInvocation{
|
||||
sjid: sjid,
|
||||
rjid: rjid,
|
||||
sfs: sfs,
|
||||
rfsRoot: rfsRoot,
|
||||
guarantee: *pdu.ReplicationConfigProtectionWithKind(pdu.ReplicationGuaranteeKind_GuaranteeIncrementalReplication), // !
|
||||
interceptSender: func(e *endpoint.Sender) logic.Sender {
|
||||
return &PartialSender{Sender: e, failAfterByteCount: 1 << 20}
|
||||
},
|
||||
}
|
||||
rfs := rep.ReceiveSideFilesystem()
|
||||
for i := 0; ; i++ {
|
||||
require.True(ctx, i < 5)
|
||||
report := rep.Do(ctx)
|
||||
ctx.Logf("retry run=%v\n%s", i, pretty.Sprint(report))
|
||||
_, err := zfs.ZFSGetFilesystemVersion(ctx, rfs+"@2")
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// assert replication worked
|
||||
fsversion(ctx, rfs, "@2")
|
||||
|
||||
// assert no step holds exist
|
||||
abs, absErrs, err := endpoint.ListAbstractions(ctx, endpoint.ListZFSHoldsAndBookmarksQuery{
|
||||
FS: endpoint.ListZFSHoldsAndBookmarksQueryFilesystemFilter{
|
||||
FS: &sfs,
|
||||
},
|
||||
Concurrency: 1,
|
||||
JobID: &sjid,
|
||||
What: endpoint.AbstractionTypeSet{endpoint.AbstractionStepHold: true},
|
||||
})
|
||||
require.NoError(ctx, err)
|
||||
require.Empty(ctx, absErrs)
|
||||
require.Len(ctx, abs, 0)
|
||||
|
||||
// assert that the replication cursor bookmark exists
|
||||
abs, absErrs, err = endpoint.ListAbstractions(ctx, endpoint.ListZFSHoldsAndBookmarksQuery{
|
||||
FS: endpoint.ListZFSHoldsAndBookmarksQueryFilesystemFilter{
|
||||
FS: &sfs,
|
||||
},
|
||||
Concurrency: 1,
|
||||
JobID: &sjid,
|
||||
What: endpoint.AbstractionTypeSet{endpoint.AbstractionReplicationCursorBookmarkV2: true},
|
||||
})
|
||||
require.NoError(ctx, err)
|
||||
require.Empty(ctx, absErrs)
|
||||
require.Len(ctx, abs, 1)
|
||||
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
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
|
||||
func ReplicationCursor(ctx *platformtest.Context) {
|
||||
func CreateReplicationCursor(ctx *platformtest.Context) {
|
||||
|
||||
platformtest.Run(ctx, platformtest.PanicErr, ctx.RootDataset, `
|
||||
CREATEROOT
|
||||
@@ -19,9 +19,10 @@ func ReplicationCursor(ctx *platformtest.Context) {
|
||||
+ "foo bar@1 with space"
|
||||
R zfs bookmark "${ROOTDS}/foo bar@1 with space" "${ROOTDS}/foo bar#1 with space"
|
||||
+ "foo bar@2 with space"
|
||||
R zfs bookmark "${ROOTDS}/foo bar@1 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"
|
||||
R zfs bookmark "${ROOTDS}/foo bar@1 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")
|
||||
@@ -32,13 +33,33 @@ func ReplicationCursor(ctx *platformtest.Context) {
|
||||
}
|
||||
|
||||
fs := ds.ToString()
|
||||
snap := fsversion(ctx, fs, "@1 with space")
|
||||
|
||||
destroyed, err := endpoint.MoveReplicationCursor(ctx, fs, &snap, jobid)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
checkCreateCursor := func(createErr error, c endpoint.Abstraction, references zfs.FilesystemVersion) {
|
||||
assert.NoError(ctx, createErr)
|
||||
expectName, err := endpoint.ReplicationCursorBookmarkName(fs, references.Guid, jobid)
|
||||
assert.NoError(ctx, err)
|
||||
require.Equal(ctx, expectName, c.GetFilesystemVersion().Name)
|
||||
}
|
||||
assert.Empty(ctx, destroyed)
|
||||
|
||||
snap := fsversion(ctx, fs, "@1 with space")
|
||||
book := fsversion(ctx, fs, "#1 with space")
|
||||
book3 := fsversion(ctx, fs, "#3 with space")
|
||||
|
||||
// create first cursor
|
||||
cursorOfSnap, err := endpoint.CreateReplicationCursor(ctx, fs, snap, jobid)
|
||||
checkCreateCursor(err, cursorOfSnap, snap)
|
||||
// check CreateReplicationCursor is idempotent (for snapshot target)
|
||||
cursorOfSnapIdemp, err := endpoint.CreateReplicationCursor(ctx, fs, snap, jobid)
|
||||
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
|
||||
_, err = endpoint.CreateReplicationCursor(ctx, fs, book3, jobid)
|
||||
assert.Equal(ctx, zfs.ErrBookmarkCloningNotSupported, err)
|
||||
// ... for target = replication cursor bookmark to be created
|
||||
cursorOfCursor, err := endpoint.CreateReplicationCursor(ctx, fs, cursorOfSnapIdemp.GetFilesystemVersion(), jobid)
|
||||
checkCreateCursor(err, cursorOfCursor, cursorOfSnap.GetFilesystemVersion())
|
||||
|
||||
snapProps, err := zfs.ZFSGetFilesystemVersion(ctx, snap.FullPath(fs))
|
||||
if err != nil {
|
||||
@@ -56,14 +77,4 @@ func ReplicationCursor(ctx *platformtest.Context) {
|
||||
panic(fmt.Sprintf("guids do not match: %v != %v", bm.Guid, snapProps.Guid))
|
||||
}
|
||||
|
||||
// try moving
|
||||
cursor1BookmarkName, err := endpoint.ReplicationCursorBookmarkName(fs, snap.Guid, jobid)
|
||||
require.NoError(ctx, err)
|
||||
|
||||
snap2 := fsversion(ctx, fs, "@2 with space")
|
||||
destroyed, err = endpoint.MoveReplicationCursor(ctx, fs, &snap2, jobid)
|
||||
require.NoError(ctx, err)
|
||||
require.Equal(ctx, 1, len(destroyed))
|
||||
require.Equal(ctx, endpoint.AbstractionReplicationCursorBookmarkV2, destroyed[0].GetType())
|
||||
require.Equal(ctx, cursor1BookmarkName, destroyed[0].GetName())
|
||||
}
|
||||
|
||||
@@ -13,22 +13,4 @@ func (c Case) String() string {
|
||||
return runtime.FuncForPC(reflect.ValueOf(c).Pointer()).Name()
|
||||
}
|
||||
|
||||
var Cases = []Case{
|
||||
BatchDestroy,
|
||||
UndestroyableSnapshotParsing,
|
||||
GetNonexistent,
|
||||
ReplicationCursor,
|
||||
IdempotentHold,
|
||||
IdempotentBookmark,
|
||||
IdempotentDestroy,
|
||||
ResumeTokenParsing,
|
||||
ResumableRecvAndTokenHandling,
|
||||
SendArgsValidationEncryptedSendOfUnencryptedDatasetForbidden,
|
||||
SendArgsValidationResumeTokenEncryptionMismatchForbidden,
|
||||
SendArgsValidationResumeTokenDifferentFilesystemForbidden,
|
||||
ListFilesystemVersionsTypeFilteringAndPrefix,
|
||||
ListFilesystemVersionsFilesystemNotExist,
|
||||
ListFilesystemVersionsFilesystemNotExist,
|
||||
ListFilesystemVersionsUserrefs,
|
||||
ListFilesystemsNoFilter,
|
||||
}
|
||||
//go:generate ../../artifacts/generate-platform-test-list github.com/zrepl/zrepl/platformtest/tests
|
||||
|
||||
@@ -2,7 +2,6 @@ package driver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"sort"
|
||||
@@ -10,7 +9,9 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/zrepl/zrepl/daemon/logging/trace"
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
|
||||
@@ -374,11 +375,28 @@ func (a *attempt) doGlobalPlanning(ctx context.Context, prev *attempt) map[*fs]*
|
||||
// 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...))
|
||||
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 {
|
||||
fs1 := f1.fs.ReportInfo().Name
|
||||
fs1 := mustDatasetPathOrPlanFail(f1.fs.ReportInfo().Name)
|
||||
if fs1 == nil {
|
||||
return nil
|
||||
}
|
||||
for _, f2 := range a.fss {
|
||||
fs2 := f2.fs.ReportInfo().Name
|
||||
if strings.HasPrefix(fs1, fs2) && fs1 != fs2 {
|
||||
fs2 := mustDatasetPathOrPlanFail(f2.fs.ReportInfo().Name)
|
||||
if fs2 == nil {
|
||||
return nil
|
||||
}
|
||||
if fs1.HasPrefix(fs2) && !fs1.Equal(fs2) {
|
||||
f1.initialRepOrd.parents = append(f1.initialRepOrd.parents, f2)
|
||||
f2.initialRepOrd.children = append(f2.initialRepOrd.children, f1)
|
||||
}
|
||||
|
||||
+230
-211
@@ -46,7 +46,36 @@ func (x Tri) String() string {
|
||||
return proto.EnumName(Tri_name, int32(x))
|
||||
}
|
||||
func (Tri) EnumDescriptor() ([]byte, []int) {
|
||||
return fileDescriptor_pdu_e59763dc61674a79, []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
|
||||
@@ -69,7 +98,7 @@ func (x FilesystemVersion_VersionType) String() string {
|
||||
return proto.EnumName(FilesystemVersion_VersionType_name, int32(x))
|
||||
}
|
||||
func (FilesystemVersion_VersionType) EnumDescriptor() ([]byte, []int) {
|
||||
return fileDescriptor_pdu_e59763dc61674a79, []int{5, 0}
|
||||
return fileDescriptor_pdu_616c27178643eca4, []int{5, 0}
|
||||
}
|
||||
|
||||
type ListFilesystemReq struct {
|
||||
@@ -82,7 +111,7 @@ func (m *ListFilesystemReq) Reset() { *m = ListFilesystemReq{} }
|
||||
func (m *ListFilesystemReq) String() string { return proto.CompactTextString(m) }
|
||||
func (*ListFilesystemReq) ProtoMessage() {}
|
||||
func (*ListFilesystemReq) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_pdu_e59763dc61674a79, []int{0}
|
||||
return fileDescriptor_pdu_616c27178643eca4, []int{0}
|
||||
}
|
||||
func (m *ListFilesystemReq) XXX_Unmarshal(b []byte) error {
|
||||
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 (*ListFilesystemRes) ProtoMessage() {}
|
||||
func (*ListFilesystemRes) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_pdu_e59763dc61674a79, []int{1}
|
||||
return fileDescriptor_pdu_616c27178643eca4, []int{1}
|
||||
}
|
||||
func (m *ListFilesystemRes) XXX_Unmarshal(b []byte) error {
|
||||
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 (*Filesystem) ProtoMessage() {}
|
||||
func (*Filesystem) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_pdu_e59763dc61674a79, []int{2}
|
||||
return fileDescriptor_pdu_616c27178643eca4, []int{2}
|
||||
}
|
||||
func (m *Filesystem) XXX_Unmarshal(b []byte) error {
|
||||
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 (*ListFilesystemVersionsReq) ProtoMessage() {}
|
||||
func (*ListFilesystemVersionsReq) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_pdu_e59763dc61674a79, []int{3}
|
||||
return fileDescriptor_pdu_616c27178643eca4, []int{3}
|
||||
}
|
||||
func (m *ListFilesystemVersionsReq) XXX_Unmarshal(b []byte) error {
|
||||
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 (*ListFilesystemVersionsRes) ProtoMessage() {}
|
||||
func (*ListFilesystemVersionsRes) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_pdu_e59763dc61674a79, []int{4}
|
||||
return fileDescriptor_pdu_616c27178643eca4, []int{4}
|
||||
}
|
||||
func (m *ListFilesystemVersionsRes) XXX_Unmarshal(b []byte) error {
|
||||
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 (*FilesystemVersion) ProtoMessage() {}
|
||||
func (*FilesystemVersion) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_pdu_e59763dc61674a79, []int{5}
|
||||
return fileDescriptor_pdu_616c27178643eca4, []int{5}
|
||||
}
|
||||
func (m *FilesystemVersion) XXX_Unmarshal(b []byte) error {
|
||||
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
|
||||
// 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.
|
||||
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"`
|
||||
DryRun bool `protobuf:"varint,6,opt,name=DryRun,proto3" json:"DryRun,omitempty"`
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
XXX_sizecache int32 `json:"-"`
|
||||
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"`
|
||||
DryRun bool `protobuf:"varint,6,opt,name=DryRun,proto3" json:"DryRun,omitempty"`
|
||||
ReplicationConfig *ReplicationConfig `protobuf:"bytes,7,opt,name=ReplicationConfig,proto3" json:"ReplicationConfig,omitempty"`
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
XXX_sizecache int32 `json:"-"`
|
||||
}
|
||||
|
||||
func (m *SendReq) Reset() { *m = SendReq{} }
|
||||
func (m *SendReq) String() string { return proto.CompactTextString(m) }
|
||||
func (*SendReq) ProtoMessage() {}
|
||||
func (*SendReq) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_pdu_e59763dc61674a79, []int{6}
|
||||
return fileDescriptor_pdu_616c27178643eca4, []int{6}
|
||||
}
|
||||
func (m *SendReq) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_SendReq.Unmarshal(m, b)
|
||||
@@ -433,6 +463,97 @@ func (m *SendReq) GetDryRun() bool {
|
||||
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 {
|
||||
Name string `protobuf:"bytes,1,opt,name=Name,proto3" json:"Name,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 (*Property) ProtoMessage() {}
|
||||
func (*Property) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_pdu_e59763dc61674a79, []int{7}
|
||||
return fileDescriptor_pdu_616c27178643eca4, []int{9}
|
||||
}
|
||||
func (m *Property) XXX_Unmarshal(b []byte) error {
|
||||
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 (*SendRes) ProtoMessage() {}
|
||||
func (*SendRes) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_pdu_e59763dc61674a79, []int{8}
|
||||
return fileDescriptor_pdu_616c27178643eca4, []int{10}
|
||||
}
|
||||
func (m *SendRes) XXX_Unmarshal(b []byte) error {
|
||||
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 (*SendCompletedReq) ProtoMessage() {}
|
||||
func (*SendCompletedReq) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_pdu_e59763dc61674a79, []int{9}
|
||||
return fileDescriptor_pdu_616c27178643eca4, []int{11}
|
||||
}
|
||||
func (m *SendCompletedReq) XXX_Unmarshal(b []byte) error {
|
||||
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 (*SendCompletedRes) ProtoMessage() {}
|
||||
func (*SendCompletedRes) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_pdu_e59763dc61674a79, []int{10}
|
||||
return fileDescriptor_pdu_616c27178643eca4, []int{12}
|
||||
}
|
||||
func (m *SendCompletedRes) XXX_Unmarshal(b []byte) error {
|
||||
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"`
|
||||
// If true, the receiver should clear the resume token before performing the
|
||||
// zfs recv of the stream in the request
|
||||
ClearResumeToken bool `protobuf:"varint,3,opt,name=ClearResumeToken,proto3" json:"ClearResumeToken,omitempty"`
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
XXX_sizecache int32 `json:"-"`
|
||||
ClearResumeToken bool `protobuf:"varint,3,opt,name=ClearResumeToken,proto3" json:"ClearResumeToken,omitempty"`
|
||||
ReplicationConfig *ReplicationConfig `protobuf:"bytes,4,opt,name=ReplicationConfig,proto3" json:"ReplicationConfig,omitempty"`
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
XXX_sizecache int32 `json:"-"`
|
||||
}
|
||||
|
||||
func (m *ReceiveReq) Reset() { *m = ReceiveReq{} }
|
||||
func (m *ReceiveReq) String() string { return proto.CompactTextString(m) }
|
||||
func (*ReceiveReq) ProtoMessage() {}
|
||||
func (*ReceiveReq) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_pdu_e59763dc61674a79, []int{11}
|
||||
return fileDescriptor_pdu_616c27178643eca4, []int{13}
|
||||
}
|
||||
func (m *ReceiveReq) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_ReceiveReq.Unmarshal(m, b)
|
||||
@@ -661,6 +783,13 @@ func (m *ReceiveReq) GetClearResumeToken() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *ReceiveReq) GetReplicationConfig() *ReplicationConfig {
|
||||
if m != nil {
|
||||
return m.ReplicationConfig
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type ReceiveRes struct {
|
||||
XXX_NoUnkeyedLiteral struct{} `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 (*ReceiveRes) ProtoMessage() {}
|
||||
func (*ReceiveRes) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_pdu_e59763dc61674a79, []int{12}
|
||||
return fileDescriptor_pdu_616c27178643eca4, []int{14}
|
||||
}
|
||||
func (m *ReceiveRes) XXX_Unmarshal(b []byte) error {
|
||||
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 (*DestroySnapshotsReq) ProtoMessage() {}
|
||||
func (*DestroySnapshotsReq) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_pdu_e59763dc61674a79, []int{13}
|
||||
return fileDescriptor_pdu_616c27178643eca4, []int{15}
|
||||
}
|
||||
func (m *DestroySnapshotsReq) XXX_Unmarshal(b []byte) error {
|
||||
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 (*DestroySnapshotRes) ProtoMessage() {}
|
||||
func (*DestroySnapshotRes) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_pdu_e59763dc61674a79, []int{14}
|
||||
return fileDescriptor_pdu_616c27178643eca4, []int{16}
|
||||
}
|
||||
func (m *DestroySnapshotRes) XXX_Unmarshal(b []byte) error {
|
||||
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 (*DestroySnapshotsRes) ProtoMessage() {}
|
||||
func (*DestroySnapshotsRes) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_pdu_e59763dc61674a79, []int{15}
|
||||
return fileDescriptor_pdu_616c27178643eca4, []int{17}
|
||||
}
|
||||
func (m *DestroySnapshotsRes) XXX_Unmarshal(b []byte) error {
|
||||
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 (*ReplicationCursorReq) ProtoMessage() {}
|
||||
func (*ReplicationCursorReq) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_pdu_e59763dc61674a79, []int{16}
|
||||
return fileDescriptor_pdu_616c27178643eca4, []int{18}
|
||||
}
|
||||
func (m *ReplicationCursorReq) XXX_Unmarshal(b []byte) error {
|
||||
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 (*ReplicationCursorRes) ProtoMessage() {}
|
||||
func (*ReplicationCursorRes) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_pdu_e59763dc61674a79, []int{17}
|
||||
return fileDescriptor_pdu_616c27178643eca4, []int{19}
|
||||
}
|
||||
func (m *ReplicationCursorRes) XXX_Unmarshal(b []byte) error {
|
||||
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 (*PingReq) ProtoMessage() {}
|
||||
func (*PingReq) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_pdu_e59763dc61674a79, []int{18}
|
||||
return fileDescriptor_pdu_616c27178643eca4, []int{20}
|
||||
}
|
||||
func (m *PingReq) XXX_Unmarshal(b []byte) error {
|
||||
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 (*PingRes) ProtoMessage() {}
|
||||
func (*PingRes) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_pdu_e59763dc61674a79, []int{19}
|
||||
return fileDescriptor_pdu_616c27178643eca4, []int{21}
|
||||
}
|
||||
func (m *PingRes) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_PingRes.Unmarshal(m, b)
|
||||
@@ -1076,91 +1205,6 @@ func (m *PingRes) GetEcho() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
type HintMostRecentCommonAncestorReq struct {
|
||||
Filesystem string `protobuf:"bytes,1,opt,name=Filesystem,proto3" json:"Filesystem,omitempty"`
|
||||
// A copy of the FilesystemVersion on the sending side that the replication
|
||||
// algorithm identified as a shared most recent common version between sending
|
||||
// and receiving side.
|
||||
//
|
||||
// If nil, this is an indication that the replication algorithm could not
|
||||
// find a common ancestor between the two sides.
|
||||
// NOTE: nilness does not mean that replication never happened - there could
|
||||
// as well be a replication conflict. thus, dont' jump to conclusions too
|
||||
// rapidly here.
|
||||
SenderVersion *FilesystemVersion `protobuf:"bytes,2,opt,name=SenderVersion,proto3" json:"SenderVersion,omitempty"`
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
XXX_sizecache int32 `json:"-"`
|
||||
}
|
||||
|
||||
func (m *HintMostRecentCommonAncestorReq) Reset() { *m = HintMostRecentCommonAncestorReq{} }
|
||||
func (m *HintMostRecentCommonAncestorReq) String() string { return proto.CompactTextString(m) }
|
||||
func (*HintMostRecentCommonAncestorReq) ProtoMessage() {}
|
||||
func (*HintMostRecentCommonAncestorReq) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_pdu_e59763dc61674a79, []int{20}
|
||||
}
|
||||
func (m *HintMostRecentCommonAncestorReq) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_HintMostRecentCommonAncestorReq.Unmarshal(m, b)
|
||||
}
|
||||
func (m *HintMostRecentCommonAncestorReq) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
|
||||
return xxx_messageInfo_HintMostRecentCommonAncestorReq.Marshal(b, m, deterministic)
|
||||
}
|
||||
func (dst *HintMostRecentCommonAncestorReq) XXX_Merge(src proto.Message) {
|
||||
xxx_messageInfo_HintMostRecentCommonAncestorReq.Merge(dst, src)
|
||||
}
|
||||
func (m *HintMostRecentCommonAncestorReq) XXX_Size() int {
|
||||
return xxx_messageInfo_HintMostRecentCommonAncestorReq.Size(m)
|
||||
}
|
||||
func (m *HintMostRecentCommonAncestorReq) XXX_DiscardUnknown() {
|
||||
xxx_messageInfo_HintMostRecentCommonAncestorReq.DiscardUnknown(m)
|
||||
}
|
||||
|
||||
var xxx_messageInfo_HintMostRecentCommonAncestorReq proto.InternalMessageInfo
|
||||
|
||||
func (m *HintMostRecentCommonAncestorReq) GetFilesystem() string {
|
||||
if m != nil {
|
||||
return m.Filesystem
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *HintMostRecentCommonAncestorReq) GetSenderVersion() *FilesystemVersion {
|
||||
if m != nil {
|
||||
return m.SenderVersion
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type HintMostRecentCommonAncestorRes struct {
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
XXX_sizecache int32 `json:"-"`
|
||||
}
|
||||
|
||||
func (m *HintMostRecentCommonAncestorRes) Reset() { *m = HintMostRecentCommonAncestorRes{} }
|
||||
func (m *HintMostRecentCommonAncestorRes) String() string { return proto.CompactTextString(m) }
|
||||
func (*HintMostRecentCommonAncestorRes) ProtoMessage() {}
|
||||
func (*HintMostRecentCommonAncestorRes) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_pdu_e59763dc61674a79, []int{21}
|
||||
}
|
||||
func (m *HintMostRecentCommonAncestorRes) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_HintMostRecentCommonAncestorRes.Unmarshal(m, b)
|
||||
}
|
||||
func (m *HintMostRecentCommonAncestorRes) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
|
||||
return xxx_messageInfo_HintMostRecentCommonAncestorRes.Marshal(b, m, deterministic)
|
||||
}
|
||||
func (dst *HintMostRecentCommonAncestorRes) XXX_Merge(src proto.Message) {
|
||||
xxx_messageInfo_HintMostRecentCommonAncestorRes.Merge(dst, src)
|
||||
}
|
||||
func (m *HintMostRecentCommonAncestorRes) XXX_Size() int {
|
||||
return xxx_messageInfo_HintMostRecentCommonAncestorRes.Size(m)
|
||||
}
|
||||
func (m *HintMostRecentCommonAncestorRes) XXX_DiscardUnknown() {
|
||||
xxx_messageInfo_HintMostRecentCommonAncestorRes.DiscardUnknown(m)
|
||||
}
|
||||
|
||||
var xxx_messageInfo_HintMostRecentCommonAncestorRes proto.InternalMessageInfo
|
||||
|
||||
func init() {
|
||||
proto.RegisterType((*ListFilesystemReq)(nil), "ListFilesystemReq")
|
||||
proto.RegisterType((*ListFilesystemRes)(nil), "ListFilesystemRes")
|
||||
@@ -1169,6 +1213,8 @@ func init() {
|
||||
proto.RegisterType((*ListFilesystemVersionsRes)(nil), "ListFilesystemVersionsRes")
|
||||
proto.RegisterType((*FilesystemVersion)(nil), "FilesystemVersion")
|
||||
proto.RegisterType((*SendReq)(nil), "SendReq")
|
||||
proto.RegisterType((*ReplicationConfig)(nil), "ReplicationConfig")
|
||||
proto.RegisterType((*ReplicationConfigProtection)(nil), "ReplicationConfigProtection")
|
||||
proto.RegisterType((*Property)(nil), "Property")
|
||||
proto.RegisterType((*SendRes)(nil), "SendRes")
|
||||
proto.RegisterType((*SendCompletedReq)(nil), "SendCompletedReq")
|
||||
@@ -1182,9 +1228,8 @@ func init() {
|
||||
proto.RegisterType((*ReplicationCursorRes)(nil), "ReplicationCursorRes")
|
||||
proto.RegisterType((*PingReq)(nil), "PingReq")
|
||||
proto.RegisterType((*PingRes)(nil), "PingRes")
|
||||
proto.RegisterType((*HintMostRecentCommonAncestorReq)(nil), "HintMostRecentCommonAncestorReq")
|
||||
proto.RegisterType((*HintMostRecentCommonAncestorRes)(nil), "HintMostRecentCommonAncestorRes")
|
||||
proto.RegisterEnum("Tri", Tri_name, Tri_value)
|
||||
proto.RegisterEnum("ReplicationGuaranteeKind", ReplicationGuaranteeKind_name, ReplicationGuaranteeKind_value)
|
||||
proto.RegisterEnum("FilesystemVersion_VersionType", FilesystemVersion_VersionType_name, FilesystemVersion_VersionType_value)
|
||||
}
|
||||
|
||||
@@ -1206,7 +1251,6 @@ type ReplicationClient interface {
|
||||
DestroySnapshots(ctx context.Context, in *DestroySnapshotsReq, opts ...grpc.CallOption) (*DestroySnapshotsRes, error)
|
||||
ReplicationCursor(ctx context.Context, in *ReplicationCursorReq, opts ...grpc.CallOption) (*ReplicationCursorRes, error)
|
||||
SendCompleted(ctx context.Context, in *SendCompletedReq, opts ...grpc.CallOption) (*SendCompletedRes, error)
|
||||
HintMostRecentCommonAncestor(ctx context.Context, in *HintMostRecentCommonAncestorReq, opts ...grpc.CallOption) (*HintMostRecentCommonAncestorRes, error)
|
||||
}
|
||||
|
||||
type replicationClient struct {
|
||||
@@ -1271,15 +1315,6 @@ func (c *replicationClient) SendCompleted(ctx context.Context, in *SendCompleted
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *replicationClient) HintMostRecentCommonAncestor(ctx context.Context, in *HintMostRecentCommonAncestorReq, opts ...grpc.CallOption) (*HintMostRecentCommonAncestorRes, error) {
|
||||
out := new(HintMostRecentCommonAncestorRes)
|
||||
err := c.cc.Invoke(ctx, "/Replication/HintMostRecentCommonAncestor", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// ReplicationServer is the server API for Replication service.
|
||||
type ReplicationServer interface {
|
||||
Ping(context.Context, *PingReq) (*PingRes, error)
|
||||
@@ -1288,7 +1323,6 @@ type ReplicationServer interface {
|
||||
DestroySnapshots(context.Context, *DestroySnapshotsReq) (*DestroySnapshotsRes, error)
|
||||
ReplicationCursor(context.Context, *ReplicationCursorReq) (*ReplicationCursorRes, error)
|
||||
SendCompleted(context.Context, *SendCompletedReq) (*SendCompletedRes, error)
|
||||
HintMostRecentCommonAncestor(context.Context, *HintMostRecentCommonAncestorReq) (*HintMostRecentCommonAncestorRes, error)
|
||||
}
|
||||
|
||||
func RegisterReplicationServer(s *grpc.Server, srv ReplicationServer) {
|
||||
@@ -1403,24 +1437,6 @@ func _Replication_SendCompleted_Handler(srv interface{}, ctx context.Context, de
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Replication_HintMostRecentCommonAncestor_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(HintMostRecentCommonAncestorReq)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(ReplicationServer).HintMostRecentCommonAncestor(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/Replication/HintMostRecentCommonAncestor",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ReplicationServer).HintMostRecentCommonAncestor(ctx, req.(*HintMostRecentCommonAncestorReq))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
var _Replication_serviceDesc = grpc.ServiceDesc{
|
||||
ServiceName: "Replication",
|
||||
HandlerType: (*ReplicationServer)(nil),
|
||||
@@ -1449,73 +1465,76 @@ var _Replication_serviceDesc = grpc.ServiceDesc{
|
||||
MethodName: "SendCompleted",
|
||||
Handler: _Replication_SendCompleted_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "HintMostRecentCommonAncestor",
|
||||
Handler: _Replication_HintMostRecentCommonAncestor_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{},
|
||||
Metadata: "pdu.proto",
|
||||
}
|
||||
|
||||
func init() { proto.RegisterFile("pdu.proto", fileDescriptor_pdu_e59763dc61674a79) }
|
||||
func init() { proto.RegisterFile("pdu.proto", fileDescriptor_pdu_616c27178643eca4) }
|
||||
|
||||
var fileDescriptor_pdu_e59763dc61674a79 = []byte{
|
||||
// 892 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x56, 0xdf, 0x6f, 0xdb, 0x36,
|
||||
0x10, 0x8e, 0x6c, 0x39, 0x91, 0xcf, 0xe9, 0xea, 0x5c, 0xb2, 0x42, 0x13, 0xba, 0xce, 0xe3, 0x86,
|
||||
0xc1, 0x0d, 0x30, 0x61, 0xc8, 0x7e, 0x60, 0xc3, 0x80, 0x02, 0x8d, 0x93, 0x34, 0xc5, 0xd6, 0xce,
|
||||
0x60, 0xbc, 0x62, 0xe8, 0x9b, 0x6a, 0x1f, 0x12, 0x21, 0xb2, 0xa8, 0x90, 0xf4, 0x50, 0x6f, 0x7b,
|
||||
0xda, 0xe3, 0xfe, 0xbd, 0xe5, 0x0f, 0x2a, 0x44, 0x4b, 0xb6, 0x6c, 0xc9, 0x89, 0x9f, 0xcc, 0xfb,
|
||||
0x78, 0x14, 0xef, 0xbe, 0xfb, 0xee, 0x68, 0x68, 0x26, 0xa3, 0x89, 0x9f, 0x48, 0xa1, 0x05, 0xdb,
|
||||
0x87, 0xbd, 0x5f, 0x43, 0xa5, 0xcf, 0xc2, 0x88, 0xd4, 0x54, 0x69, 0x1a, 0x73, 0xba, 0x61, 0xc7,
|
||||
0x65, 0x50, 0xe1, 0xd7, 0xd0, 0x5a, 0x00, 0xca, 0xb5, 0x3a, 0xf5, 0x6e, 0xeb, 0xa8, 0xe5, 0x17,
|
||||
0x9c, 0x8a, 0xfb, 0xec, 0x3f, 0x0b, 0x60, 0x61, 0x23, 0x82, 0xdd, 0x0f, 0xf4, 0x95, 0x6b, 0x75,
|
||||
0xac, 0x6e, 0x93, 0x9b, 0x35, 0x76, 0xa0, 0xc5, 0x49, 0x4d, 0xc6, 0x34, 0x10, 0xd7, 0x14, 0xbb,
|
||||
0x35, 0xb3, 0x55, 0x84, 0xf0, 0x4b, 0x78, 0xf0, 0x52, 0xf5, 0xa3, 0x60, 0x48, 0x57, 0x22, 0x1a,
|
||||
0x91, 0x74, 0xeb, 0x1d, 0xab, 0xeb, 0xf0, 0x65, 0x30, 0xfd, 0xce, 0x4b, 0x75, 0x1a, 0x0f, 0xe5,
|
||||
0x34, 0xd1, 0x34, 0x72, 0x6d, 0xe3, 0x53, 0x84, 0xd8, 0xcf, 0xf0, 0xc9, 0x72, 0x42, 0x6f, 0x48,
|
||||
0xaa, 0x50, 0xc4, 0x8a, 0xd3, 0x0d, 0x3e, 0x29, 0x06, 0x9a, 0x05, 0x58, 0x40, 0xd8, 0x2f, 0xeb,
|
||||
0x0f, 0x2b, 0xf4, 0xc1, 0xc9, 0xcd, 0x8c, 0x12, 0xf4, 0x4b, 0x9e, 0x7c, 0xee, 0xc3, 0x6e, 0x2d,
|
||||
0xd8, 0x2b, 0xed, 0xe3, 0x11, 0xd8, 0x83, 0x69, 0x42, 0xe6, 0xf2, 0x8f, 0x8e, 0x9e, 0x94, 0xbf,
|
||||
0xe0, 0x67, 0xbf, 0xa9, 0x17, 0x37, 0xbe, 0x29, 0xa3, 0xaf, 0x83, 0x31, 0x65, 0xb4, 0x99, 0x75,
|
||||
0x8a, 0xbd, 0x98, 0x84, 0x23, 0x43, 0x93, 0xcd, 0xcd, 0x1a, 0x1f, 0x43, 0xb3, 0x27, 0x29, 0xd0,
|
||||
0x34, 0xf8, 0xe3, 0x85, 0xe1, 0xc6, 0xe6, 0x0b, 0x00, 0x3d, 0x70, 0x8c, 0x11, 0x8a, 0xd8, 0x6d,
|
||||
0x98, 0x2f, 0xcd, 0x6d, 0xf6, 0x14, 0x5a, 0x85, 0x6b, 0x71, 0x17, 0x9c, 0x8b, 0x38, 0x48, 0xd4,
|
||||
0x95, 0xd0, 0xed, 0xad, 0xd4, 0x3a, 0x16, 0xe2, 0x7a, 0x1c, 0xc8, 0xeb, 0xb6, 0xc5, 0xfe, 0xb7,
|
||||
0x60, 0xe7, 0x82, 0xe2, 0xd1, 0x06, 0x7c, 0xe2, 0x57, 0x60, 0x9f, 0x49, 0x31, 0x36, 0x81, 0x57,
|
||||
0xd3, 0x65, 0xf6, 0x91, 0x41, 0x6d, 0x20, 0x4c, 0x2a, 0xd5, 0x5e, 0xb5, 0x81, 0x58, 0x95, 0x90,
|
||||
0x5d, 0x96, 0x10, 0x83, 0xe6, 0x42, 0x1a, 0x0d, 0xc3, 0xaf, 0xed, 0x0f, 0x64, 0xc8, 0x17, 0x30,
|
||||
0x3e, 0x82, 0xed, 0x13, 0x39, 0xe5, 0x93, 0xd8, 0xdd, 0x36, 0xda, 0xc9, 0x2c, 0xf6, 0x1d, 0x38,
|
||||
0x7d, 0x29, 0x12, 0x92, 0x7a, 0x3a, 0xa7, 0xdb, 0x2a, 0xd0, 0x7d, 0x00, 0x8d, 0x37, 0x41, 0x34,
|
||||
0xc9, 0x6b, 0x30, 0x33, 0xd8, 0xbf, 0x73, 0x2e, 0x14, 0x76, 0xe1, 0xe1, 0xef, 0x8a, 0x46, 0xab,
|
||||
0x32, 0x77, 0xf8, 0x2a, 0x8c, 0x0c, 0x76, 0x4f, 0xdf, 0x27, 0x34, 0xd4, 0x34, 0xba, 0x08, 0xff,
|
||||
0x22, 0x93, 0x77, 0x9d, 0x2f, 0x61, 0xf8, 0x14, 0x20, 0x8b, 0x27, 0x24, 0xe5, 0xda, 0x46, 0x6e,
|
||||
0x4d, 0x3f, 0x0f, 0x91, 0x17, 0x36, 0xd9, 0x33, 0x68, 0xa7, 0x31, 0xf4, 0xc4, 0x38, 0x89, 0x48,
|
||||
0x93, 0x29, 0xcc, 0x21, 0xb4, 0x7e, 0x93, 0xe1, 0x65, 0x18, 0x07, 0x11, 0xa7, 0x9b, 0x8c, 0x7f,
|
||||
0xc7, 0xcf, 0xea, 0xc6, 0x8b, 0x9b, 0x0c, 0x4b, 0xe7, 0x15, 0xfb, 0x07, 0x80, 0xd3, 0x90, 0xc2,
|
||||
0x3f, 0x69, 0x93, 0x32, 0xcf, 0xca, 0x57, 0xbb, 0xb3, 0x7c, 0x87, 0xd0, 0xee, 0x45, 0x14, 0xc8,
|
||||
0x22, 0x3f, 0xb3, 0x16, 0x2f, 0xe1, 0x6c, 0xb7, 0x70, 0xbb, 0x62, 0x97, 0xb0, 0x7f, 0x42, 0x4a,
|
||||
0x4b, 0x31, 0xcd, 0x35, 0xb9, 0x49, 0x2f, 0xe3, 0x37, 0xd0, 0x9c, 0xfb, 0xbb, 0xb5, 0xb5, 0xfd,
|
||||
0xba, 0x70, 0x62, 0x6f, 0x01, 0x57, 0x2e, 0xca, 0xda, 0x3e, 0x37, 0xcd, 0x2d, 0x6b, 0xda, 0x3e,
|
||||
0xf7, 0x49, 0x95, 0x72, 0x2a, 0xa5, 0x90, 0xb9, 0x52, 0x8c, 0xc1, 0x4e, 0xaa, 0x92, 0x48, 0x27,
|
||||
0xed, 0x4e, 0x9a, 0x78, 0xa4, 0xf3, 0x91, 0xb2, 0xef, 0x97, 0x43, 0xe0, 0xb9, 0x0f, 0xfb, 0x01,
|
||||
0x0e, 0x38, 0x25, 0x51, 0x38, 0x34, 0x5d, 0xdb, 0x9b, 0x48, 0x25, 0xe4, 0x26, 0x73, 0x6d, 0x50,
|
||||
0x79, 0x4e, 0xe1, 0x41, 0x36, 0x44, 0xd2, 0x13, 0xf6, 0xf9, 0xd6, 0x7c, 0x8c, 0x38, 0xaf, 0x85,
|
||||
0xa6, 0xf7, 0xa1, 0xd2, 0x33, 0x09, 0x9f, 0x6f, 0xf1, 0x39, 0x72, 0xec, 0xc0, 0xf6, 0x2c, 0x1c,
|
||||
0xf6, 0x05, 0xec, 0xf4, 0xc3, 0xf8, 0x32, 0x0d, 0xc0, 0x85, 0x9d, 0x57, 0xa4, 0x54, 0x70, 0x99,
|
||||
0x77, 0x4d, 0x6e, 0xb2, 0x4f, 0x73, 0x27, 0x95, 0xf6, 0xd5, 0xe9, 0xf0, 0x4a, 0xe4, 0x7d, 0x95,
|
||||
0xae, 0xd9, 0xdf, 0xf0, 0xd9, 0x79, 0x18, 0xeb, 0x57, 0x42, 0xe9, 0xb4, 0xe4, 0xb1, 0xee, 0x89,
|
||||
0xf1, 0x58, 0xc4, 0xcf, 0xe3, 0x21, 0x29, 0xbd, 0x51, 0x72, 0xf8, 0x23, 0x3c, 0x48, 0xf5, 0x4b,
|
||||
0x32, 0xab, 0xc5, 0x1d, 0x42, 0x5c, 0x76, 0x64, 0x9f, 0xdf, 0x77, 0xb9, 0x3a, 0xec, 0x42, 0x7d,
|
||||
0x20, 0xc3, 0x74, 0x04, 0x9e, 0x88, 0x58, 0xf7, 0x02, 0x49, 0xed, 0x2d, 0x6c, 0x42, 0xe3, 0x2c,
|
||||
0x88, 0x14, 0xb5, 0x2d, 0x74, 0xc0, 0x1e, 0xc8, 0x09, 0xb5, 0x6b, 0x47, 0xb7, 0xf5, 0x74, 0x40,
|
||||
0xcd, 0x49, 0x46, 0x0f, 0xec, 0x34, 0x71, 0x74, 0xfc, 0x8c, 0x24, 0x2f, 0x5f, 0x29, 0xfc, 0x09,
|
||||
0x1e, 0x2e, 0xbf, 0x33, 0x0a, 0xd1, 0x2f, 0x3d, 0xce, 0x5e, 0x19, 0x53, 0xd8, 0x87, 0x47, 0xd5,
|
||||
0x4f, 0x14, 0x7a, 0xfe, 0xda, 0x87, 0xcf, 0x5b, 0xbf, 0xa7, 0xf0, 0x19, 0xb4, 0x57, 0xa5, 0x89,
|
||||
0x07, 0x7e, 0x45, 0xcb, 0x79, 0x55, 0xa8, 0xc2, 0xe7, 0xb0, 0x57, 0x12, 0x17, 0x7e, 0xec, 0x57,
|
||||
0x09, 0xd5, 0xab, 0x84, 0x15, 0x7e, 0x3f, 0x2b, 0xe1, 0x7c, 0x04, 0xe1, 0x9e, 0xbf, 0x3a, 0xd2,
|
||||
0xbc, 0x12, 0xa4, 0xf0, 0x1d, 0x3c, 0xbe, 0xab, 0x7e, 0xd8, 0xf1, 0xef, 0xd1, 0x96, 0x77, 0x9f,
|
||||
0x87, 0x3a, 0x6e, 0xbc, 0xad, 0x27, 0xa3, 0xc9, 0xbb, 0x6d, 0xf3, 0x1f, 0xea, 0xdb, 0x0f, 0x01,
|
||||
0x00, 0x00, 0xff, 0xff, 0xad, 0x4e, 0x98, 0x29, 0x50, 0x09, 0x00, 0x00,
|
||||
var fileDescriptor_pdu_616c27178643eca4 = []byte{
|
||||
// 995 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x56, 0xcf, 0x6e, 0xdb, 0xc6,
|
||||
0x13, 0x36, 0x25, 0xda, 0xa2, 0x46, 0xce, 0x2f, 0xf4, 0xd8, 0x09, 0x68, 0xfd, 0xd2, 0xd4, 0xd8,
|
||||
0x14, 0x85, 0x63, 0xa0, 0x44, 0xe1, 0xb4, 0x05, 0x8a, 0x14, 0x41, 0xeb, 0xbf, 0x31, 0xd2, 0xba,
|
||||
0xea, 0x5a, 0x0d, 0x8a, 0xdc, 0x18, 0x69, 0x2a, 0x2f, 0x4c, 0x71, 0xe9, 0x5d, 0x2a, 0x88, 0x7a,
|
||||
0xec, 0xa1, 0x87, 0x5e, 0x7a, 0xea, 0xeb, 0xf4, 0x29, 0xfa, 0x20, 0x7d, 0x84, 0x82, 0x6b, 0x92,
|
||||
0xa2, 0x44, 0xca, 0x70, 0x4f, 0xda, 0xf9, 0xe6, 0xdb, 0xd9, 0xd9, 0xd1, 0x37, 0xb3, 0x84, 0x76,
|
||||
0x3c, 0x9c, 0xf8, 0xb1, 0x92, 0x89, 0x64, 0x9b, 0xb0, 0xf1, 0xad, 0xd0, 0xc9, 0x89, 0x08, 0x49,
|
||||
0x4f, 0x75, 0x42, 0x63, 0x4e, 0xd7, 0xec, 0xa0, 0x0a, 0x6a, 0xfc, 0x04, 0x3a, 0x33, 0x40, 0x7b,
|
||||
0xd6, 0x4e, 0x73, 0xb7, 0xb3, 0xdf, 0xf1, 0x4b, 0xa4, 0xb2, 0x9f, 0xfd, 0x6e, 0x01, 0xcc, 0x6c,
|
||||
0x44, 0xb0, 0x7b, 0x41, 0x72, 0xe9, 0x59, 0x3b, 0xd6, 0x6e, 0x9b, 0x9b, 0x35, 0xee, 0x40, 0x87,
|
||||
0x93, 0x9e, 0x8c, 0xa9, 0x2f, 0xaf, 0x28, 0xf2, 0x1a, 0xc6, 0x55, 0x86, 0xf0, 0x23, 0xb8, 0x77,
|
||||
0xa6, 0x7b, 0x61, 0x30, 0xa0, 0x4b, 0x19, 0x0e, 0x49, 0x79, 0xcd, 0x1d, 0x6b, 0xd7, 0xe1, 0xf3,
|
||||
0x60, 0x1a, 0xe7, 0x4c, 0x1f, 0x47, 0x03, 0x35, 0x8d, 0x13, 0x1a, 0x7a, 0xb6, 0xe1, 0x94, 0x21,
|
||||
0xf6, 0x1c, 0xb6, 0xe7, 0x2f, 0xf4, 0x9a, 0x94, 0x16, 0x32, 0xd2, 0x9c, 0xae, 0xf1, 0x71, 0x39,
|
||||
0xd1, 0x2c, 0xc1, 0x12, 0xc2, 0x5e, 0x2d, 0xdf, 0xac, 0xd1, 0x07, 0x27, 0x37, 0xb3, 0x92, 0xa0,
|
||||
0x5f, 0x61, 0xf2, 0x82, 0xc3, 0xfe, 0xb6, 0x60, 0xa3, 0xe2, 0xc7, 0x7d, 0xb0, 0xfb, 0xd3, 0x98,
|
||||
0xcc, 0xe1, 0xff, 0xdb, 0x7f, 0x5c, 0x8d, 0xe0, 0x67, 0xbf, 0x29, 0x8b, 0x1b, 0x6e, 0x5a, 0xd1,
|
||||
0xf3, 0x60, 0x4c, 0x59, 0xd9, 0xcc, 0x3a, 0xc5, 0x4e, 0x27, 0x62, 0x68, 0xca, 0x64, 0x73, 0xb3,
|
||||
0xc6, 0x47, 0xd0, 0x3e, 0x54, 0x14, 0x24, 0xd4, 0xff, 0xe9, 0xd4, 0xd4, 0xc6, 0xe6, 0x33, 0x00,
|
||||
0xbb, 0xe0, 0x18, 0x43, 0xc8, 0xc8, 0x5b, 0x35, 0x91, 0x0a, 0x9b, 0x3d, 0x85, 0x4e, 0xe9, 0x58,
|
||||
0x5c, 0x07, 0xe7, 0x22, 0x0a, 0x62, 0x7d, 0x29, 0x13, 0x77, 0x25, 0xb5, 0x0e, 0xa4, 0xbc, 0x1a,
|
||||
0x07, 0xea, 0xca, 0xb5, 0xd8, 0x9f, 0x0d, 0x68, 0x5d, 0x50, 0x34, 0xbc, 0x43, 0x3d, 0xf1, 0x63,
|
||||
0xb0, 0x4f, 0x94, 0x1c, 0x9b, 0xc4, 0xeb, 0xcb, 0x65, 0xfc, 0xc8, 0xa0, 0xd1, 0x97, 0xe6, 0x2a,
|
||||
0xf5, 0xac, 0x46, 0x5f, 0x2e, 0x4a, 0xc8, 0xae, 0x4a, 0x88, 0x41, 0x7b, 0x26, 0x8d, 0x55, 0x53,
|
||||
0x5f, 0xdb, 0xef, 0x2b, 0xc1, 0x67, 0x30, 0x3e, 0x84, 0xb5, 0x23, 0x35, 0xe5, 0x93, 0xc8, 0x5b,
|
||||
0x33, 0xda, 0xc9, 0x2c, 0xfc, 0x1a, 0x36, 0x38, 0xc5, 0xa1, 0x18, 0x98, 0x7a, 0x1c, 0xca, 0xe8,
|
||||
0x67, 0x31, 0xf2, 0x5a, 0x59, 0x42, 0x15, 0x0f, 0xaf, 0x92, 0xd9, 0x0f, 0x35, 0x11, 0xf0, 0x2b,
|
||||
0x80, 0xb4, 0xf9, 0x68, 0x60, 0xaa, 0x6e, 0x99, 0x78, 0x8f, 0xaa, 0xf1, 0x7a, 0x05, 0x87, 0x97,
|
||||
0xf8, 0xec, 0x0f, 0x0b, 0xfe, 0x7f, 0x0b, 0x17, 0x9f, 0x41, 0xeb, 0x2c, 0x12, 0x89, 0x08, 0xc2,
|
||||
0x4c, 0x4e, 0xdb, 0xe5, 0xd0, 0xa7, 0x93, 0x40, 0x05, 0x51, 0x42, 0xf4, 0x4a, 0x44, 0x43, 0x9e,
|
||||
0x33, 0xf1, 0x39, 0x74, 0xce, 0xa2, 0x81, 0xa2, 0x31, 0x45, 0x49, 0x10, 0x9a, 0xbf, 0xe6, 0xd6,
|
||||
0x8d, 0x65, 0x36, 0xfb, 0x0c, 0x9c, 0x9e, 0x92, 0x31, 0xa9, 0x64, 0x5a, 0xa8, 0xd2, 0x2a, 0xa9,
|
||||
0x72, 0x0b, 0x56, 0x5f, 0x07, 0xe1, 0x24, 0x97, 0xea, 0x8d, 0xc1, 0x7e, 0xb5, 0x72, 0xc9, 0x68,
|
||||
0xdc, 0x85, 0xfb, 0x3f, 0x6a, 0x1a, 0x2e, 0x4e, 0x03, 0x87, 0x2f, 0xc2, 0xc8, 0x60, 0xfd, 0xf8,
|
||||
0x7d, 0x4c, 0x83, 0x84, 0x86, 0x17, 0xe2, 0x17, 0x32, 0xf2, 0x68, 0xf2, 0x39, 0x0c, 0x9f, 0x02,
|
||||
0x64, 0xf9, 0x08, 0xd2, 0x9e, 0x6d, 0xba, 0xb2, 0xed, 0xe7, 0x29, 0xf2, 0x92, 0x93, 0xbd, 0x00,
|
||||
0x37, 0xcd, 0xe1, 0x50, 0x8e, 0xe3, 0x90, 0x12, 0x32, 0xfa, 0xdd, 0x83, 0xce, 0xf7, 0x4a, 0x8c,
|
||||
0x44, 0x14, 0x84, 0x9c, 0xae, 0x33, 0x99, 0x3a, 0x7e, 0x26, 0x6f, 0x5e, 0x76, 0x32, 0xac, 0xec,
|
||||
0xd7, 0xec, 0x2f, 0x0b, 0x80, 0xd3, 0x80, 0xc4, 0x3b, 0xba, 0x4b, 0x3b, 0xdc, 0xc8, 0xbc, 0x71,
|
||||
0xab, 0xcc, 0xf7, 0xc0, 0x3d, 0x0c, 0x29, 0x50, 0xe5, 0x02, 0xdd, 0x8c, 0xc2, 0x0a, 0x5e, 0x2f,
|
||||
0x5a, 0xfb, 0xbf, 0x88, 0x76, 0xbd, 0x94, 0xbf, 0x66, 0x23, 0xd8, 0x3c, 0x22, 0x9d, 0x28, 0x39,
|
||||
0xcd, 0xbb, 0xff, 0x2e, 0x53, 0x13, 0x3f, 0x85, 0x76, 0xc1, 0xf7, 0x1a, 0x4b, 0x27, 0xe3, 0x8c,
|
||||
0xc4, 0xde, 0x00, 0x2e, 0x1c, 0x94, 0x0d, 0xd8, 0xdc, 0xcc, 0x5a, 0xa5, 0x76, 0xc0, 0xe6, 0x9c,
|
||||
0x54, 0x6c, 0xc7, 0x4a, 0x49, 0x95, 0x8b, 0xcd, 0x18, 0xec, 0xa8, 0xee, 0x12, 0xe9, 0x9b, 0xd6,
|
||||
0x4a, 0x4b, 0x17, 0x26, 0xf9, 0xf0, 0xde, 0xf4, 0xab, 0x29, 0xf0, 0x9c, 0xc3, 0xbe, 0x80, 0xad,
|
||||
0x72, 0xb5, 0x26, 0x4a, 0x4b, 0x75, 0x97, 0x17, 0xa4, 0x5f, 0xbb, 0x4f, 0xe3, 0x56, 0x36, 0xae,
|
||||
0xd3, 0x1d, 0xf6, 0xcb, 0x95, 0x62, 0x60, 0x3b, 0xe7, 0x32, 0xa1, 0xf7, 0x42, 0x27, 0x37, 0x5d,
|
||||
0xf0, 0x72, 0x85, 0x17, 0xc8, 0x81, 0x03, 0x6b, 0x37, 0xe9, 0xb0, 0x27, 0xd0, 0xea, 0x89, 0x68,
|
||||
0x94, 0x26, 0xe0, 0x41, 0xeb, 0x3b, 0xd2, 0x3a, 0x18, 0xe5, 0x8d, 0x97, 0x9b, 0xec, 0x83, 0x9c,
|
||||
0xa4, 0xd3, 0xd6, 0x3c, 0x1e, 0x5c, 0xca, 0xbc, 0x35, 0xd3, 0xf5, 0xde, 0x2e, 0x34, 0xfb, 0x4a,
|
||||
0xa4, 0xc3, 0xfc, 0x48, 0x46, 0xc9, 0x61, 0xa0, 0xc8, 0x5d, 0xc1, 0x36, 0xac, 0x9e, 0x04, 0xa1,
|
||||
0x26, 0xd7, 0x42, 0x07, 0xec, 0xbe, 0x9a, 0x90, 0xdb, 0xd8, 0xfb, 0xcd, 0x02, 0x6f, 0xd9, 0x38,
|
||||
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,
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ service Replication {
|
||||
rpc DestroySnapshots(DestroySnapshotsReq) returns (DestroySnapshotsRes);
|
||||
rpc ReplicationCursor(ReplicationCursorReq) returns (ReplicationCursorRes);
|
||||
rpc SendCompleted(SendCompletedReq) returns (SendCompletedRes);
|
||||
rpc HintMostRecentCommonAncestor(HintMostRecentCommonAncestorReq) returns (HintMostRecentCommonAncestorRes);
|
||||
// for Send and Recv, see package rpc
|
||||
}
|
||||
|
||||
@@ -62,6 +61,25 @@ message SendReq {
|
||||
Tri Encrypted = 5;
|
||||
|
||||
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 {
|
||||
@@ -94,6 +112,8 @@ message ReceiveReq {
|
||||
// If true, the receiver should clear the resume token before performing the
|
||||
// zfs recv of the stream in the request
|
||||
bool ClearResumeToken = 3;
|
||||
|
||||
ReplicationConfig ReplicationConfig = 4;
|
||||
}
|
||||
|
||||
message ReceiveRes {}
|
||||
@@ -126,19 +146,3 @@ message PingRes {
|
||||
// Echo must be PingReq.Message
|
||||
string Echo = 1;
|
||||
}
|
||||
|
||||
message HintMostRecentCommonAncestorReq {
|
||||
string Filesystem = 1;
|
||||
|
||||
// A copy of the FilesystemVersion on the sending side that the replication
|
||||
// algorithm identified as a shared most recent common version between sending
|
||||
// and receiving side.
|
||||
//
|
||||
// If nil, this is an indication that the replication algorithm could not
|
||||
// find a common ancestor between the two sides.
|
||||
// NOTE: nilness does not mean that replication never happened - there could
|
||||
// as well be a replication conflict. thus, dont' jump to conclusions too
|
||||
// rapidly here.
|
||||
FilesystemVersion SenderVersion = 2;
|
||||
}
|
||||
message HintMostRecentCommonAncestorRes {}
|
||||
@@ -83,3 +83,10 @@ func (v *FilesystemVersion) ZFSFilesystemVersion() (*zfs.FilesystemVersion, erro
|
||||
Creation: ct,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func ReplicationConfigProtectionWithKind(both ReplicationGuaranteeKind) *ReplicationConfigProtection {
|
||||
return &ReplicationConfigProtection{
|
||||
Initial: both,
|
||||
Incremental: both,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,6 @@ type Endpoint interface {
|
||||
ListFilesystemVersions(ctx context.Context, req *pdu.ListFilesystemVersionsReq) (*pdu.ListFilesystemVersionsRes, error)
|
||||
DestroySnapshots(ctx context.Context, req *pdu.DestroySnapshotsReq) (*pdu.DestroySnapshotsRes, error)
|
||||
WaitForConnectivity(ctx context.Context) error
|
||||
HintMostRecentCommonAncestor(context.Context, *pdu.HintMostRecentCommonAncestorReq) (*pdu.HintMostRecentCommonAncestorRes, error)
|
||||
}
|
||||
|
||||
type Sender interface {
|
||||
@@ -53,10 +52,6 @@ type Receiver interface {
|
||||
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 {
|
||||
sender Sender
|
||||
receiver Receiver
|
||||
@@ -288,7 +283,10 @@ func (p *Planner) doPlanning(ctx context.Context) ([]*Filesystem, error) {
|
||||
}
|
||||
}
|
||||
|
||||
ctr := p.promBytesReplicated.WithLabelValues(fs.Path)
|
||||
var ctr prometheus.Counter
|
||||
if p.promBytesReplicated != nil {
|
||||
ctr = p.promBytesReplicated.WithLabelValues(fs.Path)
|
||||
}
|
||||
|
||||
q = append(q, &Filesystem{
|
||||
sender: p.sender,
|
||||
@@ -359,43 +357,6 @@ func (fs *Filesystem) doPlanning(ctx context.Context) ([]*Step, error) {
|
||||
log(ctx).WithField("token", resumeToken).Debug("decode resume token")
|
||||
}
|
||||
|
||||
// give both sides a hint about how far prior replication attempts got
|
||||
// This serves as a cummulative variant of SendCompleted and can be useful
|
||||
// for example to release stale holds from an earlier (interrupted) replication.
|
||||
// TODO FIXME: enqueue this as a replication step instead of doing it here during planning
|
||||
// then again, the step should run regardless of planning success
|
||||
// so maybe a separate phase before PLANNING, then?
|
||||
path, conflict := IncrementalPath(rfsvs, sfsvs)
|
||||
var sender_mrca *pdu.FilesystemVersion
|
||||
if conflict == nil && len(path) > 0 {
|
||||
sender_mrca = path[0] // shadow
|
||||
}
|
||||
// yes, sender_mrca may be nil, indicating that we do not have an mrca
|
||||
{
|
||||
var wg sync.WaitGroup
|
||||
doHint := func(ep Endpoint, name string) {
|
||||
defer wg.Done()
|
||||
ctx, endTask := trace.WithTask(ctx, "hint-mrca-"+name)
|
||||
defer endTask()
|
||||
|
||||
log := log(ctx).WithField("to_side", name).
|
||||
WithField("sender_mrca", sender_mrca.String())
|
||||
log.Debug("hint most recent common ancestor")
|
||||
hint := &pdu.HintMostRecentCommonAncestorReq{
|
||||
Filesystem: fs.Path,
|
||||
SenderVersion: sender_mrca,
|
||||
}
|
||||
_, err := ep.HintMostRecentCommonAncestor(ctx, hint)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("error hinting most recent common ancestor")
|
||||
}
|
||||
}
|
||||
wg.Add(2)
|
||||
go doHint(fs.sender, "sender")
|
||||
go doHint(fs.receiver, "receiver")
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
var steps []*Step
|
||||
// build the list of replication steps
|
||||
//
|
||||
@@ -589,19 +550,25 @@ func (s *Step) updateSizeEstimate(ctx context.Context) error {
|
||||
log.WithError(err).Error("dry run send request failed")
|
||||
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
|
||||
}
|
||||
|
||||
func (s *Step) buildSendRequest(dryRun bool) (sr *pdu.SendReq) {
|
||||
fs := s.parent.Path
|
||||
sr = &pdu.SendReq{
|
||||
Filesystem: fs,
|
||||
From: s.from, // may be nil
|
||||
To: s.to,
|
||||
Encrypted: s.encrypt.ToPDU(),
|
||||
ResumeToken: s.resumeToken,
|
||||
DryRun: dryRun,
|
||||
Filesystem: fs,
|
||||
From: s.from, // may be nil
|
||||
To: s.to,
|
||||
Encrypted: s.encrypt.ToPDU(),
|
||||
ResumeToken: s.resumeToken,
|
||||
DryRun: dryRun,
|
||||
ReplicationConfig: &s.parent.policy.ReplicationConfig,
|
||||
}
|
||||
return sr
|
||||
}
|
||||
@@ -619,6 +586,11 @@ func (s *Step) doReplication(ctx context.Context) error {
|
||||
log.WithError(err).Error("send request failed")
|
||||
return err
|
||||
}
|
||||
if sres == nil {
|
||||
err := fmt.Errorf("send request returned nil send result")
|
||||
log.Error(err.Error())
|
||||
return err
|
||||
}
|
||||
if stream == nil {
|
||||
err := errors.New("send request did not return a stream, broken endpoint implementation")
|
||||
return err
|
||||
@@ -632,13 +604,16 @@ func (s *Step) doReplication(ctx context.Context) error {
|
||||
s.byteCounterMtx.Unlock()
|
||||
defer func() {
|
||||
defer s.byteCounterMtx.Lock().Unlock()
|
||||
s.parent.promBytesReplicated.Add(float64(s.byteCounter.Count()))
|
||||
if s.parent.promBytesReplicated != nil {
|
||||
s.parent.promBytesReplicated.Add(float64(s.byteCounter.Count()))
|
||||
}
|
||||
}()
|
||||
|
||||
rr := &pdu.ReceiveReq{
|
||||
Filesystem: fs,
|
||||
To: sr.GetTo(),
|
||||
ClearResumeToken: !sres.UsedResumeToken,
|
||||
Filesystem: fs,
|
||||
To: sr.GetTo(),
|
||||
ClearResumeToken: !sres.UsedResumeToken,
|
||||
ReplicationConfig: &s.parent.policy.ReplicationConfig,
|
||||
}
|
||||
log.Debug("initiate receive request")
|
||||
_, err = s.receiver.Receive(ctx, rr, byteCountingStream)
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -239,6 +239,10 @@ func (s *Server) serveConnRequest(ctx context.Context, endpoint string, c *strea
|
||||
|
||||
if sendStream != nil {
|
||||
err := c.SendStream(ctx, sendStream, ZFSStream)
|
||||
closeErr := sendStream.Close()
|
||||
if closeErr != nil {
|
||||
s.log.WithError(err).Error("cannot close send stream")
|
||||
}
|
||||
if err != nil {
|
||||
s.log.WithError(err).Error("cannot write send stream")
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@ func TestNoPartialReadsDueToDeadline(t *testing.T) {
|
||||
// 50ms for the second read, 100ms after that one for the deadline
|
||||
// allow for some jitter
|
||||
assert.True(t, readDuration > 140*time.Millisecond)
|
||||
assert.True(t, readDuration < 160*time.Millisecond)
|
||||
assert.True(t, readDuration < 200*time.Millisecond)
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
|
||||
@@ -142,13 +142,6 @@ func (c *Client) SendCompleted(ctx context.Context, in *pdu.SendCompletedReq) (*
|
||||
return c.controlClient.SendCompleted(ctx, in)
|
||||
}
|
||||
|
||||
func (c *Client) HintMostRecentCommonAncestor(ctx context.Context, in *pdu.HintMostRecentCommonAncestorReq) (*pdu.HintMostRecentCommonAncestorRes, error) {
|
||||
ctx, endSpan := trace.WithSpan(ctx, "rpc.client.HintMostRecentCommonAncestor")
|
||||
defer endSpan()
|
||||
|
||||
return c.controlClient.HintMostRecentCommonAncestor(ctx, in)
|
||||
}
|
||||
|
||||
func (c *Client) WaitForConnectivity(ctx context.Context) error {
|
||||
ctx, endSpan := trace.WithSpan(ctx, "rpc.client.WaitForConnectivity")
|
||||
defer endSpan()
|
||||
|
||||
@@ -152,7 +152,7 @@ func (m *HandshakeMessage) DecodeReader(r io.Reader, maxLen int) error {
|
||||
|
||||
func DoHandshakeCurrentVersion(conn net.Conn, deadline time.Time) *HandshakeError {
|
||||
// current protocol version is hardcoded here
|
||||
return DoHandshakeVersion(conn, deadline, 3)
|
||||
return DoHandshakeVersion(conn, deadline, 5)
|
||||
}
|
||||
|
||||
const HandshakeMessageMaxLen = 16 * 4096
|
||||
|
||||
@@ -11,39 +11,6 @@ import (
|
||||
"github.com/zrepl/zrepl/util/tcpsock"
|
||||
)
|
||||
|
||||
type ipMapEntry struct {
|
||||
ip net.IP
|
||||
ident string
|
||||
}
|
||||
|
||||
type ipMap struct {
|
||||
entries []ipMapEntry
|
||||
}
|
||||
|
||||
func ipMapFromConfig(clients map[string]string) (*ipMap, error) {
|
||||
entries := make([]ipMapEntry, 0, len(clients))
|
||||
for clientIPString, clientIdent := range clients {
|
||||
clientIP := net.ParseIP(clientIPString)
|
||||
if clientIP == nil {
|
||||
return nil, errors.Errorf("cannot parse client IP %q", clientIPString)
|
||||
}
|
||||
if err := transport.ValidateClientIdentity(clientIdent); err != nil {
|
||||
return nil, errors.Wrapf(err, "invalid client identity for IP %q", clientIPString)
|
||||
}
|
||||
entries = append(entries, ipMapEntry{clientIP, clientIdent})
|
||||
}
|
||||
return &ipMap{entries: entries}, nil
|
||||
}
|
||||
|
||||
func (m *ipMap) Get(ip net.IP) (string, error) {
|
||||
for _, e := range m.entries {
|
||||
if e.ip.Equal(ip) {
|
||||
return e.ident, nil
|
||||
}
|
||||
}
|
||||
return "", errors.Errorf("no identity mapping for client IP %s", ip)
|
||||
}
|
||||
|
||||
func TCPListenerFactoryFromConfig(c *config.Global, in *config.TCPServe) (transport.AuthenticatedListenerFactory, error) {
|
||||
clientMap, err := ipMapFromConfig(in.Clients)
|
||||
if err != nil {
|
||||
@@ -75,10 +42,13 @@ func (f *TCPAuthListener) Accept(ctx context.Context) (*transport.AuthConn, erro
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
clientIP := nc.RemoteAddr().(*net.TCPAddr).IP
|
||||
clientIdent, err := f.clientMap.Get(clientIP)
|
||||
clientAddr := &net.IPAddr{
|
||||
IP: nc.RemoteAddr().(*net.TCPAddr).IP,
|
||||
Zone: nc.RemoteAddr().(*net.TCPAddr).Zone,
|
||||
}
|
||||
clientIdent, err := f.clientMap.Get(clientAddr)
|
||||
if err != nil {
|
||||
transport.GetLogger(ctx).WithField("ip", clientIP).Error("client IP not in client map")
|
||||
transport.GetLogger(ctx).WithField("ipaddr", clientAddr).Error("client IP not in client map")
|
||||
nc.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user