Compare commits
55 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 42ffca09db | |||
| 05a39eaddf | |||
| 347d0f1aa2 | |||
| 5aaac49382 | |||
| c1c9d99a6f | |||
| d59b64df86 | |||
| 1540a478b0 | |||
| 15715b1e2a | |||
| afab031b7d | |||
| d52acfe10b | |||
| 07ae6f2aad | |||
| 7b34d6cba5 | |||
| 70f9c6482f | |||
| aed6149c8c | |||
| 0834a184b8 | |||
| f61295f76c | |||
| 0920a40751 | |||
| e0b5bd75f8 | |||
| 96e188d7c4 | |||
| 1336c91865 | |||
| 9568e46f05 | |||
| 3187129672 | |||
| 44bd354eae | |||
| 94caf8b8db | |||
| 3ff1966cab | |||
| a3842155c5 | |||
| 02b3b4f80c | |||
| 0882290595 | |||
| b8d9f4ba92 | |||
| 1462c5caa5 | |||
| 58c08c855f | |||
| 9a4763ceee | |||
| f8200a6386 | |||
| e35320f8ee | |||
| 5b52e5e331 | |||
| 6ebd9f1037 | |||
| 99ab16d7be | |||
| e675b35cda | |||
| ddd7acec49 | |||
| e7aa08564b | |||
| 93ccdb8024 | |||
| 501645f918 | |||
| 4fd9d30c98 | |||
| e2b9c16959 | |||
| 4ba85c40cc | |||
| cca95f613b | |||
| 5b50a66c6c | |||
| dd508280f0 | |||
| 2927d0ca15 | |||
| d35e2400b2 | |||
| 47ed599db7 | |||
| f899f4cbe4 | |||
| b3231d2bed | |||
| c24c327151 | |||
| 5e17d7ba80 |
@@ -6,8 +6,11 @@ workflows:
|
||||
- build-1.11
|
||||
- build-1.12
|
||||
- build-1.13
|
||||
- build-1.14
|
||||
- build-latest
|
||||
- test-build-in-docker
|
||||
jobs:
|
||||
|
||||
# build-latest serves as the template
|
||||
# we use YAML anchors & aliases to exchange the docker image (and hence Go version used for the build)
|
||||
build-latest: &build-latest
|
||||
@@ -108,3 +111,35 @@ jobs:
|
||||
<<: *build-latest
|
||||
docker:
|
||||
- image: circleci/golang:1.13
|
||||
|
||||
build-1.14:
|
||||
<<: *build-latest
|
||||
docker:
|
||||
- image: circleci/golang:1.14
|
||||
|
||||
# this job tries to mimic the build-in-docker instructions
|
||||
# given in docs/installation.rst
|
||||
#
|
||||
# However, CircleCI doesn't support volume mounts, so we have to copy
|
||||
# the source into the build-container by modifying the Dockerfile
|
||||
test-build-in-docker:
|
||||
description: Check that build-in-docker works
|
||||
docker:
|
||||
- image: circleci/golang:latest
|
||||
environment:
|
||||
working_directory: /go/src/github.com/zrepl/zrepl
|
||||
steps:
|
||||
- checkout
|
||||
- setup_remote_docker
|
||||
- run:
|
||||
name: (hacky) circleci doesn't allow volume mounts, so copy src to container
|
||||
command: echo "ADD . /src" >> build.Dockerfile
|
||||
- run:
|
||||
name: (hacky) commit modified Dockerfile to avoid failing git clean check in Makefile
|
||||
command: git -c user.name='circleci' -c user.email='circleci@localhost' commit -m 'CIRCLECI modified Dockerfile with zrepl src' --author 'autoauthor <circleci@localhost>' -- build.Dockerfile
|
||||
- run:
|
||||
name: build the build image (build deps)
|
||||
command: docker build -t zrepl_build -f build.Dockerfile .
|
||||
- run:
|
||||
name: try compiling
|
||||
command: docker run -it zrepl_build make release
|
||||
@@ -17,19 +17,27 @@ endif
|
||||
GO := go
|
||||
GOOS ?= $(shell bash -c 'source <($(GO) env) && echo "$$GOOS"')
|
||||
GOARCH ?= $(shell bash -c 'source <($(GO) env) && echo "$$GOARCH"')
|
||||
GOARM ?= $(shell bash -c 'source <($(GO) env) && echo "$$GOARM"')
|
||||
GOHOSTOS ?= $(shell bash -c 'source <($(GO) env) && echo "$$GOHOSTOS"')
|
||||
GOHOSTARCH ?= $(shell bash -c 'source <($(GO) env) && echo "$$GOHOSTARCH"')
|
||||
GO_ENV_VARS := GO111MODULE=on
|
||||
GO_LDFLAGS := "-X github.com/zrepl/zrepl/version.zreplVersion=$(_ZREPL_VERSION)"
|
||||
GO_MOD_READONLY := -mod=readonly
|
||||
GO_BUILDFLAGS := $(GO_MOD_READONLY)
|
||||
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
|
||||
ifneq ($(GOARM),)
|
||||
ZREPL_TARGET_TUPLE := $(GOOS)-$(GOARCH)v$(GOARM)
|
||||
else
|
||||
ZREPL_TARGET_TUPLE := $(GOOS)-$(GOARCH)
|
||||
endif
|
||||
|
||||
.PHONY: printvars
|
||||
printvars:
|
||||
@echo GOOS=$(GOOS)
|
||||
@echo GOARCH=$(GOARCH)
|
||||
@echo GOARM=$(GOARM)
|
||||
|
||||
|
||||
##################### PRODUCING A RELEASE #############
|
||||
@@ -58,6 +66,7 @@ wrapup-and-checksum:
|
||||
-acf $(NOARCH_TARBALL) \
|
||||
$(ARTIFACTDIR)/docs/html \
|
||||
$(ARTIFACTDIR)/bash_completion \
|
||||
$(ARTIFACTDIR)/_zrepl.zsh_completion \
|
||||
$(ARTIFACTDIR)/go_env.txt \
|
||||
dist \
|
||||
config/samples
|
||||
@@ -99,6 +108,7 @@ bins-all:
|
||||
$(MAKE) $(BINS_ALL_TARGETS) GOOS=freebsd GOARCH=386
|
||||
$(MAKE) $(BINS_ALL_TARGETS) GOOS=linux GOARCH=amd64
|
||||
$(MAKE) $(BINS_ALL_TARGETS) GOOS=linux GOARCH=arm64
|
||||
$(MAKE) $(BINS_ALL_TARGETS) GOOS=linux GOARCH=arm GOARM=7
|
||||
$(MAKE) $(BINS_ALL_TARGETS) GOOS=linux GOARCH=386
|
||||
$(MAKE) $(BINS_ALL_TARGETS) GOOS=darwin GOARCH=amd64
|
||||
$(MAKE) $(BINS_ALL_TARGETS) GOOS=solaris GOARCH=amd64
|
||||
@@ -118,10 +128,10 @@ vet:
|
||||
$(GO_ENV_VARS) $(GO) vet $(GO_BUILDFLAGS) ./...
|
||||
|
||||
zrepl-bin:
|
||||
$(GO_BUILD) -o "$(ARTIFACTDIR)/zrepl-$(GOOS)-$(GOARCH)"
|
||||
$(GO_BUILD) -o "$(ARTIFACTDIR)/zrepl-$(ZREPL_TARGET_TUPLE)"
|
||||
|
||||
platformtest-bin:
|
||||
$(GO_BUILD) -o "$(ARTIFACTDIR)/platformtest-$(GOOS)-$(GOARCH)" ./platformtest/harness
|
||||
$(GO_BUILD) -o "$(ARTIFACTDIR)/platformtest-$(ZREPL_TARGET_TUPLE)" ./platformtest/harness
|
||||
|
||||
##################### DEV TARGETS #####################
|
||||
# not part of the build, must do that manually
|
||||
@@ -136,11 +146,22 @@ format:
|
||||
|
||||
ZREPL_PLATFORMTEST_POOLNAME := zreplplatformtest
|
||||
ZREPL_PLATFORMTEST_IMAGEPATH := /tmp/zreplplatformtest.pool.img
|
||||
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
|
||||
"$(ARTIFACTDIR)/platformtest-$(GOOS)-$(GOARCH)" -poolname "$(ZREPL_PLATFORMTEST_POOLNAME)" -imagepath "$(ZREPL_PLATFORMTEST_IMAGEPATH)"
|
||||
rm -f "$(ZREPL_PLATFORMTEST_ZFS_LOG)"
|
||||
platformtest/logmockzfs/logzfsenv "$(ZREPL_PLATFORMTEST_ZFS_LOG)" `which zfs` \
|
||||
"$(ARTIFACTDIR)/platformtest-$(ZREPL_TARGET_TUPLE)" \
|
||||
-poolname "$(ZREPL_PLATFORMTEST_POOLNAME)" \
|
||||
-imagepath "$(ZREPL_PLATFORMTEST_IMAGEPATH)" \
|
||||
-mountpoint "$(ZREPL_PLATFORMTEST_MOUNTPOINT)" \
|
||||
$(ZREPL_PLATFORMTEST_STOP_AND_KEEP) \
|
||||
$(ZREPL_PLATFORMTEST_ARGS)
|
||||
|
||||
##################### NOARCH #####################
|
||||
.PHONY: noarch $(ARTIFACTDIR)/bash_completion $(ARTIFACTDIR)/go_env.txt docs docs-clean
|
||||
.PHONY: noarch $(ARTIFACTDIR)/bash_completion $(ARTIFACTDIR)/_zrepl.zsh_completion $(ARTIFACTDIR)/go_env.txt docs docs-clean
|
||||
|
||||
|
||||
$(ARTIFACTDIR):
|
||||
@@ -148,12 +169,16 @@ $(ARTIFACTDIR):
|
||||
$(ARTIFACTDIR)/docs: $(ARTIFACTDIR)
|
||||
mkdir -p "$@"
|
||||
|
||||
noarch: $(ARTIFACTDIR)/bash_completion $(ARTIFACTDIR)/go_env.txt docs
|
||||
noarch: $(ARTIFACTDIR)/bash_completion $(ARTIFACTDIR)/_zrepl.zsh_completion $(ARTIFACTDIR)/go_env.txt docs
|
||||
# pass
|
||||
|
||||
$(ARTIFACTDIR)/bash_completion:
|
||||
$(MAKE) zrepl-bin GOOS=$(GOHOSTOS) GOARCH=$(GOHOSTARCH)
|
||||
artifacts/zrepl-$(GOHOSTOS)-$(GOHOSTARCH) bashcomp "$@"
|
||||
artifacts/zrepl-$(GOHOSTOS)-$(GOHOSTARCH) gencompletion bash "$@"
|
||||
|
||||
$(ARTIFACTDIR)/_zrepl.zsh_completion:
|
||||
$(MAKE) zrepl-bin GOOS=$(GOHOSTOS) GOARCH=$(GOHOSTARCH)
|
||||
artifacts/zrepl-$(GOHOSTOS)-$(GOHOSTARCH) gencompletion zsh "$@"
|
||||
|
||||
$(ARTIFACTDIR)/go_env.txt:
|
||||
$(GO_ENV_VARS) $(GO) env > $@
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
[](https://github.com/zrepl/zrepl/blob/master/LICENSE)
|
||||
[](https://golang.org/)
|
||||
[](https://zrepl.github.io)
|
||||
[](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=R5QSXJVYHGX96)
|
||||
[](https://www.patreon.com/zrepl)
|
||||
[](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)
|
||||
|
||||
# zrepl
|
||||
@@ -23,6 +24,7 @@ zrepl is a one-stop ZFS backup & replication solution.
|
||||
If so, think of an expressive configuration example.
|
||||
2. Think of at least one use case that generalizes from your concrete application.
|
||||
3. Open an issue on GitHub with example conf & use case attached.
|
||||
4. **Optional**: [Post a bounty](https://www.bountysource.com/teams/zrepl) on the issue, or [contact Christian Schwarz](https://cschwarz.com) for contract work.
|
||||
|
||||
The above does not apply if you already implemented everything.
|
||||
Check out the *Coding Workflow* section below for details.
|
||||
@@ -46,11 +48,8 @@ zrepl is written in [Go](https://golang.org) and uses [Go modules](https://githu
|
||||
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`.
|
||||
|
||||
### Overall Architecture
|
||||
|
||||
The application architecture is documented as part of the user docs in the *Implementation* section (`docs/content/impl`).
|
||||
Make sure to develop an understanding how zrepl is typically used by studying the user docs first.
|
||||
`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`.
|
||||
|
||||
### Project Structure
|
||||
|
||||
@@ -62,14 +61,15 @@ Make sure to develop an understanding how zrepl is typically used by studying th
|
||||
│ └── samples
|
||||
├── daemon # the implementation of `zrepl daemon` subcommand
|
||||
│ ├── filters
|
||||
│ ├── hooks # snapshot hooks
|
||||
│ ├── job # job implementations
|
||||
│ ├── logging # logging outlets + formatters
|
||||
│ ├── nethelpers
|
||||
│ ├── prometheus
|
||||
│ ├── pruner # pruner implementation
|
||||
│ ├── snapper # snapshotter implementation
|
||||
├── docs # sphinx-based documentation
|
||||
├── dist # supplemental material for users & package maintainers
|
||||
├── docs # sphinx-based documentation
|
||||
│ ├── **/*.rst # documentation in reStructuredText
|
||||
│ ├── sphinxconf
|
||||
│ │ └── conf.py # sphinx config (see commit 445a280 why its not in docs/)
|
||||
@@ -78,6 +78,7 @@ Make sure to develop an understanding how zrepl is typically used by studying th
|
||||
│ └── public_git # checkout of zrepl.github.io managed by above shell script
|
||||
├── endpoint # implementation of replication endpoints (=> package replication)
|
||||
├── logger # our own logger package
|
||||
├── platformtest # test suite for our zfs abstractions (error classification, etc)
|
||||
├── pruning # pruning rules (the logic, not the actual execution)
|
||||
│ └── retentiongrid
|
||||
├── replication
|
||||
@@ -92,14 +93,13 @@ Make sure to develop an understanding how zrepl is typically used by studying th
|
||||
│ ├── transportmux # TCP connecter and listener used to split control & data traffic
|
||||
│ └── versionhandshake # replication protocol version handshake perfomed on newly established connections
|
||||
├── tlsconf # abstraction for Go TLS server + client config
|
||||
├── transport # transports implementation
|
||||
├── transport # transport implementations
|
||||
│ ├── fromconfig
|
||||
│ ├── local
|
||||
│ ├── ssh
|
||||
│ ├── tcp
|
||||
│ └── tls
|
||||
├── util
|
||||
├── vendor # managed by dep
|
||||
├── version # abstraction for versions (filled during build by Makefile)
|
||||
└── zfs # zfs(8) wrappers
|
||||
```
|
||||
@@ -134,3 +134,15 @@ There will not be a big refactoring (an attempt was made, but it's destroying to
|
||||
|
||||
However, new contributions & patches should fix naming without further notice in the commit message.
|
||||
|
||||
### RPC debugging
|
||||
|
||||
Optionally, there are various RPC-related environment variables, that if set to something != `""` will produce additional debug output on stderr:
|
||||
|
||||
https://github.com/zrepl/zrepl/blob/master/rpc/rpc_debug.go#L11
|
||||
|
||||
https://github.com/zrepl/zrepl/blob/master/rpc/dataconn/dataconn_debug.go#L11
|
||||
|
||||
https://github.com/zrepl/zrepl/blob/master/rpc/dataconn/stream/stream_debug.go#L11
|
||||
|
||||
https://github.com/zrepl/zrepl/blob/master/rpc/dataconn/heartbeatconn/heartbeatconn_debug.go#L11
|
||||
|
||||
|
||||
+3
-2
@@ -2,7 +2,8 @@ FROM golang:latest
|
||||
|
||||
RUN apt-get update && apt-get install -y \
|
||||
python3-pip \
|
||||
unzip
|
||||
unzip \
|
||||
gawk
|
||||
|
||||
ADD build.installprotoc.bash ./
|
||||
RUN bash build.installprotoc.bash
|
||||
@@ -22,7 +23,7 @@ RUN mkdir -p /.cache && chmod -R 0777 /.cache
|
||||
WORKDIR /src
|
||||
|
||||
# Install build tools (e.g. protobuf generator, stringer) into $GOPATH/bin
|
||||
ADD go.mod go.sum ./
|
||||
ADD build/ /tmp/build
|
||||
RUN /tmp/lazy.sh godep
|
||||
|
||||
RUN chmod -R 0777 /go
|
||||
|
||||
+3
-3
@@ -3,9 +3,9 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
_ "golang.org/x/tools/cmd/stringer"
|
||||
_ "github.com/golang/protobuf/protoc-gen-go"
|
||||
_ "github.com/alvaroloes/enumer"
|
||||
_ "golang.org/x/tools/cmd/goimports"
|
||||
_ "github.com/golang/protobuf/protoc-gen-go"
|
||||
_ "github.com/golangci/golangci-lint/cmd/golangci-lint"
|
||||
_ "golang.org/x/tools/cmd/goimports"
|
||||
_ "golang.org/x/tools/cmd/stringer"
|
||||
)
|
||||
|
||||
+45
-19
@@ -19,29 +19,55 @@ var rootCmd = &cobra.Command{
|
||||
Short: "One-stop ZFS replication solution",
|
||||
}
|
||||
|
||||
var bashcompCmd = &cobra.Command{
|
||||
Use: "bashcomp path/to/out/file",
|
||||
Short: "generate bash completions",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 1 {
|
||||
fmt.Fprintf(os.Stderr, "specify exactly one positional agument\n")
|
||||
err := cmd.Usage()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := rootCmd.GenBashCompletionFile(args[0]); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error generating bash completion: %s", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
func init() {
|
||||
rootCmd.PersistentFlags().StringVar(&rootArgs.configPath, "config", "", "config file path")
|
||||
}
|
||||
|
||||
var genCompletionCmd = &cobra.Command{
|
||||
Use: "gencompletion",
|
||||
Short: "generate shell auto-completions",
|
||||
}
|
||||
|
||||
type completionCmdInfo struct {
|
||||
genFunc func(outpath string) error
|
||||
help string
|
||||
}
|
||||
|
||||
var completionCmdMap = map[string]completionCmdInfo{
|
||||
"zsh": {
|
||||
rootCmd.GenZshCompletionFile,
|
||||
" save to file `_zrepl` in your zsh's $fpath",
|
||||
},
|
||||
"bash": {
|
||||
rootCmd.GenBashCompletionFile,
|
||||
" save to a path and source that path in your .bashrc",
|
||||
},
|
||||
Hidden: true,
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.PersistentFlags().StringVar(&rootArgs.configPath, "config", "", "config file path")
|
||||
rootCmd.AddCommand(bashcompCmd)
|
||||
for sh, info := range completionCmdMap {
|
||||
sh, info := sh, info
|
||||
genCompletionCmd.AddCommand(&cobra.Command{
|
||||
Use: fmt.Sprintf("%s path/to/out/file", sh),
|
||||
Short: fmt.Sprintf("generate %s completions", sh),
|
||||
Example: info.help,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 1 {
|
||||
fmt.Fprintf(os.Stderr, "specify exactly one positional agument\n")
|
||||
err := cmd.Usage()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := info.genFunc(args[0]); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error generating %s completion: %s", sh, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
rootCmd.AddCommand(genCompletionCmd)
|
||||
}
|
||||
|
||||
type Subcommand struct {
|
||||
|
||||
+168
-1
@@ -4,9 +4,13 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/kr/pretty"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
"github.com/zrepl/zrepl/daemon/job"
|
||||
"github.com/zrepl/zrepl/endpoint"
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
|
||||
"github.com/zrepl/zrepl/cli"
|
||||
@@ -31,6 +35,13 @@ var migrations = []*cli.Subcommand{
|
||||
f.BoolVar(&migratePlaceholder0_1Args.dryRun, "dry-run", false, "dry run")
|
||||
},
|
||||
},
|
||||
&cli.Subcommand{
|
||||
Use: "replication-cursor:v1-v2",
|
||||
Run: doMigrateReplicationCursor,
|
||||
SetupFlags: func(f *pflag.FlagSet) {
|
||||
f.BoolVar(&migrateReplicationCursorArgs.dryRun, "dry-run", false, "dry run")
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var migratePlaceholder0_1Args struct {
|
||||
@@ -88,7 +99,7 @@ func doMigratePlaceholder0_1(sc *cli.Subcommand, args []string) error {
|
||||
}
|
||||
for _, fs := range wi.fss {
|
||||
fmt.Printf("\t%q ... ", fs.ToString())
|
||||
r, err := zfs.ZFSMigrateHashBasedPlaceholderToCurrent(fs, migratePlaceholder0_1Args.dryRun)
|
||||
r, err := zfs.ZFSMigrateHashBasedPlaceholderToCurrent(ctx, fs, migratePlaceholder0_1Args.dryRun)
|
||||
if err != nil {
|
||||
fmt.Printf("error: %s\n", err)
|
||||
} else if !r.NeedsModification {
|
||||
@@ -102,3 +113,159 @@ func doMigratePlaceholder0_1(sc *cli.Subcommand, args []string) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var migrateReplicationCursorArgs struct {
|
||||
dryRun bool
|
||||
}
|
||||
|
||||
var bold = color.New(color.Bold)
|
||||
var succ = color.New(color.FgGreen)
|
||||
var fail = color.New(color.FgRed)
|
||||
|
||||
var migrateReplicationCursorSkipSentinel = fmt.Errorf("skipping this filesystem")
|
||||
|
||||
func doMigrateReplicationCursor(sc *cli.Subcommand, args []string) error {
|
||||
if len(args) != 0 {
|
||||
return fmt.Errorf("migration does not take arguments, got %v", args)
|
||||
}
|
||||
|
||||
cfg := sc.Config()
|
||||
jobs, err := job.JobsFromConfig(cfg)
|
||||
if err != nil {
|
||||
fmt.Printf("cannot parse config:\n%s\n\n", err)
|
||||
fmt.Printf("NOTE: this migration was released together with a change in job name requirements.\n")
|
||||
return fmt.Errorf("exiting migration after error")
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
v1cursorJobs := make([]job.Job, 0, len(cfg.Jobs))
|
||||
for i, j := range cfg.Jobs {
|
||||
if jobs[i].Name() != j.Name() {
|
||||
panic("implementation error")
|
||||
}
|
||||
switch j.Ret.(type) {
|
||||
case *config.PushJob:
|
||||
v1cursorJobs = append(v1cursorJobs, jobs[i])
|
||||
case *config.SourceJob:
|
||||
v1cursorJobs = append(v1cursorJobs, jobs[i])
|
||||
default:
|
||||
fmt.Printf("ignoring job %q (%d/%d, type %T), not supposed to create v1 replication cursors\n", j.Name(), i, len(cfg.Jobs), j.Ret)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// scan all filesystems for v1 replication cursors
|
||||
|
||||
fss, err := zfs.ZFSListMapping(ctx, zfs.NoFilter())
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "list filesystems")
|
||||
}
|
||||
|
||||
var hadError bool
|
||||
for _, fs := range fss {
|
||||
|
||||
bold.Printf("INSPECT FILESYSTEM %q\n", fs.ToString())
|
||||
|
||||
err := doMigrateReplicationCursorFS(ctx, v1cursorJobs, fs)
|
||||
if err == migrateReplicationCursorSkipSentinel {
|
||||
bold.Printf("FILESYSTEM SKIPPED\n")
|
||||
} else if err != nil {
|
||||
hadError = true
|
||||
fail.Printf("MIGRATION FAILED: %s\n", err)
|
||||
} else {
|
||||
succ.Printf("FILESYSTEM %q COMPLETE\n", fs.ToString())
|
||||
}
|
||||
}
|
||||
|
||||
if hadError {
|
||||
fail.Printf("\n\none or more filesystems could not be migrated, please inspect output and or re-run migration")
|
||||
return errors.Errorf("")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func doMigrateReplicationCursorFS(ctx context.Context, v1CursorJobs []job.Job, fs *zfs.DatasetPath) error {
|
||||
|
||||
var owningJob job.Job = nil
|
||||
for _, job := range v1CursorJobs {
|
||||
conf := job.SenderConfig()
|
||||
if conf == nil {
|
||||
continue
|
||||
}
|
||||
pass, err := conf.FSF.Filter(fs)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "filesystem filter error in job %q for fs %q", job.Name(), fs.ToString())
|
||||
}
|
||||
if !pass {
|
||||
continue
|
||||
}
|
||||
if owningJob != nil {
|
||||
return errors.Errorf("jobs %q and %q both match %q\ncannot attribute replication cursor to either one", owningJob.Name(), job.Name(), fs)
|
||||
}
|
||||
owningJob = job
|
||||
}
|
||||
if owningJob == nil {
|
||||
fmt.Printf("no job's Filesystems filter matches\n")
|
||||
return migrateReplicationCursorSkipSentinel
|
||||
}
|
||||
fmt.Printf("identified owning job %q\n", owningJob.Name())
|
||||
|
||||
bookmarks, err := zfs.ZFSListFilesystemVersions(ctx, fs, zfs.ListFilesystemVersionsOptions{
|
||||
Types: zfs.Bookmarks,
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "list filesystem versions of %q", fs.ToString())
|
||||
}
|
||||
|
||||
var oldCursor *zfs.FilesystemVersion
|
||||
for i, fsv := range bookmarks {
|
||||
_, _, err := endpoint.ParseReplicationCursorBookmarkName(fsv.ToAbsPath(fs))
|
||||
if err != endpoint.ErrV1ReplicationCursor {
|
||||
continue
|
||||
}
|
||||
|
||||
if oldCursor != nil {
|
||||
fmt.Printf("unexpected v1 replication cursor candidate: %q", fsv.ToAbsPath(fs))
|
||||
return errors.Wrap(err, "multiple filesystem versions identified as v1 replication cursors")
|
||||
}
|
||||
|
||||
oldCursor = &bookmarks[i]
|
||||
|
||||
}
|
||||
|
||||
if oldCursor == nil {
|
||||
bold.Printf("no v1 replication cursor found for filesystem %q\n", fs.ToString())
|
||||
return migrateReplicationCursorSkipSentinel
|
||||
}
|
||||
|
||||
fmt.Printf("found v1 replication cursor:\n%s\n", pretty.Sprint(oldCursor))
|
||||
|
||||
mostRecentNew, err := endpoint.GetMostRecentReplicationCursorOfJob(ctx, fs.ToString(), owningJob.SenderConfig().JobID)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "get most recent v2 replication cursor")
|
||||
}
|
||||
|
||||
if mostRecentNew == nil {
|
||||
return errors.Errorf("no v2 replication cursor found for job %q on filesystem %q", owningJob.SenderConfig().JobID, fs.ToString())
|
||||
}
|
||||
|
||||
fmt.Printf("most recent v2 replication cursor:\n%#v", oldCursor)
|
||||
|
||||
if !(mostRecentNew.CreateTXG >= oldCursor.CreateTXG) {
|
||||
return errors.Errorf("v1 replication cursor createtxg is higher than v2 cursor's, skipping this filesystem")
|
||||
}
|
||||
|
||||
fmt.Printf("determined that v2 cursor is bookmark of same or newer version than v1 cursor\n")
|
||||
fmt.Printf("destroying v1 cursor %q\n", oldCursor.ToAbsPath(fs))
|
||||
|
||||
if migrateReplicationCursorArgs.dryRun {
|
||||
succ.Printf("DRY RUN\n")
|
||||
} else {
|
||||
if err := zfs.ZFSDestroyFilesystemVersion(ctx, fs, oldCursor); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
+64
-11
@@ -11,7 +11,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
// tcell is the termbox-compatbile library for abstracting away escape sequences, etc.
|
||||
// tcell is the termbox-compatible library for abstracting away escape sequences, etc.
|
||||
// as of tcell#252, the number of default distributed terminals is relatively limited
|
||||
// additional terminal definitions can be included via side-effect import
|
||||
// See https://github.com/gdamore/tcell/blob/master/terminfo/base/base.go
|
||||
@@ -81,7 +81,7 @@ type tui struct {
|
||||
indent int
|
||||
|
||||
lock sync.Mutex //For report and error
|
||||
report map[string]job.Status
|
||||
report map[string]*job.Status
|
||||
err error
|
||||
|
||||
jobFilter string
|
||||
@@ -219,7 +219,7 @@ func runStatus(s *cli.Subcommand, args []string) error {
|
||||
defer termbox.Close()
|
||||
|
||||
update := func() {
|
||||
m := make(map[string]job.Status)
|
||||
var m daemon.Status
|
||||
|
||||
err2 := jsonRequestResponse(httpc, daemon.ControlJobEndpointStatus,
|
||||
struct{}{},
|
||||
@@ -228,7 +228,7 @@ func runStatus(s *cli.Subcommand, args []string) error {
|
||||
|
||||
t.lock.Lock()
|
||||
t.err = err2
|
||||
t.report = m
|
||||
t.report = m.Jobs
|
||||
t.lock.Unlock()
|
||||
t.draw()
|
||||
}
|
||||
@@ -264,7 +264,7 @@ loop:
|
||||
|
||||
}
|
||||
|
||||
func (t *tui) getReplicationProgresHistory(jobName string) *bytesProgressHistory {
|
||||
func (t *tui) getReplicationProgressHistory(jobName string) *bytesProgressHistory {
|
||||
p, ok := t.replicationProgress[jobName]
|
||||
if !ok {
|
||||
p = &bytesProgressHistory{}
|
||||
@@ -329,7 +329,7 @@ func (t *tui) draw() {
|
||||
t.printf("Replication:")
|
||||
t.newline()
|
||||
t.addIndent(1)
|
||||
t.renderReplicationReport(activeStatus.Replication, t.getReplicationProgresHistory(k))
|
||||
t.renderReplicationReport(activeStatus.Replication, t.getReplicationProgressHistory(k))
|
||||
t.addIndent(-1)
|
||||
|
||||
t.printf("Pruning Sender:")
|
||||
@@ -461,12 +461,23 @@ func (t *tui) renderReplicationReport(rep *report.Report, history *bytesProgress
|
||||
if latest.State != report.AttemptPlanning && latest.State != report.AttemptPlanningError {
|
||||
// Draw global progress bar
|
||||
// Progress: [---------------]
|
||||
expected, replicated := latest.BytesSum()
|
||||
expected, replicated, containsInvalidSizeEstimates := latest.BytesSum()
|
||||
rate, changeCount := history.Update(replicated)
|
||||
eta := time.Duration(0)
|
||||
if rate > 0 {
|
||||
eta = time.Duration((expected-replicated)/rate) * time.Second
|
||||
}
|
||||
t.write("Progress: ")
|
||||
t.drawBar(50, replicated, expected, changeCount)
|
||||
t.write(fmt.Sprintf(" %s / %s @ %s/s", ByteCountBinary(replicated), ByteCountBinary(expected), ByteCountBinary(rate)))
|
||||
if eta != 0 {
|
||||
t.write(fmt.Sprintf(" (%s remaining)", humanizeDuration(eta)))
|
||||
}
|
||||
t.newline()
|
||||
if containsInvalidSizeEstimates {
|
||||
t.write("NOTE: not all steps could be size-estimated, total estimate is likely imprecise!")
|
||||
t.newline()
|
||||
}
|
||||
|
||||
var maxFSLen int
|
||||
for _, fs := range latest.Filesystems {
|
||||
@@ -686,7 +697,7 @@ func rightPad(str string, length int, pad string) string {
|
||||
|
||||
var arrowPositions = `>\|/`
|
||||
|
||||
// changeCount = 0 indicates stall / no progresss
|
||||
// changeCount = 0 indicates stall / no progress
|
||||
func (t *tui) drawBar(length int, bytes, totalBytes int64, changeCount int) {
|
||||
var completedLength int
|
||||
if totalBytes > 0 {
|
||||
@@ -707,11 +718,20 @@ func (t *tui) drawBar(length int, bytes, totalBytes int64, changeCount int) {
|
||||
|
||||
func (t *tui) printFilesystemStatus(rep *report.FilesystemReport, active bool, maxFS int) {
|
||||
|
||||
expected, replicated := rep.BytesSum()
|
||||
status := fmt.Sprintf("%s (step %d/%d, %s/%s)",
|
||||
expected, replicated, containsInvalidSizeEstimates := rep.BytesSum()
|
||||
sizeEstimationImpreciseNotice := ""
|
||||
if containsInvalidSizeEstimates {
|
||||
sizeEstimationImpreciseNotice = " (some steps lack size estimation)"
|
||||
}
|
||||
if rep.CurrentStep < len(rep.Steps) && rep.Steps[rep.CurrentStep].Info.BytesExpected == 0 {
|
||||
sizeEstimationImpreciseNotice = " (step lacks size estimation)"
|
||||
}
|
||||
|
||||
status := fmt.Sprintf("%s (step %d/%d, %s/%s)%s",
|
||||
strings.ToUpper(string(rep.State)),
|
||||
rep.CurrentStep, len(rep.Steps),
|
||||
ByteCountBinary(replicated), ByteCountBinary(expected),
|
||||
sizeEstimationImpreciseNotice,
|
||||
)
|
||||
|
||||
activeIndicator := " "
|
||||
@@ -731,8 +751,17 @@ func (t *tui) printFilesystemStatus(rep *report.FilesystemReport, active bool, m
|
||||
if nextStep.IsIncremental() {
|
||||
next = fmt.Sprintf("next: %s => %s", nextStep.Info.From, nextStep.Info.To)
|
||||
} else {
|
||||
next = fmt.Sprintf("next: %s (full)", nextStep.Info.To)
|
||||
next = fmt.Sprintf("next: full send %s", nextStep.Info.To)
|
||||
}
|
||||
attribs := []string{}
|
||||
|
||||
if nextStep.Info.Resumed {
|
||||
attribs = append(attribs, "resumed")
|
||||
}
|
||||
|
||||
attribs = append(attribs, fmt.Sprintf("encrypted=%s", nextStep.Info.Encrypted))
|
||||
|
||||
next += fmt.Sprintf(" (%s)", strings.Join(attribs, ", "))
|
||||
} else {
|
||||
next = "" // individual FSes may still be in planning state
|
||||
}
|
||||
@@ -755,3 +784,27 @@ func ByteCountBinary(b int64) string {
|
||||
}
|
||||
return fmt.Sprintf("%.1f %ciB", float64(b)/float64(div), "KMGTPE"[exp])
|
||||
}
|
||||
|
||||
func humanizeDuration(duration time.Duration) string {
|
||||
days := int64(duration.Hours() / 24)
|
||||
hours := int64(math.Mod(duration.Hours(), 24))
|
||||
minutes := int64(math.Mod(duration.Minutes(), 60))
|
||||
seconds := int64(math.Mod(duration.Seconds(), 60))
|
||||
|
||||
var parts []string
|
||||
|
||||
force := false
|
||||
chunks := []int64{days, hours, minutes, seconds}
|
||||
for i, chunk := range chunks {
|
||||
if force || chunk > 0 {
|
||||
padding := 0
|
||||
if force {
|
||||
padding = 2
|
||||
}
|
||||
parts = append(parts, fmt.Sprintf("%*d%c", padding, chunk, "dhms"[i]))
|
||||
force = true
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(parts, " ")
|
||||
}
|
||||
|
||||
+40
-4
@@ -1,7 +1,10 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/pflag"
|
||||
@@ -15,7 +18,7 @@ import (
|
||||
var TestCmd = &cli.Subcommand{
|
||||
Use: "test",
|
||||
SetupSubcommands: func() []*cli.Subcommand {
|
||||
return []*cli.Subcommand{testFilter, testPlaceholder}
|
||||
return []*cli.Subcommand{testFilter, testPlaceholder, testDecodeResumeToken}
|
||||
},
|
||||
}
|
||||
|
||||
@@ -46,6 +49,7 @@ func runTestFilterCmd(subcommand *cli.Subcommand, args []string) error {
|
||||
}
|
||||
|
||||
conf := subcommand.Config()
|
||||
ctx := context.Background()
|
||||
|
||||
var confFilter config.FilesystemsFilter
|
||||
job, err := conf.Job(testFilterArgs.job)
|
||||
@@ -57,6 +61,8 @@ func runTestFilterCmd(subcommand *cli.Subcommand, args []string) error {
|
||||
confFilter = j.Filesystems
|
||||
case *config.PushJob:
|
||||
confFilter = j.Filesystems
|
||||
case *config.SnapJob:
|
||||
confFilter = j.Filesystems
|
||||
default:
|
||||
return fmt.Errorf("job type %T does not have filesystems filter", j)
|
||||
}
|
||||
@@ -70,7 +76,7 @@ func runTestFilterCmd(subcommand *cli.Subcommand, args []string) error {
|
||||
if testFilterArgs.input != "" {
|
||||
fsnames = []string{testFilterArgs.input}
|
||||
} else {
|
||||
out, err := zfs.ZFSList([]string{"name"})
|
||||
out, err := zfs.ZFSList(ctx, []string{"name"})
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not list ZFS filesystems: %s", err)
|
||||
}
|
||||
@@ -134,10 +140,11 @@ var testPlaceholder = &cli.Subcommand{
|
||||
func runTestPlaceholder(subcommand *cli.Subcommand, args []string) error {
|
||||
|
||||
var checkDPs []*zfs.DatasetPath
|
||||
ctx := context.Background()
|
||||
|
||||
// all actions first
|
||||
if testPlaceholderArgs.all {
|
||||
out, err := zfs.ZFSList([]string{"name"})
|
||||
out, err := zfs.ZFSList(ctx, []string{"name"})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not list ZFS filesystems")
|
||||
}
|
||||
@@ -161,7 +168,7 @@ func runTestPlaceholder(subcommand *cli.Subcommand, args []string) error {
|
||||
|
||||
fmt.Printf("IS_PLACEHOLDER\tDATASET\tzrepl:placeholder\n")
|
||||
for _, dp := range checkDPs {
|
||||
ph, err := zfs.ZFSGetFilesystemPlaceholderState(dp)
|
||||
ph, err := zfs.ZFSGetFilesystemPlaceholderState(ctx, dp)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "cannot get placeholder state")
|
||||
}
|
||||
@@ -176,3 +183,32 @@ func runTestPlaceholder(subcommand *cli.Subcommand, args []string) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var testDecodeResumeTokenArgs struct {
|
||||
token string
|
||||
}
|
||||
|
||||
var testDecodeResumeToken = &cli.Subcommand{
|
||||
Use: "decoderesumetoken --token TOKEN",
|
||||
Short: "decode resume token",
|
||||
SetupFlags: func(f *pflag.FlagSet) {
|
||||
f.StringVar(&testDecodeResumeTokenArgs.token, "token", "", "the resume token obtained from the receive_resume_token property")
|
||||
},
|
||||
Run: runTestDecodeResumeTokenCmd,
|
||||
}
|
||||
|
||||
func runTestDecodeResumeTokenCmd(subcommand *cli.Subcommand, args []string) error {
|
||||
if testDecodeResumeTokenArgs.token == "" {
|
||||
return fmt.Errorf("token argument must be specified")
|
||||
}
|
||||
token, err := zfs.ParseResumeToken(context.Background(), testDecodeResumeTokenArgs.token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
enc := json.NewEncoder(os.Stdout)
|
||||
enc.SetIndent("", " ")
|
||||
if err := enc.Encode(&token); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
"github.com/zrepl/zrepl/cli"
|
||||
"github.com/zrepl/zrepl/daemon/filters"
|
||||
"github.com/zrepl/zrepl/endpoint"
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
|
||||
var (
|
||||
ZFSAbstractionsCmd = &cli.Subcommand{
|
||||
Use: "zfs-abstraction",
|
||||
Short: "manage abstractions that zrepl builds on top of ZFS",
|
||||
SetupSubcommands: func() []*cli.Subcommand {
|
||||
return []*cli.Subcommand{
|
||||
zabsCmdList,
|
||||
zabsCmdReleaseAll,
|
||||
zabsCmdReleaseStale,
|
||||
zabsCmdCreate,
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// a common set of CLI flags that map to the fields of an
|
||||
// endpoint.ListZFSHoldsAndBookmarksQuery
|
||||
type zabsFilterFlags struct {
|
||||
Filesystems FilesystemsFilterFlag
|
||||
Job JobIDFlag
|
||||
Types AbstractionTypesFlag
|
||||
Concurrency int64
|
||||
}
|
||||
|
||||
// produce a query from the CLI flags
|
||||
func (f zabsFilterFlags) Query() (endpoint.ListZFSHoldsAndBookmarksQuery, error) {
|
||||
q := endpoint.ListZFSHoldsAndBookmarksQuery{
|
||||
FS: f.Filesystems.FlagValue(),
|
||||
What: f.Types.FlagValue(),
|
||||
JobID: f.Job.FlagValue(),
|
||||
Concurrency: f.Concurrency,
|
||||
}
|
||||
return q, q.Validate()
|
||||
}
|
||||
|
||||
func (f *zabsFilterFlags) registerZabsFilterFlags(s *pflag.FlagSet, verb string) {
|
||||
// Note: the default value is defined in the .FlagValue methods
|
||||
s.Var(&f.Filesystems, "fs", fmt.Sprintf("only %s holds on the specified filesystem [default: all filesystems] [comma-separated list of <dataset-pattern>:<ok|!> pairs]", verb))
|
||||
s.Var(&f.Job, "job", fmt.Sprintf("only %s holds created by the specified job [default: any job]", verb))
|
||||
|
||||
variants := make([]string, 0, len(endpoint.AbstractionTypesAll))
|
||||
for v := range endpoint.AbstractionTypesAll {
|
||||
variants = append(variants, string(v))
|
||||
}
|
||||
variants = sort.StringSlice(variants)
|
||||
variantsJoined := strings.Join(variants, "|")
|
||||
s.Var(&f.Types, "type", fmt.Sprintf("only %s holds of the specified type [default: all] [comma-separated list of %s]", verb, variantsJoined))
|
||||
|
||||
s.Int64VarP(&f.Concurrency, "concurrency", "p", 1, "number of concurrently queried filesystems")
|
||||
}
|
||||
|
||||
type JobIDFlag struct{ J *endpoint.JobID }
|
||||
|
||||
func (f *JobIDFlag) Set(s string) error {
|
||||
if len(s) == 0 {
|
||||
*f = JobIDFlag{J: nil}
|
||||
return nil
|
||||
}
|
||||
|
||||
jobID, err := endpoint.MakeJobID(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*f = JobIDFlag{J: &jobID}
|
||||
return nil
|
||||
}
|
||||
func (f JobIDFlag) Type() string { return "job-ID" }
|
||||
func (f JobIDFlag) String() string { return fmt.Sprint(f.J) }
|
||||
func (f JobIDFlag) FlagValue() *endpoint.JobID { return f.J }
|
||||
|
||||
type AbstractionTypesFlag map[endpoint.AbstractionType]bool
|
||||
|
||||
func (f *AbstractionTypesFlag) Set(s string) error {
|
||||
ats, err := endpoint.AbstractionTypeSetFromStrings(strings.Split(s, ","))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*f = AbstractionTypesFlag(ats)
|
||||
return nil
|
||||
}
|
||||
func (f AbstractionTypesFlag) Type() string { return "abstraction-type" }
|
||||
func (f AbstractionTypesFlag) String() string {
|
||||
return endpoint.AbstractionTypeSet(f).String()
|
||||
}
|
||||
func (f AbstractionTypesFlag) FlagValue() map[endpoint.AbstractionType]bool {
|
||||
if len(f) > 0 {
|
||||
return f
|
||||
}
|
||||
return endpoint.AbstractionTypesAll
|
||||
}
|
||||
|
||||
type FilesystemsFilterFlag struct {
|
||||
F endpoint.ListZFSHoldsAndBookmarksQueryFilesystemFilter
|
||||
}
|
||||
|
||||
func (flag *FilesystemsFilterFlag) Set(s string) error {
|
||||
mappings := strings.Split(s, ",")
|
||||
if len(mappings) == 1 && !strings.Contains(mappings[0], ":") {
|
||||
flag.F = endpoint.ListZFSHoldsAndBookmarksQueryFilesystemFilter{
|
||||
FS: &mappings[0],
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
f := filters.NewDatasetMapFilter(len(mappings), true)
|
||||
for _, m := range mappings {
|
||||
thisMappingErr := fmt.Errorf("expecting comma-separated list of <dataset-pattern>:<ok|!> pairs, got %q", m)
|
||||
lhsrhs := strings.SplitN(m, ":", 2)
|
||||
if len(lhsrhs) != 2 {
|
||||
return thisMappingErr
|
||||
}
|
||||
err := f.Add(lhsrhs[0], lhsrhs[1])
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: %s", thisMappingErr, err)
|
||||
}
|
||||
}
|
||||
flag.F = endpoint.ListZFSHoldsAndBookmarksQueryFilesystemFilter{
|
||||
Filter: f,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (flag FilesystemsFilterFlag) Type() string { return "filesystem filter spec" }
|
||||
func (flag FilesystemsFilterFlag) String() string {
|
||||
return fmt.Sprintf("%v", flag.F)
|
||||
}
|
||||
func (flag FilesystemsFilterFlag) FlagValue() endpoint.ListZFSHoldsAndBookmarksQueryFilesystemFilter {
|
||||
var z FilesystemsFilterFlag
|
||||
if flag == z {
|
||||
return endpoint.ListZFSHoldsAndBookmarksQueryFilesystemFilter{Filter: zfs.NoFilter()}
|
||||
}
|
||||
return flag.F
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package client
|
||||
|
||||
import "github.com/zrepl/zrepl/cli"
|
||||
|
||||
var zabsCmdCreate = &cli.Subcommand{
|
||||
Use: "create",
|
||||
NoRequireConfig: true,
|
||||
Short: `create zrepl ZFS abstractions (mostly useful for debugging & development, users should not need to use this command)`,
|
||||
SetupSubcommands: func() []*cli.Subcommand {
|
||||
return []*cli.Subcommand{
|
||||
zabsCmdCreateStepHold,
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
"github.com/zrepl/zrepl/cli"
|
||||
"github.com/zrepl/zrepl/endpoint"
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
|
||||
var zabsCreateStepHoldFlags struct {
|
||||
target string
|
||||
jobid JobIDFlag
|
||||
}
|
||||
|
||||
var zabsCmdCreateStepHold = &cli.Subcommand{
|
||||
Use: "step",
|
||||
Run: doZabsCreateStep,
|
||||
NoRequireConfig: true,
|
||||
Short: `create a step hold or bookmark`,
|
||||
SetupFlags: func(f *pflag.FlagSet) {
|
||||
f.StringVarP(&zabsCreateStepHoldFlags.target, "target", "t", "", "snapshot to be held / bookmark to be held")
|
||||
f.VarP(&zabsCreateStepHoldFlags.jobid, "jobid", "j", "jobid for which the hold is installed")
|
||||
},
|
||||
}
|
||||
|
||||
func doZabsCreateStep(sc *cli.Subcommand, args []string) error {
|
||||
if len(args) > 0 {
|
||||
return errors.New("subcommand takes no arguments")
|
||||
}
|
||||
|
||||
f := &zabsCreateStepHoldFlags
|
||||
|
||||
fs, _, _, err := zfs.DecomposeVersionString(f.target)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "%q invalid target", f.target)
|
||||
}
|
||||
|
||||
if f.jobid.FlagValue() == nil {
|
||||
return errors.Errorf("jobid must be set")
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
v, err := zfs.ZFSGetFilesystemVersion(ctx, f.target)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "get info about target %q", f.target)
|
||||
}
|
||||
|
||||
step, err := endpoint.HoldStep(ctx, fs, v, *f.jobid.FlagValue())
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "create step hold")
|
||||
}
|
||||
fmt.Println(step.String())
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
"github.com/zrepl/zrepl/cli"
|
||||
"github.com/zrepl/zrepl/endpoint"
|
||||
"github.com/zrepl/zrepl/util/chainlock"
|
||||
)
|
||||
|
||||
var zabsListFlags struct {
|
||||
Filter zabsFilterFlags
|
||||
Json bool
|
||||
}
|
||||
|
||||
var zabsCmdList = &cli.Subcommand{
|
||||
Use: "list",
|
||||
Short: `list zrepl ZFS abstractions`,
|
||||
Run: doZabsList,
|
||||
NoRequireConfig: true,
|
||||
SetupFlags: func(f *pflag.FlagSet) {
|
||||
zabsListFlags.Filter.registerZabsFilterFlags(f, "list")
|
||||
f.BoolVar(&zabsListFlags.Json, "json", false, "emit JSON")
|
||||
},
|
||||
}
|
||||
|
||||
func doZabsList(sc *cli.Subcommand, args []string) error {
|
||||
var err error
|
||||
ctx := context.Background()
|
||||
|
||||
if len(args) > 0 {
|
||||
return errors.New("this subcommand takes no positional arguments")
|
||||
}
|
||||
|
||||
q, err := zabsListFlags.Filter.Query()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "invalid filter specification on command line")
|
||||
}
|
||||
|
||||
abstractions, errors, err := endpoint.ListAbstractionsStreamed(ctx, q)
|
||||
if err != nil {
|
||||
return err // context clear by invocation of command
|
||||
}
|
||||
|
||||
var line chainlock.L
|
||||
var wg sync.WaitGroup
|
||||
defer wg.Wait()
|
||||
wg.Add(1)
|
||||
|
||||
// print results
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
enc := json.NewEncoder(os.Stdout)
|
||||
for a := range abstractions {
|
||||
func() {
|
||||
defer line.Lock().Unlock()
|
||||
if zabsListFlags.Json {
|
||||
enc.SetIndent("", " ")
|
||||
if err := enc.Encode(abstractions); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Println()
|
||||
} else {
|
||||
fmt.Println(a)
|
||||
}
|
||||
}()
|
||||
}
|
||||
}()
|
||||
|
||||
// print errors to stderr
|
||||
errorColor := color.New(color.FgRed)
|
||||
var errorsSlice []endpoint.ListAbstractionsError
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for err := range errors {
|
||||
func() {
|
||||
defer line.Lock().Unlock()
|
||||
errorsSlice = append(errorsSlice, err)
|
||||
errorColor.Fprintf(os.Stderr, "%s\n", err)
|
||||
}()
|
||||
}
|
||||
}()
|
||||
wg.Wait()
|
||||
if len(errorsSlice) > 0 {
|
||||
errorColor.Add(color.Bold).Fprintf(os.Stderr, "there were errors in listing the abstractions")
|
||||
return fmt.Errorf("")
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
"github.com/zrepl/zrepl/cli"
|
||||
"github.com/zrepl/zrepl/endpoint"
|
||||
)
|
||||
|
||||
// shared between release-all and release-step
|
||||
var zabsReleaseFlags struct {
|
||||
Filter zabsFilterFlags
|
||||
Json bool
|
||||
DryRun bool
|
||||
}
|
||||
|
||||
func registerZabsReleaseFlags(s *pflag.FlagSet) {
|
||||
zabsReleaseFlags.Filter.registerZabsFilterFlags(s, "release")
|
||||
s.BoolVar(&zabsReleaseFlags.Json, "json", false, "emit json instead of pretty-printed")
|
||||
s.BoolVar(&zabsReleaseFlags.DryRun, "dry-run", false, "do a dry-run")
|
||||
}
|
||||
|
||||
var zabsCmdReleaseAll = &cli.Subcommand{
|
||||
Use: "release-all",
|
||||
Run: doZabsReleaseAll,
|
||||
NoRequireConfig: true,
|
||||
Short: `(DANGEROUS) release ALL zrepl ZFS abstractions (mostly useful for uninstalling zrepl completely or for "de-zrepl-ing" a filesystem)`,
|
||||
SetupFlags: registerZabsReleaseFlags,
|
||||
}
|
||||
|
||||
var zabsCmdReleaseStale = &cli.Subcommand{
|
||||
Use: "release-stale",
|
||||
Run: doZabsReleaseStale,
|
||||
NoRequireConfig: true,
|
||||
Short: `release stale zrepl ZFS abstractions (useful if zrepl has a bug and does not do it by itself)`,
|
||||
SetupFlags: registerZabsReleaseFlags,
|
||||
}
|
||||
|
||||
func doZabsReleaseAll(sc *cli.Subcommand, args []string) error {
|
||||
var err error
|
||||
ctx := context.Background()
|
||||
|
||||
if len(args) > 0 {
|
||||
return errors.New("this subcommand takes no positional arguments")
|
||||
}
|
||||
|
||||
q, err := zabsReleaseFlags.Filter.Query()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "invalid filter specification on command line")
|
||||
}
|
||||
|
||||
abstractions, listErrors, err := endpoint.ListAbstractions(ctx, q)
|
||||
if err != nil {
|
||||
return err // context clear by invocation of command
|
||||
}
|
||||
if len(listErrors) > 0 {
|
||||
color.New(color.FgRed).Fprintf(os.Stderr, "there were errors in listing the abstractions:\n%s\n", listErrors)
|
||||
// proceed anyways with rest of abstractions
|
||||
}
|
||||
|
||||
return doZabsRelease_Common(ctx, abstractions)
|
||||
}
|
||||
|
||||
func doZabsReleaseStale(sc *cli.Subcommand, args []string) error {
|
||||
|
||||
var err error
|
||||
ctx := context.Background()
|
||||
|
||||
if len(args) > 0 {
|
||||
return errors.New("this subcommand takes no positional arguments")
|
||||
}
|
||||
|
||||
q, err := zabsReleaseFlags.Filter.Query()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "invalid filter specification on command line")
|
||||
}
|
||||
|
||||
stalenessInfo, err := endpoint.ListStale(ctx, q)
|
||||
if err != nil {
|
||||
return err // context clear by invocation of command
|
||||
}
|
||||
|
||||
return doZabsRelease_Common(ctx, stalenessInfo.Stale)
|
||||
}
|
||||
|
||||
func doZabsRelease_Common(ctx context.Context, destroy []endpoint.Abstraction) error {
|
||||
|
||||
if zabsReleaseFlags.DryRun {
|
||||
if zabsReleaseFlags.Json {
|
||||
m, err := json.MarshalIndent(destroy, "", " ")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if _, err := os.Stdout.Write(m); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Println()
|
||||
} else {
|
||||
for _, a := range destroy {
|
||||
fmt.Printf("would destroy %s\n", a)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
outcome := endpoint.BatchDestroy(ctx, destroy)
|
||||
hadErr := false
|
||||
|
||||
enc := json.NewEncoder(os.Stdout)
|
||||
enc.SetIndent("", " ")
|
||||
colorErr := color.New(color.FgRed)
|
||||
printfSuccess := color.New(color.FgGreen).FprintfFunc()
|
||||
printfSection := color.New(color.Bold).FprintfFunc()
|
||||
|
||||
for res := range outcome {
|
||||
hadErr = hadErr || res.DestroyErr != nil
|
||||
if zabsReleaseFlags.Json {
|
||||
err := enc.Encode(res)
|
||||
if err != nil {
|
||||
colorErr.Fprintf(os.Stderr, "cannot marshal there were errors in destroying the abstractions")
|
||||
}
|
||||
} else {
|
||||
printfSection(os.Stdout, "destroy %s ...", res.Abstraction)
|
||||
if res.DestroyErr != nil {
|
||||
colorErr.Fprintf(os.Stdout, " failed:\n%s\n", res.DestroyErr)
|
||||
} else {
|
||||
printfSuccess(os.Stdout, " OK\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if hadErr {
|
||||
colorErr.Add(color.Bold).Fprintf(os.Stderr, "there were errors in destroying the abstractions")
|
||||
return fmt.Errorf("")
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
+38
-7
@@ -75,16 +75,42 @@ type SnapJob struct {
|
||||
Filesystems FilesystemsFilter `yaml:"filesystems"`
|
||||
}
|
||||
|
||||
type SendOptions struct {
|
||||
Encrypted bool `yaml:"encrypted"`
|
||||
}
|
||||
|
||||
var _ yaml.Defaulter = (*SendOptions)(nil)
|
||||
|
||||
func (l *SendOptions) SetDefault() {
|
||||
*l = SendOptions{Encrypted: false}
|
||||
}
|
||||
|
||||
type RecvOptions struct {
|
||||
// Note: we cannot enforce encrypted recv as the ZFS cli doesn't provide a mechanism for it
|
||||
// Encrypted bool `yaml:"may_encrypted"`
|
||||
|
||||
// Future:
|
||||
// Reencrypt bool `yaml:"reencrypt"`
|
||||
}
|
||||
|
||||
var _ yaml.Defaulter = (*RecvOptions)(nil)
|
||||
|
||||
func (l *RecvOptions) SetDefault() {
|
||||
*l = RecvOptions{}
|
||||
}
|
||||
|
||||
type PushJob struct {
|
||||
ActiveJob `yaml:",inline"`
|
||||
Snapshotting SnapshottingEnum `yaml:"snapshotting"`
|
||||
Filesystems FilesystemsFilter `yaml:"filesystems"`
|
||||
Send *SendOptions `yaml:"send,fromdefaults,optional"`
|
||||
}
|
||||
|
||||
type PullJob struct {
|
||||
ActiveJob `yaml:",inline"`
|
||||
RootFS string `yaml:"root_fs"`
|
||||
Interval PositiveDurationOrManual `yaml:"interval"`
|
||||
Recv *RecvOptions `yaml:"recv,fromdefaults,optional"`
|
||||
}
|
||||
|
||||
type PositiveDurationOrManual struct {
|
||||
@@ -120,13 +146,15 @@ func (i *PositiveDurationOrManual) UnmarshalYAML(u func(interface{}, bool) error
|
||||
|
||||
type SinkJob struct {
|
||||
PassiveJob `yaml:",inline"`
|
||||
RootFS string `yaml:"root_fs"`
|
||||
RootFS string `yaml:"root_fs"`
|
||||
Recv *RecvOptions `yaml:"recv,optional,fromdefaults"`
|
||||
}
|
||||
|
||||
type SourceJob struct {
|
||||
PassiveJob `yaml:",inline"`
|
||||
Snapshotting SnapshottingEnum `yaml:"snapshotting"`
|
||||
Filesystems FilesystemsFilter `yaml:"filesystems"`
|
||||
Send *SendOptions `yaml:"send,optional,fromdefaults"`
|
||||
}
|
||||
|
||||
type FilesystemsFilter map[string]bool
|
||||
@@ -245,14 +273,16 @@ type ServeCommon struct {
|
||||
}
|
||||
|
||||
type TCPServe struct {
|
||||
ServeCommon `yaml:",inline"`
|
||||
Listen string `yaml:"listen,hostport"`
|
||||
Clients map[string]string `yaml:"clients"`
|
||||
ServeCommon `yaml:",inline"`
|
||||
Listen string `yaml:"listen,hostport"`
|
||||
ListenFreeBind bool `yaml:"listen_freebind,default=false"`
|
||||
Clients map[string]string `yaml:"clients"`
|
||||
}
|
||||
|
||||
type TLSServe struct {
|
||||
ServeCommon `yaml:",inline"`
|
||||
Listen string `yaml:"listen,hostport"`
|
||||
ListenFreeBind bool `yaml:"listen_freebind,default=false"`
|
||||
Ca string `yaml:"ca"`
|
||||
Cert string `yaml:"cert"`
|
||||
Key string `yaml:"key"`
|
||||
@@ -331,8 +361,9 @@ type MonitoringEnum struct {
|
||||
}
|
||||
|
||||
type PrometheusMonitoring struct {
|
||||
Type string `yaml:"type"`
|
||||
Listen string `yaml:"listen,hostport"`
|
||||
Type string `yaml:"type"`
|
||||
Listen string `yaml:"listen,hostport"`
|
||||
ListenFreeBind bool `yaml:"listen_freebind,default=false"`
|
||||
}
|
||||
|
||||
type SyslogFacility syslog.Priority
|
||||
@@ -589,7 +620,7 @@ func ParseConfigBytes(bytes []byte) (*Config, error) {
|
||||
|
||||
var durationStringRegex *regexp.Regexp = regexp.MustCompile(`^\s*(\d+)\s*(s|m|h|d|w)\s*$`)
|
||||
|
||||
func parsePostitiveDuration(e string) (d time.Duration, err error) {
|
||||
func parsePositiveDuration(e string) (d time.Duration, err error) {
|
||||
comps := durationStringRegex.FindStringSubmatch(e)
|
||||
if len(comps) != 3 {
|
||||
err = fmt.Errorf("does not match regex: %s %#v", e, comps)
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
clients: {
|
||||
"10.0.0.1":"foo"
|
||||
}
|
||||
root_fs: zoot/foo
|
||||
root_fs: zroot/foo
|
||||
`
|
||||
_, err := ParseConfigBytes([]byte(jobdef))
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
package config
|
||||
@@ -0,0 +1,74 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestSendOptions(t *testing.T) {
|
||||
tmpl := `
|
||||
jobs:
|
||||
- name: foo
|
||||
type: push
|
||||
connect:
|
||||
type: local
|
||||
listener_name: foo
|
||||
client_identity: bar
|
||||
filesystems: {"<": true}
|
||||
%s
|
||||
snapshotting:
|
||||
type: manual
|
||||
pruning:
|
||||
keep_sender:
|
||||
- type: last_n
|
||||
count: 10
|
||||
keep_receiver:
|
||||
- type: last_n
|
||||
count: 10
|
||||
`
|
||||
encrypted_false := `
|
||||
send:
|
||||
encrypted: false
|
||||
`
|
||||
|
||||
encrypted_true := `
|
||||
send:
|
||||
encrypted: true
|
||||
`
|
||||
|
||||
encrypted_unspecified := `
|
||||
send: {}
|
||||
`
|
||||
|
||||
send_not_specified := `
|
||||
`
|
||||
|
||||
fill := func(s string) string { return fmt.Sprintf(tmpl, s) }
|
||||
var c *Config
|
||||
|
||||
t.Run("encrypted_false", func(t *testing.T) {
|
||||
c = testValidConfig(t, fill(encrypted_false))
|
||||
encrypted := c.Jobs[0].Ret.(*PushJob).Send.Encrypted
|
||||
assert.Equal(t, false, encrypted)
|
||||
})
|
||||
t.Run("encrypted_true", func(t *testing.T) {
|
||||
c = testValidConfig(t, fill(encrypted_true))
|
||||
encrypted := c.Jobs[0].Ret.(*PushJob).Send.Encrypted
|
||||
assert.Equal(t, true, encrypted)
|
||||
})
|
||||
|
||||
t.Run("encrypted_unspecified", func(t *testing.T) {
|
||||
c, err := testConfig(t, fill(encrypted_unspecified))
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, c)
|
||||
})
|
||||
|
||||
t.Run("send_not_specified", func(t *testing.T) {
|
||||
c, err := testConfig(t, fill(send_not_specified))
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, c)
|
||||
})
|
||||
|
||||
}
|
||||
@@ -42,7 +42,7 @@ func TestSampleConfigsAreParsedWithoutErrors(t *testing.T) {
|
||||
|
||||
}
|
||||
|
||||
// template must be a template/text template with a single '{{ . }}' as placehodler for val
|
||||
// template must be a template/text template with a single '{{ . }}' as placeholder for val
|
||||
//nolint[:deadcode,unused]
|
||||
func testValidConfigTemplate(t *testing.T, tmpl string, val string) *Config {
|
||||
tmp, err := template.New("master").Parse(tmpl)
|
||||
|
||||
@@ -64,7 +64,7 @@ func parseRetentionGridIntervalString(e string) (intervals []RetentionInterval,
|
||||
return nil, fmt.Errorf("contains factor <= 0")
|
||||
}
|
||||
|
||||
duration, err := parsePostitiveDuration(comps[2])
|
||||
duration, err := parsePositiveDuration(comps[2])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ jobs:
|
||||
address: "backup-server.foo.bar:8888"
|
||||
snapshotting:
|
||||
type: manual
|
||||
send:
|
||||
encrypted: false
|
||||
pruning:
|
||||
keep_sender:
|
||||
- type: not_replicated
|
||||
|
||||
+9
-3
@@ -15,10 +15,12 @@ import (
|
||||
|
||||
"github.com/zrepl/zrepl/daemon/job"
|
||||
"github.com/zrepl/zrepl/daemon/nethelpers"
|
||||
"github.com/zrepl/zrepl/endpoint"
|
||||
"github.com/zrepl/zrepl/logger"
|
||||
"github.com/zrepl/zrepl/util/envconst"
|
||||
"github.com/zrepl/zrepl/version"
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
"github.com/zrepl/zrepl/zfs/zfscmd"
|
||||
)
|
||||
|
||||
type controlJob struct {
|
||||
@@ -44,6 +46,8 @@ func (j *controlJob) Status() *job.Status { return &job.Status{Type: job.TypeInt
|
||||
|
||||
func (j *controlJob) OwnedDatasetSubtreeRoot() (p *zfs.DatasetPath, ok bool) { return nil, false }
|
||||
|
||||
func (j *controlJob) SenderConfig() *endpoint.SenderConfig { return nil }
|
||||
|
||||
var promControl struct {
|
||||
requestBegin *prometheus.CounterVec
|
||||
requestFinished *prometheus.HistogramVec
|
||||
@@ -61,7 +65,7 @@ func (j *controlJob) RegisterMetrics(registerer prometheus.Registerer) {
|
||||
Namespace: "zrepl",
|
||||
Subsystem: "control",
|
||||
Name: "request_finished",
|
||||
Help: "time it took a request to finih",
|
||||
Help: "time it took a request to finish",
|
||||
Buckets: []float64{1e-6, 10e-6, 100e-6, 500e-6, 1e-3, 10e-3, 100e-3, 200e-3, 400e-3, 800e-3, 1, 10, 20},
|
||||
}, []string{"endpoint"})
|
||||
registerer.MustRegister(promControl.requestBegin)
|
||||
@@ -114,7 +118,9 @@ func (j *controlJob) Run(ctx context.Context) {
|
||||
mux.Handle(ControlJobEndpointStatus,
|
||||
// don't log requests to status endpoint, too spammy
|
||||
jsonResponder{log, func() (interface{}, error) {
|
||||
s := j.jobs.status()
|
||||
jobs := j.jobs.status()
|
||||
globalZFS := zfscmd.GetReport()
|
||||
s := Status{Jobs: jobs, Global: GlobalStatus{ZFSCmds: globalZFS}}
|
||||
return s, nil
|
||||
}})
|
||||
|
||||
@@ -247,7 +253,7 @@ func (j jsonRequestResponder) ServeHTTP(w http.ResponseWriter, r *http.Request)
|
||||
var buf bytes.Buffer
|
||||
encodeErr := json.NewEncoder(&buf).Encode(res)
|
||||
if encodeErr != nil {
|
||||
j.log.WithError(producerErr).Error("control handler json marhsal error")
|
||||
j.log.WithError(producerErr).Error("control handler json marshal error")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
_, err := io.WriteString(w, encodeErr.Error())
|
||||
logIoErr(err)
|
||||
|
||||
+15
-1
@@ -20,6 +20,7 @@ import (
|
||||
"github.com/zrepl/zrepl/daemon/logging"
|
||||
"github.com/zrepl/zrepl/logger"
|
||||
"github.com/zrepl/zrepl/version"
|
||||
"github.com/zrepl/zrepl/zfs/zfscmd"
|
||||
)
|
||||
|
||||
func Run(conf *config.Config) error {
|
||||
@@ -76,11 +77,14 @@ func Run(conf *config.Config) error {
|
||||
return errors.Errorf("unknown monitoring job #%d (type %T)", i, v)
|
||||
}
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "cannot build monitorin gjob #%d", i)
|
||||
return errors.Wrapf(err, "cannot build monitoring job #%d", i)
|
||||
}
|
||||
jobs.start(ctx, job, true)
|
||||
}
|
||||
|
||||
// register global (=non job-local) metrics
|
||||
zfscmd.RegisterMetrics(prometheus.DefaultRegisterer)
|
||||
|
||||
log.Info("starting daemon")
|
||||
|
||||
// start regular jobs
|
||||
@@ -128,6 +132,15 @@ func (s *jobs) wait() <-chan struct{} {
|
||||
return ch
|
||||
}
|
||||
|
||||
type Status struct {
|
||||
Jobs map[string]*job.Status
|
||||
Global GlobalStatus
|
||||
}
|
||||
|
||||
type GlobalStatus struct {
|
||||
ZFSCmds *zfscmd.Report
|
||||
}
|
||||
|
||||
func (s *jobs) status() map[string]*job.Status {
|
||||
s.m.RLock()
|
||||
defer s.m.RUnlock()
|
||||
@@ -207,6 +220,7 @@ func (s *jobs) start(ctx context.Context, j job.Job, internal bool) {
|
||||
|
||||
s.jobs[jobName] = j
|
||||
ctx = job.WithLogger(ctx, jobLog)
|
||||
ctx = zfscmd.WithJobID(ctx, j.Name())
|
||||
ctx, wakeup := wakeup.Context(ctx)
|
||||
ctx, resetFunc := reset.Context(ctx)
|
||||
s.wakeups[jobName] = wakeup
|
||||
|
||||
@@ -161,7 +161,7 @@ func (m DatasetMapFilter) Filter(p *zfs.DatasetPath) (pass bool, err error) {
|
||||
}
|
||||
|
||||
// Construct a new filter-only DatasetMapFilter from a mapping
|
||||
// The new filter allows excactly those paths that were not forbidden by the mapping.
|
||||
// The new filter allows exactly those paths that were not forbidden by the mapping.
|
||||
func (m DatasetMapFilter) InvertedFilter() (inv *DatasetMapFilter, err error) {
|
||||
|
||||
if m.filterMode {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
package filters
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestDatasetMapFilter(t *testing.T) {
|
||||
|
||||
type testCase struct {
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
package filters
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
|
||||
type AnyFSVFilter struct{}
|
||||
|
||||
func NewAnyFSVFilter() AnyFSVFilter {
|
||||
return AnyFSVFilter{}
|
||||
}
|
||||
|
||||
var _ zfs.FilesystemVersionFilter = AnyFSVFilter{}
|
||||
|
||||
func (AnyFSVFilter) Filter(t zfs.VersionType, name string) (accept bool, err error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
type PrefixFilter struct {
|
||||
prefix string
|
||||
fstype zfs.VersionType
|
||||
fstypeSet bool // optionals anyone?
|
||||
}
|
||||
|
||||
var _ zfs.FilesystemVersionFilter = &PrefixFilter{}
|
||||
|
||||
func NewPrefixFilter(prefix string) *PrefixFilter {
|
||||
return &PrefixFilter{prefix: prefix}
|
||||
}
|
||||
|
||||
func NewTypedPrefixFilter(prefix string, versionType zfs.VersionType) *PrefixFilter {
|
||||
return &PrefixFilter{prefix, versionType, true}
|
||||
}
|
||||
|
||||
func (f *PrefixFilter) Filter(t zfs.VersionType, name string) (accept bool, err error) {
|
||||
fstypeMatches := (!f.fstypeSet || t == f.fstype)
|
||||
prefixMatches := strings.HasPrefix(name, f.prefix)
|
||||
return fstypeMatches && prefixMatches, nil
|
||||
}
|
||||
+82
-25
@@ -2,6 +2,7 @@ package job
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -28,7 +29,7 @@ import (
|
||||
|
||||
type ActiveSide struct {
|
||||
mode activeMode
|
||||
name string
|
||||
name endpoint.JobID
|
||||
connecter transport.Connecter
|
||||
|
||||
prunerFactory *pruner.PrunerFactory
|
||||
@@ -82,17 +83,19 @@ type activeMode interface {
|
||||
DisconnectEndpoints()
|
||||
SenderReceiver() (logic.Sender, logic.Receiver)
|
||||
Type() Type
|
||||
PlannerPolicy() logic.PlannerPolicy
|
||||
RunPeriodic(ctx context.Context, wakeUpCommon chan<- struct{})
|
||||
SnapperReport() *snapper.Report
|
||||
ResetConnectBackoff()
|
||||
}
|
||||
|
||||
type modePush struct {
|
||||
setupMtx sync.Mutex
|
||||
sender *endpoint.Sender
|
||||
receiver *rpc.Client
|
||||
fsfilter endpoint.FSFilter
|
||||
snapper *snapper.PeriodicOrManual
|
||||
setupMtx sync.Mutex
|
||||
sender *endpoint.Sender
|
||||
receiver *rpc.Client
|
||||
senderConfig *endpoint.SenderConfig
|
||||
plannerPolicy *logic.PlannerPolicy
|
||||
snapper *snapper.PeriodicOrManual
|
||||
}
|
||||
|
||||
func (m *modePush) ConnectEndpoints(loggers rpc.Loggers, connecter transport.Connecter) {
|
||||
@@ -101,7 +104,7 @@ func (m *modePush) ConnectEndpoints(loggers rpc.Loggers, connecter transport.Con
|
||||
if m.receiver != nil || m.sender != nil {
|
||||
panic("inconsistent use of ConnectEndpoints and DisconnectEndpoints")
|
||||
}
|
||||
m.sender = endpoint.NewSender(m.fsfilter)
|
||||
m.sender = endpoint.NewSender(*m.senderConfig)
|
||||
m.receiver = rpc.NewClient(connecter, loggers)
|
||||
}
|
||||
|
||||
@@ -121,6 +124,8 @@ func (m *modePush) SenderReceiver() (logic.Sender, logic.Receiver) {
|
||||
|
||||
func (m *modePush) Type() Type { return TypePush }
|
||||
|
||||
func (m *modePush) PlannerPolicy() logic.PlannerPolicy { return *m.plannerPolicy }
|
||||
|
||||
func (m *modePush) RunPeriodic(ctx context.Context, wakeUpCommon chan<- struct{}) {
|
||||
m.snapper.Run(ctx, wakeUpCommon)
|
||||
}
|
||||
@@ -137,13 +142,22 @@ func (m *modePush) ResetConnectBackoff() {
|
||||
}
|
||||
}
|
||||
|
||||
func modePushFromConfig(g *config.Global, in *config.PushJob) (*modePush, error) {
|
||||
func modePushFromConfig(g *config.Global, in *config.PushJob, jobID endpoint.JobID) (*modePush, error) {
|
||||
m := &modePush{}
|
||||
|
||||
fsf, err := filters.DatasetMapFilterFromConfig(in.Filesystems)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "cannnot build filesystem filter")
|
||||
return nil, errors.Wrap(err, "cannot build filesystem filter")
|
||||
}
|
||||
|
||||
m.senderConfig = &endpoint.SenderConfig{
|
||||
FSF: fsf,
|
||||
Encrypt: &zfs.NilBool{B: in.Send.Encrypted},
|
||||
JobID: jobID,
|
||||
}
|
||||
m.plannerPolicy = &logic.PlannerPolicy{
|
||||
EncryptedSend: logic.TriFromBool(in.Send.Encrypted),
|
||||
}
|
||||
m.fsfilter = fsf
|
||||
|
||||
if m.snapper, err = snapper.FromConfig(g, fsf, in.Snapshotting); err != nil {
|
||||
return nil, errors.Wrap(err, "cannot build snapper")
|
||||
@@ -153,11 +167,13 @@ func modePushFromConfig(g *config.Global, in *config.PushJob) (*modePush, error)
|
||||
}
|
||||
|
||||
type modePull struct {
|
||||
setupMtx sync.Mutex
|
||||
receiver *endpoint.Receiver
|
||||
sender *rpc.Client
|
||||
rootFS *zfs.DatasetPath
|
||||
interval config.PositiveDurationOrManual
|
||||
setupMtx sync.Mutex
|
||||
receiver *endpoint.Receiver
|
||||
receiverConfig endpoint.ReceiverConfig
|
||||
sender *rpc.Client
|
||||
rootFS *zfs.DatasetPath
|
||||
plannerPolicy *logic.PlannerPolicy
|
||||
interval config.PositiveDurationOrManual
|
||||
}
|
||||
|
||||
func (m *modePull) ConnectEndpoints(loggers rpc.Loggers, connecter transport.Connecter) {
|
||||
@@ -166,7 +182,7 @@ func (m *modePull) ConnectEndpoints(loggers rpc.Loggers, connecter transport.Con
|
||||
if m.receiver != nil || m.sender != nil {
|
||||
panic("inconsistent use of ConnectEndpoints and DisconnectEndpoints")
|
||||
}
|
||||
m.receiver = endpoint.NewReceiver(m.rootFS, false)
|
||||
m.receiver = endpoint.NewReceiver(m.receiverConfig)
|
||||
m.sender = rpc.NewClient(connecter, loggers)
|
||||
}
|
||||
|
||||
@@ -186,6 +202,8 @@ func (m *modePull) SenderReceiver() (logic.Sender, logic.Receiver) {
|
||||
|
||||
func (*modePull) Type() Type { return TypePull }
|
||||
|
||||
func (m *modePull) PlannerPolicy() logic.PlannerPolicy { return *m.plannerPolicy }
|
||||
|
||||
func (m *modePull) RunPeriodic(ctx context.Context, wakeUpCommon chan<- struct{}) {
|
||||
if m.interval.Manual {
|
||||
GetLogger(ctx).Info("manual pull configured, periodic pull disabled")
|
||||
@@ -223,7 +241,7 @@ func (m *modePull) ResetConnectBackoff() {
|
||||
}
|
||||
}
|
||||
|
||||
func modePullFromConfig(g *config.Global, in *config.PullJob) (m *modePull, err error) {
|
||||
func modePullFromConfig(g *config.Global, in *config.PullJob, jobID endpoint.JobID) (m *modePull, err error) {
|
||||
m = &modePull{}
|
||||
m.interval = in.Interval
|
||||
|
||||
@@ -235,26 +253,56 @@ func modePullFromConfig(g *config.Global, in *config.PullJob) (m *modePull, err
|
||||
return nil, errors.New("RootFS must not be empty") // duplicates error check of receiver
|
||||
}
|
||||
|
||||
m.plannerPolicy = &logic.PlannerPolicy{
|
||||
EncryptedSend: logic.DontCare,
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func activeSide(g *config.Global, in *config.ActiveJob, mode activeMode) (j *ActiveSide, err error) {
|
||||
func activeSide(g *config.Global, in *config.ActiveJob, configJob interface{}) (j *ActiveSide, err error) {
|
||||
|
||||
j = &ActiveSide{}
|
||||
j.name, err = endpoint.MakeJobID(in.Name)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "invalid job name")
|
||||
}
|
||||
|
||||
switch v := configJob.(type) {
|
||||
case *config.PushJob:
|
||||
j.mode, err = modePushFromConfig(g, v, j.name) // shadow
|
||||
case *config.PullJob:
|
||||
j.mode, err = modePullFromConfig(g, v, j.name) // shadow
|
||||
default:
|
||||
panic(fmt.Sprintf("implementation error: unknown job type %T", v))
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err // no wrapping required
|
||||
}
|
||||
|
||||
j = &ActiveSide{mode: mode}
|
||||
j.name = in.Name
|
||||
j.promRepStateSecs = prometheus.NewHistogramVec(prometheus.HistogramOpts{
|
||||
Namespace: "zrepl",
|
||||
Subsystem: "replication",
|
||||
Name: "state_time",
|
||||
Help: "seconds spent during replication",
|
||||
ConstLabels: prometheus.Labels{"zrepl_job": j.name},
|
||||
ConstLabels: prometheus.Labels{"zrepl_job": j.name.String()},
|
||||
}, []string{"state"})
|
||||
j.promBytesReplicated = prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: "zrepl",
|
||||
Subsystem: "replication",
|
||||
Name: "bytes_replicated",
|
||||
Help: "number of bytes replicated from sender to receiver per filesystem",
|
||||
ConstLabels: prometheus.Labels{"zrepl_job": j.name},
|
||||
ConstLabels: prometheus.Labels{"zrepl_job": j.name.String()},
|
||||
}, []string{"filesystem"})
|
||||
|
||||
j.connecter, err = fromconfig.ConnecterFromConfig(g, in.Connect)
|
||||
@@ -267,7 +315,7 @@ func activeSide(g *config.Global, in *config.ActiveJob, mode activeMode) (j *Act
|
||||
Subsystem: "pruning",
|
||||
Name: "time",
|
||||
Help: "seconds spent in pruner",
|
||||
ConstLabels: prometheus.Labels{"zrepl_job": j.name},
|
||||
ConstLabels: prometheus.Labels{"zrepl_job": j.name.String()},
|
||||
}, []string{"prune_side"})
|
||||
j.prunerFactory, err = pruner.NewPrunerFactory(in.Pruning, j.promPruneSecs)
|
||||
if err != nil {
|
||||
@@ -283,7 +331,7 @@ func (j *ActiveSide) RegisterMetrics(registerer prometheus.Registerer) {
|
||||
registerer.MustRegister(j.promBytesReplicated)
|
||||
}
|
||||
|
||||
func (j *ActiveSide) Name() string { return j.name }
|
||||
func (j *ActiveSide) Name() string { return j.name.String() }
|
||||
|
||||
type ActiveSideStatus struct {
|
||||
Replication *report.Report
|
||||
@@ -318,6 +366,15 @@ func (j *ActiveSide) OwnedDatasetSubtreeRoot() (rfs *zfs.DatasetPath, ok bool) {
|
||||
return pull.rootFS.Copy(), true
|
||||
}
|
||||
|
||||
func (j *ActiveSide) SenderConfig() *endpoint.SenderConfig {
|
||||
push, ok := j.mode.(*modePush)
|
||||
if !ok {
|
||||
_ = j.mode.(*modePull) // make sure we didn't introduce a new job type
|
||||
return nil
|
||||
}
|
||||
return push.senderConfig
|
||||
}
|
||||
|
||||
func (j *ActiveSide) Run(ctx context.Context) {
|
||||
log := GetLogger(ctx)
|
||||
ctx = logging.WithSubsystemLoggers(ctx, log)
|
||||
@@ -383,7 +440,7 @@ func (j *ActiveSide) do(ctx context.Context) {
|
||||
*tasks = activeSideTasks{}
|
||||
tasks.replicationCancel = repCancel
|
||||
tasks.replicationReport, repWait = replication.Do(
|
||||
ctx, logic.NewPlanner(j.promRepStateSecs, j.promBytesReplicated, sender, receiver),
|
||||
ctx, logic.NewPlanner(j.promRepStateSecs, j.promBytesReplicated, sender, receiver, j.mode.PlannerPolicy()),
|
||||
)
|
||||
tasks.state = ActiveSideReplicating
|
||||
})
|
||||
|
||||
@@ -48,20 +48,12 @@ func buildJob(c *config.Global, in config.JobEnum) (j Job, err error) {
|
||||
// FIXME prettify this
|
||||
switch v := in.Ret.(type) {
|
||||
case *config.SinkJob:
|
||||
m, err := modeSinkFromConfig(c, v)
|
||||
if err != nil {
|
||||
return cannotBuildJob(err, v.Name)
|
||||
}
|
||||
j, err = passiveSideFromConfig(c, &v.PassiveJob, m)
|
||||
j, err = passiveSideFromConfig(c, &v.PassiveJob, v)
|
||||
if err != nil {
|
||||
return cannotBuildJob(err, v.Name)
|
||||
}
|
||||
case *config.SourceJob:
|
||||
m, err := modeSourceFromConfig(c, v)
|
||||
if err != nil {
|
||||
return cannotBuildJob(err, v.Name)
|
||||
}
|
||||
j, err = passiveSideFromConfig(c, &v.PassiveJob, m)
|
||||
j, err = passiveSideFromConfig(c, &v.PassiveJob, v)
|
||||
if err != nil {
|
||||
return cannotBuildJob(err, v.Name)
|
||||
}
|
||||
@@ -71,20 +63,12 @@ func buildJob(c *config.Global, in config.JobEnum) (j Job, err error) {
|
||||
return cannotBuildJob(err, v.Name)
|
||||
}
|
||||
case *config.PushJob:
|
||||
m, err := modePushFromConfig(c, v)
|
||||
if err != nil {
|
||||
return cannotBuildJob(err, v.Name)
|
||||
}
|
||||
j, err = activeSide(c, &v.ActiveJob, m)
|
||||
j, err = activeSide(c, &v.ActiveJob, v)
|
||||
if err != nil {
|
||||
return cannotBuildJob(err, v.Name)
|
||||
}
|
||||
case *config.PullJob:
|
||||
m, err := modePullFromConfig(c, v)
|
||||
if err != nil {
|
||||
return cannotBuildJob(err, v.Name)
|
||||
}
|
||||
j, err = activeSide(c, &v.ActiveJob, m)
|
||||
j, err = activeSide(c, &v.ActiveJob, v)
|
||||
if err != nil {
|
||||
return cannotBuildJob(err, v.Name)
|
||||
}
|
||||
@@ -104,6 +88,11 @@ func validateReceivingSidesDoNotOverlap(receivingRootFSs []string) error {
|
||||
sort.Slice(rfss, func(i, j int) bool {
|
||||
return strings.Compare(rfss[i], rfss[j]) == -1
|
||||
})
|
||||
// add tailing slash because of hierarchy-simulation
|
||||
// rootfs/ is not root of rootfs2/
|
||||
for i := 0; i < len(rfss); i++ {
|
||||
rfss[i] += "/"
|
||||
}
|
||||
// idea:
|
||||
// no path in rfss must be prefix of another
|
||||
//
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
package job
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/zrepl/zrepl/config"
|
||||
)
|
||||
|
||||
func TestValidateReceivingSidesDoNotOverlap(t *testing.T) {
|
||||
@@ -18,6 +22,7 @@ func TestValidateReceivingSidesDoNotOverlap(t *testing.T) {
|
||||
{false, []string{"a"}},
|
||||
{false, []string{"some/path"}},
|
||||
{false, []string{"zroot/sink1", "zroot/sink2", "zroot/sink3"}},
|
||||
{false, []string{"zroot/foo", "zroot/foobar"}},
|
||||
{true, []string{"zroot/b", "zroot/b"}},
|
||||
{true, []string{"zroot/foo", "zroot/foo/bar", "zroot/baz"}},
|
||||
{false, []string{"a/x", "b/x"}},
|
||||
@@ -40,3 +45,66 @@ func TestValidateReceivingSidesDoNotOverlap(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestJobIDErrorHandling(t *testing.T) {
|
||||
tmpl := `
|
||||
jobs:
|
||||
- name: %s
|
||||
type: push
|
||||
connect:
|
||||
type: local
|
||||
listener_name: foo
|
||||
client_identity: bar
|
||||
filesystems: {"<": true}
|
||||
snapshotting:
|
||||
type: manual
|
||||
pruning:
|
||||
keep_sender:
|
||||
- type: last_n
|
||||
count: 10
|
||||
keep_receiver:
|
||||
- type: last_n
|
||||
count: 10
|
||||
`
|
||||
fill := func(s string) string { return fmt.Sprintf(tmpl, s) }
|
||||
|
||||
type Case struct {
|
||||
jobName string
|
||||
valid bool
|
||||
}
|
||||
cases := []Case{
|
||||
{"validjobname", true},
|
||||
{"valid with spaces", true},
|
||||
{"invalid\twith\ttabs", false},
|
||||
{"invalid#withdelimiter", false},
|
||||
{"invalid@withdelimiter", false},
|
||||
{"withnewline\\nmiddle", false},
|
||||
{"withnewline\\n", false},
|
||||
{"withslash/", false},
|
||||
{"withslash/inthemiddle", false},
|
||||
{"/", false},
|
||||
}
|
||||
|
||||
for i := range cases {
|
||||
t.Run(cases[i].jobName, func(t *testing.T) {
|
||||
c := cases[i]
|
||||
|
||||
conf, err := config.ParseConfigBytes([]byte(fill(c.jobName)))
|
||||
require.NoError(t, err, "not expecting yaml-config to know about job ids")
|
||||
require.NotNil(t, conf)
|
||||
jobs, err := JobsFromConfig(conf)
|
||||
|
||||
if c.valid {
|
||||
assert.NoError(t, err)
|
||||
require.Len(t, jobs, 1)
|
||||
assert.Equal(t, c.jobName, jobs[0].Name())
|
||||
} else {
|
||||
t.Logf("error: %s", err)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, jobs)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
|
||||
"github.com/zrepl/zrepl/endpoint"
|
||||
"github.com/zrepl/zrepl/logger"
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
@@ -38,6 +39,7 @@ type Job interface {
|
||||
// Jobs that return a subtree of the dataset hierarchy
|
||||
// must return the root of that subtree as rfs and ok = true
|
||||
OwnedDatasetSubtreeRoot() (rfs *zfs.DatasetPath, ok bool)
|
||||
SenderConfig() *endpoint.SenderConfig
|
||||
}
|
||||
|
||||
type Type string
|
||||
|
||||
+55
-17
@@ -20,7 +20,7 @@ import (
|
||||
|
||||
type PassiveSide struct {
|
||||
mode passiveMode
|
||||
name string
|
||||
name endpoint.JobID
|
||||
listen transport.AuthenticatedListenerFactory
|
||||
}
|
||||
|
||||
@@ -32,43 +32,56 @@ type passiveMode interface {
|
||||
}
|
||||
|
||||
type modeSink struct {
|
||||
rootDataset *zfs.DatasetPath
|
||||
receiverConfig endpoint.ReceiverConfig
|
||||
}
|
||||
|
||||
func (m *modeSink) Type() Type { return TypeSink }
|
||||
|
||||
func (m *modeSink) Handler() rpc.Handler {
|
||||
return endpoint.NewReceiver(m.rootDataset, true)
|
||||
return endpoint.NewReceiver(m.receiverConfig)
|
||||
}
|
||||
|
||||
func (m *modeSink) RunPeriodic(_ context.Context) {}
|
||||
func (m *modeSink) SnapperReport() *snapper.Report { return nil }
|
||||
|
||||
func modeSinkFromConfig(g *config.Global, in *config.SinkJob) (m *modeSink, err error) {
|
||||
func modeSinkFromConfig(g *config.Global, in *config.SinkJob, jobID endpoint.JobID) (m *modeSink, err error) {
|
||||
m = &modeSink{}
|
||||
m.rootDataset, err = zfs.NewDatasetPath(in.RootFS)
|
||||
|
||||
rootDataset, err := zfs.NewDatasetPath(in.RootFS)
|
||||
if err != nil {
|
||||
return nil, errors.New("root dataset is not a valid zfs filesystem path")
|
||||
}
|
||||
if m.rootDataset.Length() <= 0 {
|
||||
return nil, errors.New("root dataset must not be empty") // duplicates error check of receiver
|
||||
|
||||
m.receiverConfig = endpoint.ReceiverConfig{
|
||||
JobID: jobID,
|
||||
RootWithoutClientComponent: rootDataset,
|
||||
AppendClientIdentity: true, // !
|
||||
UpdateLastReceivedHold: true,
|
||||
}
|
||||
if err := m.receiverConfig.Validate(); err != nil {
|
||||
return nil, errors.Wrap(err, "cannot build receiver config")
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
type modeSource struct {
|
||||
fsfilter zfs.DatasetFilter
|
||||
snapper *snapper.PeriodicOrManual
|
||||
senderConfig *endpoint.SenderConfig
|
||||
snapper *snapper.PeriodicOrManual
|
||||
}
|
||||
|
||||
func modeSourceFromConfig(g *config.Global, in *config.SourceJob) (m *modeSource, err error) {
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "cannnot build filesystem filter")
|
||||
return nil, errors.Wrap(err, "cannot build filesystem filter")
|
||||
}
|
||||
m.senderConfig = &endpoint.SenderConfig{
|
||||
FSF: fsf,
|
||||
Encrypt: &zfs.NilBool{B: in.Send.Encrypted},
|
||||
JobID: jobID,
|
||||
}
|
||||
m.fsfilter = fsf
|
||||
|
||||
if m.snapper, err = snapper.FromConfig(g, fsf, in.Snapshotting); err != nil {
|
||||
return nil, errors.Wrap(err, "cannot build snapper")
|
||||
@@ -80,7 +93,7 @@ func modeSourceFromConfig(g *config.Global, in *config.SourceJob) (m *modeSource
|
||||
func (m *modeSource) Type() Type { return TypeSource }
|
||||
|
||||
func (m *modeSource) Handler() rpc.Handler {
|
||||
return endpoint.NewSender(m.fsfilter)
|
||||
return endpoint.NewSender(*m.senderConfig)
|
||||
}
|
||||
|
||||
func (m *modeSource) RunPeriodic(ctx context.Context) {
|
||||
@@ -91,9 +104,25 @@ func (m *modeSource) SnapperReport() *snapper.Report {
|
||||
return m.snapper.Report()
|
||||
}
|
||||
|
||||
func passiveSideFromConfig(g *config.Global, in *config.PassiveJob, mode passiveMode) (s *PassiveSide, err error) {
|
||||
func passiveSideFromConfig(g *config.Global, in *config.PassiveJob, configJob interface{}) (s *PassiveSide, err error) {
|
||||
|
||||
s = &PassiveSide{}
|
||||
|
||||
s.name, err = endpoint.MakeJobID(in.Name)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "invalid job name")
|
||||
}
|
||||
|
||||
switch v := configJob.(type) {
|
||||
case *config.SinkJob:
|
||||
s.mode, err = modeSinkFromConfig(g, v, s.name) // shadow
|
||||
case *config.SourceJob:
|
||||
s.mode, err = modeSourceFromConfig(g, v, s.name) // shadow
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err // no wrapping necessary
|
||||
}
|
||||
|
||||
s = &PassiveSide{mode: mode, name: in.Name}
|
||||
if s.listen, err = fromconfig.ListenerFactoryFromConfig(g, in.Serve); err != nil {
|
||||
return nil, errors.Wrap(err, "cannot build listener factory")
|
||||
}
|
||||
@@ -101,7 +130,7 @@ func passiveSideFromConfig(g *config.Global, in *config.PassiveJob, mode passive
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (j *PassiveSide) Name() string { return j.name }
|
||||
func (j *PassiveSide) Name() string { return j.name.String() }
|
||||
|
||||
type PassiveStatus struct {
|
||||
Snapper *snapper.Report
|
||||
@@ -120,7 +149,16 @@ func (j *PassiveSide) OwnedDatasetSubtreeRoot() (rfs *zfs.DatasetPath, ok bool)
|
||||
_ = j.mode.(*modeSource) // make sure we didn't introduce a new job type
|
||||
return nil, false
|
||||
}
|
||||
return sink.rootDataset.Copy(), true
|
||||
return sink.receiverConfig.RootWithoutClientComponent.Copy(), true
|
||||
}
|
||||
|
||||
func (j *PassiveSide) SenderConfig() *endpoint.SenderConfig {
|
||||
source, ok := j.mode.(*modeSource)
|
||||
if !ok {
|
||||
_ = j.mode.(*modeSink) // make sure we didn't introduce a new job type
|
||||
return nil
|
||||
}
|
||||
return source.senderConfig
|
||||
}
|
||||
|
||||
func (*PassiveSide) RegisterMetrics(registerer prometheus.Registerer) {}
|
||||
|
||||
+16
-10
@@ -2,7 +2,6 @@ package job
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
@@ -20,7 +19,7 @@ import (
|
||||
)
|
||||
|
||||
type SnapJob struct {
|
||||
name string
|
||||
name endpoint.JobID
|
||||
fsfilter zfs.DatasetFilter
|
||||
snapper *snapper.PeriodicOrManual
|
||||
|
||||
@@ -31,7 +30,7 @@ type SnapJob struct {
|
||||
pruner *pruner.Pruner
|
||||
}
|
||||
|
||||
func (j *SnapJob) Name() string { return j.name }
|
||||
func (j *SnapJob) Name() string { return j.name.String() }
|
||||
|
||||
func (j *SnapJob) Type() Type { return TypeSnap }
|
||||
|
||||
@@ -39,20 +38,23 @@ func snapJobFromConfig(g *config.Global, in *config.SnapJob) (j *SnapJob, err er
|
||||
j = &SnapJob{}
|
||||
fsf, err := filters.DatasetMapFilterFromConfig(in.Filesystems)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "cannnot build filesystem filter")
|
||||
return nil, errors.Wrap(err, "cannot build filesystem filter")
|
||||
}
|
||||
j.fsfilter = fsf
|
||||
|
||||
if j.snapper, err = snapper.FromConfig(g, fsf, in.Snapshotting); err != nil {
|
||||
return nil, errors.Wrap(err, "cannot build snapper")
|
||||
}
|
||||
j.name = in.Name
|
||||
j.name, err = endpoint.MakeJobID(in.Name)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "invalid job name")
|
||||
}
|
||||
j.promPruneSecs = prometheus.NewHistogramVec(prometheus.HistogramOpts{
|
||||
Namespace: "zrepl",
|
||||
Subsystem: "pruning",
|
||||
Name: "time",
|
||||
Help: "seconds spent in pruner",
|
||||
ConstLabels: prometheus.Labels{"zrepl_job": j.name},
|
||||
ConstLabels: prometheus.Labels{"zrepl_job": j.name.String()},
|
||||
}, []string{"prune_side"})
|
||||
j.prunerFactory, err = pruner.NewLocalPrunerFactory(in.Pruning, j.promPruneSecs)
|
||||
if err != nil {
|
||||
@@ -84,6 +86,8 @@ func (j *SnapJob) OwnedDatasetSubtreeRoot() (rfs *zfs.DatasetPath, ok bool) {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (j *SnapJob) SenderConfig() *endpoint.SenderConfig { return nil }
|
||||
|
||||
func (j *SnapJob) Run(ctx context.Context) {
|
||||
log := GetLogger(ctx)
|
||||
ctx = logging.WithSubsystemLoggers(ctx, log)
|
||||
@@ -133,9 +137,6 @@ type alwaysUpToDateReplicationCursorHistory struct {
|
||||
var _ pruner.History = (*alwaysUpToDateReplicationCursorHistory)(nil)
|
||||
|
||||
func (h alwaysUpToDateReplicationCursorHistory) ReplicationCursor(ctx context.Context, req *pdu.ReplicationCursorReq) (*pdu.ReplicationCursorRes, error) {
|
||||
if req.GetGet() == nil {
|
||||
return nil, fmt.Errorf("unsupported ReplicationCursor request: SnapJob only supports GETting a (faked) cursor")
|
||||
}
|
||||
fsvReq := &pdu.ListFilesystemVersionsReq{
|
||||
Filesystem: req.GetFilesystem(),
|
||||
}
|
||||
@@ -162,7 +163,12 @@ func (h alwaysUpToDateReplicationCursorHistory) ListFilesystems(ctx context.Cont
|
||||
func (j *SnapJob) doPrune(ctx context.Context) {
|
||||
log := GetLogger(ctx)
|
||||
ctx = logging.WithSubsystemLoggers(ctx, log)
|
||||
sender := endpoint.NewSender(j.fsfilter)
|
||||
sender := endpoint.NewSender(endpoint.SenderConfig{
|
||||
JobID: j.name,
|
||||
FSF: j.fsfilter,
|
||||
// FIXME encryption setting is irrelevant for SnapJob because the endpoint is only used as pruner.Target
|
||||
Encrypt: &zfs.NilBool{B: true},
|
||||
})
|
||||
j.pruner = j.prunerFactory.BuildLocalPruner(ctx, sender, alwaysUpToDateReplicationCursorHistory{sender})
|
||||
log.Info("start pruning")
|
||||
j.pruner.Prune()
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"github.com/zrepl/zrepl/rpc/transportmux"
|
||||
"github.com/zrepl/zrepl/tlsconf"
|
||||
"github.com/zrepl/zrepl/transport"
|
||||
"github.com/zrepl/zrepl/zfs/zfscmd"
|
||||
)
|
||||
|
||||
func OutletsFromConfig(in config.LoggingOutletEnumList) (*logger.Outlets, error) {
|
||||
@@ -70,7 +71,7 @@ type Subsystem string
|
||||
|
||||
const (
|
||||
SubsysReplication Subsystem = "repl"
|
||||
SubsyEndpoint Subsystem = "endpoint"
|
||||
SubsysEndpoint Subsystem = "endpoint"
|
||||
SubsysPruning Subsystem = "pruning"
|
||||
SubsysSnapshot Subsystem = "snapshot"
|
||||
SubsysHooks Subsystem = "hook"
|
||||
@@ -79,17 +80,19 @@ const (
|
||||
SubsysRPC Subsystem = "rpc"
|
||||
SubsysRPCControl Subsystem = "rpc.ctrl"
|
||||
SubsysRPCData Subsystem = "rpc.data"
|
||||
SubsysZFSCmd Subsystem = "zfs.cmd"
|
||||
)
|
||||
|
||||
func WithSubsystemLoggers(ctx context.Context, log logger.Logger) context.Context {
|
||||
ctx = logic.WithLogger(ctx, log.WithField(SubsysField, SubsysReplication))
|
||||
ctx = driver.WithLogger(ctx, log.WithField(SubsysField, SubsysReplication))
|
||||
ctx = endpoint.WithLogger(ctx, log.WithField(SubsysField, SubsyEndpoint))
|
||||
ctx = endpoint.WithLogger(ctx, log.WithField(SubsysField, SubsysEndpoint))
|
||||
ctx = pruner.WithLogger(ctx, log.WithField(SubsysField, SubsysPruning))
|
||||
ctx = snapper.WithLogger(ctx, log.WithField(SubsysField, SubsysSnapshot))
|
||||
ctx = hooks.WithLogger(ctx, log.WithField(SubsysField, SubsysHooks))
|
||||
ctx = transport.WithLogger(ctx, log.WithField(SubsysField, SubsysTransport))
|
||||
ctx = transportmux.WithLogger(ctx, log.WithField(SubsysField, SubsysTransportMux))
|
||||
ctx = zfscmd.WithLogger(ctx, log.WithField(SubsysField, SubsysZFSCmd))
|
||||
ctx = rpc.WithLoggers(ctx,
|
||||
rpc.Loggers{
|
||||
General: log.WithField(SubsysField, SubsysRPC),
|
||||
|
||||
@@ -211,7 +211,7 @@ func logfmtTryEncodeKeyval(enc *logfmt.Encoder, field, value interface{}) error
|
||||
case logfmt.ErrUnsupportedValueType:
|
||||
err := enc.EncodeKeyval(field, fmt.Sprintf("<%T>", value))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "cannot encode unsuuported value type Go type")
|
||||
return errors.Wrap(err, "cannot encode unsupported value type Go type")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ func NewTCPOutlet(formatter EntryFormatter, network, address string, tlsConfig *
|
||||
return
|
||||
}
|
||||
|
||||
entryChan := make(chan *bytes.Buffer, 1) // allow one message in flight while previos is in io.Copy()
|
||||
entryChan := make(chan *bytes.Buffer, 1) // allow one message in flight while previous is in io.Copy()
|
||||
|
||||
o := &TCPOutlet{
|
||||
formatter: formatter,
|
||||
|
||||
@@ -10,20 +10,23 @@ import (
|
||||
|
||||
"github.com/zrepl/zrepl/config"
|
||||
"github.com/zrepl/zrepl/daemon/job"
|
||||
"github.com/zrepl/zrepl/endpoint"
|
||||
"github.com/zrepl/zrepl/logger"
|
||||
"github.com/zrepl/zrepl/rpc/dataconn/frameconn"
|
||||
"github.com/zrepl/zrepl/util/tcpsock"
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
|
||||
type prometheusJob struct {
|
||||
listen string
|
||||
listen string
|
||||
freeBind bool
|
||||
}
|
||||
|
||||
func newPrometheusJobFromConfig(in *config.PrometheusMonitoring) (*prometheusJob, error) {
|
||||
if _, _, err := net.SplitHostPort(in.Listen); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &prometheusJob{in.Listen}, nil
|
||||
return &prometheusJob{in.Listen, in.ListenFreeBind}, nil
|
||||
}
|
||||
|
||||
var prom struct {
|
||||
@@ -46,6 +49,8 @@ func (j *prometheusJob) Status() *job.Status { return &job.Status{Type: job.Type
|
||||
|
||||
func (j *prometheusJob) OwnedDatasetSubtreeRoot() (p *zfs.DatasetPath, ok bool) { return nil, false }
|
||||
|
||||
func (j *prometheusJob) SenderConfig() *endpoint.SenderConfig { return nil }
|
||||
|
||||
func (j *prometheusJob) RegisterMetrics(registerer prometheus.Registerer) {}
|
||||
|
||||
func (j *prometheusJob) Run(ctx context.Context) {
|
||||
@@ -60,7 +65,7 @@ func (j *prometheusJob) Run(ctx context.Context) {
|
||||
|
||||
log := job.GetLogger(ctx)
|
||||
|
||||
l, err := net.Listen("tcp", j.listen)
|
||||
l, err := tcpsock.Listen(j.listen, j.freeBind)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("cannot listen")
|
||||
return
|
||||
|
||||
@@ -18,13 +18,13 @@ import (
|
||||
"github.com/zrepl/zrepl/util/envconst"
|
||||
)
|
||||
|
||||
// Try to keep it compatible with gitub.com/zrepl/zrepl/endpoint.Endpoint
|
||||
// Try to keep it compatible with github.com/zrepl/zrepl/endpoint.Endpoint
|
||||
type History interface {
|
||||
ReplicationCursor(ctx context.Context, req *pdu.ReplicationCursorReq) (*pdu.ReplicationCursorRes, error)
|
||||
ListFilesystems(ctx context.Context, req *pdu.ListFilesystemReq) (*pdu.ListFilesystemRes, error)
|
||||
}
|
||||
|
||||
// Try to keep it compatible with gitub.com/zrepl/zrepl/endpoint.Endpoint
|
||||
// Try to keep it compatible with github.com/zrepl/zrepl/endpoint.Endpoint
|
||||
type Target interface {
|
||||
ListFilesystems(ctx context.Context, req *pdu.ListFilesystemReq) (*pdu.ListFilesystemRes, error)
|
||||
ListFilesystemVersions(ctx context.Context, req *pdu.ListFilesystemVersionsReq) (*pdu.ListFilesystemVersionsRes, error)
|
||||
@@ -412,9 +412,6 @@ tfss_loop:
|
||||
|
||||
rcReq := &pdu.ReplicationCursorReq{
|
||||
Filesystem: tfs.Path,
|
||||
Op: &pdu.ReplicationCursorReq_Get{
|
||||
Get: &pdu.ReplicationCursorReq_GetOp{},
|
||||
},
|
||||
}
|
||||
rc, err := receiver.ReplicationCursor(ctx, rcReq)
|
||||
if err != nil {
|
||||
|
||||
+83
-35
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/zrepl/zrepl/daemon/filters"
|
||||
"github.com/zrepl/zrepl/daemon/hooks"
|
||||
"github.com/zrepl/zrepl/logger"
|
||||
"github.com/zrepl/zrepl/util/envconst"
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
|
||||
@@ -276,7 +277,7 @@ func snapshot(a args, u updater) state {
|
||||
|
||||
jobCallback := hooks.NewCallbackHookForFilesystem("snapshot", fs, func(_ context.Context) (err error) {
|
||||
l.Debug("create snapshot")
|
||||
err = zfs.ZFSSnapshot(fs, snapname, false) // TODO propagagte context to ZFSSnapshot
|
||||
err = zfs.ZFSSnapshot(a.ctx, fs, snapname, false) // TODO propagate context to ZFSSnapshot
|
||||
if err != nil {
|
||||
l.WithError(err).Error("cannot create snapshot")
|
||||
}
|
||||
@@ -400,9 +401,19 @@ func listFSes(ctx context.Context, mf *filters.DatasetMapFilter) (fss []*zfs.Dat
|
||||
return zfs.ZFSListMapping(ctx, mf)
|
||||
}
|
||||
|
||||
var syncUpWarnNoSnapshotUntilSyncupMinDuration = envconst.Duration("ZREPL_SNAPPER_SYNCUP_WARN_MIN_DURATION", 1*time.Second)
|
||||
|
||||
// see docs/snapshotting.rst
|
||||
func findSyncPoint(log Logger, fss []*zfs.DatasetPath, prefix string, interval time.Duration) (syncPoint time.Time, err error) {
|
||||
|
||||
const (
|
||||
prioHasVersions int = iota
|
||||
prioNoVersions
|
||||
)
|
||||
|
||||
type snapTime struct {
|
||||
ds *zfs.DatasetPath
|
||||
prio int // lower is higher
|
||||
time time.Time
|
||||
}
|
||||
|
||||
@@ -411,55 +422,92 @@ func findSyncPoint(log Logger, fss []*zfs.DatasetPath, prefix string, interval t
|
||||
}
|
||||
|
||||
snaptimes := make([]snapTime, 0, len(fss))
|
||||
hardErrs := 0
|
||||
|
||||
now := time.Now()
|
||||
|
||||
log.Debug("examine filesystem state")
|
||||
log.Debug("examine filesystem state to find sync point")
|
||||
for _, d := range fss {
|
||||
|
||||
l := log.WithField("fs", d.ToString())
|
||||
|
||||
fsvs, err := zfs.ZFSListFilesystemVersions(d, filters.NewTypedPrefixFilter(prefix, zfs.Snapshot))
|
||||
if err != nil {
|
||||
l.WithError(err).Error("cannot list filesystem versions")
|
||||
continue
|
||||
}
|
||||
if len(fsvs) <= 0 {
|
||||
l.WithField("prefix", prefix).Debug("no filesystem versions with prefix")
|
||||
continue
|
||||
syncPoint, err := findSyncPointFSNextOptimalSnapshotTime(l, now, interval, prefix, d)
|
||||
if err == findSyncPointFSNoFilesystemVersionsErr {
|
||||
snaptimes = append(snaptimes, snapTime{
|
||||
ds: d,
|
||||
prio: prioNoVersions,
|
||||
time: now,
|
||||
})
|
||||
} else if err != nil {
|
||||
hardErrs++
|
||||
l.WithError(err).Error("cannot determine optimal sync point for this filesystem")
|
||||
} else {
|
||||
l.WithField("syncPoint", syncPoint).Debug("found optimal sync point for this filesystem")
|
||||
snaptimes = append(snaptimes, snapTime{
|
||||
ds: d,
|
||||
prio: prioHasVersions,
|
||||
time: syncPoint,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Sort versions by creation
|
||||
sort.SliceStable(fsvs, func(i, j int) bool {
|
||||
return fsvs[i].CreateTXG < fsvs[j].CreateTXG
|
||||
})
|
||||
|
||||
latest := fsvs[len(fsvs)-1]
|
||||
l.WithField("creation", latest.Creation).
|
||||
Debug("found latest snapshot")
|
||||
|
||||
since := now.Sub(latest.Creation)
|
||||
if since < 0 {
|
||||
l.WithField("snapshot", latest.Name).
|
||||
WithField("creation", latest.Creation).
|
||||
Error("snapshot is from the future")
|
||||
continue
|
||||
}
|
||||
next := now
|
||||
if since < interval {
|
||||
next = latest.Creation.Add(interval)
|
||||
}
|
||||
snaptimes = append(snaptimes, snapTime{d, next})
|
||||
if hardErrs == len(fss) {
|
||||
return time.Time{}, fmt.Errorf("hard errors in determining sync point for every matching filesystem")
|
||||
}
|
||||
|
||||
if len(snaptimes) == 0 {
|
||||
snaptimes = append(snaptimes, snapTime{nil, now})
|
||||
panic("implementation error: loop must either inc hardErrs or add result to snaptimes")
|
||||
}
|
||||
|
||||
// sort ascending by (prio,time)
|
||||
// => those filesystems with versions win over those without any
|
||||
sort.Slice(snaptimes, func(i, j int) bool {
|
||||
return snaptimes[i].time.Before(snaptimes[j].time)
|
||||
if snaptimes[i].prio == snaptimes[j].prio {
|
||||
return snaptimes[i].time.Before(snaptimes[j].time)
|
||||
}
|
||||
return snaptimes[i].prio < snaptimes[j].prio
|
||||
})
|
||||
|
||||
winnerSyncPoint := snaptimes[0].time
|
||||
l := log.WithField("syncPoint", winnerSyncPoint.String())
|
||||
l.Info("determined sync point")
|
||||
if winnerSyncPoint.Sub(now) > syncUpWarnNoSnapshotUntilSyncupMinDuration {
|
||||
for _, st := range snaptimes {
|
||||
if st.prio == prioNoVersions {
|
||||
l.WithField("fs", st.ds.ToString()).Warn("filesystem will not be snapshotted until sync point")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return snaptimes[0].time, nil
|
||||
|
||||
}
|
||||
|
||||
var findSyncPointFSNoFilesystemVersionsErr = fmt.Errorf("no filesystem versions")
|
||||
|
||||
func findSyncPointFSNextOptimalSnapshotTime(l Logger, now time.Time, interval time.Duration, prefix string, d *zfs.DatasetPath) (time.Time, error) {
|
||||
|
||||
fsvs, err := zfs.ZFSListFilesystemVersions(context.TODO(), d, zfs.ListFilesystemVersionsOptions{
|
||||
Types: zfs.Snapshots,
|
||||
ShortnamePrefix: prefix,
|
||||
})
|
||||
if err != nil {
|
||||
return time.Time{}, errors.Wrap(err, "list filesystem versions")
|
||||
}
|
||||
if len(fsvs) <= 0 {
|
||||
return time.Time{}, findSyncPointFSNoFilesystemVersionsErr
|
||||
}
|
||||
|
||||
// Sort versions by creation
|
||||
sort.SliceStable(fsvs, func(i, j int) bool {
|
||||
return fsvs[i].CreateTXG < fsvs[j].CreateTXG
|
||||
})
|
||||
|
||||
latest := fsvs[len(fsvs)-1]
|
||||
l.WithField("creation", latest.Creation).Debug("found latest snapshot")
|
||||
|
||||
since := now.Sub(latest.Creation)
|
||||
if since < 0 {
|
||||
return time.Time{}, fmt.Errorf("snapshot %q is from the future: creation=%q now=%q", latest.ToAbsPath(d), latest.Creation, now)
|
||||
}
|
||||
|
||||
return latest.Creation.Add(interval), nil
|
||||
}
|
||||
|
||||
+40
-1
@@ -27,6 +27,45 @@ We use the following annotations for classifying changes:
|
||||
* |bugfix| Change that fixes a bug, no regressions or incompatibilities expected.
|
||||
* |docs| Change to the documentation.
|
||||
|
||||
0.3
|
||||
---
|
||||
|
||||
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.
|
||||
|
||||
Actual changelog:
|
||||
|
||||
* |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.
|
||||
|
||||
* |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.
|
||||
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.
|
||||
|
||||
* |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
|
||||
that will not be snapshotted until the sync-up phase is over
|
||||
* |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.
|
||||
|
||||
0.2.1
|
||||
-----
|
||||
|
||||
@@ -68,7 +107,7 @@ We use the following annotations for classifying changes:
|
||||
| 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 **commerical support**, please `contact Christian directly <https://cschwarz.com>`_.
|
||||
| For SEPA wire transfer and **commercial support**, please `contact Christian directly <https://cschwarz.com>`_.
|
||||
|
||||
|
||||
0.1.1
|
||||
|
||||
@@ -11,6 +11,7 @@ Configuration
|
||||
configuration/jobs
|
||||
configuration/transports
|
||||
configuration/filter_syntax
|
||||
configuration/sendrecvoptions
|
||||
configuration/snapshotting
|
||||
configuration/prune
|
||||
configuration/logging
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
Filter Syntax
|
||||
=============
|
||||
|
||||
For :ref:`source jobs <job-source>` and :ref:`push jobs <job-push>`, a filesystem filter must be defined (field ``filesystems``).
|
||||
For :ref:`source<job-source>`, :ref:`push<job-push>` and :ref:`snap<job-snap>` jobs, a filesystem filter must be defined (field ``filesystems``).
|
||||
A filter takes a filesystem path (in the ZFS filesystem hierarchy) as parameter and returns ``true`` (pass) or ``false`` (block).
|
||||
|
||||
A filter is specified as a **YAML dictionary** with patterns as keys and booleans as values.
|
||||
|
||||
@@ -24,6 +24,8 @@ Job Type ``push``
|
||||
- |connect-transport|
|
||||
* - ``filesystems``
|
||||
- |filter-spec| for filesystems to be snapshotted and pushed to the sink
|
||||
* - ``send``
|
||||
- |send-options|
|
||||
* - ``snapshotting``
|
||||
- |snapshotting-spec|
|
||||
* - ``pruning``
|
||||
@@ -101,6 +103,8 @@ Job Type ``source``
|
||||
- |serve-transport|
|
||||
* - ``filesystems``
|
||||
- |filter-spec| for filesystems to be snapshotted and exposed to connecting clients
|
||||
* - ``send``
|
||||
- |send-options|
|
||||
* - ``snapshotting``
|
||||
- |snapshotting-spec|
|
||||
|
||||
|
||||
@@ -202,7 +202,7 @@ The latter is particularly useful in combination with log aggregation services.
|
||||
.. WARNING::
|
||||
|
||||
zrepl drops log messages to the TCP outlet if the underlying connection is not fast enough.
|
||||
Note that TCP buffering in the kernel must first run full becfore messages are dropped.
|
||||
Note that TCP buffering in the kernel must first run full before messages are dropped.
|
||||
|
||||
Make sure to always configure a ``stdout`` outlet as the special error outlet to be informed about problems
|
||||
with the TCP outlet (see :ref:`above <logging-error-outlet>` ).
|
||||
|
||||
@@ -14,7 +14,8 @@ Prometheus & Grafana
|
||||
|
||||
zrepl can expose `Prometheus metrics <https://prometheus.io/docs/instrumenting/exposition_formats/>`_ via HTTP.
|
||||
The ``listen`` attribute is a `net.Listen <https://golang.org/pkg/net/#Listen>`_ string for tcp, e.g. ``:9091`` or ``127.0.0.1:9091``.
|
||||
The Prometheues monitoring job appears in the ``zrepl control`` job list and may be specified **at most once**.
|
||||
The ``listen_freebind`` attribute is :ref:`explained here <listen-freebind-explanation>`.
|
||||
The Prometheus monitoring job appears in the ``zrepl control`` job list and may be specified **at most once**.
|
||||
|
||||
zrepl also ships with an importable `Grafana <https://grafana.com>`_ dashboard that consumes the Prometheus metrics:
|
||||
see :repomasterlink:`dist/grafana`.
|
||||
@@ -30,6 +31,7 @@ The dashboard also contains some advice on which metrics are important to monito
|
||||
monitoring:
|
||||
- type: prometheus
|
||||
listen: ':9091'
|
||||
listen_freebind: true # optional, default false
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -26,9 +26,9 @@ Config File Structure
|
||||
type: push
|
||||
- ...
|
||||
|
||||
zrepl is confgured using a single YAML configuration file with two main sections: ``global`` and ``jobs``.
|
||||
zrepl is configured using a single YAML configuration file with two main sections: ``global`` and ``jobs``.
|
||||
The ``global`` section is filled with sensible defaults and is covered later in this chapter.
|
||||
The ``jobs`` section is a list of jobs which we are goind to explain now.
|
||||
The ``jobs`` section is a list of jobs which we are going to explain now.
|
||||
|
||||
.. _job-overview:
|
||||
|
||||
@@ -41,7 +41,7 @@ Jobs are identified by their ``name``, both in log files and the ``zrepl status`
|
||||
|
||||
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 persmissions.
|
||||
The passive side responds to requests from the active side after checking its permissions.
|
||||
|
||||
The following table shows how different job types can be combined to achieve **both push and pull mode setups**.
|
||||
Note that snapshot-creation denoted by "(snap)" is orthogonal to whether a job is active or passive.
|
||||
@@ -90,6 +90,8 @@ It uses the client identity for access control:
|
||||
.. TIP::
|
||||
The implementation of the ``sink`` job requires that the connecting client identities be a valid ZFS filesystem name components.
|
||||
|
||||
.. _overview-how-replication-works:
|
||||
|
||||
How Replication Works
|
||||
---------------------
|
||||
|
||||
@@ -107,9 +109,8 @@ The following steps take place during replication and can be monitored using the
|
||||
* Per filesystem, compute a diff between sender and receiver snapshots
|
||||
* Build a list of replication steps
|
||||
|
||||
* If possible, use incremental sends (``zfs send -i``)
|
||||
* If possible, use incremental and resumable sends (``zfs send -i``)
|
||||
* Otherwise, use full send of most recent snapshot on sender
|
||||
* Give up on filesystems that cannot be replicated without data loss
|
||||
|
||||
* Retry on errors that are likely temporary (i.e. network failures).
|
||||
* Give up on filesystems where a permanent error was received over RPC.
|
||||
@@ -119,22 +120,28 @@ The following steps take place during replication and can be monitored using the
|
||||
* Perform replication steps in the following order:
|
||||
Among all filesystems with pending replication steps, pick the filesystem whose next replication step's snapshot is the oldest.
|
||||
* Create placeholder filesystems on the receiving side to mirror the dataset paths on the sender to ``root_fs/${client_identity}``.
|
||||
* After a successful replication step, update the replication cursor bookmark (see below).
|
||||
* Acquire send-side *step-holds* on the step's `from` and `to` snapshots.
|
||||
* Perform the replication step.
|
||||
* Move the **replication cursor** bookmark on the sending side (see below).
|
||||
* Move the **last-received-hold** on the receiving side (see below).
|
||||
* Release the send-side step-holds.
|
||||
|
||||
The idea behind the execution order of replication steps is that if the sender snapshots all filesystems simultaneously at fixed intervals, the receiver will have all filesystems snapshotted at time ``T1`` before the first snapshot at ``T2 = T1 + $interval`` is replicated.
|
||||
|
||||
.. _replication-cursor-bookmark:
|
||||
.. _replication-cursor-and-last-received-hold:
|
||||
|
||||
The **replication cursor bookmark** ``#zrepl_replication_cursor`` is kept per filesystem on the sending side of a replication setup:
|
||||
It is a bookmark of the most recent successfully replicated snapshot to the receiving side.
|
||||
It is is used by the :ref:`not_replicated <prune-keep-not-replicated>` keep rule to identify all snapshots that have not yet been replicated to the receiving side.
|
||||
Regardless of whether that keep rule is used, the bookmark ensures that replication can always continue incrementally.
|
||||
Note that there is only one cursor bookmark per filesystem, which prohibits multiple jobs to replicate the same filesystem (:ref:`see below<jobs-multiple-jobs>`).
|
||||
**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.
|
||||
|
||||
.. _replication-placeholder-property:
|
||||
|
||||
**Placeholder filesystems** on the receiving side are regular ZFS filesystems with the placeholder property ``zrepl:placeholder=on``.
|
||||
Placeholders allow the receiving side to mirror the sender's ZFS dataset hierachy without replicating every filesystem at every intermediary dataset path component.
|
||||
Placeholders allow the receiving side to mirror the sender's ZFS dataset hierarchy without replicating every filesystem at every intermediary dataset path component.
|
||||
Consider the following example: ``S/H/J`` shall be replicated to ``R/sink/job/S/H/J``, but neither ``S/H`` nor ``S`` shall be replicated.
|
||||
ZFS requires the existence of ``R/sink/job/S`` and ``R/sink/job/S/H`` in order to receive into ``R/sink/job/S/H/J``.
|
||||
Thus, zrepl creates the parent filesystems as placeholders on the receiving side.
|
||||
@@ -144,9 +151,13 @@ The ``zrepl test placeholder`` command can be used to check whether a filesystem
|
||||
.. ATTENTION::
|
||||
|
||||
Currently, zrepl does not replicate filesystem properties.
|
||||
Whe receiving a filesystem, it is never mounted (`-u` flag) and `mountpoint=none` is set.
|
||||
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:
|
||||
|
||||
@@ -170,8 +181,8 @@ 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.
|
||||
For example, if job A prunes snapshots that job B is planning to replicate, the replication will fail because B asssumed the snapshot to still be present.
|
||||
More subtle race conditions can occur with the :ref:`replication cursor bookmark <replication-cursor-bookmark>`, which currently only exists once per filesystem.
|
||||
For example, if job A prunes snapshots that job B is planning to replicate, the replication will fail because B assumed the snapshot to still be present.
|
||||
However, the next replication attempt will re-examine the situation from scratch and should work.
|
||||
|
||||
N push jobs to 1 sink
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
@@ -187,5 +198,5 @@ Multiple pull jobs pulling from the same source have potential for race conditio
|
||||
each pull job prunes the source side independently, causing replication-prune and prune-prune races.
|
||||
|
||||
There is currently no way for a pull job to filter which snapshots it should attempt to replicate.
|
||||
Thus, it is not possibe to just manually assert that the prune rules of all pull jobs are disjoint to avoid replication-prune and prune-prune races.
|
||||
Thus, it is not possible to just manually assert that the prune rules of all pull jobs are disjoint to avoid replication-prune and prune-prune races.
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ Pruning Policies
|
||||
================
|
||||
|
||||
In zrepl, *pruning* means *destroying snapshots*.
|
||||
Pruning must happen on both sides of a replication or the systems would inevitable run out of disk space at some point.
|
||||
Pruning must happen on both sides of a replication or the systems would inevitably run out of disk space at some point.
|
||||
|
||||
Typically, the requirements to temporal resolution and maximum retention time differ per side.
|
||||
For example, when using zrepl to back up a busy database server, you will want high temporal resolution (snapshots every 10 min) for the last 24h in case of administrative disasters, but cannot afford to store them for much longer because you might have high turnover volume in the database.
|
||||
@@ -66,7 +66,7 @@ Policy ``not_replicated``
|
||||
|
||||
``not_replicated`` keeps all snapshots that have not been replicated to the receiving side.
|
||||
It only makes sense to specify this rule on a sender (source or push job).
|
||||
The state required to evaluate this rule is stored in the :ref:`replication cursor bookmark <replication-cursor-bookmark>` on the sending side.
|
||||
The state required to evaluate this rule is stored in the :ref:`replication cursor bookmark <replication-cursor-and-last-received-hold>` on the sending side.
|
||||
|
||||
.. _prune-keep-retention-grid:
|
||||
|
||||
@@ -154,7 +154,7 @@ Policy ``regex``
|
||||
negate: true
|
||||
regex: "^zrepl_.*"
|
||||
|
||||
``regex`` keeps all snapshots whose names are matched by the regular expressionin ``regex``.
|
||||
``regex`` keeps all snapshots whose names are matched by the regular expression in ``regex``.
|
||||
Like all other regular expression fields in prune policies, zrepl uses Go's `regexp.Regexp <https://golang.org/pkg/regexp/#Compile>`_ Perl-compatible regular expressions (`Syntax <https://golang.org/pkg/regexp/syntax>`_).
|
||||
The optional `negate` boolean field inverts the semantics: Use it if you want to keep all snapshots that *do not* match the given regex.
|
||||
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
.. include:: ../global.rst.inc
|
||||
|
||||
|
||||
Send & Recv Options
|
||||
===================
|
||||
|
||||
.. _job-send-options:
|
||||
|
||||
Send Options
|
||||
~~~~~~~~~~~~
|
||||
|
||||
::
|
||||
|
||||
jobs:
|
||||
- type: push
|
||||
filesystems: ...
|
||||
send:
|
||||
encrypted: true
|
||||
...
|
||||
|
||||
:ref:`Source<job-source>` and :ref:`push<job-push>` jobs have an optional ``send`` configuration section.
|
||||
|
||||
``encryption`` option
|
||||
---------------------
|
||||
|
||||
The ``encryption`` variable controls whether the matched filesystems are sent as `OpenZFS native encryption <http://open-zfs.org/wiki/ZFS-Native_Encryption>`_ raw sends.
|
||||
More specifically, if ``encryption=true``, zrepl
|
||||
|
||||
* checks for any of the filesystems matched by ``filesystems`` whether the ZFS ``encryption`` property indicates that the filesystem is actually encrypted with ZFS native encryption and
|
||||
* invokes the ``zfs send`` subcommand with the ``-w`` option (raw sends) and
|
||||
* expects the receiving side to support OpenZFS native encryption (recv will fail otherwise)
|
||||
|
||||
Filesystems matched by ``filesystems`` that are not encrypted are not sent and will cause error log messages.
|
||||
|
||||
If ``encryption=false``, zrepl expects that filesystems matching ``filesystems`` are not encrypted or have loaded encryption keys.
|
||||
|
||||
.. _job-recv-options:
|
||||
|
||||
Recv Options
|
||||
~~~~~~~~~~~~
|
||||
|
||||
:ref:`Sink<job-sink>` and :ref:`pull<job-pull>` jobs have an optional ``recv`` configuration section.
|
||||
However, there are currently no variables to configure there.
|
||||
|
||||
|
||||
@@ -9,8 +9,15 @@ The ``push``, ``source`` and ``snap`` jobs can automatically take periodic snaps
|
||||
The snapshot names are composed of a user-defined prefix followed by a UTC date formatted like ``20060102_150405_000``.
|
||||
We use UTC because it will avoid name conflicts when switching time zones or between summer and winter time.
|
||||
|
||||
When a job is started, the snapshotter attempts to get the snapshotting rhythms of the matched ``filesystems`` in sync because snapshotting all filesystems at the same time results in a more consistent backup.
|
||||
To find that sync point, the most recent snapshot, made by the snapshotter, in any of the matched ``filesystems`` is used.
|
||||
A filesystem that does not have snapshots by the snapshotter has lower priority than filesystem that do, and thus might not be snapshotted (and replicated) until it is snapshotted at the next sync point.
|
||||
|
||||
For ``push`` jobs, replication is automatically triggered after all filesystems have been snapshotted.
|
||||
|
||||
Note that the ``zrepl signal wakeup JOB`` subcommand does not trigger snapshotting.
|
||||
|
||||
|
||||
::
|
||||
|
||||
jobs:
|
||||
|
||||
@@ -50,12 +50,18 @@ Serve
|
||||
serve:
|
||||
type: tcp
|
||||
listen: ":8888"
|
||||
listen_freebind: true # optional, default false
|
||||
clients: {
|
||||
"192.168.122.123" : "mysql01"
|
||||
"192.168.122.123" : "mx01"
|
||||
}
|
||||
...
|
||||
|
||||
.. _listen-freebind-explanation:
|
||||
|
||||
``listen_freebind`` controls whether the socket is allowed to bind to non-local or unconfigured IP addresses (Linux ``IP_FREEBIND`` , FreeBSD ``IP_BINDANY``).
|
||||
Enable this option if you want to ``listen`` on a specific IP address that might not yet be configured when the zrepl daemon starts.
|
||||
|
||||
Connect
|
||||
~~~~~~~
|
||||
|
||||
@@ -101,6 +107,7 @@ Serve
|
||||
serve:
|
||||
type: tls
|
||||
listen: ":8888"
|
||||
listen_freebind: true # optional, default false
|
||||
ca: /etc/zrepl/ca.crt
|
||||
cert: /etc/zrepl/prod.fullchain
|
||||
key: /etc/zrepl/prod.key
|
||||
@@ -110,6 +117,7 @@ Serve
|
||||
|
||||
The ``ca`` field specified the certificate authority used to validate client certificates.
|
||||
The ``client_cns`` list specifies a list of accepted client common names (which are also the client identities for this transport).
|
||||
The ``listen_freebind`` field is :ref:`explained here <listen-freebind-explanation>`.
|
||||
|
||||
Connect
|
||||
~~~~~~~
|
||||
|
||||
@@ -21,6 +21,8 @@
|
||||
|
||||
.. |serve-transport| replace:: :ref:`serve specification<transport>`
|
||||
.. |connect-transport| replace:: :ref:`connect specification<transport>`
|
||||
.. |send-options| replace:: :ref:`send options<job-send-options>`, e.g. for encrypted sends
|
||||
.. |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>`
|
||||
+5
-2
@@ -57,9 +57,11 @@ Main Features
|
||||
* Advanced replication features
|
||||
|
||||
* [x] Automatic retries for temporary network errors
|
||||
* [ ] Resumable send & receive
|
||||
* [x] Automatic resumable send & receive
|
||||
* [x] Automatic ZFS holds during send & receive
|
||||
* [x] Automatic bookmark \& hold management for guaranteed incremental send & recv
|
||||
* [x] Encrypted raw send & receive to untrusted receivers (OpenZFS native encryption)
|
||||
* [ ] Compressed send & receive
|
||||
* [ ] Raw encrypted send & receive
|
||||
|
||||
* **Automatic snapshot management**
|
||||
|
||||
@@ -83,6 +85,7 @@ Main Features
|
||||
* **Maintainable implementation in Go**
|
||||
|
||||
* [x] Cross platform
|
||||
* [x] Dynamic feature checking
|
||||
* [x] Type safe & testable code
|
||||
|
||||
|
||||
|
||||
@@ -52,6 +52,9 @@ The following list may be incomplete, feel free to submit a PR with an update:
|
||||
* - 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.
|
||||
@@ -98,9 +101,9 @@ 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 .
|
||||
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)" \
|
||||
@@ -124,7 +127,7 @@ Either way, all build results are located in the ``artifacts/`` directory.
|
||||
|
||||
.. NOTE::
|
||||
|
||||
It is your job to install the apropriate binary in the zrepl users's ``$PATH``, e.g. ``/usr/local/bin/zrepl``.
|
||||
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?
|
||||
|
||||
+6
-1
@@ -6,7 +6,7 @@
|
||||
|
||||
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.
|
||||
For SEPA wire transfer and **commerical support**, please `contact Christian directly <https://cschwarz.com>`_.
|
||||
For SEPA wire transfer and **commercial support**, please `contact Christian directly <https://cschwarz.com>`_.
|
||||
|
||||
**Thanks for your support!**
|
||||
|
||||
@@ -32,7 +32,11 @@ We would like to thank the following people and organizations for supporting zre
|
||||
|
||||
<div class="fa fa-code" style="width: 1em;"></div>
|
||||
|
||||
* |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>`_
|
||||
* |supporter-std| `Lapo Luchini <https://github.com/lapo-luchini>`_
|
||||
* |supporter-std| `F. Schmid <https://github.com/tdschmidl>`_
|
||||
* |supporter-gold| `MNX.io <https://mnx.io>`_
|
||||
* |supporter-std| `Marshall Clyburn <https://github.com/mdclyburn>`_
|
||||
* |supporter-code| `Ross Williams <https://github.com/overhacked>`_
|
||||
@@ -40,4 +44,5 @@ We would like to thank the following people and organizations for supporting zre
|
||||
* |supporter-code| `Justin Scholz <https://github.com/JMoVS>`_
|
||||
* |supporter-code| `InsanePrawn <https://github.com/InsanePrawn>`_
|
||||
* |supporter-code| `Ben Woods <https://www.freshports.org/sysutils/zrepl/>`_
|
||||
* |supporter-code| `Janis Streib <https://github.com/janisstreib>`_
|
||||
* |supporter-code| `Anton Schirg <https://github.com/antonxy>`_
|
||||
|
||||
+3
-4
@@ -52,9 +52,9 @@ Follow the :ref:`OS-specific installation instructions <installation>` and come
|
||||
Generate TLS Certificates
|
||||
-------------------------
|
||||
|
||||
We use the `TLS client authentication transport <transport-tcp+tlsclientauth>` to protect our data on the wire.
|
||||
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>`.
|
||||
Again, for convenience, We generate the key pairs on our local machine and distribute them using ssh:
|
||||
For convenience, we generate the key pairs on our local machine and distribute them using ssh:
|
||||
|
||||
.. code-block:: bash
|
||||
:emphasize-lines: 6,13
|
||||
@@ -138,7 +138,7 @@ We define a corresponding **sink job** named ``sink`` in ``/etc/zrepl/zrepl.yml`
|
||||
Apply Configuration Changes
|
||||
---------------------------
|
||||
|
||||
We use ``zrepl configcheck`` before to catch any configuration errors: no output indicates that everything is fine.
|
||||
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``.
|
||||
|
||||
|
||||
@@ -175,4 +175,3 @@ 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>`.
|
||||
|
||||
|
||||
+4
-2
@@ -38,6 +38,8 @@ CLI Overview
|
||||
* - ``zrepl migrate``
|
||||
- | perform on-disk state / ZFS property migrations
|
||||
| (see :ref:`changelog <changelog>` for details)
|
||||
* - ``zrepl zfs-abstractions``
|
||||
- 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:
|
||||
|
||||
@@ -48,7 +50,7 @@ zrepl daemon
|
||||
All actual work zrepl does is performed by a daemon process.
|
||||
The daemon supports structured :ref:`logging <logging>` and provides :ref:`monitoring endpoints <monitoring>`.
|
||||
|
||||
When installating from a package, the package maintainer should have provided an init script / systemd.service file.
|
||||
When installing from a package, the package maintainer should have provided an init script / systemd.service file.
|
||||
You should thus be able to start zrepl daemon using your init system.
|
||||
|
||||
Alternatively, or for running zrepl in the foreground, simply execute ``zrepl daemon``.
|
||||
@@ -73,6 +75,6 @@ The daemon exits as soon as all jobs have reported shut down.
|
||||
Systemd Unit File
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
A systemd service defintion template is available in :repomasterlink:`dist/systemd`.
|
||||
A systemd service definition template is available in :repomasterlink:`dist/systemd`.
|
||||
Note that some of the options only work on recent versions of systemd.
|
||||
Any help & improvements are very welcome, see :issue:`145`.
|
||||
+548
-62
@@ -2,26 +2,56 @@
|
||||
package endpoint
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"path"
|
||||
"sync"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/zrepl/zrepl/replication/logic/pdu"
|
||||
"github.com/zrepl/zrepl/util/chainedio"
|
||||
"github.com/zrepl/zrepl/util/chainlock"
|
||||
"github.com/zrepl/zrepl/util/envconst"
|
||||
"github.com/zrepl/zrepl/util/semaphore"
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
|
||||
type SenderConfig struct {
|
||||
FSF zfs.DatasetFilter
|
||||
Encrypt *zfs.NilBool
|
||||
JobID JobID
|
||||
}
|
||||
|
||||
func (c *SenderConfig) Validate() error {
|
||||
c.JobID.MustValidate()
|
||||
if err := c.Encrypt.Validate(); err != nil {
|
||||
return errors.Wrap(err, "`Encrypt` field invalid")
|
||||
}
|
||||
if _, err := StepHoldTag(c.JobID); err != nil {
|
||||
return fmt.Errorf("JobID cannot be used for hold tag: %s", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Sender implements replication.ReplicationEndpoint for a sending side
|
||||
type Sender struct {
|
||||
FSFilter zfs.DatasetFilter
|
||||
encrypt *zfs.NilBool
|
||||
jobId JobID
|
||||
}
|
||||
|
||||
func NewSender(fsf zfs.DatasetFilter) *Sender {
|
||||
return &Sender{FSFilter: fsf}
|
||||
func NewSender(conf SenderConfig) *Sender {
|
||||
if err := conf.Validate(); err != nil {
|
||||
panic("invalid config" + err.Error())
|
||||
}
|
||||
return &Sender{
|
||||
FSFilter: conf.FSF,
|
||||
encrypt: conf.Encrypt,
|
||||
jobId: conf.JobID,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Sender) filterCheckFS(fs string) (*zfs.DatasetPath, error) {
|
||||
@@ -49,10 +79,15 @@ func (s *Sender) ListFilesystems(ctx context.Context, r *pdu.ListFilesystemReq)
|
||||
}
|
||||
rfss := make([]*pdu.Filesystem, len(fss))
|
||||
for i := range fss {
|
||||
encEnabled, err := zfs.ZFSGetEncryptionEnabled(ctx, fss[i].ToString())
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "cannot get filesystem encryption status")
|
||||
}
|
||||
rfss[i] = &pdu.Filesystem{
|
||||
Path: fss[i].ToString(),
|
||||
// FIXME: not supporting ResumeToken yet
|
||||
// ResumeToken does not make sense from Sender
|
||||
IsPlaceholder: false, // sender FSs are never placeholders
|
||||
IsEncrypted: encEnabled,
|
||||
}
|
||||
}
|
||||
res := &pdu.ListFilesystemRes{Filesystems: rfss}
|
||||
@@ -64,7 +99,7 @@ func (s *Sender) ListFilesystemVersions(ctx context.Context, r *pdu.ListFilesyst
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fsvs, err := zfs.ZFSListFilesystemVersions(lp, nil)
|
||||
fsvs, err := zfs.ZFSListFilesystemVersions(ctx, lp, zfs.ListFilesystemVersionsOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -77,13 +112,174 @@ func (s *Sender) ListFilesystemVersions(ctx context.Context, r *pdu.ListFilesyst
|
||||
|
||||
}
|
||||
|
||||
func (p *Sender) HintMostRecentCommonAncestor(ctx context.Context, r *pdu.HintMostRecentCommonAncestorReq) (*pdu.HintMostRecentCommonAncestorRes, error) {
|
||||
|
||||
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))
|
||||
|
||||
func (s *Sender) Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, zfs.StreamCopier, error) {
|
||||
func uncheckedSendArgsFromPDU(fsv *pdu.FilesystemVersion) *zfs.ZFSSendArgVersion {
|
||||
if fsv == nil {
|
||||
return nil
|
||||
}
|
||||
return &zfs.ZFSSendArgVersion{RelName: fsv.GetRelName(), GUID: fsv.Guid}
|
||||
}
|
||||
|
||||
func sendArgsFromPDUAndValidateExistsAndGetVersion(ctx context.Context, fs string, fsv *pdu.FilesystemVersion) (v zfs.FilesystemVersion, err error) {
|
||||
sendArgs := uncheckedSendArgsFromPDU(fsv)
|
||||
if sendArgs == nil {
|
||||
return v, errors.New("must not be nil")
|
||||
}
|
||||
version, err := sendArgs.ValidateExistsAndGetVersion(ctx, fs)
|
||||
if err != nil {
|
||||
return v, err
|
||||
}
|
||||
return version, nil
|
||||
}
|
||||
|
||||
func (s *Sender) Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, io.ReadCloser, error) {
|
||||
|
||||
_, err := s.filterCheckFS(r.Filesystem)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
switch r.Encrypted {
|
||||
case pdu.Tri_DontCare:
|
||||
// use s.encrypt setting
|
||||
// ok, fallthrough outer
|
||||
case pdu.Tri_False:
|
||||
if s.encrypt.B {
|
||||
return nil, nil, errors.New("only encrypted sends allowed (send -w + encryption!= off), but unencrypted send requested")
|
||||
}
|
||||
// fallthrough outer
|
||||
case pdu.Tri_True:
|
||||
if !s.encrypt.B {
|
||||
return nil, nil, errors.New("only unencrypted sends allowed, but encrypted send requested")
|
||||
}
|
||||
// fallthrough outer
|
||||
default:
|
||||
return nil, nil, fmt.Errorf("unknown pdu.Tri variant %q", r.Encrypted)
|
||||
}
|
||||
|
||||
sendArgsUnvalidated := zfs.ZFSSendArgsUnvalidated{
|
||||
FS: r.Filesystem,
|
||||
From: uncheckedSendArgsFromPDU(r.GetFrom()), // validated by zfs.ZFSSendDry / zfs.ZFSSend
|
||||
To: uncheckedSendArgsFromPDU(r.GetTo()), // validated by zfs.ZFSSendDry / zfs.ZFSSend
|
||||
Encrypted: s.encrypt,
|
||||
ResumeToken: r.ResumeToken, // nil or not nil, depending on decoding success
|
||||
}
|
||||
|
||||
sendArgs, err := sendArgsUnvalidated.Validate(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrap(err, "validate send arguments")
|
||||
}
|
||||
|
||||
getLogger(ctx).Debug("acquire concurrent send semaphore")
|
||||
// TODO use try-acquire and fail with resource-exhaustion rpc status
|
||||
@@ -95,26 +291,153 @@ func (s *Sender) Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, zfs.St
|
||||
}
|
||||
defer guard.Release()
|
||||
|
||||
si, err := zfs.ZFSSendDry(r.Filesystem, r.From, r.To, "")
|
||||
si, err := zfs.ZFSSendDry(ctx, sendArgs)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, nil, errors.Wrap(err, "zfs send dry failed")
|
||||
}
|
||||
|
||||
// From now on, assume that sendArgs has been validated by ZFSSendDry
|
||||
// (because validation involves shelling out, it's actually a little expensive)
|
||||
|
||||
var expSize int64 = 0 // protocol says 0 means no estimate
|
||||
if si.SizeEstimate != -1 { // but si returns -1 for no size estimate
|
||||
expSize = si.SizeEstimate
|
||||
}
|
||||
res := &pdu.SendRes{ExpectedSize: expSize}
|
||||
res := &pdu.SendRes{
|
||||
ExpectedSize: expSize,
|
||||
UsedResumeToken: r.ResumeToken != "",
|
||||
}
|
||||
|
||||
if r.DryRun {
|
||||
return res, nil, nil
|
||||
}
|
||||
|
||||
streamCopier, err := zfs.ZFSSend(ctx, r.Filesystem, r.From, r.To, "")
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
// 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")
|
||||
}
|
||||
}
|
||||
return res, streamCopier, nil
|
||||
|
||||
// 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)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrapf(err, "cannot hold `to` version %q before starting send", sendArgs.ToVersion)
|
||||
}
|
||||
|
||||
// step holds & replication cursor released / moved forward in s.SendCompleted => s.moveCursorAndReleaseSendHolds
|
||||
|
||||
sendStream, err := zfs.ZFSSend(ctx, sendArgs)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrap(err, "zfs send failed")
|
||||
}
|
||||
return res, sendStream, nil
|
||||
}
|
||||
|
||||
func (p *Sender) SendCompleted(ctx context.Context, r *pdu.SendCompletedReq) (*pdu.SendCompletedRes, error) {
|
||||
|
||||
orig := r.GetOriginalReq() // may be nil, always use proto getters
|
||||
fsp, err := p.filterCheckFS(orig.GetFilesystem())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fs := fsp.ToString()
|
||||
|
||||
var from *zfs.FilesystemVersion
|
||||
if orig.GetFrom() != nil {
|
||||
f, err := sendArgsFromPDUAndValidateExistsAndGetVersion(ctx, fs, orig.GetFrom()) // no shadow
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "validate `from` exists")
|
||||
}
|
||||
from = &f
|
||||
}
|
||||
to, err := sendArgsFromPDUAndValidateExistsAndGetVersion(ctx, fs, orig.GetTo())
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "validate `to` exists")
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
log.Debug("move replication cursor to most recent common version")
|
||||
destroyedCursors, err := MoveReplicationCursor(ctx, fs, to, p.jobId)
|
||||
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)
|
||||
// it is correct to not release the hold if we can't move the cursor!
|
||||
return &pdu.SendCompletedRes{}, err
|
||||
}
|
||||
} else {
|
||||
log.Info("successfully moved replication cursor")
|
||||
}
|
||||
|
||||
// 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()
|
||||
log.Debug("release step-hold of or step-bookmark on `to`")
|
||||
err = ReleaseStep(ctx, fs, to, p.jobId)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("cannot release step-holds on or destroy step-bookmark of `to`")
|
||||
} else {
|
||||
log.Info("successfully released step-holds on or destroyed step-bookmark of `to`")
|
||||
}
|
||||
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
if from == nil {
|
||||
return
|
||||
}
|
||||
log.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.Info("`from` was a replication cursor and has already been destroyed")
|
||||
return
|
||||
}
|
||||
}
|
||||
// fallthrough
|
||||
}
|
||||
log.WithError(err).Error("cannot release step-holds on or destroy step-bookmark of `from`")
|
||||
} else {
|
||||
log.Info("successfully released step-holds on or destroyed step-bookmark of `from`")
|
||||
}
|
||||
}()
|
||||
wg.Wait()
|
||||
|
||||
return &pdu.SendCompletedRes{}, nil
|
||||
}
|
||||
|
||||
func (p *Sender) DestroySnapshots(ctx context.Context, req *pdu.DestroySnapshotsReq) (*pdu.DestroySnapshotsRes, error) {
|
||||
@@ -146,28 +469,17 @@ func (p *Sender) ReplicationCursor(ctx context.Context, req *pdu.ReplicationCurs
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch op := req.Op.(type) {
|
||||
case *pdu.ReplicationCursorReq_Get:
|
||||
cursor, err := zfs.ZFSGetReplicationCursor(dp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if cursor == nil {
|
||||
return &pdu.ReplicationCursorRes{Result: &pdu.ReplicationCursorRes_Notexist{Notexist: true}}, nil
|
||||
}
|
||||
return &pdu.ReplicationCursorRes{Result: &pdu.ReplicationCursorRes_Guid{Guid: cursor.Guid}}, nil
|
||||
case *pdu.ReplicationCursorReq_Set:
|
||||
guid, err := zfs.ZFSSetReplicationCursor(dp, op.Set.Snapshot)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &pdu.ReplicationCursorRes{Result: &pdu.ReplicationCursorRes_Guid{Guid: guid}}, nil
|
||||
default:
|
||||
return nil, errors.Errorf("unknown op %T", op)
|
||||
cursor, err := GetMostRecentReplicationCursorOfJob(ctx, dp.ToString(), p.jobId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if cursor == nil {
|
||||
return &pdu.ReplicationCursorRes{Result: &pdu.ReplicationCursorRes_Notexist{Notexist: true}}, nil
|
||||
}
|
||||
return &pdu.ReplicationCursorRes{Result: &pdu.ReplicationCursorRes_Guid{Guid: cursor.Guid}}, nil
|
||||
}
|
||||
|
||||
func (p *Sender) Receive(ctx context.Context, r *pdu.ReceiveReq, receive zfs.StreamCopier) (*pdu.ReceiveRes, error) {
|
||||
func (p *Sender) Receive(ctx context.Context, r *pdu.ReceiveReq, _ io.ReadCloser) (*pdu.ReceiveRes, error) {
|
||||
return nil, fmt.Errorf("sender does not implement Receive()")
|
||||
}
|
||||
|
||||
@@ -183,22 +495,42 @@ type FSMap interface { // FIXME unused
|
||||
AsFilter() FSFilter
|
||||
}
|
||||
|
||||
type ReceiverConfig struct {
|
||||
JobID JobID
|
||||
|
||||
RootWithoutClientComponent *zfs.DatasetPath // TODO use
|
||||
AppendClientIdentity bool
|
||||
|
||||
UpdateLastReceivedHold bool
|
||||
}
|
||||
|
||||
func (c *ReceiverConfig) copyIn() {
|
||||
c.RootWithoutClientComponent = c.RootWithoutClientComponent.Copy()
|
||||
}
|
||||
|
||||
func (c *ReceiverConfig) Validate() error {
|
||||
c.JobID.MustValidate()
|
||||
if c.RootWithoutClientComponent.Length() <= 0 {
|
||||
return errors.New("RootWithoutClientComponent must not be an empty dataset path")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Receiver implements replication.ReplicationEndpoint for a receiving side
|
||||
type Receiver struct {
|
||||
rootWithoutClientComponent *zfs.DatasetPath
|
||||
appendClientIdentity bool
|
||||
conf ReceiverConfig // validated
|
||||
|
||||
recvParentCreationMtx *chainlock.L
|
||||
}
|
||||
|
||||
func NewReceiver(rootDataset *zfs.DatasetPath, appendClientIdentity bool) *Receiver {
|
||||
if rootDataset.Length() <= 0 {
|
||||
panic(fmt.Sprintf("root dataset must not be an empty path: %v", rootDataset))
|
||||
func NewReceiver(config ReceiverConfig) *Receiver {
|
||||
config.copyIn()
|
||||
if err := config.Validate(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return &Receiver{
|
||||
rootWithoutClientComponent: rootDataset.Copy(),
|
||||
appendClientIdentity: appendClientIdentity,
|
||||
recvParentCreationMtx: chainlock.New(),
|
||||
conf: config,
|
||||
recvParentCreationMtx: chainlock.New(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,8 +553,8 @@ func clientRoot(rootFS *zfs.DatasetPath, clientIdentity string) (*zfs.DatasetPat
|
||||
}
|
||||
|
||||
func (s *Receiver) clientRootFromCtx(ctx context.Context) *zfs.DatasetPath {
|
||||
if !s.appendClientIdentity {
|
||||
return s.rootWithoutClientComponent.Copy()
|
||||
if !s.conf.AppendClientIdentity {
|
||||
return s.conf.RootWithoutClientComponent.Copy()
|
||||
}
|
||||
|
||||
clientIdentity, ok := ctx.Value(ClientIdentityKey).(string)
|
||||
@@ -230,7 +562,7 @@ func (s *Receiver) clientRootFromCtx(ctx context.Context) *zfs.DatasetPath {
|
||||
panic(fmt.Sprintf("ClientIdentityKey context value must be set"))
|
||||
}
|
||||
|
||||
clientRoot, err := clientRoot(s.rootWithoutClientComponent, clientIdentity)
|
||||
clientRoot, err := clientRoot(s.conf.RootWithoutClientComponent, clientIdentity)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("ClientIdentityContextKey must have been validated before invoking Receiver: %s", err))
|
||||
}
|
||||
@@ -271,7 +603,7 @@ func (s *Receiver) ListFilesystems(ctx context.Context, req *pdu.ListFilesystemR
|
||||
fss := make([]*pdu.Filesystem, 0, len(filtered))
|
||||
for _, a := range filtered {
|
||||
l := getLogger(ctx).WithField("fs", a)
|
||||
ph, err := zfs.ZFSGetFilesystemPlaceholderState(a)
|
||||
ph, err := zfs.ZFSGetFilesystemPlaceholderState(ctx, a)
|
||||
if err != nil {
|
||||
l.WithError(err).Error("error getting placeholder state")
|
||||
return nil, errors.Wrapf(err, "cannot get placeholder state for fs %q", a)
|
||||
@@ -282,8 +614,27 @@ func (s *Receiver) ListFilesystems(ctx context.Context, req *pdu.ListFilesystemR
|
||||
err := errors.Errorf("inconsistent placeholder state: filesystem %q must exist in this context", a.ToString())
|
||||
return nil, err
|
||||
}
|
||||
token, err := zfs.ZFSGetReceiveResumeTokenOrEmptyStringIfNotSupported(ctx, a)
|
||||
if err != nil {
|
||||
l.WithError(err).Error("cannot get receive resume token")
|
||||
return nil, err
|
||||
}
|
||||
encEnabled, err := zfs.ZFSGetEncryptionEnabled(ctx, a.ToString())
|
||||
if err != nil {
|
||||
l.WithError(err).Error("cannot get encryption enabled status")
|
||||
return nil, err
|
||||
}
|
||||
l.WithField("receive_resume_token", token).Debug("receive resume token")
|
||||
|
||||
a.TrimPrefix(root)
|
||||
fss = append(fss, &pdu.Filesystem{Path: a.ToString(), IsPlaceholder: ph.IsPlaceholder})
|
||||
|
||||
fs := &pdu.Filesystem{
|
||||
Path: a.ToString(),
|
||||
IsPlaceholder: ph.IsPlaceholder,
|
||||
ResumeToken: token,
|
||||
IsEncrypted: encEnabled,
|
||||
}
|
||||
fss = append(fss, fs)
|
||||
}
|
||||
if len(fss) == 0 {
|
||||
getLogger(ctx).Debug("no filesystems found")
|
||||
@@ -298,8 +649,9 @@ func (s *Receiver) ListFilesystemVersions(ctx context.Context, req *pdu.ListFile
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// TODO share following code with sender
|
||||
|
||||
fsvs, err := zfs.ZFSListFilesystemVersions(lp, nil)
|
||||
fsvs, err := zfs.ZFSListFilesystemVersions(ctx, lp, zfs.ListFilesystemVersionsOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -331,20 +683,28 @@ func (s *Receiver) ReplicationCursor(context.Context, *pdu.ReplicationCursorReq)
|
||||
return nil, fmt.Errorf("ReplicationCursor not implemented for Receiver")
|
||||
}
|
||||
|
||||
func (s *Receiver) Send(ctx context.Context, req *pdu.SendReq) (*pdu.SendRes, zfs.StreamCopier, error) {
|
||||
func (s *Receiver) Send(ctx context.Context, req *pdu.SendReq) (*pdu.SendRes, io.ReadCloser, error) {
|
||||
return nil, nil, fmt.Errorf("receiver does not implement Send()")
|
||||
}
|
||||
|
||||
var maxConcurrentZFSRecvSemaphore = semaphore.New(envconst.Int64("ZREPL_ENDPOINT_MAX_CONCURRENT_RECV", 10))
|
||||
|
||||
func (s *Receiver) Receive(ctx context.Context, req *pdu.ReceiveReq, receive zfs.StreamCopier) (*pdu.ReceiveRes, error) {
|
||||
func (s *Receiver) Receive(ctx context.Context, req *pdu.ReceiveReq, receive io.ReadCloser) (*pdu.ReceiveRes, error) {
|
||||
getLogger(ctx).Debug("incoming Receive")
|
||||
defer receive.Close()
|
||||
|
||||
root := s.clientRootFromCtx(ctx)
|
||||
lp, err := subroot{root}.MapToLocal(req.Filesystem)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, errors.Wrap(err, "`Filesystem` invalid")
|
||||
}
|
||||
|
||||
to := uncheckedSendArgsFromPDU(req.GetTo())
|
||||
if to == nil {
|
||||
return nil, errors.New("`To` must not be nil")
|
||||
}
|
||||
if !to.IsSnapshot() {
|
||||
return nil, errors.New("`To` must be a snapshot")
|
||||
}
|
||||
|
||||
// create placeholder parent filesystems as appropriate
|
||||
@@ -355,9 +715,9 @@ func (s *Receiver) Receive(ctx context.Context, req *pdu.ReceiveReq, receive zfs
|
||||
// ZFS dataset hierarchy subtrees.
|
||||
var visitErr error
|
||||
func() {
|
||||
getLogger(ctx).Debug("begin aquire recvParentCreationMtx")
|
||||
getLogger(ctx).Debug("begin acquire recvParentCreationMtx")
|
||||
defer s.recvParentCreationMtx.Lock().Unlock()
|
||||
getLogger(ctx).Debug("end aquire recvParentCreationMtx")
|
||||
getLogger(ctx).Debug("end acquire recvParentCreationMtx")
|
||||
defer getLogger(ctx).Debug("release recvParentCreationMtx")
|
||||
|
||||
f := zfs.NewDatasetPathForest()
|
||||
@@ -367,7 +727,7 @@ func (s *Receiver) Receive(ctx context.Context, req *pdu.ReceiveReq, receive zfs
|
||||
if v.Path.Equal(lp) {
|
||||
return false
|
||||
}
|
||||
ph, err := zfs.ZFSGetFilesystemPlaceholderState(v.Path)
|
||||
ph, err := zfs.ZFSGetFilesystemPlaceholderState(ctx, v.Path)
|
||||
getLogger(ctx).
|
||||
WithField("fs", v.Path.ToString()).
|
||||
WithField("placeholder_state", fmt.Sprintf("%#v", ph)).
|
||||
@@ -380,18 +740,18 @@ func (s *Receiver) Receive(ctx context.Context, req *pdu.ReceiveReq, receive zfs
|
||||
}
|
||||
|
||||
if !ph.FSExists {
|
||||
if s.rootWithoutClientComponent.HasPrefix(v.Path) {
|
||||
if s.conf.RootWithoutClientComponent.HasPrefix(v.Path) {
|
||||
if v.Path.Length() == 1 {
|
||||
visitErr = fmt.Errorf("pool %q not imported", v.Path.ToString())
|
||||
} else {
|
||||
visitErr = fmt.Errorf("root_fs %q does not exist", s.rootWithoutClientComponent.ToString())
|
||||
visitErr = fmt.Errorf("root_fs %q does not exist", s.conf.RootWithoutClientComponent.ToString())
|
||||
}
|
||||
getLogger(ctx).WithError(visitErr).Error("placeholders are only created automatically below root_fs")
|
||||
return false
|
||||
}
|
||||
l := getLogger(ctx).WithField("placeholder_fs", v.Path)
|
||||
l.Debug("create placeholder filesystem")
|
||||
err := zfs.ZFSCreatePlaceholderFilesystem(v.Path)
|
||||
err := zfs.ZFSCreatePlaceholderFilesystem(ctx, v.Path)
|
||||
if err != nil {
|
||||
l.WithError(err).Error("cannot create placeholder filesystem")
|
||||
visitErr = err
|
||||
@@ -408,21 +768,41 @@ func (s *Receiver) Receive(ctx context.Context, req *pdu.ReceiveReq, receive zfs
|
||||
return nil, visitErr
|
||||
}
|
||||
|
||||
log := getLogger(ctx).WithField("proto_fs", req.GetFilesystem()).WithField("local_fs", lp.ToString())
|
||||
|
||||
// determine whether we need to rollback the filesystem / change its placeholder state
|
||||
var clearPlaceholderProperty bool
|
||||
var recvOpts zfs.RecvOptions
|
||||
ph, err := zfs.ZFSGetFilesystemPlaceholderState(lp)
|
||||
if err == nil && ph.FSExists && ph.IsPlaceholder {
|
||||
ph, err := zfs.ZFSGetFilesystemPlaceholderState(ctx, lp)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "cannot get placeholder state")
|
||||
}
|
||||
log.WithField("placeholder_state", fmt.Sprintf("%#v", ph)).Debug("placeholder state")
|
||||
if ph.FSExists && ph.IsPlaceholder {
|
||||
recvOpts.RollbackAndForceRecv = true
|
||||
clearPlaceholderProperty = true
|
||||
}
|
||||
|
||||
if clearPlaceholderProperty {
|
||||
if err := zfs.ZFSSetPlaceholder(lp, false); err != nil {
|
||||
log.Info("clearing placeholder property")
|
||||
if err := zfs.ZFSSetPlaceholder(ctx, lp, false); err != nil {
|
||||
return nil, fmt.Errorf("cannot clear placeholder property for forced receive: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
getLogger(ctx).Debug("acquire concurrent recv semaphore")
|
||||
if req.ClearResumeToken && ph.FSExists {
|
||||
log.Info("clearing resume token")
|
||||
if err := zfs.ZFSRecvClearResumeToken(ctx, lp.ToString()); err != nil {
|
||||
return nil, errors.Wrap(err, "cannot clear resume token")
|
||||
}
|
||||
}
|
||||
|
||||
recvOpts.SavePartialRecvState, err = zfs.ResumeRecvSupported(ctx, lp)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "cannot determine whether we can use resumable send & recv")
|
||||
}
|
||||
|
||||
log.Debug("acquire concurrent recv semaphore")
|
||||
// TODO use try-acquire and fail with resource-exhaustion rpc status
|
||||
// => would require handling on the client-side
|
||||
// => this is a dataconn endpoint, doesn't have the status code semantics of gRPC
|
||||
@@ -432,15 +812,108 @@ func (s *Receiver) Receive(ctx context.Context, req *pdu.ReceiveReq, receive zfs
|
||||
}
|
||||
defer guard.Release()
|
||||
|
||||
getLogger(ctx).WithField("opts", fmt.Sprintf("%#v", recvOpts)).Debug("start receive command")
|
||||
log.Info("peeking 1M ahead")
|
||||
var peek bytes.Buffer
|
||||
var MaxPeek = envconst.Int64("ZREPL_ENDPOINT_RECV_PEEK_SIZE", 1<<20)
|
||||
if _, err := io.Copy(&peek, io.LimitReader(receive, MaxPeek)); err != nil {
|
||||
log.WithError(err).Error("cannot read peek-buffer from send stream")
|
||||
}
|
||||
var peekCopy bytes.Buffer
|
||||
if n, err := peekCopy.Write(peek.Bytes()); err != nil || n != peek.Len() {
|
||||
panic(peek.Len())
|
||||
}
|
||||
|
||||
if err := zfs.ZFSRecv(ctx, lp.ToString(), receive, recvOpts); err != nil {
|
||||
getLogger(ctx).
|
||||
log.WithField("opts", fmt.Sprintf("%#v", recvOpts)).Debug("start receive command")
|
||||
|
||||
snapFullPath := to.FullPath(lp.ToString())
|
||||
if err := zfs.ZFSRecv(ctx, lp.ToString(), to, chainedio.NewChainedReader(&peek, receive), recvOpts); err != nil {
|
||||
|
||||
// best-effort rollback of placeholder state if the recv didn't start
|
||||
_, resumableStatePresent := err.(*zfs.RecvFailedWithResumeTokenErr)
|
||||
disablePlaceholderRestoration := envconst.Bool("ZREPL_ENDPOINT_DISABLE_PLACEHOLDER_RESTORATION", false)
|
||||
placeholderRestored := !ph.IsPlaceholder
|
||||
if !disablePlaceholderRestoration && !resumableStatePresent && recvOpts.RollbackAndForceRecv && ph.FSExists && ph.IsPlaceholder && clearPlaceholderProperty {
|
||||
log.Info("restoring placeholder property")
|
||||
if phErr := zfs.ZFSSetPlaceholder(ctx, lp, true); phErr != nil {
|
||||
log.WithError(phErr).Error("cannot restore placeholder property after failed receive, subsequent replications will likely fail with a different error")
|
||||
// fallthrough
|
||||
} else {
|
||||
placeholderRestored = true
|
||||
}
|
||||
// fallthrough
|
||||
}
|
||||
|
||||
// deal with failing initial encrypted send & recv
|
||||
if _, ok := err.(*zfs.RecvDestroyOrOverwriteEncryptedErr); ok && ph.IsPlaceholder && placeholderRestored {
|
||||
msg := `cannot automatically replace placeholder filesystem with incoming send stream - please see receive-side log for details`
|
||||
err := errors.New(msg)
|
||||
log.Error(msg)
|
||||
|
||||
log.Error(`zrepl creates placeholder filesystems on the receiving side of a replication to match the sending side's dataset hierarchy`)
|
||||
log.Error(`zrepl uses zfs receive -F to replace those placeholders with incoming full sends`)
|
||||
log.Error(`OpenZFS native encryption prohibits zfs receive -F for encrypted filesystems`)
|
||||
log.Error(`the current zrepl placeholder filesystem concept is thus incompatible with OpenZFS native encryption`)
|
||||
|
||||
tempStartFullRecvFS := lp.Copy().ToString() + ".zrepl.initial-recv"
|
||||
tempStartFullRecvFSDP, dpErr := zfs.NewDatasetPath(tempStartFullRecvFS)
|
||||
if dpErr != nil {
|
||||
log.WithError(dpErr).Error("cannot determine temporary filesystem name for initial encrypted recv workaround")
|
||||
return nil, err // yes, err, not dpErr
|
||||
}
|
||||
|
||||
log := log.WithField("temp_recv_fs", tempStartFullRecvFS)
|
||||
log.Error(`as a workaround, zrepl will now attempt to re-receive the beginning of the stream into a temporary filesystem temp_recv_fs`)
|
||||
log.Error(`if that step succeeds: shut down zrepl and use 'zfs rename' to swap temp_recv_fs with local_fs, then restart zrepl`)
|
||||
log.Error(`replication will then resume using resumable send+recv`)
|
||||
|
||||
tempPH, phErr := zfs.ZFSGetFilesystemPlaceholderState(ctx, tempStartFullRecvFSDP)
|
||||
if phErr != nil {
|
||||
log.WithError(phErr).Error("cannot determine placeholder state of temp_recv_fs")
|
||||
return nil, err // yes, err, not dpErr
|
||||
}
|
||||
if tempPH.FSExists {
|
||||
log.Error("temp_recv_fs already exists, assuming a (partial) initial recv to that filesystem has already been done")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
recvOpts.RollbackAndForceRecv = false
|
||||
recvOpts.SavePartialRecvState = true
|
||||
rerecvErr := zfs.ZFSRecv(ctx, tempStartFullRecvFS, to, chainedio.NewChainedReader(&peekCopy), recvOpts)
|
||||
if _, isResumable := rerecvErr.(*zfs.RecvFailedWithResumeTokenErr); rerecvErr == nil || isResumable {
|
||||
log.Error("completed re-receive into temporary filesystem temp_recv_fs, now shut down zrepl and use zfs rename to swap temp_recv_fs with local_fs")
|
||||
} else {
|
||||
log.WithError(rerecvErr).Error("failed to receive the beginning of the stream into temporary filesystem temp_recv_fs")
|
||||
log.Error("we advise you to collect the error log and current configuration, open an issue on GitHub, and revert to your previous configuration in the meantime")
|
||||
}
|
||||
|
||||
log.Error(`if you would like to see improvements to this situation, please open an issue on GitHub`)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.
|
||||
WithError(err).
|
||||
WithField("opts", recvOpts).
|
||||
WithField("opts", fmt.Sprintf("%#v", recvOpts)).
|
||||
Error("zfs receive failed")
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// validate that we actually received what the sender claimed
|
||||
toRecvd, err := to.ValidateExistsAndGetVersion(ctx, lp.ToString())
|
||||
if err != nil {
|
||||
msg := "receive request's `To` version does not match what we received in the stream"
|
||||
log.WithError(err).WithField("snap", snapFullPath).Error(msg)
|
||||
log.Error("aborting recv request, but keeping received snapshot for inspection")
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
return &pdu.ReceiveRes{}, nil
|
||||
}
|
||||
|
||||
@@ -453,6 +926,19 @@ 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) {
|
||||
// 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(context.Context, *pdu.SendCompletedReq) (*pdu.SendCompletedRes, error) {
|
||||
return &pdu.SendCompletedRes{}, nil
|
||||
}
|
||||
|
||||
func doDestroySnapshots(ctx context.Context, lp *zfs.DatasetPath, snaps []*pdu.FilesystemVersion) (*pdu.DestroySnapshotsRes, error) {
|
||||
reqs := make([]*zfs.DestroySnapOp, len(snaps))
|
||||
ress := make([]*pdu.DestroySnapshotRes, len(snaps))
|
||||
|
||||
@@ -0,0 +1,835 @@
|
||||
package endpoint
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/zrepl/zrepl/util/envconst"
|
||||
"github.com/zrepl/zrepl/util/semaphore"
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
|
||||
type AbstractionType string
|
||||
|
||||
// Implementation note:
|
||||
// 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"
|
||||
)
|
||||
|
||||
var AbstractionTypesAll = map[AbstractionType]bool{
|
||||
AbstractionStepBookmark: true,
|
||||
AbstractionStepHold: true,
|
||||
AbstractionLastReceivedHold: true,
|
||||
AbstractionReplicationCursorBookmarkV1: true,
|
||||
AbstractionReplicationCursorBookmarkV2: true,
|
||||
}
|
||||
|
||||
// Implementation Note:
|
||||
// Whenever you add a new accessor, adjust AbstractionJSON.MarshalJSON accordingly
|
||||
type Abstraction interface {
|
||||
GetType() AbstractionType
|
||||
GetFS() string
|
||||
GetName() string
|
||||
GetFullPath() string
|
||||
GetJobID() *JobID // may return nil if the abstraction does not have a JobID
|
||||
GetCreateTXG() uint64
|
||||
GetFilesystemVersion() zfs.FilesystemVersion
|
||||
String() string
|
||||
// destroy the abstraction: either releases the hold or destroys the bookmark
|
||||
Destroy(context.Context) error
|
||||
json.Marshaler
|
||||
}
|
||||
|
||||
func (t AbstractionType) Validate() error {
|
||||
switch t {
|
||||
case AbstractionStepBookmark:
|
||||
return nil
|
||||
case AbstractionStepHold:
|
||||
return nil
|
||||
case AbstractionLastReceivedHold:
|
||||
return nil
|
||||
case AbstractionReplicationCursorBookmarkV1:
|
||||
return nil
|
||||
case AbstractionReplicationCursorBookmarkV2:
|
||||
return nil
|
||||
default:
|
||||
return errors.Errorf("unknown abstraction type %q", t)
|
||||
}
|
||||
}
|
||||
|
||||
func (t AbstractionType) MustValidate() error {
|
||||
if err := t.Validate(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type AbstractionJSON struct{ Abstraction }
|
||||
|
||||
var _ json.Marshaler = (*AbstractionJSON)(nil)
|
||||
|
||||
func (a AbstractionJSON) MarshalJSON() ([]byte, error) {
|
||||
type S struct {
|
||||
Type AbstractionType
|
||||
FS string
|
||||
Name string
|
||||
FullPath string
|
||||
JobID *JobID // may return nil if the abstraction does not have a JobID
|
||||
CreateTXG uint64
|
||||
FilesystemVersion zfs.FilesystemVersion
|
||||
String string
|
||||
}
|
||||
v := S{
|
||||
Type: a.Abstraction.GetType(),
|
||||
FS: a.Abstraction.GetFS(),
|
||||
Name: a.Abstraction.GetName(),
|
||||
FullPath: a.Abstraction.GetFullPath(),
|
||||
JobID: a.Abstraction.GetJobID(),
|
||||
CreateTXG: a.Abstraction.GetCreateTXG(),
|
||||
FilesystemVersion: a.Abstraction.GetFilesystemVersion(),
|
||||
String: a.Abstraction.String(),
|
||||
}
|
||||
return json.Marshal(v)
|
||||
}
|
||||
|
||||
type AbstractionTypeSet map[AbstractionType]bool
|
||||
|
||||
func AbstractionTypeSetFromStrings(sts []string) (AbstractionTypeSet, error) {
|
||||
ats := make(map[AbstractionType]bool, len(sts))
|
||||
for i, t := range sts {
|
||||
at := AbstractionType(t)
|
||||
if err := at.Validate(); err != nil {
|
||||
return nil, errors.Wrapf(err, "invalid abstraction type #%d %q", i+1, t)
|
||||
}
|
||||
ats[at] = true
|
||||
}
|
||||
return ats, nil
|
||||
}
|
||||
|
||||
func (s AbstractionTypeSet) ContainsAll(q AbstractionTypeSet) bool {
|
||||
for k := range q {
|
||||
if _, ok := s[k]; !ok {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (s AbstractionTypeSet) ContainsAnyOf(q AbstractionTypeSet) bool {
|
||||
for k := range q {
|
||||
if _, ok := s[k]; ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s AbstractionTypeSet) String() string {
|
||||
sts := make([]string, 0, len(s))
|
||||
for i := range s {
|
||||
sts = append(sts, string(i))
|
||||
}
|
||||
sts = sort.StringSlice(sts)
|
||||
return strings.Join(sts, ",")
|
||||
}
|
||||
|
||||
func (s AbstractionTypeSet) Validate() error {
|
||||
for k := range s {
|
||||
if err := k.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type BookmarkExtractor func(fs *zfs.DatasetPath, v zfs.FilesystemVersion) Abstraction
|
||||
|
||||
// returns nil if the abstraction type is not bookmark-based
|
||||
func (t AbstractionType) BookmarkExtractor() BookmarkExtractor {
|
||||
switch t {
|
||||
case AbstractionStepBookmark:
|
||||
return StepBookmarkExtractor
|
||||
case AbstractionReplicationCursorBookmarkV1:
|
||||
return ReplicationCursorV1Extractor
|
||||
case AbstractionReplicationCursorBookmarkV2:
|
||||
return ReplicationCursorV2Extractor
|
||||
case AbstractionStepHold:
|
||||
return nil
|
||||
case AbstractionLastReceivedHold:
|
||||
return nil
|
||||
default:
|
||||
panic(fmt.Sprintf("unimpl: %q", t))
|
||||
}
|
||||
}
|
||||
|
||||
type HoldExtractor = func(fs *zfs.DatasetPath, v zfs.FilesystemVersion, tag string) Abstraction
|
||||
|
||||
// returns nil if the abstraction type is not hold-based
|
||||
func (t AbstractionType) HoldExtractor() HoldExtractor {
|
||||
switch t {
|
||||
case AbstractionStepBookmark:
|
||||
return nil
|
||||
case AbstractionReplicationCursorBookmarkV1:
|
||||
return nil
|
||||
case AbstractionReplicationCursorBookmarkV2:
|
||||
return nil
|
||||
case AbstractionStepHold:
|
||||
return StepHoldExtractor
|
||||
case AbstractionLastReceivedHold:
|
||||
return LastReceivedHoldExtractor
|
||||
default:
|
||||
panic(fmt.Sprintf("unimpl: %q", t))
|
||||
}
|
||||
}
|
||||
|
||||
type ListZFSHoldsAndBookmarksQuery struct {
|
||||
FS ListZFSHoldsAndBookmarksQueryFilesystemFilter
|
||||
// What abstraction types should match (any contained in the set)
|
||||
What AbstractionTypeSet
|
||||
|
||||
// The output for the query must satisfy _all_ (AND) requirements of all fields in this query struct.
|
||||
|
||||
// if not nil: JobID of the hold or bookmark in question must be equal
|
||||
// else: JobID of the hold or bookmark can be any value
|
||||
JobID *JobID
|
||||
|
||||
// zero-value means any CreateTXG is acceptable
|
||||
CreateTXG CreateTXGRange
|
||||
|
||||
// Number of concurrently queried filesystems. Must be >= 1
|
||||
Concurrency int64
|
||||
}
|
||||
|
||||
type CreateTXGRangeBound struct {
|
||||
CreateTXG uint64
|
||||
Inclusive *zfs.NilBool // must not be nil
|
||||
}
|
||||
|
||||
// A non-empty range of CreateTXGs
|
||||
//
|
||||
// If both Since and Until are nil, any CreateTXG is acceptable
|
||||
type CreateTXGRange struct {
|
||||
// if not nil: The hold's snapshot or the bookmark's createtxg must be greater than (or equal) Since
|
||||
// else: CreateTXG of the hold or bookmark can be any value accepted by Until
|
||||
Since *CreateTXGRangeBound
|
||||
// if not nil: The hold's snapshot or the bookmark's createtxg must be less than (or equal) Until
|
||||
// else: CreateTXG of the hold or bookmark can be any value accepted by Since
|
||||
Until *CreateTXGRangeBound
|
||||
}
|
||||
|
||||
// FS == nil XOR Filter == nil
|
||||
type ListZFSHoldsAndBookmarksQueryFilesystemFilter struct {
|
||||
FS *string
|
||||
Filter zfs.DatasetFilter
|
||||
}
|
||||
|
||||
func (q *ListZFSHoldsAndBookmarksQuery) Validate() error {
|
||||
if err := q.FS.Validate(); err != nil {
|
||||
return errors.Wrap(err, "FS")
|
||||
}
|
||||
if q.JobID != nil {
|
||||
q.JobID.MustValidate() // FIXME
|
||||
}
|
||||
if err := q.CreateTXG.Validate(); err != nil {
|
||||
return errors.Wrap(err, "CreateTXGRange")
|
||||
}
|
||||
if err := q.What.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
if q.Concurrency < 1 {
|
||||
return errors.New("Concurrency must be >= 1")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var createTXGRangeBoundAllowCreateTXG0 = envconst.Bool("ZREPL_ENDPOINT_LIST_ABSTRACTIONS_QUERY_CREATETXG_RANGE_BOUND_ALLOW_0", false)
|
||||
|
||||
func (i *CreateTXGRangeBound) Validate() error {
|
||||
if err := i.Inclusive.Validate(); err != nil {
|
||||
return errors.Wrap(err, "Inclusive")
|
||||
}
|
||||
if i.CreateTXG == 0 && !createTXGRangeBoundAllowCreateTXG0 {
|
||||
return errors.New("CreateTXG must be non-zero")
|
||||
}
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func (f *ListZFSHoldsAndBookmarksQueryFilesystemFilter) Validate() error {
|
||||
if f == nil {
|
||||
return nil
|
||||
}
|
||||
fsSet := f.FS != nil
|
||||
filterSet := f.Filter != nil
|
||||
if fsSet && filterSet || !fsSet && !filterSet {
|
||||
return fmt.Errorf("must set FS or Filter field, but fsIsSet=%v and filterIsSet=%v", fsSet, filterSet)
|
||||
}
|
||||
if fsSet {
|
||||
if err := zfs.EntityNamecheck(*f.FS, zfs.EntityTypeFilesystem); err != nil {
|
||||
return errors.Wrap(err, "FS invalid")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *ListZFSHoldsAndBookmarksQueryFilesystemFilter) Filesystems(ctx context.Context) ([]string, error) {
|
||||
if err := f.Validate(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if f.FS != nil {
|
||||
return []string{*f.FS}, nil
|
||||
}
|
||||
if f.Filter != nil {
|
||||
dps, err := zfs.ZFSListMapping(ctx, f.Filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fss := make([]string, len(dps))
|
||||
for i, dp := range dps {
|
||||
fss[i] = dp.ToString()
|
||||
}
|
||||
return fss, nil
|
||||
}
|
||||
panic("unreachable")
|
||||
}
|
||||
|
||||
func (r *CreateTXGRange) Validate() error {
|
||||
if r.Since != nil {
|
||||
if err := r.Since.Validate(); err != nil {
|
||||
return errors.Wrap(err, "Since")
|
||||
}
|
||||
}
|
||||
if r.Until != nil {
|
||||
if err := r.Until.Validate(); err != nil {
|
||||
return errors.Wrap(err, "Until")
|
||||
}
|
||||
}
|
||||
if _, err := r.effectiveBounds(); err != nil {
|
||||
return errors.Wrapf(err, "specified range %s is semantically invalid", r)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// inclusive-inclusive bounds
|
||||
type effectiveBounds struct {
|
||||
sinceInclusive uint64
|
||||
sinceUnbounded bool
|
||||
untilInclusive uint64
|
||||
untilUnbounded bool
|
||||
}
|
||||
|
||||
// callers must have validated r.Since and r.Until before calling this method
|
||||
func (r *CreateTXGRange) effectiveBounds() (bounds effectiveBounds, err error) {
|
||||
|
||||
bounds.sinceUnbounded = r.Since == nil
|
||||
bounds.untilUnbounded = r.Until == nil
|
||||
|
||||
if r.Since == nil && r.Until == nil {
|
||||
return bounds, nil
|
||||
}
|
||||
|
||||
if r.Since != nil {
|
||||
bounds.sinceInclusive = r.Since.CreateTXG
|
||||
if !r.Since.Inclusive.B {
|
||||
if r.Since.CreateTXG == math.MaxUint64 {
|
||||
return bounds, errors.Errorf("Since-exclusive (%v) must be less than math.MaxUint64 (%v)",
|
||||
r.Since.CreateTXG, uint64(math.MaxUint64))
|
||||
}
|
||||
bounds.sinceInclusive++
|
||||
}
|
||||
}
|
||||
|
||||
if r.Until != nil {
|
||||
bounds.untilInclusive = r.Until.CreateTXG
|
||||
if !r.Until.Inclusive.B {
|
||||
if r.Until.CreateTXG == 0 {
|
||||
return bounds, errors.Errorf("Until-exclusive (%v) must be greater than 0", r.Until.CreateTXG)
|
||||
}
|
||||
bounds.untilInclusive--
|
||||
}
|
||||
}
|
||||
|
||||
if !bounds.sinceUnbounded && !bounds.untilUnbounded {
|
||||
if bounds.sinceInclusive >= bounds.untilInclusive {
|
||||
return bounds, errors.Errorf("effective range bounds are [%v,%v] which is empty or invalid", bounds.sinceInclusive, bounds.untilInclusive)
|
||||
}
|
||||
// fallthrough
|
||||
}
|
||||
|
||||
return bounds, nil
|
||||
}
|
||||
|
||||
func (r *CreateTXGRange) String() string {
|
||||
var buf strings.Builder
|
||||
if r.Since == nil {
|
||||
fmt.Fprintf(&buf, "~")
|
||||
} else {
|
||||
if err := r.Since.Inclusive.Validate(); err != nil {
|
||||
fmt.Fprintf(&buf, "?")
|
||||
} else if r.Since.Inclusive.B {
|
||||
fmt.Fprintf(&buf, "[")
|
||||
} else {
|
||||
fmt.Fprintf(&buf, "(")
|
||||
}
|
||||
fmt.Fprintf(&buf, "%d", r.Since.CreateTXG)
|
||||
}
|
||||
|
||||
fmt.Fprintf(&buf, ",")
|
||||
|
||||
if r.Until == nil {
|
||||
fmt.Fprintf(&buf, "~")
|
||||
} else {
|
||||
fmt.Fprintf(&buf, "%d", r.Until.CreateTXG)
|
||||
if err := r.Until.Inclusive.Validate(); err != nil {
|
||||
fmt.Fprintf(&buf, "?")
|
||||
} else if r.Until.Inclusive.B {
|
||||
fmt.Fprintf(&buf, "]")
|
||||
} else {
|
||||
fmt.Fprintf(&buf, ")")
|
||||
}
|
||||
}
|
||||
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// panics if not .Validate()
|
||||
func (r *CreateTXGRange) IsUnbounded() bool {
|
||||
if err := r.Validate(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
bounds, err := r.effectiveBounds()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return bounds.sinceUnbounded && bounds.untilUnbounded
|
||||
}
|
||||
|
||||
// panics if not .Validate()
|
||||
func (r *CreateTXGRange) Contains(qCreateTxg uint64) bool {
|
||||
if err := r.Validate(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
bounds, err := r.effectiveBounds()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
sinceMatches := bounds.sinceUnbounded || bounds.sinceInclusive <= qCreateTxg
|
||||
untilMatches := bounds.untilUnbounded || qCreateTxg <= bounds.untilInclusive
|
||||
|
||||
return sinceMatches && untilMatches
|
||||
}
|
||||
|
||||
type ListAbstractionsError struct {
|
||||
FS string
|
||||
Snap string
|
||||
What string
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e ListAbstractionsError) Error() string {
|
||||
if e.FS == "" {
|
||||
return fmt.Sprintf("list endpoint abstractions: %s: %s", e.What, e.Err)
|
||||
} else {
|
||||
v := e.FS
|
||||
if e.Snap != "" {
|
||||
v = fmt.Sprintf("%s@%s", e.FS, e.Snap)
|
||||
}
|
||||
return fmt.Sprintf("list endpoint abstractions on %q: %s: %s", v, e.What, e.Err)
|
||||
}
|
||||
}
|
||||
|
||||
type putListAbstractionErr func(err error, fs string, what string)
|
||||
type putListAbstraction func(a Abstraction)
|
||||
|
||||
type ListAbstractionsErrors []ListAbstractionsError
|
||||
|
||||
func (e ListAbstractionsErrors) Error() string {
|
||||
if len(e) == 0 {
|
||||
panic(e)
|
||||
}
|
||||
if len(e) == 1 {
|
||||
return fmt.Sprintf("list endpoint abstractions: %s", e[0])
|
||||
}
|
||||
msgs := make([]string, len(e))
|
||||
for i := range e {
|
||||
msgs[i] = e.Error()
|
||||
}
|
||||
return fmt.Sprintf("list endpoint abstractions: multiple errors:\n%s", strings.Join(msgs, "\n"))
|
||||
}
|
||||
|
||||
func ListAbstractions(ctx context.Context, query ListZFSHoldsAndBookmarksQuery) (out []Abstraction, outErrs []ListAbstractionsError, err error) {
|
||||
outChan, outErrsChan, err := ListAbstractionsStreamed(ctx, query)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for a := range outChan {
|
||||
out = append(out, a)
|
||||
}
|
||||
}()
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for err := range outErrsChan {
|
||||
outErrs = append(outErrs, err)
|
||||
}
|
||||
}()
|
||||
wg.Wait()
|
||||
return out, outErrs, nil
|
||||
}
|
||||
|
||||
// if err != nil, the returned channels are both nil
|
||||
// if err == nil, both channels must be fully drained by the caller to avoid leaking goroutines
|
||||
func ListAbstractionsStreamed(ctx context.Context, query ListZFSHoldsAndBookmarksQuery) (<-chan Abstraction, <-chan ListAbstractionsError, error) {
|
||||
|
||||
// impl note: structure the query processing in such a way that
|
||||
// a minimum amount of zfs shell-outs needs to be done
|
||||
|
||||
if err := query.Validate(); err != nil {
|
||||
return nil, nil, errors.Wrap(err, "validate query")
|
||||
}
|
||||
|
||||
fss, err := query.FS.Filesystems(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrap(err, "list filesystems")
|
||||
}
|
||||
|
||||
outErrs := make(chan ListAbstractionsError)
|
||||
out := make(chan Abstraction)
|
||||
|
||||
errCb := func(err error, fs string, what string) {
|
||||
outErrs <- ListAbstractionsError{Err: err, FS: fs, What: what}
|
||||
}
|
||||
emitAbstraction := func(a Abstraction) {
|
||||
jobIdMatches := query.JobID == nil || a.GetJobID() == nil || *a.GetJobID() == *query.JobID
|
||||
|
||||
createTXGMatches := query.CreateTXG.Contains(a.GetCreateTXG())
|
||||
|
||||
if jobIdMatches && createTXGMatches {
|
||||
out <- a
|
||||
}
|
||||
}
|
||||
|
||||
sem := semaphore.New(int64(query.Concurrency))
|
||||
go func() {
|
||||
defer close(out)
|
||||
defer close(outErrs)
|
||||
var wg sync.WaitGroup
|
||||
defer wg.Wait()
|
||||
for i := range fss {
|
||||
wg.Add(1)
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
g, err := sem.Acquire(ctx)
|
||||
if err != nil {
|
||||
errCb(err, fss[i], err.Error())
|
||||
return
|
||||
}
|
||||
func() {
|
||||
defer g.Release()
|
||||
listAbstractionsImplFS(ctx, fss[i], &query, emitAbstraction, errCb)
|
||||
}()
|
||||
}(i)
|
||||
}
|
||||
}()
|
||||
|
||||
return out, outErrs, nil
|
||||
}
|
||||
|
||||
func listAbstractionsImplFS(ctx context.Context, fs string, query *ListZFSHoldsAndBookmarksQuery, emitCandidate putListAbstraction, errCb putListAbstractionErr) {
|
||||
fsp, err := zfs.NewDatasetPath(fs)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if len(query.What) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
whatTypes := zfs.VersionTypeSet{}
|
||||
for what := range query.What {
|
||||
if e := what.BookmarkExtractor(); e != nil {
|
||||
whatTypes[zfs.Bookmark] = true
|
||||
}
|
||||
if e := what.HoldExtractor(); e != nil {
|
||||
whatTypes[zfs.Snapshot] = true
|
||||
}
|
||||
}
|
||||
fsvs, err := zfs.ZFSListFilesystemVersions(ctx, fsp, zfs.ListFilesystemVersionsOptions{
|
||||
Types: whatTypes,
|
||||
})
|
||||
if err != nil {
|
||||
errCb(err, fs, "list filesystem versions")
|
||||
return
|
||||
}
|
||||
|
||||
for at := range query.What {
|
||||
bmE := at.BookmarkExtractor()
|
||||
holdE := at.HoldExtractor()
|
||||
if bmE == nil && holdE == nil || bmE != nil && holdE != nil {
|
||||
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 v.Type == zfs.Snapshot && holdE != nil && query.CreateTXG.Contains(v.GetCreateTXG()) && (!v.UserRefs.Valid || v.UserRefs.Value > 0) {
|
||||
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 != nil {
|
||||
emitCandidate(a)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type BatchDestroyResult struct {
|
||||
Abstraction
|
||||
DestroyErr error
|
||||
}
|
||||
|
||||
var _ json.Marshaler = (*BatchDestroyResult)(nil)
|
||||
|
||||
func (r BatchDestroyResult) MarshalJSON() ([]byte, error) {
|
||||
err := ""
|
||||
if r.DestroyErr != nil {
|
||||
err = r.DestroyErr.Error()
|
||||
}
|
||||
s := struct {
|
||||
Abstraction AbstractionJSON
|
||||
DestroyErr string
|
||||
}{
|
||||
AbstractionJSON{r.Abstraction},
|
||||
err,
|
||||
}
|
||||
return json.Marshal(s)
|
||||
}
|
||||
|
||||
func BatchDestroy(ctx context.Context, abs []Abstraction) <-chan BatchDestroyResult {
|
||||
// hold-based batching: per snapshot
|
||||
// bookmark-based batching: none possible via CLI
|
||||
// => not worth the trouble for now, will be worth it once we start using channel programs
|
||||
// => TODO: actual batching using channel programs
|
||||
res := make(chan BatchDestroyResult, len(abs))
|
||||
go func() {
|
||||
for _, a := range abs {
|
||||
res <- BatchDestroyResult{
|
||||
a,
|
||||
a.Destroy(ctx),
|
||||
}
|
||||
}
|
||||
close(res)
|
||||
}()
|
||||
return res
|
||||
}
|
||||
|
||||
type StalenessInfo struct {
|
||||
ConstructedWithQuery ListZFSHoldsAndBookmarksQuery
|
||||
Live []Abstraction
|
||||
Stale []Abstraction
|
||||
}
|
||||
|
||||
type fsAndJobId struct {
|
||||
fs string
|
||||
jobId JobID
|
||||
}
|
||||
|
||||
type ListStaleQueryError struct {
|
||||
error
|
||||
}
|
||||
|
||||
// returns *ListStaleQueryError if the given query cannot be used for determining staleness info
|
||||
func ListStale(ctx context.Context, q ListZFSHoldsAndBookmarksQuery) (*StalenessInfo, error) {
|
||||
if !q.CreateTXG.IsUnbounded() {
|
||||
// we must determine the most recent step per FS, can't allow that
|
||||
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)
|
||||
ifAnyThenAll := AbstractionTypeSet{
|
||||
AbstractionStepHold: true,
|
||||
AbstractionStepBookmark: true,
|
||||
AbstractionReplicationCursorBookmarkV2: true,
|
||||
}
|
||||
if q.What.ContainsAnyOf(ifAnyThenAll) && !q.What.ContainsAll(ifAnyThenAll) {
|
||||
return nil, &ListStaleQueryError{errors.Errorf("ListStale requires query to ask for all of %s", ifAnyThenAll.String())}
|
||||
}
|
||||
|
||||
// ----------------- done validating query for listStaleFiltering -----------------------
|
||||
|
||||
qAbs, absErr, err := ListAbstractions(ctx, q)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(absErr) > 0 {
|
||||
// can't go on here because we can't determine the most recent step
|
||||
return nil, ListAbstractionsErrors(absErr)
|
||||
}
|
||||
|
||||
si := listStaleFiltering(qAbs, q.CreateTXG.Since)
|
||||
si.ConstructedWithQuery = q
|
||||
return si, nil
|
||||
}
|
||||
|
||||
type fsAjobAtype struct {
|
||||
fsAndJobId
|
||||
Type AbstractionType
|
||||
}
|
||||
|
||||
// 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
|
||||
// (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.
|
||||
//
|
||||
// the returned StalenessInfo.ConstructedWithQuery is not set
|
||||
func listStaleFiltering(abs []Abstraction, sinceBound *CreateTXGRangeBound) *StalenessInfo {
|
||||
|
||||
var noJobId []Abstraction
|
||||
by := make(map[fsAjobAtype][]Abstraction)
|
||||
for _, a := range abs {
|
||||
if a.GetJobID() == nil {
|
||||
noJobId = append(noJobId, a)
|
||||
continue
|
||||
}
|
||||
faj := fsAjobAtype{fsAndJobId{a.GetFS(), *a.GetJobID()}, a.GetType()}
|
||||
l := by[faj]
|
||||
l = append(l, a)
|
||||
by[faj] = l
|
||||
}
|
||||
|
||||
type stepFirstNotStaleCandidate struct {
|
||||
cursor *Abstraction
|
||||
step *Abstraction
|
||||
}
|
||||
stepFirstNotStaleCandidates := make(map[fsAndJobId]stepFirstNotStaleCandidate) // empty map => will always return nil
|
||||
for _, a := range abs {
|
||||
key := fsAndJobId{a.GetFS(), *a.GetJobID()}
|
||||
c := stepFirstNotStaleCandidates[key]
|
||||
|
||||
switch a.GetType() {
|
||||
// stepFirstNotStaleCandidate.cursor
|
||||
case AbstractionReplicationCursorBookmarkV2:
|
||||
if c.cursor == nil || (*c.cursor).GetCreateTXG() < a.GetCreateTXG() {
|
||||
a := a
|
||||
c.cursor = &a
|
||||
}
|
||||
|
||||
// stepFirstNotStaleCandidate.step
|
||||
case AbstractionStepBookmark:
|
||||
fallthrough
|
||||
case AbstractionStepHold:
|
||||
if c.step == nil || (*c.step).GetCreateTXG() < a.GetCreateTXG() {
|
||||
a := a
|
||||
c.step = &a
|
||||
}
|
||||
|
||||
// not interested in the others
|
||||
default:
|
||||
continue // not relevant
|
||||
|
||||
}
|
||||
|
||||
stepFirstNotStaleCandidates[key] = c
|
||||
}
|
||||
|
||||
ret := &StalenessInfo{
|
||||
Live: noJobId,
|
||||
Stale: []Abstraction{},
|
||||
}
|
||||
|
||||
for k := range by {
|
||||
l := by[k]
|
||||
|
||||
if k.Type == AbstractionStepHold || k.Type == AbstractionStepBookmark {
|
||||
// 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
|
||||
// to consider all steps stale (...at first)
|
||||
var untilBound *CreateTXGRangeBound
|
||||
{
|
||||
sfnsc := stepFirstNotStaleCandidates[k.fsAndJobId]
|
||||
|
||||
// if there's a replication cursor, use it as a cutoff between live and stale
|
||||
// if there's none, we are in initial replication and only need to keep
|
||||
// the most recent step hold live, since that's what our initial replication strategy
|
||||
// uses (both initially and on resume)
|
||||
// (FIXME hardcoded replication strategy)
|
||||
if sfnsc.cursor != nil {
|
||||
untilBound = &CreateTXGRangeBound{
|
||||
CreateTXG: (*sfnsc.cursor).GetCreateTXG(),
|
||||
// if we have a cursor, can throw away step hold on both From and To
|
||||
Inclusive: &zfs.NilBool{B: true},
|
||||
}
|
||||
} else if sfnsc.step != nil {
|
||||
untilBound = &CreateTXGRangeBound{
|
||||
CreateTXG: (*sfnsc.step).GetCreateTXG(),
|
||||
// if we don't have a cursor, the step most recent step hold is our
|
||||
// initial replication cursor and it's possibly still live (interrupted initial replication)
|
||||
Inclusive: &zfs.NilBool{B: false},
|
||||
}
|
||||
} else {
|
||||
untilBound = nil // consider everything stale
|
||||
}
|
||||
}
|
||||
staleRange := CreateTXGRange{
|
||||
Since: sinceBound,
|
||||
Until: untilBound,
|
||||
}
|
||||
|
||||
// partition by staleRange
|
||||
for _, a := range l {
|
||||
if staleRange.Contains(a.GetCreateTXG()) {
|
||||
ret.Stale = append(ret.Stale, a)
|
||||
} else {
|
||||
ret.Live = append(ret.Live, a)
|
||||
}
|
||||
}
|
||||
|
||||
} else if k.Type == AbstractionReplicationCursorBookmarkV2 || k.Type == AbstractionLastReceivedHold {
|
||||
// all but the most recent are stale by definition (we always _move_ them)
|
||||
// NOTE: must not use firstNotStale in this branch, not computed for these types
|
||||
|
||||
// sort descending (highest createtxg first), then cut off
|
||||
sort.Slice(l, func(i, j int) bool {
|
||||
return l[i].GetCreateTXG() > l[j].GetCreateTXG()
|
||||
})
|
||||
if len(l) > 0 {
|
||||
ret.Live = append(ret.Live, l[0])
|
||||
ret.Stale = append(ret.Stale, l[1:]...)
|
||||
}
|
||||
} else {
|
||||
ret.Live = append(ret.Live, l...)
|
||||
}
|
||||
}
|
||||
|
||||
return ret
|
||||
|
||||
}
|
||||
@@ -0,0 +1,386 @@
|
||||
package endpoint
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sort"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/zrepl/zrepl/util/errorarray"
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
|
||||
const replicationCursorBookmarkNamePrefix = "zrepl_CURSOR"
|
||||
|
||||
func ReplicationCursorBookmarkName(fs string, guid uint64, id JobID) (string, error) {
|
||||
return replicationCursorBookmarkNameImpl(fs, guid, id.String())
|
||||
}
|
||||
|
||||
func replicationCursorBookmarkNameImpl(fs string, guid uint64, jobid string) (string, error) {
|
||||
return makeJobAndGuidBookmarkName(replicationCursorBookmarkNamePrefix, fs, guid, jobid)
|
||||
}
|
||||
|
||||
var ErrV1ReplicationCursor = fmt.Errorf("bookmark name is a v1-replication cursor")
|
||||
|
||||
//err != nil always means that the bookmark is not a valid replication bookmark
|
||||
//
|
||||
// Returns ErrV1ReplicationCursor as error if the bookmark is a v1 replication cursor
|
||||
func ParseReplicationCursorBookmarkName(fullname string) (uint64, JobID, error) {
|
||||
|
||||
// check for legacy cursors
|
||||
{
|
||||
if err := zfs.EntityNamecheck(fullname, zfs.EntityTypeBookmark); err != nil {
|
||||
return 0, JobID{}, errors.Wrap(err, "parse replication cursor bookmark name")
|
||||
}
|
||||
_, _, name, err := zfs.DecomposeVersionString(fullname)
|
||||
if err != nil {
|
||||
return 0, JobID{}, errors.Wrap(err, "parse replication cursor bookmark name: decompose version string")
|
||||
}
|
||||
const V1ReplicationCursorBookmarkName = "zrepl_replication_cursor"
|
||||
if name == V1ReplicationCursorBookmarkName {
|
||||
return 0, JobID{}, ErrV1ReplicationCursor
|
||||
}
|
||||
// fallthrough to main parser
|
||||
}
|
||||
|
||||
guid, jobID, err := parseJobAndGuidBookmarkName(fullname, replicationCursorBookmarkNamePrefix)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "parse replication cursor 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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
candidates, err := GetReplicationCursors(ctx, fsp, jobID)
|
||||
if err != nil || len(candidates) == 0 {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sort.Slice(candidates, func(i, j int) bool {
|
||||
return candidates[i].CreateTXG < candidates[j].CreateTXG
|
||||
})
|
||||
|
||||
mostRecent := candidates[len(candidates)-1]
|
||||
return &mostRecent, nil
|
||||
}
|
||||
|
||||
func GetReplicationCursors(ctx context.Context, dp *zfs.DatasetPath, jobID JobID) ([]zfs.FilesystemVersion, error) {
|
||||
|
||||
fs := dp.ToString()
|
||||
q := ListZFSHoldsAndBookmarksQuery{
|
||||
FS: ListZFSHoldsAndBookmarksQueryFilesystemFilter{FS: &fs},
|
||||
What: map[AbstractionType]bool{
|
||||
AbstractionReplicationCursorBookmarkV1: true,
|
||||
AbstractionReplicationCursorBookmarkV2: true,
|
||||
},
|
||||
JobID: &jobID,
|
||||
CreateTXG: CreateTXGRange{},
|
||||
Concurrency: 1,
|
||||
}
|
||||
abs, absErr, err := ListAbstractions(ctx, q)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "get replication cursor: list bookmarks and holds")
|
||||
}
|
||||
if len(absErr) > 0 {
|
||||
return nil, ListAbstractionsErrors(absErr)
|
||||
}
|
||||
|
||||
var v1, v2 []Abstraction
|
||||
for _, a := range abs {
|
||||
switch a.GetType() {
|
||||
case AbstractionReplicationCursorBookmarkV1:
|
||||
v1 = append(v1, a)
|
||||
case AbstractionReplicationCursorBookmarkV2:
|
||||
v2 = append(v2, a)
|
||||
default:
|
||||
panic("unexpected abstraction: " + a.GetType())
|
||||
}
|
||||
}
|
||||
|
||||
if len(v1) > 0 {
|
||||
getLogger(ctx).WithField("bookmark", v1).
|
||||
Warn("found v1-replication cursor bookmarks, consider running migration 'replication-cursor:v1-v2' after successful replication with this zrepl version")
|
||||
}
|
||||
|
||||
candidates := make([]zfs.FilesystemVersion, 0)
|
||||
for _, v := range v2 {
|
||||
candidates = append(candidates, v.GetFilesystemVersion())
|
||||
}
|
||||
|
||||
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.
|
||||
//
|
||||
// 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) {
|
||||
|
||||
if !target.IsSnapshot() {
|
||||
return nil, zfs.ErrBookmarkCloningNotSupported
|
||||
}
|
||||
|
||||
bookmarkname, err := ReplicationCursorBookmarkName(fs, target.GetGuid(), jobID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "determine replication cursor name")
|
||||
}
|
||||
|
||||
// idempotently create bookmark (guid is encoded in it, hence we'll most likely add a new one
|
||||
// cleanup the old one afterwards
|
||||
|
||||
err = zfs.ZFSBookmark(ctx, fs, target.ToSendArgVersion(), bookmarkname)
|
||||
if err != nil {
|
||||
if err == zfs.ErrBookmarkCloningNotSupported {
|
||||
return nil, err // TODO go1.13 use wrapping
|
||||
}
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
func ReplicationCursorV2Extractor(fs *zfs.DatasetPath, v zfs.FilesystemVersion) (_ Abstraction) {
|
||||
if v.Type != zfs.Bookmark {
|
||||
panic("impl error")
|
||||
}
|
||||
fullname := v.ToAbsPath(fs)
|
||||
guid, jobid, err := ParseReplicationCursorBookmarkName(fullname)
|
||||
if err == nil {
|
||||
if guid != v.Guid {
|
||||
// TODO log this possibly tinkered-with bookmark
|
||||
return nil
|
||||
}
|
||||
return &bookmarkBasedAbstraction{
|
||||
Type: AbstractionReplicationCursorBookmarkV2,
|
||||
FS: fs.ToString(),
|
||||
FilesystemVersion: v,
|
||||
JobID: jobid,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ReplicationCursorV1Extractor(fs *zfs.DatasetPath, v zfs.FilesystemVersion) (_ Abstraction) {
|
||||
if v.Type != zfs.Bookmark {
|
||||
panic("impl error")
|
||||
}
|
||||
fullname := v.ToAbsPath(fs)
|
||||
_, _, err := ParseReplicationCursorBookmarkName(fullname)
|
||||
if err == ErrV1ReplicationCursor {
|
||||
return &ReplicationCursorV1{
|
||||
Type: AbstractionReplicationCursorBookmarkV1,
|
||||
FS: fs.ToString(),
|
||||
FilesystemVersion: v,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
type ReplicationCursorV1 struct {
|
||||
Type AbstractionType
|
||||
FS string
|
||||
zfs.FilesystemVersion
|
||||
}
|
||||
|
||||
func (c ReplicationCursorV1) GetType() AbstractionType { return c.Type }
|
||||
func (c ReplicationCursorV1) GetFS() string { return c.FS }
|
||||
func (c ReplicationCursorV1) GetFullPath() string { return fmt.Sprintf("%s#%s", c.FS, c.GetName()) }
|
||||
func (c ReplicationCursorV1) GetJobID() *JobID { return nil }
|
||||
func (c ReplicationCursorV1) GetFilesystemVersion() zfs.FilesystemVersion { return c.FilesystemVersion }
|
||||
func (c ReplicationCursorV1) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(AbstractionJSON{c})
|
||||
}
|
||||
func (c ReplicationCursorV1) String() string {
|
||||
return fmt.Sprintf("%s %s", c.Type, c.GetFullPath())
|
||||
}
|
||||
func (c ReplicationCursorV1) Destroy(ctx context.Context) error {
|
||||
if err := zfs.ZFSDestroyIdempotent(ctx, c.GetFullPath()); err != nil {
|
||||
return errors.Wrapf(err, "destroy %s %s: zfs", c.Type, c.GetFullPath())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
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,234 @@
|
||||
package endpoint
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"runtime/debug"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
|
||||
func TestCreateTXGRange(t *testing.T) {
|
||||
|
||||
type testCaseExpectation struct {
|
||||
input uint64
|
||||
expect bool
|
||||
}
|
||||
type testCase struct {
|
||||
name string
|
||||
config *CreateTXGRange
|
||||
configAllowZeroCreateTXG bool
|
||||
expectInvalid bool
|
||||
expectString string
|
||||
expect []testCaseExpectation
|
||||
}
|
||||
|
||||
tcs := []testCase{
|
||||
{
|
||||
name: "unbounded",
|
||||
expectInvalid: false,
|
||||
config: &CreateTXGRange{
|
||||
Since: nil,
|
||||
Until: nil,
|
||||
},
|
||||
expectString: "~,~",
|
||||
expect: []testCaseExpectation{
|
||||
{0, true},
|
||||
{math.MaxUint64, true},
|
||||
{1, true},
|
||||
{math.MaxUint64 - 1, true},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "wrong order obvious",
|
||||
expectInvalid: true,
|
||||
config: &CreateTXGRange{
|
||||
Since: &CreateTXGRangeBound{23, &zfs.NilBool{B: true}},
|
||||
Until: &CreateTXGRangeBound{20, &zfs.NilBool{B: true}},
|
||||
},
|
||||
expectString: "[23,20]",
|
||||
},
|
||||
{
|
||||
name: "wrong order edge-case could also be empty",
|
||||
expectInvalid: true,
|
||||
config: &CreateTXGRange{
|
||||
Since: &CreateTXGRangeBound{23, &zfs.NilBool{B: false}},
|
||||
Until: &CreateTXGRangeBound{22, &zfs.NilBool{B: true}},
|
||||
},
|
||||
expectString: "(23,22]",
|
||||
},
|
||||
{
|
||||
name: "empty",
|
||||
expectInvalid: true,
|
||||
config: &CreateTXGRange{
|
||||
Since: &CreateTXGRangeBound{2, &zfs.NilBool{B: false}},
|
||||
Until: &CreateTXGRangeBound{2, &zfs.NilBool{B: false}},
|
||||
},
|
||||
expectString: "(2,2)",
|
||||
},
|
||||
{
|
||||
name: "inclusive-since-exclusive-until",
|
||||
expectInvalid: false,
|
||||
config: &CreateTXGRange{
|
||||
Since: &CreateTXGRangeBound{2, &zfs.NilBool{B: true}},
|
||||
Until: &CreateTXGRangeBound{5, &zfs.NilBool{B: false}},
|
||||
},
|
||||
expectString: "[2,5)",
|
||||
expect: []testCaseExpectation{
|
||||
{0, false},
|
||||
{1, false},
|
||||
{2, true},
|
||||
{3, true},
|
||||
{4, true},
|
||||
{5, false},
|
||||
{6, false},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "exclusive-since-inclusive-until",
|
||||
expectInvalid: false,
|
||||
config: &CreateTXGRange{
|
||||
Since: &CreateTXGRangeBound{2, &zfs.NilBool{B: false}},
|
||||
Until: &CreateTXGRangeBound{5, &zfs.NilBool{B: true}},
|
||||
},
|
||||
expectString: "(2,5]",
|
||||
expect: []testCaseExpectation{
|
||||
{0, false},
|
||||
{1, false},
|
||||
{2, false},
|
||||
{3, true},
|
||||
{4, true},
|
||||
{5, true},
|
||||
{6, false},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "zero-createtxg-not-allowed-because-likely-programmer-error",
|
||||
expectInvalid: true,
|
||||
config: &CreateTXGRange{
|
||||
Since: nil,
|
||||
Until: &CreateTXGRangeBound{0, &zfs.NilBool{B: true}},
|
||||
},
|
||||
expectString: "~,0]",
|
||||
},
|
||||
{
|
||||
name: "half-open-no-until",
|
||||
expectInvalid: false,
|
||||
config: &CreateTXGRange{
|
||||
Since: &CreateTXGRangeBound{2, &zfs.NilBool{B: false}},
|
||||
Until: nil,
|
||||
},
|
||||
expectString: "(2,~",
|
||||
expect: []testCaseExpectation{
|
||||
{0, false},
|
||||
{1, false},
|
||||
{2, false},
|
||||
{3, true},
|
||||
{4, true},
|
||||
{5, true},
|
||||
{6, true},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "half-open-no-since",
|
||||
expectInvalid: false,
|
||||
config: &CreateTXGRange{
|
||||
Since: nil,
|
||||
Until: &CreateTXGRangeBound{4, &zfs.NilBool{B: true}},
|
||||
},
|
||||
expectString: "~,4]",
|
||||
expect: []testCaseExpectation{
|
||||
{0, true},
|
||||
{1, true},
|
||||
{2, true},
|
||||
{3, true},
|
||||
{4, true},
|
||||
{5, false},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "edgeSince",
|
||||
expectInvalid: false,
|
||||
config: &CreateTXGRange{
|
||||
Since: &CreateTXGRangeBound{math.MaxUint64, &zfs.NilBool{B: true}},
|
||||
Until: nil,
|
||||
},
|
||||
expectString: "[18446744073709551615,~",
|
||||
expect: []testCaseExpectation{
|
||||
{math.MaxUint64, true},
|
||||
{math.MaxUint64 - 1, false},
|
||||
{0, false},
|
||||
{1, false},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "edgeSinceNegative",
|
||||
expectInvalid: true,
|
||||
config: &CreateTXGRange{
|
||||
Since: &CreateTXGRangeBound{math.MaxUint64, &zfs.NilBool{B: false}},
|
||||
Until: nil,
|
||||
},
|
||||
expectString: "(18446744073709551615,~",
|
||||
},
|
||||
{
|
||||
name: "edgeUntil",
|
||||
expectInvalid: false,
|
||||
config: &CreateTXGRange{
|
||||
Until: &CreateTXGRangeBound{0, &zfs.NilBool{B: true}},
|
||||
},
|
||||
configAllowZeroCreateTXG: true,
|
||||
expectString: "~,0]",
|
||||
expect: []testCaseExpectation{
|
||||
{0, true},
|
||||
{math.MaxUint64, false},
|
||||
{1, false},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "edgeUntilNegative",
|
||||
expectInvalid: true,
|
||||
configAllowZeroCreateTXG: true,
|
||||
config: &CreateTXGRange{
|
||||
Until: &CreateTXGRangeBound{0, &zfs.NilBool{B: false}},
|
||||
},
|
||||
expectString: "~,0)",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tcs {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
require.True(t, tc.expectInvalid != (len(tc.expect) > 0), "invalid test config: must either expect invalid or have expectations: %s", tc.name)
|
||||
require.NotEmpty(t, tc.expectString)
|
||||
assert.Equal(t, tc.expectString, tc.config.String())
|
||||
|
||||
save := createTXGRangeBoundAllowCreateTXG0
|
||||
createTXGRangeBoundAllowCreateTXG0 = tc.configAllowZeroCreateTXG
|
||||
defer func() {
|
||||
createTXGRangeBoundAllowCreateTXG0 = save
|
||||
}()
|
||||
|
||||
if tc.expectInvalid {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
assert.Error(t, tc.config.Validate())
|
||||
})
|
||||
} else {
|
||||
for i, e := range tc.expect {
|
||||
t.Run(fmt.Sprint(i), func(t *testing.T) {
|
||||
defer func() {
|
||||
v := recover()
|
||||
if v != nil {
|
||||
t.Fatalf("should not panic: %T %v\n%s", v, v, debug.Stack())
|
||||
}
|
||||
}()
|
||||
assert.Equal(t, e.expect, tc.config.Contains(e.input))
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package endpoint
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
|
||||
// returns the short name (no fs# prefix)
|
||||
func makeJobAndGuidBookmarkName(prefix string, fs string, guid uint64, jobid string) (string, error) {
|
||||
bmname := fmt.Sprintf(prefix+"_G_%016x_J_%s", guid, jobid)
|
||||
if err := zfs.EntityNamecheck(fmt.Sprintf("%s#%s", fs, bmname), zfs.EntityTypeBookmark); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return bmname, nil
|
||||
}
|
||||
|
||||
var jobAndGuidBookmarkRE = regexp.MustCompile(`(.+)_G_([0-9a-f]{16})_J_(.+)$`)
|
||||
|
||||
func parseJobAndGuidBookmarkName(fullname string, prefix string) (guid uint64, jobID JobID, _ error) {
|
||||
|
||||
if len(prefix) == 0 {
|
||||
panic("prefix must not be empty")
|
||||
}
|
||||
|
||||
if err := zfs.EntityNamecheck(fullname, zfs.EntityTypeBookmark); err != nil {
|
||||
return 0, JobID{}, err
|
||||
}
|
||||
|
||||
_, _, name, err := zfs.DecomposeVersionString(fullname)
|
||||
if err != nil {
|
||||
return 0, JobID{}, errors.Wrap(err, "decompose bookmark name")
|
||||
}
|
||||
|
||||
match := jobAndGuidBookmarkRE.FindStringSubmatch(name)
|
||||
if match == nil {
|
||||
return 0, JobID{}, errors.Errorf("bookmark name does not match regex %q", jobAndGuidBookmarkRE.String())
|
||||
}
|
||||
if match[1] != prefix {
|
||||
return 0, JobID{}, errors.Errorf("prefix component does not match: expected %q, got %q", prefix, match[1])
|
||||
}
|
||||
|
||||
guid, err = strconv.ParseUint(match[2], 16, 64)
|
||||
if err != nil {
|
||||
return 0, JobID{}, errors.Wrapf(err, "parse guid component: %q", match[2])
|
||||
}
|
||||
|
||||
jobID, err = MakeJobID(match[3])
|
||||
if err != nil {
|
||||
return 0, JobID{}, errors.Wrapf(err, "parse jobid component: %q", match[3])
|
||||
}
|
||||
|
||||
return guid, jobID, nil
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package endpoint
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestParseJobAndGuidBookmarkName(t *testing.T) {
|
||||
|
||||
type Case struct {
|
||||
input string
|
||||
expectErr bool
|
||||
|
||||
guid uint64
|
||||
jobid string
|
||||
}
|
||||
|
||||
cases := []Case{
|
||||
{
|
||||
`p1/sync#zrepl_CURSOR_G_932f3a7089080ce2_J_push with legitimate name`,
|
||||
false, 0x932f3a7089080ce2, "push with legitimate name",
|
||||
},
|
||||
{
|
||||
input: `p1/sync#zrepl_CURSOR_G_932f3a7089_J_push with legitimate name`,
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
input: `p1/sync#zrepl_CURSOR_G_932f3a7089080ce2_J_push with il\tlegitimate name`,
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
input: `p1/sync#otherprefix_G_932f3a7089080ce2_J_push with legitimate name`,
|
||||
expectErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for i := range cases {
|
||||
t.Run(cases[i].input, func(t *testing.T) {
|
||||
guid, jobid, err := parseJobAndGuidBookmarkName(cases[i].input, replicationCursorBookmarkNamePrefix)
|
||||
if cases[i].expectErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, cases[i].guid, guid)
|
||||
assert.Equal(t, MustMakeJobID(cases[i].jobid), jobid)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package endpoint
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
|
||||
type bookmarkBasedAbstraction struct {
|
||||
Type AbstractionType
|
||||
FS string
|
||||
zfs.FilesystemVersion
|
||||
JobID JobID
|
||||
}
|
||||
|
||||
func (b bookmarkBasedAbstraction) GetType() AbstractionType { return b.Type }
|
||||
func (b bookmarkBasedAbstraction) GetFS() string { return b.FS }
|
||||
func (b bookmarkBasedAbstraction) GetJobID() *JobID { return &b.JobID }
|
||||
func (b bookmarkBasedAbstraction) GetFullPath() string {
|
||||
return fmt.Sprintf("%s#%s", b.FS, b.Name) // TODO use zfs.FilesystemVersion.ToAbsPath
|
||||
}
|
||||
func (b bookmarkBasedAbstraction) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(AbstractionJSON{b})
|
||||
}
|
||||
func (b bookmarkBasedAbstraction) String() string {
|
||||
return fmt.Sprintf("%s %s", b.Type, b.GetFullPath())
|
||||
}
|
||||
|
||||
func (b bookmarkBasedAbstraction) GetFilesystemVersion() zfs.FilesystemVersion {
|
||||
return b.FilesystemVersion
|
||||
}
|
||||
|
||||
func (b bookmarkBasedAbstraction) Destroy(ctx context.Context) error {
|
||||
if err := zfs.ZFSDestroyIdempotent(ctx, b.GetFullPath()); err != nil {
|
||||
return errors.Wrapf(err, "destroy %s: zfs", b)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type holdBasedAbstraction struct {
|
||||
Type AbstractionType
|
||||
FS string
|
||||
zfs.FilesystemVersion
|
||||
Tag string
|
||||
JobID JobID
|
||||
}
|
||||
|
||||
func (h holdBasedAbstraction) GetType() AbstractionType { return h.Type }
|
||||
func (h holdBasedAbstraction) GetFS() string { return h.FS }
|
||||
func (h holdBasedAbstraction) GetJobID() *JobID { return &h.JobID }
|
||||
func (h holdBasedAbstraction) GetFullPath() string {
|
||||
return fmt.Sprintf("%s@%s", h.FS, h.GetName()) // TODO use zfs.FilesystemVersion.ToAbsPath
|
||||
}
|
||||
func (h holdBasedAbstraction) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(AbstractionJSON{h})
|
||||
}
|
||||
func (h holdBasedAbstraction) String() string {
|
||||
return fmt.Sprintf("%s %q on %s", h.Type, h.Tag, h.GetFullPath())
|
||||
}
|
||||
|
||||
func (h holdBasedAbstraction) GetFilesystemVersion() zfs.FilesystemVersion {
|
||||
return h.FilesystemVersion
|
||||
}
|
||||
|
||||
func (h holdBasedAbstraction) Destroy(ctx context.Context) error {
|
||||
if err := zfs.ZFSRelease(ctx, h.Tag, h.GetFullPath()); err != nil {
|
||||
return errors.Wrapf(err, "release %s: zfs", h)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package endpoint
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
|
||||
// JobID instances returned by MakeJobID() guarantee their JobID.String()
|
||||
// can be used in ZFS dataset names and hold tags.
|
||||
type JobID struct {
|
||||
jid string
|
||||
}
|
||||
|
||||
func MakeJobID(s string) (JobID, error) {
|
||||
if len(s) == 0 {
|
||||
return JobID{}, fmt.Errorf("must not be empty string")
|
||||
}
|
||||
|
||||
if err := zfs.ComponentNamecheck(s); err != nil {
|
||||
return JobID{}, errors.Wrap(err, "must be usable as a dataset path component")
|
||||
}
|
||||
|
||||
if _, err := stepBookmarkNameImpl("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")
|
||||
}
|
||||
|
||||
if _, err := stepHoldTagImpl(s); err != nil {
|
||||
return JobID{}, errors.Wrap(err, "must be usable for a step hold tag")
|
||||
}
|
||||
|
||||
if _, err := lastReceivedHoldImpl(s); err != nil {
|
||||
return JobID{}, errors.Wrap(err, "must be usable as a last-received-hold tag")
|
||||
}
|
||||
|
||||
// FIXME replication cursor bookmark name
|
||||
|
||||
_, err := zfs.NewDatasetPath(s)
|
||||
if err != nil {
|
||||
return JobID{}, fmt.Errorf("must be usable in a ZFS dataset path: %s", err)
|
||||
}
|
||||
|
||||
return JobID{s}, nil
|
||||
}
|
||||
|
||||
func MustMakeJobID(s string) JobID {
|
||||
jid, err := MakeJobID(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return jid
|
||||
}
|
||||
|
||||
func (j JobID) expectInitialized() {
|
||||
if j.jid == "" {
|
||||
panic("use of uninitialized JobID")
|
||||
}
|
||||
}
|
||||
|
||||
func (j JobID) String() string {
|
||||
j.expectInitialized()
|
||||
return j.jid
|
||||
}
|
||||
|
||||
var _ json.Marshaler = JobID{}
|
||||
var _ json.Unmarshaler = (*JobID)(nil)
|
||||
|
||||
func (j JobID) MarshalJSON() ([]byte, error) { return json.Marshal(j.jid) }
|
||||
|
||||
func (j *JobID) UnmarshalJSON(b []byte) error {
|
||||
return json.Unmarshal(b, &j.jid)
|
||||
}
|
||||
|
||||
func (j JobID) MustValidate() { j.expectInitialized() }
|
||||
@@ -21,7 +21,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-20200601114649-26439f9f0dc5
|
||||
github.com/problame/go-netssh v0.0.0-20191209123953-18d8aa6923c7
|
||||
github.com/prometheus/client_golang v1.2.1
|
||||
github.com/sergi/go-diff v1.0.1-0.20180205163309-da645544ed44 // go1.12 thinks it needs this
|
||||
github.com/spf13/cobra v0.0.2
|
||||
@@ -29,24 +29,10 @@ require (
|
||||
github.com/stretchr/testify v1.4.0
|
||||
github.com/yudai/gojsondiff v0.0.0-20170107030110-7b1b7adf999d
|
||||
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // go1.12 thinks it needs this
|
||||
github.com/zrepl/yaml-config v0.0.0-20190928121844-af7ca3f8448f
|
||||
github.com/zrepl/yaml-config v0.0.0-20191220194647-cbb6b0cf4bdd
|
||||
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-20191010194322-b09406accb47
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037
|
||||
gonum.org/v1/gonum v0.7.0 // indirect
|
||||
google.golang.org/grpc v1.17.0
|
||||
)
|
||||
|
||||
// invalid dates in transitive dependencies (first validated in Go 1.13, didn't fail in earlier Go versions)
|
||||
replace (
|
||||
github.com/go-critic/go-critic v0.0.0-20181204210945-1df300866540 => github.com/go-critic/go-critic v0.3.5-0.20190526074819-1df300866540
|
||||
github.com/go-critic/go-critic v0.0.0-20181204210945-ee9bf5809ead => github.com/go-critic/go-critic v0.3.5-0.20190210220443-ee9bf5809ead
|
||||
github.com/golangci/errcheck v0.0.0-20181003203344-ef45e06d44b6 => github.com/golangci/errcheck v0.0.0-20181223084120-ef45e06d44b6
|
||||
github.com/golangci/go-tools v0.0.0-20180109140146-35a9f45a5db0 => github.com/golangci/go-tools v0.0.0-20190124090046-35a9f45a5db0
|
||||
github.com/golangci/go-tools v0.0.0-20180109140146-af6baa5dc196 => github.com/golangci/go-tools v0.0.0-20190318060251-af6baa5dc196
|
||||
github.com/golangci/gofmt v0.0.0-20181105071733-0b8337e80d98 => github.com/golangci/gofmt v0.0.0-20181222123516-0b8337e80d98
|
||||
github.com/golangci/gosec v0.0.0-20180901114220-66fb7fc33547 => github.com/golangci/gosec v0.0.0-20190211064107-66fb7fc33547
|
||||
github.com/golangci/ineffassign v0.0.0-20180808204949-42439a7714cc => github.com/golangci/ineffassign v0.0.0-20190609212857-42439a7714cc
|
||||
github.com/golangci/lint-1 v0.0.0-20180610141402-ee948d087217 => github.com/golangci/lint-1 v0.0.0-20190420132249-ee948d087217
|
||||
golang.org/x/tools v0.0.0-20190125232054-379209517ffe => golang.org/x/tools v0.0.0-20190205201329-379209517ffe
|
||||
mvdan.cc/unparam v0.0.0-20190124213536-fbb59629db34 => mvdan.cc/unparam v0.0.0-20190209190245-fbb59629db34
|
||||
)
|
||||
|
||||
@@ -5,6 +5,7 @@ github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q
|
||||
github.com/OpenPeeDeeP/depguard v0.0.0-20180806142446-a69c782687b2/go.mod h1:7/4sitnI9YlQgTLLk734QlzXT8DuHVnAyztLplQjk+o=
|
||||
github.com/OpenPeeDeeP/depguard v0.0.0-20181229194401-1f388ab2d810/go.mod h1:7/4sitnI9YlQgTLLk734QlzXT8DuHVnAyztLplQjk+o=
|
||||
github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
|
||||
github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
@@ -28,6 +29,7 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
|
||||
github.com/fatih/color v1.6.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
|
||||
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/ftrvxmtrx/fd v0.0.0-20150925145434-c6d800382fff h1:zk1wwii7uXmI0znwU+lqg+wFL9G5+vm5I+9rv2let60=
|
||||
@@ -70,6 +72,7 @@ github.com/go-toolsmith/typep v1.0.0/go.mod h1:JSQCQMUPdRlMZFswiq3TGpNp1GMktqkR2
|
||||
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
|
||||
@@ -122,6 +125,7 @@ github.com/jinzhu/copier v0.0.0-20170922082739-db4671f3a9b8/go.mod h1:yL958EeXv8
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
|
||||
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88 h1:uC1QfSlInpQF+M0ao65imhwqKnz3Q2z/d8PWZRMQvDM=
|
||||
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k=
|
||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||
@@ -202,8 +206,8 @@ github.com/problame/go-netssh v0.0.0-20190110232351-09d6bc45d284 h1:GX8YFBfSL6Gl
|
||||
github.com/problame/go-netssh v0.0.0-20190110232351-09d6bc45d284/go.mod h1:XMR9fjP5/wMyFK0Ifk9ZHDFem4Rukm+ChvoYr2nxrgw=
|
||||
github.com/problame/go-netssh v0.0.0-20191026123024-f34099f4f6b1 h1:HH8yzlaZq/A8xdJaSj/eu32yFFySWAJMTmOQPAGwfYg=
|
||||
github.com/problame/go-netssh v0.0.0-20191026123024-f34099f4f6b1/go.mod h1:JRs3nyBjvOv/9YHC+Vlw9z1Jfzl5s5t0BNnTzmclj1c=
|
||||
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/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/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=
|
||||
@@ -281,12 +285,19 @@ github.com/yudai/pp v2.0.1+incompatible h1:Q4//iY4pNF6yPLZIigmvcl7k/bPgrcTPIFIcm
|
||||
github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc=
|
||||
github.com/zrepl/yaml-config v0.0.0-20190928121844-af7ca3f8448f h1:3MuiGfgMHCSwKUcsuI7ODbi50j+evTB7SsoOBMNC5Fk=
|
||||
github.com/zrepl/yaml-config v0.0.0-20190928121844-af7ca3f8448f/go.mod h1:JmNwisZzOvW4GfpfLvhZ+gtyKLsIiA+WC+wNKJGJaFg=
|
||||
github.com/zrepl/yaml-config v0.0.0-20191220194647-cbb6b0cf4bdd h1:SSo67WLS+99QESvbW8Meibz7zCrxshP71U9dH5KOCXM=
|
||||
github.com/zrepl/yaml-config v0.0.0-20191220194647-cbb6b0cf4bdd/go.mod h1:JmNwisZzOvW4GfpfLvhZ+gtyKLsIiA+WC+wNKJGJaFg=
|
||||
github.com/zrepl/zrepl v0.2.0/go.mod h1:M3Zv2IGSO8iYpUjsZD6ayZ2LHy7zyMfzet9XatKOrZ8=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
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=
|
||||
golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2 h1:y102fOLFqhV41b+4GPiJoa0k/x+pJcEi2/HB1Y5T6fU=
|
||||
golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
|
||||
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/net v0.0.0-20170915142106-8351a756f30f/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -319,22 +330,31 @@ golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756 h1:9nuHUbU8dRnRRfj9KjWUVrJeo
|
||||
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191010194322-b09406accb47 h1:/XfQ9z7ib8eEJX2hdgFTZJ/ntt0swNk5oYBziWeTCvY=
|
||||
golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.0.0-20170915090833-1cbadb444a80/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/tools v0.0.0-20170915040203-e531a2a1c15f/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181117154741-2ddaf7f79a09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
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/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=
|
||||
golang.org/x/tools v0.0.0-20190311215038-5c2858a9cfe5/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190322203728-c1a832b0ad89/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190521203540-521d6ed310dd/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190524210228-3d17549cdc6b/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo=
|
||||
gonum.org/v1/gonum v0.7.0 h1:Hdks0L0hgznZLG9nzXb8vZ0rRvqNvAcgAp84y7Mwkgw=
|
||||
gonum.org/v1/gonum v0.7.0/go.mod h1:L02bwd0sqlsvRv41G7wGWFCsVNZFv/k1xzGIxeANHGM=
|
||||
gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
|
||||
gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc=
|
||||
google.golang.org/appengine v1.1.0 h1:igQkv0AAhEIvTEpD5LIpAfav2eeVO9HBTjvKHVJPRSs=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc=
|
||||
@@ -346,6 +366,7 @@ google.golang.org/grpc v1.17.0 h1:TRJYBgMclJvGYn2rIMjj+h9KtMt5r1Ij7ODVRIZkwhk=
|
||||
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
|
||||
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
@@ -364,5 +385,6 @@ mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed/go.mod h1:Xkxe497xwlCKkIa
|
||||
mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b/go.mod h1:2odslEg/xrtNQqCYg2/jCoyKnw3vv5biOc3JnIcYfL4=
|
||||
mvdan.cc/unparam v0.0.0-20190209190245-fbb59629db34/go.mod h1:H6SUd1XjIs+qQCyskXg5OFSrilMRUkD8ePJpHKDPaeY=
|
||||
mvdan.cc/unparam v0.0.0-20190310220240-1b9ccfa71afe/go.mod h1:BnhuWBAqxH3+J5bDybdxgw5ZfS+DsVd4iylsKQePN8o=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0=
|
||||
sourcegraph.com/sqs/pbtypes v1.0.0/go.mod h1:3AciMUv4qUuRHRHhOG4TZOB+72GdPVz5k+c648qsFS4=
|
||||
|
||||
@@ -17,6 +17,7 @@ func init() {
|
||||
cli.AddSubcommand(client.PprofCmd)
|
||||
cli.AddSubcommand(client.TestCmd)
|
||||
cli.AddSubcommand(client.MigrateCmd)
|
||||
cli.AddSubcommand(client.ZFSAbstractionsCmd)
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
||||
+151
-45
@@ -4,11 +4,14 @@ import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/zrepl/zrepl/config"
|
||||
"github.com/zrepl/zrepl/daemon/logging"
|
||||
"github.com/zrepl/zrepl/logger"
|
||||
@@ -16,19 +19,38 @@ import (
|
||||
"github.com/zrepl/zrepl/platformtest/tests"
|
||||
)
|
||||
|
||||
func main() {
|
||||
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
|
||||
var args struct {
|
||||
createArgs platformtest.ZpoolCreateArgs
|
||||
stopAndKeepPoolOnFail bool
|
||||
|
||||
run string
|
||||
runRE *regexp.Regexp
|
||||
}
|
||||
|
||||
func main() {
|
||||
if err := doMain(); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
var exitWithErr = fmt.Errorf("exit with error")
|
||||
|
||||
func doMain() error {
|
||||
|
||||
flag.StringVar(&args.createArgs.PoolName, "poolname", "", "")
|
||||
flag.StringVar(&args.createArgs.ImagePath, "imagepath", "", "")
|
||||
flag.Int64Var(&args.createArgs.ImageSize, "imagesize", 100*(1<<20), "")
|
||||
flag.StringVar(&args.createArgs.Mountpoint, "mountpoint", "none", "")
|
||||
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()
|
||||
|
||||
args.runRE = regexp.MustCompile(args.run)
|
||||
|
||||
outlets := logger.NewOutlets()
|
||||
outlet, level, err := logging.ParseOutlet(config.LoggingOutletEnum{Ret: &config.StdoutLoggingOutlet{
|
||||
LoggingOutletCommon: config.LoggingOutletCommon{
|
||||
@@ -49,48 +71,132 @@ func main() {
|
||||
ctx := platformtest.WithLogger(context.Background(), logger)
|
||||
ex := platformtest.NewEx(logger)
|
||||
|
||||
bold := color.New(color.Bold)
|
||||
boldRed := color.New(color.Bold, color.FgHiRed)
|
||||
boldGreen := color.New(color.Bold, color.FgHiGreen)
|
||||
type invocation struct {
|
||||
runFunc tests.Case
|
||||
result *testCaseResult
|
||||
}
|
||||
|
||||
invocations := make([]*invocation, 0, len(tests.Cases))
|
||||
for _, c := range tests.Cases {
|
||||
// ATTENTION future parallelism must pass c by value into closure!
|
||||
err := func() (err error) {
|
||||
var oErr = &err
|
||||
bold.Printf("BEGIN TEST CASE %s\n", c)
|
||||
pool, err := platformtest.CreateOrReplaceZpool(ctx, ex, args.createArgs)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "create test pool")
|
||||
}
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
*oErr = errors.Errorf("panic while running test: %v", err) // shadow
|
||||
if args.stopAndKeepPoolOnFail {
|
||||
return
|
||||
}
|
||||
// fallthrough
|
||||
}
|
||||
if err := pool.Destroy(ctx, ex); err != nil {
|
||||
fmt.Printf("error destroying test pool: %s", err)
|
||||
}
|
||||
}()
|
||||
ctx := &platformtest.Context{
|
||||
Context: ctx,
|
||||
RootDataset: filepath.Join(pool.Name(), "rootds"),
|
||||
}
|
||||
c(ctx)
|
||||
return nil
|
||||
}()
|
||||
if err != nil {
|
||||
boldRed.Printf("TEST CASE FAILED WITH ERROR:\n")
|
||||
fmt.Printf("%+v\n", err) // print with stack trace
|
||||
if args.stopAndKeepPoolOnFail {
|
||||
bold.Printf("STOPPING TEST RUN AT FAILING TEST PER USER REQUEST\n")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
boldGreen.Printf("DONE TEST CASE %s\n", c)
|
||||
if args.runRE.MatchString(c.String()) {
|
||||
invocations = append(invocations, &invocation{runFunc: c})
|
||||
}
|
||||
}
|
||||
|
||||
for _, inv := range invocations {
|
||||
|
||||
bold.Printf("BEGIN TEST CASE %s\n", inv.runFunc.String())
|
||||
|
||||
pool, err := platformtest.CreateOrReplaceZpool(ctx, ex, args.createArgs)
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "create test pool"))
|
||||
}
|
||||
|
||||
ctx := &platformtest.Context{
|
||||
Context: ctx,
|
||||
RootDataset: filepath.Join(pool.Name(), "rootds"),
|
||||
}
|
||||
|
||||
res := runTestCase(ctx, ex, inv.runFunc)
|
||||
inv.result = res
|
||||
if res.failed {
|
||||
fmt.Printf("%+v\n", res.failedStack) // print with stack trace
|
||||
}
|
||||
|
||||
if res.failed && args.stopAndKeepPoolOnFail {
|
||||
boldRed.Printf("STOPPING TEST RUN AT FAILING TEST PER USER REQUEST\n")
|
||||
return exitWithErr
|
||||
}
|
||||
|
||||
if err := pool.Destroy(ctx, ex); err != nil {
|
||||
panic(fmt.Sprintf("error destroying test pool: %s", err))
|
||||
}
|
||||
|
||||
if res.failed {
|
||||
boldRed.Printf("TEST FAILED\n")
|
||||
} else if res.skipped {
|
||||
bold.Printf("TEST SKIPPED\n")
|
||||
} else if res.succeeded {
|
||||
boldGreen.Printf("TEST PASSED\n")
|
||||
} else {
|
||||
panic("unreachable")
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
var summary struct {
|
||||
succ, fail, skip []*invocation
|
||||
}
|
||||
for _, inv := range invocations {
|
||||
var bucket *[]*invocation
|
||||
if inv.result.failed {
|
||||
bucket = &summary.fail
|
||||
} else if inv.result.skipped {
|
||||
bucket = &summary.skip
|
||||
} else if inv.result.succeeded {
|
||||
bucket = &summary.succ
|
||||
} else {
|
||||
panic("unreachable")
|
||||
}
|
||||
*bucket = append(*bucket, inv)
|
||||
}
|
||||
printBucket := func(bucketName string, c *color.Color, bucket []*invocation) {
|
||||
c.Printf("%s:", bucketName)
|
||||
if len(bucket) == 0 {
|
||||
fmt.Printf(" []\n")
|
||||
return
|
||||
}
|
||||
fmt.Printf("\n")
|
||||
for _, inv := range bucket {
|
||||
fmt.Printf(" %s\n", inv.runFunc.String())
|
||||
}
|
||||
}
|
||||
printBucket("PASSING TESTS", boldGreen, summary.succ)
|
||||
printBucket("SKIPPED TESTS", bold, summary.skip)
|
||||
printBucket("FAILED TESTS", boldRed, summary.fail)
|
||||
|
||||
if len(summary.fail) > 0 {
|
||||
return errors.New("at least one test failed")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type testCaseResult struct {
|
||||
// oneof
|
||||
failed, skipped, succeeded bool
|
||||
|
||||
failedStack error // has stack inside, valid if failed=true
|
||||
}
|
||||
|
||||
func runTestCase(ctx *platformtest.Context, ex platformtest.Execer, c tests.Case) *testCaseResult {
|
||||
|
||||
// run case
|
||||
var panicked = false
|
||||
var panicValue interface{} = nil
|
||||
var panicStack error
|
||||
func() {
|
||||
defer func() {
|
||||
if item := recover(); item != nil {
|
||||
panicValue = item
|
||||
panicked = true
|
||||
panicStack = errors.Errorf("panic while running test: %v", panicValue)
|
||||
}
|
||||
}()
|
||||
c(ctx)
|
||||
}()
|
||||
|
||||
if panicked {
|
||||
switch panicValue {
|
||||
case platformtest.SkipNowSentinel:
|
||||
return &testCaseResult{skipped: true}
|
||||
case platformtest.FailNowSentinel:
|
||||
return &testCaseResult{failed: true, failedStack: panicStack}
|
||||
default:
|
||||
return &testCaseResult{failed: true, failedStack: panicStack}
|
||||
}
|
||||
} else {
|
||||
return &testCaseResult{succeeded: true}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package platformtest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -12,13 +13,26 @@ type Context struct {
|
||||
RootDataset string
|
||||
}
|
||||
|
||||
var FailNowSentinel = fmt.Errorf("platformtest: FailNow called on context")
|
||||
|
||||
var SkipNowSentinel = fmt.Errorf("platformtest: SkipNow called on context")
|
||||
|
||||
var _ assert.TestingT = (*Context)(nil)
|
||||
var _ require.TestingT = (*Context)(nil)
|
||||
|
||||
func (c *Context) Logf(format string, args ...interface{}) {
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
GetLog(c).Info(msg)
|
||||
}
|
||||
|
||||
func (c *Context) Errorf(format string, args ...interface{}) {
|
||||
GetLog(c).Printf(format, args...)
|
||||
}
|
||||
|
||||
func (c *Context) FailNow() {
|
||||
panic(nil)
|
||||
panic(FailNowSentinel)
|
||||
}
|
||||
|
||||
func (c *Context) SkipNow() {
|
||||
panic(SkipNowSentinel)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
@@ -23,6 +24,7 @@ type Stmt interface {
|
||||
type Op string
|
||||
|
||||
const (
|
||||
Comment Op = "#"
|
||||
AssertExists Op = "!E"
|
||||
AssertNotExists Op = "!N"
|
||||
Add Op = "+"
|
||||
@@ -46,8 +48,9 @@ func (o *DestroyRootOp) Run(ctx context.Context, e Execer) error {
|
||||
}
|
||||
|
||||
type FSOp struct {
|
||||
Op Op
|
||||
Path string
|
||||
Op Op
|
||||
Path string
|
||||
Encrypted bool // only for Op=Add
|
||||
}
|
||||
|
||||
func (o *FSOp) Run(ctx context.Context, e Execer) error {
|
||||
@@ -57,7 +60,22 @@ func (o *FSOp) Run(ctx context.Context, e Execer) error {
|
||||
case AssertNotExists:
|
||||
return e.RunExpectFailureNoOutput(ctx, "zfs", "get", "-H", "name", o.Path)
|
||||
case Add:
|
||||
return e.RunExpectSuccessNoOutput(ctx, "zfs", "create", o.Path)
|
||||
opts := []string{"create"}
|
||||
if o.Encrypted {
|
||||
const passphraseFilePath = "/tmp/zreplplatformtest.encryption.passphrase"
|
||||
const passphrase = "foobar2342"
|
||||
err := ioutil.WriteFile(passphraseFilePath, []byte(passphrase), 0600)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
opts = append(opts,
|
||||
"-o", "encryption=on",
|
||||
"-o", "keylocation=file:///"+passphraseFilePath,
|
||||
"-o", "keyformat=passphrase",
|
||||
)
|
||||
}
|
||||
opts = append(opts, o.Path)
|
||||
return e.RunExpectSuccessNoOutput(ctx, "zfs", opts...)
|
||||
case Del:
|
||||
return e.RunExpectSuccessNoOutput(ctx, "zfs", "destroy", o.Path)
|
||||
default:
|
||||
@@ -85,6 +103,26 @@ func (o *SnapOp) Run(ctx context.Context, e Execer) error {
|
||||
}
|
||||
}
|
||||
|
||||
type BookmarkOp struct {
|
||||
Op Op
|
||||
Existing string
|
||||
Bookmark string
|
||||
}
|
||||
|
||||
func (o *BookmarkOp) Run(ctx context.Context, e Execer) error {
|
||||
switch o.Op {
|
||||
case Add:
|
||||
return e.RunExpectSuccessNoOutput(ctx, "zfs", "bookmark", o.Existing, o.Bookmark)
|
||||
case Del:
|
||||
if o.Existing != "" {
|
||||
panic("existing must be empty for destroy, got " + o.Existing)
|
||||
}
|
||||
return e.RunExpectSuccessNoOutput(ctx, "zfs", "destroy", o.Bookmark)
|
||||
default:
|
||||
panic(o.Op)
|
||||
}
|
||||
}
|
||||
|
||||
type RunOp struct {
|
||||
RootDS string
|
||||
Script string
|
||||
@@ -163,8 +201,8 @@ func splitQuotedWords(data []byte, atEOF bool) (advance int, token []byte, err e
|
||||
// unescaped quote, end of this string
|
||||
// remove backslash-escapes
|
||||
withBackslash := data[begin+1 : end]
|
||||
withoutBaskslash := bytes.Replace(withBackslash, []byte("\\\""), []byte("\""), -1)
|
||||
return end + 1, withoutBaskslash, nil
|
||||
withoutBackslash := bytes.Replace(withBackslash, []byte("\\\""), []byte("\""), -1)
|
||||
return end + 1, withoutBackslash, nil
|
||||
} else {
|
||||
// continue to next quote
|
||||
end += 1
|
||||
@@ -238,18 +276,46 @@ nextLine:
|
||||
op = AssertExists
|
||||
case string(AssertNotExists):
|
||||
op = AssertNotExists
|
||||
case string(Comment):
|
||||
op = Comment
|
||||
continue
|
||||
default:
|
||||
return nil, &LineError{scan.Text(), fmt.Sprintf("invalid op %q", comps.Text())}
|
||||
}
|
||||
|
||||
// FS / SNAP
|
||||
// FS / SNAP / BOOKMARK
|
||||
if err := expectMoreTokens(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if strings.ContainsAny(comps.Text(), "@") {
|
||||
stmts = append(stmts, &SnapOp{Op: op, Path: fmt.Sprintf("%s/%s", rootds, comps.Text())})
|
||||
} else if strings.ContainsAny(comps.Text(), "#") {
|
||||
bookmark := fmt.Sprintf("%s/%s", rootds, comps.Text())
|
||||
if err := expectMoreTokens(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
existing := fmt.Sprintf("%s/%s", rootds, comps.Text())
|
||||
stmts = append(stmts, &BookmarkOp{Op: op, Existing: existing, Bookmark: bookmark})
|
||||
} else {
|
||||
stmts = append(stmts, &FSOp{Op: op, Path: fmt.Sprintf("%s/%s", rootds, comps.Text())})
|
||||
// FS
|
||||
fs := comps.Text()
|
||||
var encrypted bool = false
|
||||
if op == Add {
|
||||
if comps.Scan() {
|
||||
t := comps.Text()
|
||||
switch t {
|
||||
case "encrypted":
|
||||
encrypted = true
|
||||
default:
|
||||
panic(fmt.Sprintf("unexpected token %q", t))
|
||||
}
|
||||
}
|
||||
}
|
||||
stmts = append(stmts, &FSOp{
|
||||
Op: op,
|
||||
Path: fmt.Sprintf("%s/%s", rootds, fs),
|
||||
Encrypted: encrypted,
|
||||
})
|
||||
}
|
||||
|
||||
if comps.Scan() {
|
||||
|
||||
@@ -2,10 +2,12 @@ package platformtest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
|
||||
@@ -28,11 +30,11 @@ func (a ZpoolCreateArgs) Validate() error {
|
||||
if a.ImageSize < minImageSize {
|
||||
return errors.Errorf("ImageSize must be > %v, got %v", minImageSize, a.ImageSize)
|
||||
}
|
||||
if a.Mountpoint != "none" {
|
||||
return errors.Errorf("Mountpoint must be \"none\"")
|
||||
if a.Mountpoint == "" || a.Mountpoint[0] != '/' {
|
||||
return errors.Errorf("Mountpoint must be an absolute path to a directory")
|
||||
}
|
||||
if a.PoolName == "" {
|
||||
return errors.Errorf("PoolName must not be emtpy")
|
||||
return errors.Errorf("PoolName must not be empty")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -43,7 +45,7 @@ func CreateOrReplaceZpool(ctx context.Context, e Execer, args ZpoolCreateArgs) (
|
||||
}
|
||||
|
||||
// export pool if it already exists (idempotence)
|
||||
if _, err := zfs.ZFSGetRawAnySource(args.PoolName, []string{"name"}); err != nil {
|
||||
if _, err := zfs.ZFSGetRawAnySource(ctx, args.PoolName, []string{"name"}); err != nil {
|
||||
if _, ok := err.(*zfs.DatasetDoesNotExist); ok {
|
||||
// we'll create it shortly
|
||||
} else {
|
||||
@@ -68,7 +70,10 @@ func CreateOrReplaceZpool(ctx context.Context, e Execer, args ZpoolCreateArgs) (
|
||||
image.Close()
|
||||
|
||||
// create the pool
|
||||
err = e.RunExpectSuccessNoOutput(ctx, "zpool", "create", "-f", "-O", "mountpoint=none", args.PoolName, args.ImagePath)
|
||||
err = e.RunExpectSuccessNoOutput(ctx, "zpool", "create", "-f",
|
||||
"-O", fmt.Sprintf("mountpoint=%s", args.Mountpoint),
|
||||
args.PoolName, args.ImagePath,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "zpool create")
|
||||
}
|
||||
|
||||
@@ -17,14 +17,14 @@ func GetNonexistent(ctx *platformtest.Context) {
|
||||
`)
|
||||
|
||||
// test raw
|
||||
_, err := zfs.ZFSGetRawAnySource(fmt.Sprintf("%s/foo bar", ctx.RootDataset), []string{"name"})
|
||||
_, err := zfs.ZFSGetRawAnySource(ctx, fmt.Sprintf("%s/foo bar", ctx.RootDataset), []string{"name"})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// test nonexistent filesystem
|
||||
nonexistent := fmt.Sprintf("%s/nonexistent filesystem", ctx.RootDataset)
|
||||
props, err := zfs.ZFSGetRawAnySource(nonexistent, []string{"name"})
|
||||
props, err := zfs.ZFSGetRawAnySource(ctx, nonexistent, []string{"name"})
|
||||
if err == nil {
|
||||
panic(props)
|
||||
}
|
||||
@@ -37,7 +37,7 @@ func GetNonexistent(ctx *platformtest.Context) {
|
||||
|
||||
// test nonexistent snapshot
|
||||
nonexistent = fmt.Sprintf("%s/foo bar@non existent", ctx.RootDataset)
|
||||
props, err = zfs.ZFSGetRawAnySource(nonexistent, []string{"name"})
|
||||
props, err = zfs.ZFSGetRawAnySource(ctx, nonexistent, []string{"name"})
|
||||
if err == nil {
|
||||
panic(props)
|
||||
}
|
||||
@@ -50,7 +50,7 @@ func GetNonexistent(ctx *platformtest.Context) {
|
||||
|
||||
// test nonexistent bookmark
|
||||
nonexistent = fmt.Sprintf("%s/foo bar#non existent", ctx.RootDataset)
|
||||
props, err = zfs.ZFSGetRawAnySource(nonexistent, []string{"name"})
|
||||
props, err = zfs.ZFSGetRawAnySource(ctx, nonexistent, []string{"name"})
|
||||
if err == nil {
|
||||
panic(props)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"io"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/zrepl/zrepl/platformtest"
|
||||
"github.com/zrepl/zrepl/util/limitio"
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
|
||||
func sendArgVersion(ctx *platformtest.Context, fs, relName string) zfs.ZFSSendArgVersion {
|
||||
guid, err := zfs.ZFSGetGUID(ctx, fs, relName)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return zfs.ZFSSendArgVersion{
|
||||
RelName: relName,
|
||||
GUID: guid,
|
||||
}
|
||||
}
|
||||
|
||||
func fsversion(ctx *platformtest.Context, fs, relname string) zfs.FilesystemVersion {
|
||||
v, err := zfs.ZFSGetFilesystemVersion(ctx, fs+relname)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func mustDatasetPath(fs string) *zfs.DatasetPath {
|
||||
p, err := zfs.NewDatasetPath(fs)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func mustSnapshot(ctx *platformtest.Context, snap string) {
|
||||
if err := zfs.EntityNamecheck(snap, zfs.EntityTypeSnapshot); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
comps := strings.Split(snap, "@")
|
||||
if len(comps) != 2 {
|
||||
panic(comps)
|
||||
}
|
||||
err := zfs.ZFSSnapshot(ctx, mustDatasetPath(comps[0]), comps[1], false)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func mustGetFilesystemVersion(ctx *platformtest.Context, snapOrBookmark string) zfs.FilesystemVersion {
|
||||
v, err := zfs.ZFSGetFilesystemVersion(ctx, snapOrBookmark)
|
||||
check(err)
|
||||
return v
|
||||
}
|
||||
|
||||
func check(err error) {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
var dummyDataRand = rand.New(rand.NewSource(99))
|
||||
|
||||
func writeDummyData(path string, numBytes int64) {
|
||||
r := io.LimitReader(dummyDataRand, numBytes)
|
||||
d, err := os.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
|
||||
check(err)
|
||||
defer d.Close()
|
||||
_, err = io.Copy(d, r)
|
||||
check(err)
|
||||
}
|
||||
|
||||
type dummySnapshotSituation struct {
|
||||
sendFS string
|
||||
dummyDataLen int64
|
||||
snapA *zfs.ZFSSendArgVersion
|
||||
snapB *zfs.ZFSSendArgVersion
|
||||
}
|
||||
|
||||
type resumeSituation struct {
|
||||
sendArgs zfs.ZFSSendArgsUnvalidated
|
||||
recvOpts zfs.RecvOptions
|
||||
sendErr, recvErr error
|
||||
recvErrDecoded *zfs.RecvFailedWithResumeTokenErr
|
||||
}
|
||||
|
||||
func makeDummyDataSnapshots(ctx *platformtest.Context, sendFS string) (situation dummySnapshotSituation) {
|
||||
|
||||
situation.sendFS = sendFS
|
||||
sendFSMount, err := zfs.ZFSGetMountpoint(ctx, sendFS)
|
||||
require.NoError(ctx, err)
|
||||
require.True(ctx, sendFSMount.Mounted)
|
||||
|
||||
const dummyLen = int64(10 * (1 << 20))
|
||||
situation.dummyDataLen = dummyLen
|
||||
|
||||
writeDummyData(path.Join(sendFSMount.Mountpoint, "dummy_data"), dummyLen)
|
||||
mustSnapshot(ctx, sendFS+"@a snapshot")
|
||||
snapA := sendArgVersion(ctx, sendFS, "@a snapshot")
|
||||
situation.snapA = &snapA
|
||||
|
||||
writeDummyData(path.Join(sendFSMount.Mountpoint, "dummy_data"), dummyLen)
|
||||
mustSnapshot(ctx, sendFS+"@b snapshot")
|
||||
snapB := sendArgVersion(ctx, sendFS, "@b snapshot")
|
||||
situation.snapB = &snapB
|
||||
|
||||
return situation
|
||||
}
|
||||
|
||||
func makeResumeSituation(ctx *platformtest.Context, src dummySnapshotSituation, recvFS string, sendArgs zfs.ZFSSendArgsUnvalidated, recvOptions zfs.RecvOptions) *resumeSituation {
|
||||
|
||||
situation := &resumeSituation{}
|
||||
|
||||
situation.sendArgs = sendArgs
|
||||
situation.recvOpts = recvOptions
|
||||
require.True(ctx, recvOptions.SavePartialRecvState, "this method would be pointless otherwise")
|
||||
require.Equal(ctx, sendArgs.FS, src.sendFS)
|
||||
sendArgsValidated, err := sendArgs.Validate(ctx)
|
||||
situation.sendErr = err
|
||||
if err != nil {
|
||||
return situation
|
||||
}
|
||||
|
||||
copier, err := zfs.ZFSSend(ctx, sendArgsValidated)
|
||||
situation.sendErr = err
|
||||
if err != nil {
|
||||
return situation
|
||||
}
|
||||
|
||||
limitedCopier := limitio.ReadCloser(copier, src.dummyDataLen/2)
|
||||
defer limitedCopier.Close()
|
||||
|
||||
require.NotNil(ctx, sendArgs.To)
|
||||
err = zfs.ZFSRecv(ctx, recvFS, sendArgs.To, limitedCopier, recvOptions)
|
||||
situation.recvErr = err
|
||||
ctx.Logf("zfs recv exit with %T %s", err, err)
|
||||
|
||||
require.NotNil(ctx, err)
|
||||
resumeErr, ok := err.(*zfs.RecvFailedWithResumeTokenErr)
|
||||
require.True(ctx, ok)
|
||||
situation.recvErrDecoded = resumeErr
|
||||
|
||||
return situation
|
||||
}
|
||||
|
||||
func versionRelnamesSorted(versions []zfs.FilesystemVersion) []string {
|
||||
var vstrs []string
|
||||
for _, v := range versions {
|
||||
vstrs = append(vstrs, v.RelName())
|
||||
}
|
||||
sort.Strings(vstrs)
|
||||
return vstrs
|
||||
}
|
||||
|
||||
func datasetToStringSortedTrimPrefix(prefix *zfs.DatasetPath, paths []*zfs.DatasetPath) []string {
|
||||
var pstrs []string
|
||||
for _, p := range paths {
|
||||
trimmed := p.Copy()
|
||||
trimmed.TrimPrefix(prefix)
|
||||
if trimmed.Length() == 0 {
|
||||
continue
|
||||
}
|
||||
pstrs = append(pstrs, trimmed.ToString())
|
||||
}
|
||||
sort.Strings(pstrs)
|
||||
return pstrs
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/zrepl/zrepl/platformtest"
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
|
||||
func IdempotentBookmark(ctx *platformtest.Context) {
|
||||
|
||||
platformtest.Run(ctx, platformtest.PanicErr, ctx.RootDataset, `
|
||||
DESTROYROOT
|
||||
CREATEROOT
|
||||
+ "foo bar"
|
||||
+ "foo bar@a snap"
|
||||
+ "foo bar@another snap"
|
||||
`)
|
||||
|
||||
fs := fmt.Sprintf("%s/foo bar", ctx.RootDataset)
|
||||
|
||||
asnap := sendArgVersion(ctx, fs, "@a snap")
|
||||
anotherSnap := sendArgVersion(ctx, fs, "@another snap")
|
||||
|
||||
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")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// should fail for another snapshot
|
||||
err = zfs.ZFSBookmark(ctx, fs, anotherSnap, "a bookmark")
|
||||
if err == nil {
|
||||
panic(err)
|
||||
}
|
||||
if _, ok := err.(*zfs.BookmarkExists); !ok {
|
||||
panic(fmt.Sprintf("has type %T", err))
|
||||
}
|
||||
|
||||
// destroy the snapshot
|
||||
if err := zfs.ZFSDestroy(ctx, fmt.Sprintf("%s@a snap", fs)); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// do it again, should fail with special error type
|
||||
err = zfs.ZFSBookmark(ctx, fs, asnap, "a bookmark")
|
||||
if err == nil {
|
||||
panic(err)
|
||||
}
|
||||
if _, ok := err.(*zfs.DatasetDoesNotExist); !ok {
|
||||
panic(fmt.Sprintf("has type %T", err))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/zrepl/zrepl/platformtest"
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
|
||||
func IdempotentDestroy(ctx *platformtest.Context) {
|
||||
|
||||
platformtest.Run(ctx, platformtest.PanicErr, ctx.RootDataset, `
|
||||
DESTROYROOT
|
||||
CREATEROOT
|
||||
+ "foo bar"
|
||||
+ "foo bar@a snap"
|
||||
`)
|
||||
|
||||
fs := fmt.Sprintf("%s/foo bar", ctx.RootDataset)
|
||||
asnap := sendArgVersion(ctx, fs, "@a snap")
|
||||
err := zfs.ZFSBookmark(ctx, fs, asnap, "a bookmark")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
type testCase struct {
|
||||
description, path string
|
||||
}
|
||||
|
||||
cases := []testCase{
|
||||
{"snapshot", fmt.Sprintf("%s@a snap", fs)},
|
||||
{"bookmark", fmt.Sprintf("%s#a bookmark", fs)},
|
||||
{"filesystem", fs},
|
||||
}
|
||||
|
||||
for i := range cases {
|
||||
func() {
|
||||
c := cases[i]
|
||||
|
||||
log.Printf("SUBBEGIN testing idempotent destroy %q for path %q", c.description, c.path)
|
||||
|
||||
log.Println("destroy existing")
|
||||
err = zfs.ZFSDestroy(ctx, c.path)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
log.Println("destroy again, non-idempotently, must error")
|
||||
err = zfs.ZFSDestroy(ctx, c.path)
|
||||
if _, ok := err.(*zfs.DatasetDoesNotExist); !ok {
|
||||
panic(fmt.Sprintf("%T: %s", err, err))
|
||||
}
|
||||
log.Println("destroy again, idempotently, must not error")
|
||||
err = zfs.ZFSDestroyIdempotent(ctx, c.path)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
log.Println("SUBEND")
|
||||
|
||||
}()
|
||||
}
|
||||
|
||||
// also test idempotent destroy for cases where the parent dataset does not exist
|
||||
err = zfs.ZFSDestroyIdempotent(ctx, fmt.Sprintf("%s/not foo bar@nonexistent snapshot", ctx.RootDataset))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
err = zfs.ZFSDestroyIdempotent(ctx, fmt.Sprintf("%s/not foo bar#nonexistent bookmark", ctx.RootDataset))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/zrepl/zrepl/platformtest"
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
|
||||
func IdempotentHold(ctx *platformtest.Context) {
|
||||
|
||||
platformtest.Run(ctx, platformtest.PanicErr, ctx.RootDataset, `
|
||||
DESTROYROOT
|
||||
CREATEROOT
|
||||
+ "foo bar"
|
||||
+ "foo bar@1"
|
||||
`)
|
||||
defer platformtest.Run(ctx, platformtest.PanicErr, ctx.RootDataset, `
|
||||
R zfs release zrepl_platformtest "${ROOTDS}/foo bar@1"
|
||||
- "foo bar@1"
|
||||
- "foo bar"
|
||||
`)
|
||||
|
||||
fs := fmt.Sprintf("%s/foo bar", ctx.RootDataset)
|
||||
v1 := fsversion(ctx, fs, "@1")
|
||||
|
||||
tag := "zrepl_platformtest"
|
||||
err := zfs.ZFSHold(ctx, fs, v1, tag)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
err = zfs.ZFSHold(ctx, fs, v1, tag)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/zrepl/zrepl/platformtest"
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
|
||||
func ListFilesystemVersionsTypeFilteringAndPrefix(t *platformtest.Context) {
|
||||
platformtest.Run(t, platformtest.PanicErr, t.RootDataset, `
|
||||
DESTROYROOT
|
||||
CREATEROOT
|
||||
+ "foo bar"
|
||||
+ "foo bar@foo 1"
|
||||
+ "foo bar#foo 1" "foo bar@foo 1"
|
||||
+ "foo bar#bookfoo 1" "foo bar@foo 1"
|
||||
+ "foo bar@foo 2"
|
||||
+ "foo bar#foo 2" "foo bar@foo 2"
|
||||
+ "foo bar#bookfoo 2" "foo bar@foo 2"
|
||||
+ "foo bar@blup 1"
|
||||
+ "foo bar#blup 1" "foo bar@blup 1"
|
||||
+ "foo bar@ foo with leading whitespace"
|
||||
|
||||
# repeat the whole thing for a child dataset to make sure we disable recursion
|
||||
|
||||
+ "foo bar/child dataset"
|
||||
+ "foo bar/child dataset@foo 1"
|
||||
+ "foo bar/child dataset#foo 1" "foo bar/child dataset@foo 1"
|
||||
+ "foo bar/child dataset#bookfoo 1" "foo bar/child dataset@foo 1"
|
||||
+ "foo bar/child dataset@foo 2"
|
||||
+ "foo bar/child dataset#foo 2" "foo bar/child dataset@foo 2"
|
||||
+ "foo bar/child dataset#bookfoo 2" "foo bar/child dataset@foo 2"
|
||||
+ "foo bar/child dataset@blup 1"
|
||||
+ "foo bar/child dataset#blup 1" "foo bar/child dataset@blup 1"
|
||||
+ "foo bar/child dataset@ foo with leading whitespace"
|
||||
`)
|
||||
|
||||
fs := fmt.Sprintf("%s/foo bar", t.RootDataset)
|
||||
|
||||
// no options := all types
|
||||
vs, err := zfs.ZFSListFilesystemVersions(t, mustDatasetPath(fs), zfs.ListFilesystemVersionsOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []string{
|
||||
"#blup 1", "#bookfoo 1", "#bookfoo 2", "#foo 1", "#foo 2",
|
||||
"@ foo with leading whitespace", "@blup 1", "@foo 1", "@foo 2",
|
||||
}, versionRelnamesSorted(vs))
|
||||
|
||||
// just snapshots
|
||||
vs, err = zfs.ZFSListFilesystemVersions(t, mustDatasetPath(fs), zfs.ListFilesystemVersionsOptions{
|
||||
Types: zfs.Snapshots,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []string{"@ foo with leading whitespace", "@blup 1", "@foo 1", "@foo 2"}, versionRelnamesSorted(vs))
|
||||
|
||||
// just bookmarks
|
||||
vs, err = zfs.ZFSListFilesystemVersions(t, mustDatasetPath(fs), zfs.ListFilesystemVersionsOptions{
|
||||
Types: zfs.Bookmarks,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []string{"#blup 1", "#bookfoo 1", "#bookfoo 2", "#foo 1", "#foo 2"}, versionRelnamesSorted(vs))
|
||||
|
||||
// just with prefix foo
|
||||
vs, err = zfs.ZFSListFilesystemVersions(t, mustDatasetPath(fs), zfs.ListFilesystemVersionsOptions{
|
||||
ShortnamePrefix: "foo",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []string{"#foo 1", "#foo 2", "@foo 1", "@foo 2"}, versionRelnamesSorted(vs))
|
||||
|
||||
}
|
||||
|
||||
func ListFilesystemVersionsZeroExistIsNotAnError(t *platformtest.Context) {
|
||||
platformtest.Run(t, platformtest.PanicErr, t.RootDataset, `
|
||||
DESTROYROOT
|
||||
CREATEROOT
|
||||
+ "foo bar"
|
||||
`)
|
||||
|
||||
fs := fmt.Sprintf("%s/foo bar", t.RootDataset)
|
||||
|
||||
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) {
|
||||
platformtest.Run(t, platformtest.PanicErr, t.RootDataset, `
|
||||
DESTROYROOT
|
||||
CREATEROOT
|
||||
`)
|
||||
|
||||
nonexistentFS := fmt.Sprintf("%s/not existent", t.RootDataset)
|
||||
|
||||
vs, err := zfs.ZFSListFilesystemVersions(t, mustDatasetPath(nonexistentFS), zfs.ListFilesystemVersionsOptions{})
|
||||
require.Empty(t, vs)
|
||||
require.Error(t, err)
|
||||
t.Logf("err = %T\n%s", err, err)
|
||||
dsne, ok := err.(*zfs.DatasetDoesNotExist)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, nonexistentFS, dsne.Path)
|
||||
}
|
||||
|
||||
func ListFilesystemVersionsUserrefs(t *platformtest.Context) {
|
||||
platformtest.Run(t, platformtest.PanicErr, t.RootDataset, `
|
||||
DESTROYROOT
|
||||
CREATEROOT
|
||||
+ "foo bar"
|
||||
+ "foo bar@snap 1"
|
||||
+ "foo bar#snap 1" "foo bar@snap 1"
|
||||
+ "foo bar@snap 2"
|
||||
+ "foo bar#snap 2" "foo bar@snap 2"
|
||||
R zfs hold zrepl_platformtest "${ROOTDS}/foo bar@snap 2"
|
||||
+ "foo bar@snap 3"
|
||||
+ "foo bar#snap 3" "foo bar@snap 3"
|
||||
R zfs hold zrepl_platformtest "${ROOTDS}/foo bar@snap 3"
|
||||
R zfs hold zrepl_platformtest_second_hold "${ROOTDS}/foo bar@snap 3"
|
||||
+ "foo bar@snap 4"
|
||||
+ "foo bar#snap 4" "foo bar@snap 4"
|
||||
|
||||
|
||||
+ "foo bar/child datset"
|
||||
+ "foo bar/child datset@snap 1"
|
||||
+ "foo bar/child datset#snap 1" "foo bar/child datset@snap 1"
|
||||
+ "foo bar/child datset@snap 2"
|
||||
+ "foo bar/child datset#snap 2" "foo bar/child datset@snap 2"
|
||||
R zfs hold zrepl_platformtest "${ROOTDS}/foo bar/child datset@snap 2"
|
||||
+ "foo bar/child datset@snap 3"
|
||||
+ "foo bar/child datset#snap 3" "foo bar/child datset@snap 3"
|
||||
R zfs hold zrepl_platformtest "${ROOTDS}/foo bar/child datset@snap 3"
|
||||
R zfs hold zrepl_platformtest_second_hold "${ROOTDS}/foo bar/child datset@snap 3"
|
||||
+ "foo bar/child datset@snap 4"
|
||||
+ "foo bar/child datset#snap 4" "foo bar/child datset@snap 4"
|
||||
`)
|
||||
|
||||
fs := fmt.Sprintf("%s/foo bar", t.RootDataset)
|
||||
|
||||
vs, err := zfs.ZFSListFilesystemVersions(t, mustDatasetPath(fs), zfs.ListFilesystemVersionsOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
type expectation struct {
|
||||
relName string
|
||||
userrefs zfs.OptionUint64
|
||||
}
|
||||
|
||||
expect := []expectation{
|
||||
{"#snap 1", zfs.OptionUint64{Valid: false}},
|
||||
{"#snap 2", zfs.OptionUint64{Valid: false}},
|
||||
{"#snap 3", zfs.OptionUint64{Valid: false}},
|
||||
{"#snap 4", zfs.OptionUint64{Valid: false}},
|
||||
{"@snap 1", zfs.OptionUint64{Value: 0, Valid: true}},
|
||||
{"@snap 2", zfs.OptionUint64{Value: 1, Valid: true}},
|
||||
{"@snap 3", zfs.OptionUint64{Value: 2, Valid: true}},
|
||||
{"@snap 4", zfs.OptionUint64{Value: 0, Valid: true}},
|
||||
}
|
||||
|
||||
sort.Slice(vs, func(i, j int) bool {
|
||||
return strings.Compare(vs[i].RelName(), vs[j].RelName()) < 0
|
||||
})
|
||||
|
||||
var expectRelNames []string
|
||||
for _, e := range expect {
|
||||
expectRelNames = append(expectRelNames, e.relName)
|
||||
}
|
||||
|
||||
require.Equal(t, expectRelNames, versionRelnamesSorted(vs))
|
||||
|
||||
for i, e := range expect {
|
||||
require.Equal(t, e.relName, vs[i].RelName())
|
||||
require.Equal(t, e.userrefs, vs[i].UserRefs)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/zrepl/zrepl/platformtest"
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
|
||||
func ListFilesystemsNoFilter(t *platformtest.Context) {
|
||||
platformtest.Run(t, platformtest.PanicErr, t.RootDataset, `
|
||||
DESTROYROOT
|
||||
CREATEROOT
|
||||
R zfs create -V 10M "${ROOTDS}/bar baz"
|
||||
+ "foo bar"
|
||||
+ "foo bar/bar blup"
|
||||
+ "foo bar/blah"
|
||||
R zfs create -V 10M "${ROOTDS}/foo bar/blah/a volume"
|
||||
`)
|
||||
|
||||
fss, err := zfs.ZFSListMapping(t, zfs.NoFilter())
|
||||
require.NoError(t, err)
|
||||
var onlyTestPool []*zfs.DatasetPath
|
||||
for _, fs := range fss {
|
||||
if strings.HasPrefix(fs.ToString(), t.RootDataset) {
|
||||
onlyTestPool = append(onlyTestPool, fs)
|
||||
}
|
||||
}
|
||||
onlyTestPoolStr := datasetToStringSortedTrimPrefix(mustDatasetPath(t.RootDataset), onlyTestPool)
|
||||
require.Equal(t, []string{"bar baz", "foo bar", "foo bar/bar blup", "foo bar/blah", "foo bar/blah/a volume"}, onlyTestPoolStr)
|
||||
|
||||
}
|
||||
@@ -3,6 +3,10 @@ package tests
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/zrepl/zrepl/endpoint"
|
||||
"github.com/zrepl/zrepl/platformtest"
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
@@ -13,25 +17,35 @@ func ReplicationCursor(ctx *platformtest.Context) {
|
||||
CREATEROOT
|
||||
+ "foo bar"
|
||||
+ "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"
|
||||
+ "foo bar@3 with space"
|
||||
R zfs bookmark "${ROOTDS}/foo bar@1 with space" "${ROOTDS}/foo bar#3 with space"
|
||||
`)
|
||||
|
||||
jobid := endpoint.MustMakeJobID("zreplplatformtest")
|
||||
|
||||
ds, err := zfs.NewDatasetPath(ctx.RootDataset + "/foo bar")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
guid, err := zfs.ZFSSetReplicationCursor(ds, "1 with space")
|
||||
|
||||
fs := ds.ToString()
|
||||
snap := fsversion(ctx, fs, "@1 with space")
|
||||
|
||||
destroyed, err := endpoint.MoveReplicationCursor(ctx, fs, &snap, jobid)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
snapProps, err := zfs.ZFSGetCreateTXGAndGuid(ds.ToString() + "@1 with space")
|
||||
assert.Empty(ctx, destroyed)
|
||||
|
||||
snapProps, err := zfs.ZFSGetFilesystemVersion(ctx, snap.FullPath(fs))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if guid != snapProps.Guid {
|
||||
panic(fmt.Sprintf("guids to not match: %v != %v", guid, snapProps.Guid))
|
||||
}
|
||||
|
||||
bm, err := zfs.ZFSGetReplicationCursor(ds)
|
||||
bm, err := endpoint.GetMostRecentReplicationCursorOfJob(ctx, fs, jobid)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -41,22 +55,15 @@ func ReplicationCursor(ctx *platformtest.Context) {
|
||||
if bm.Guid != snapProps.Guid {
|
||||
panic(fmt.Sprintf("guids do not match: %v != %v", bm.Guid, snapProps.Guid))
|
||||
}
|
||||
if bm.Guid != guid {
|
||||
panic(fmt.Sprintf("guids do not match: %v != %v", bm.Guid, guid))
|
||||
}
|
||||
|
||||
// test nonexistent
|
||||
err = zfs.ZFSDestroyFilesystemVersion(ds, bm)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
bm2, err := zfs.ZFSGetReplicationCursor(ds)
|
||||
if bm2 != nil {
|
||||
panic(fmt.Sprintf("expecting no replication cursor after deleting it, got %v", bm))
|
||||
}
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("expecting no error for getting nonexistent replication cursor, bot %v", err))
|
||||
}
|
||||
// try moving
|
||||
cursor1BookmarkName, err := endpoint.ReplicationCursorBookmarkName(fs, snap.Guid, jobid)
|
||||
require.NoError(ctx, err)
|
||||
|
||||
// TODO test moving the replication cursor
|
||||
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())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/zrepl/zrepl/platformtest"
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
|
||||
func ResumableRecvAndTokenHandling(ctx *platformtest.Context) {
|
||||
|
||||
platformtest.Run(ctx, platformtest.PanicErr, ctx.RootDataset, `
|
||||
DESTROYROOT
|
||||
CREATEROOT
|
||||
+ "send er"
|
||||
`)
|
||||
|
||||
sendFS := fmt.Sprintf("%s/send er", ctx.RootDataset)
|
||||
recvFS := fmt.Sprintf("%s/recv er", ctx.RootDataset)
|
||||
|
||||
supported, err := zfs.ResumeRecvSupported(ctx, mustDatasetPath(sendFS))
|
||||
check(err)
|
||||
|
||||
src := makeDummyDataSnapshots(ctx, sendFS)
|
||||
|
||||
s := makeResumeSituation(ctx, src, recvFS, zfs.ZFSSendArgsUnvalidated{
|
||||
FS: sendFS,
|
||||
To: src.snapA,
|
||||
Encrypted: &zfs.NilBool{B: false},
|
||||
ResumeToken: "",
|
||||
}, zfs.RecvOptions{
|
||||
RollbackAndForceRecv: false, // doesnt' exist yet
|
||||
SavePartialRecvState: true,
|
||||
})
|
||||
|
||||
if !supported {
|
||||
_, ok := s.recvErr.(*zfs.ErrRecvResumeNotSupported)
|
||||
require.True(ctx, ok)
|
||||
|
||||
// we know that support on sendFS implies support on recvFS
|
||||
// => assert that if we don't support resumed recv, the method returns ""
|
||||
tok, err := zfs.ZFSGetReceiveResumeTokenOrEmptyStringIfNotSupported(ctx, mustDatasetPath(recvFS))
|
||||
check(err)
|
||||
require.Equal(ctx, "", tok)
|
||||
|
||||
return // nothing more to test for recv that doesn't support -s
|
||||
}
|
||||
|
||||
getTokenRaw, err := zfs.ZFSGetReceiveResumeTokenOrEmptyStringIfNotSupported(ctx, mustDatasetPath(recvFS))
|
||||
check(err)
|
||||
|
||||
require.NotEmpty(ctx, getTokenRaw)
|
||||
decodedToken, err := zfs.ParseResumeToken(ctx, getTokenRaw)
|
||||
check(err)
|
||||
|
||||
require.True(ctx, decodedToken.HasToGUID)
|
||||
require.Equal(ctx, s.sendArgs.To.GUID, decodedToken.ToGUID)
|
||||
|
||||
recvErr := s.recvErr.(*zfs.RecvFailedWithResumeTokenErr)
|
||||
require.Equal(ctx, recvErr.ResumeTokenRaw, getTokenRaw)
|
||||
require.Equal(ctx, recvErr.ResumeTokenParsed, decodedToken)
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/zrepl/zrepl/platformtest"
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
|
||||
type resumeTokenTest struct {
|
||||
Msg string
|
||||
Token string
|
||||
ExpectToken *zfs.ResumeToken
|
||||
ExpectError error
|
||||
}
|
||||
|
||||
func (rtt *resumeTokenTest) Test(t *platformtest.Context) {
|
||||
|
||||
resumeSendSupported, err := zfs.ResumeSendSupported(t)
|
||||
if err != nil {
|
||||
t.Errorf("cannot determine whether resume supported: %T %s", err, err)
|
||||
t.FailNow()
|
||||
return
|
||||
}
|
||||
|
||||
res, err := zfs.ParseResumeToken(t, rtt.Token)
|
||||
|
||||
// if decoding is not supported, don't bother with the expectations
|
||||
if !resumeSendSupported {
|
||||
require.Error(t, err)
|
||||
require.Equal(t, zfs.ResumeTokenDecodingNotSupported, err)
|
||||
return
|
||||
}
|
||||
|
||||
if rtt.ExpectError != nil {
|
||||
require.EqualValues(t, rtt.ExpectError, err)
|
||||
return
|
||||
}
|
||||
if rtt.ExpectToken != nil {
|
||||
require.Nil(t, err)
|
||||
require.EqualValues(t, rtt.ExpectToken, res)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func ResumeTokenParsing(ctx *platformtest.Context) {
|
||||
|
||||
// cases generated using resumeTokensGenerate.bash on ZoL 0.8.1
|
||||
cases := []resumeTokenTest{
|
||||
{
|
||||
Msg: "zreplplatformtest/dst/full",
|
||||
Token: "1-b338b54f3-c0-789c636064000310a500c4ec50360710e72765a52697303030419460caa7a515a796806474e0f26c48f2499525a9c540ba42430fabfe92fcf4d2cc140686c88a76d578ae45530c90e439c1f27989b9a90c0c5545a905390539892569f945b940234bf48b8b921d12c1660200c61a1aba",
|
||||
ExpectToken: &zfs.ResumeToken{
|
||||
HasToGUID: true,
|
||||
ToGUID: 0x94a20a5f25877859,
|
||||
ToName: "zreplplatformtest/src@a",
|
||||
},
|
||||
},
|
||||
{
|
||||
Msg: "zreplplatformtest/dst/full_raw",
|
||||
Token: "1-e3f40c323-f8-789c636064000310a500c4ec50360710e72765a52697303030419460caa7a515a796806474e0f26c48f2499525a9c540da454f0fabfe92fcf4d2cc140686c88a76d578ae45530c90e439c1f27989b9a90c0c5545a905390539892569f945b940234bf48b8b921d12c1e6713320dc9f9c9f5b50945a5c9c9f0d119380ba07265f94580e936200004ff12141",
|
||||
ExpectToken: &zfs.ResumeToken{
|
||||
HasToGUID: true,
|
||||
ToGUID: 0x94a20a5f25877859,
|
||||
ToName: "zreplplatformtest/src@a",
|
||||
HasCompressOK: true, CompressOK: true,
|
||||
HasRawOk: true, RawOK: true,
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
Msg: "zreplplatformtest/dst/inc",
|
||||
Token: "1-eadabb296-e8-789c636064000310a501c49c50360710a715e5e7a69766a63040416445bb6a3cd7a2290a40363b92bafca4acd4e412060626a83a0cf9b4b4e2d412908c0e5c9e0d493ea9b224b518483ba8ea61d55f920f714515bf9b3fc3c396ef0648f29c60f9bcc4dc54a07c516a414e414e62495a7e512ed0c812fde2a2648724b09900d43e2191",
|
||||
ExpectToken: &zfs.ResumeToken{
|
||||
HasFromGUID: true, FromGUID: 0x94a20a5f25877859,
|
||||
HasToGUID: true, ToGUID: 0xf784e1004f460f7a,
|
||||
ToName: "zreplplatformtest/src@b",
|
||||
},
|
||||
},
|
||||
{
|
||||
Msg: "zreplplatformtest/dst/inc_raw",
|
||||
Token: "1-1164f8d409-120-789c636064000310a501c49c50360710a715e5e7a69766a63040416445bb6a3cd7a2290a40363b92bafca4acd4e412060626a83a0cf9b4b4e2d412908c0e5c9e0d493ea9b224b51848f368eb61d55f920f714515bf9b3fc3c396ef0648f29c60f9bcc4dc54a07c516a414e414e62495a7e512ed0c812fde2a2648724b079dc0c087f26e7e71614a51617e76743c424a0ee81c9172596c3a41800dd2c2818",
|
||||
ExpectToken: &zfs.ResumeToken{
|
||||
HasFromGUID: true, FromGUID: 0x94a20a5f25877859,
|
||||
HasToGUID: true, ToGUID: 0xf784e1004f460f7a,
|
||||
ToName: "zreplplatformtest/src@b",
|
||||
HasCompressOK: true, CompressOK: true,
|
||||
HasRawOk: true, RawOK: true,
|
||||
},
|
||||
},
|
||||
|
||||
// manual test csaes
|
||||
{
|
||||
Msg: "corrupted",
|
||||
Token: "1-1164f8d409-120-badf00d064000310a501c49c50360710a715e5e7a69766a63040416445bb6a3cd7a2290a40363b92bafca4acd4e412060626a83a0cf9b4b4e2d412908c0e5c9e0d493ea9b224b51848f368eb61d55f920f714515bf9b3fc3c396ef0648f29c60f9bcc4dc54a07c516a414e414e62495a7e512ed0c812fde2a2648724b079dc0c087f26e7e71614a51617e76743c424a0ee81c9172596c3a41800dd2c2818",
|
||||
ExpectError: zfs.ResumeTokenCorruptError,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range cases {
|
||||
ctx.Logf("BEGIN SUBTEST: %s", test.Msg)
|
||||
test.Test(ctx)
|
||||
ctx.Logf("COMPLETE SUBTEST: %s", test.Msg)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
set -x
|
||||
|
||||
POOLNAME="zreplplatformtest"
|
||||
POOLIMG="$1"
|
||||
|
||||
if zpool status "$POOLNAME"; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -e "$POOLIMG" ]; then
|
||||
exit 1
|
||||
fi
|
||||
fallocate -l 500M "$POOLIMG"
|
||||
zpool create "$POOLNAME" "$POOLIMG"
|
||||
|
||||
SRC_DATASET="$POOLNAME/src"
|
||||
DST_ROOT="$POOLNAME/dst"
|
||||
|
||||
keylocation="$(mktemp /tmp/ZREPL_PLATFORMTEST_GENERATE_TOKENS_KEYFILE_XXX)"
|
||||
echo "foobar123" > "$keylocation"
|
||||
zfs create -o encryption=on -o keylocation="file:///$keylocation" -o keyformat=passphrase "$SRC_DATASET"
|
||||
rm "$keylocation"
|
||||
|
||||
SRC_MOUNT="$(zfs get -H -o value mountpoint "$SRC_DATASET")"
|
||||
test -d "$SRC_MOUNT"
|
||||
|
||||
A="$SRC_DATASET"@a
|
||||
B="$SRC_DATASET"@b
|
||||
dd if=/dev/urandom of="$SRC_MOUNT"/dummy_data bs=1M count=5
|
||||
zfs snapshot "$A"
|
||||
dd if=/dev/urandom of="$SRC_MOUNT"/dummy_data bs=1M count=5
|
||||
zfs snapshot "$B"
|
||||
|
||||
zfs create "$DST_ROOT"
|
||||
|
||||
cutoff="dd bs=1024 count=3K"
|
||||
|
||||
set +e
|
||||
zfs send "$A" | $cutoff | zfs recv -s "$DST_ROOT"/full
|
||||
|
||||
zfs send "$A" | zfs recv "$DST_ROOT"/inc
|
||||
zfs send -i "$A" "$B" | $cutoff | zfs recv -s "$DST_ROOT"/inc
|
||||
|
||||
zfs send -w "$A" | $cutoff | zfs recv -s "$DST_ROOT"/full_raw
|
||||
|
||||
zfs send -w "$A" | zfs recv "$DST_ROOT"/inc_raw
|
||||
zfs send -w -i "$A" "$B" | $cutoff | zfs recv -s "$DST_ROOT"/inc_raw
|
||||
set -e
|
||||
|
||||
TOKENS="$(zfs list -H -o name,receive_resume_token -r "$DST_ROOT")"
|
||||
|
||||
echo "$TOKENS" | awk -e '//{ if ($2 == "-") { } else { system("zfs send -nvt " + $2);} }'
|
||||
|
||||
zpool destroy "$POOLNAME"
|
||||
rm "$POOLIMG"
|
||||
@@ -0,0 +1,198 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/zrepl/zrepl/platformtest"
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
|
||||
func SendArgsValidationEncryptedSendOfUnencryptedDatasetForbidden(ctx *platformtest.Context) {
|
||||
|
||||
supported, err := zfs.EncryptionCLISupported(ctx)
|
||||
check(err)
|
||||
expectNotSupportedErr := !supported
|
||||
|
||||
platformtest.Run(ctx, platformtest.PanicErr, ctx.RootDataset, `
|
||||
DESTROYROOT
|
||||
CREATEROOT
|
||||
+ "send er"
|
||||
+ "send er@a snap"
|
||||
`)
|
||||
|
||||
fs := fmt.Sprintf("%s/send er", ctx.RootDataset)
|
||||
props := mustGetFilesystemVersion(ctx, fs+"@a snap")
|
||||
|
||||
sendArgs, err := zfs.ZFSSendArgsUnvalidated{
|
||||
FS: fs,
|
||||
To: &zfs.ZFSSendArgVersion{
|
||||
RelName: "@a snap",
|
||||
GUID: props.Guid,
|
||||
},
|
||||
Encrypted: &zfs.NilBool{B: true},
|
||||
ResumeToken: "",
|
||||
}.Validate(ctx)
|
||||
|
||||
var stream *zfs.SendStream
|
||||
if err == nil {
|
||||
stream, err = zfs.ZFSSend(ctx, sendArgs) // no shadow
|
||||
if err == nil {
|
||||
defer stream.Close()
|
||||
}
|
||||
// fallthrough
|
||||
}
|
||||
|
||||
if expectNotSupportedErr {
|
||||
require.Error(ctx, err)
|
||||
require.Equal(ctx, zfs.ErrEncryptedSendNotSupported, err)
|
||||
return
|
||||
}
|
||||
require.Error(ctx, err)
|
||||
ctx.Logf("send err: %T %s", err, err)
|
||||
validationErr, ok := err.(*zfs.ZFSSendArgsValidationError)
|
||||
require.True(ctx, ok)
|
||||
require.True(ctx, validationErr.What == zfs.ZFSSendArgsEncryptedSendRequestedButFSUnencrypted)
|
||||
}
|
||||
|
||||
func SendArgsValidationResumeTokenEncryptionMismatchForbidden(ctx *platformtest.Context) {
|
||||
|
||||
supported, err := zfs.EncryptionCLISupported(ctx)
|
||||
check(err)
|
||||
if !supported {
|
||||
ctx.SkipNow()
|
||||
}
|
||||
supported, err = zfs.ResumeSendSupported(ctx)
|
||||
check(err)
|
||||
if !supported {
|
||||
ctx.SkipNow()
|
||||
}
|
||||
|
||||
platformtest.Run(ctx, platformtest.PanicErr, ctx.RootDataset, `
|
||||
DESTROYROOT
|
||||
CREATEROOT
|
||||
+ "send er" encrypted
|
||||
`)
|
||||
|
||||
sendFS := fmt.Sprintf("%s/send er", ctx.RootDataset)
|
||||
unencRecvFS := fmt.Sprintf("%s/unenc recv", ctx.RootDataset)
|
||||
encRecvFS := fmt.Sprintf("%s/enc recv", ctx.RootDataset)
|
||||
|
||||
src := makeDummyDataSnapshots(ctx, sendFS)
|
||||
|
||||
unencS := makeResumeSituation(ctx, src, unencRecvFS, zfs.ZFSSendArgsUnvalidated{
|
||||
FS: sendFS,
|
||||
To: src.snapA,
|
||||
Encrypted: &zfs.NilBool{B: false}, // !
|
||||
}, zfs.RecvOptions{
|
||||
RollbackAndForceRecv: false,
|
||||
SavePartialRecvState: true,
|
||||
})
|
||||
|
||||
encS := makeResumeSituation(ctx, src, encRecvFS, zfs.ZFSSendArgsUnvalidated{
|
||||
FS: sendFS,
|
||||
To: src.snapA,
|
||||
Encrypted: &zfs.NilBool{B: true}, // !
|
||||
}, zfs.RecvOptions{
|
||||
RollbackAndForceRecv: false,
|
||||
SavePartialRecvState: true,
|
||||
})
|
||||
|
||||
// threat model: use of a crafted resume token that requests an unencrypted send
|
||||
// but send args require encrypted send
|
||||
{
|
||||
var maliciousSend zfs.ZFSSendArgsUnvalidated = encS.sendArgs
|
||||
maliciousSend.ResumeToken = unencS.recvErrDecoded.ResumeTokenRaw
|
||||
|
||||
_, err := maliciousSend.Validate(ctx)
|
||||
validationErr, ok := err.(*zfs.ZFSSendArgsValidationError)
|
||||
require.True(ctx, ok)
|
||||
require.Equal(ctx, validationErr.What, zfs.ZFSSendArgsResumeTokenMismatch)
|
||||
ctx.Logf("%s", validationErr)
|
||||
|
||||
mismatchError, ok := validationErr.Msg.(*zfs.ZFSSendArgsResumeTokenMismatchError)
|
||||
require.True(ctx, ok)
|
||||
require.Equal(ctx, mismatchError.What, zfs.ZFSSendArgsResumeTokenMismatchEncryptionNotSet)
|
||||
}
|
||||
|
||||
// threat model: use of a crafted resume token that requests an encrypted send
|
||||
// but send args require unencrypted send
|
||||
{
|
||||
var maliciousSend zfs.ZFSSendArgsUnvalidated = unencS.sendArgs
|
||||
maliciousSend.ResumeToken = encS.recvErrDecoded.ResumeTokenRaw
|
||||
|
||||
_, err := maliciousSend.Validate(ctx)
|
||||
require.Error(ctx, err)
|
||||
ctx.Logf("send err: %T %s", err, err)
|
||||
validationErr, ok := err.(*zfs.ZFSSendArgsValidationError)
|
||||
require.True(ctx, ok)
|
||||
require.Equal(ctx, validationErr.What, zfs.ZFSSendArgsResumeTokenMismatch)
|
||||
ctx.Logf("%s", validationErr)
|
||||
|
||||
mismatchError, ok := validationErr.Msg.(*zfs.ZFSSendArgsResumeTokenMismatchError)
|
||||
require.True(ctx, ok)
|
||||
require.Equal(ctx, mismatchError.What, zfs.ZFSSendArgsResumeTokenMismatchEncryptionSet)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func SendArgsValidationResumeTokenDifferentFilesystemForbidden(ctx *platformtest.Context) {
|
||||
|
||||
supported, err := zfs.EncryptionCLISupported(ctx)
|
||||
check(err)
|
||||
if !supported {
|
||||
ctx.SkipNow()
|
||||
}
|
||||
supported, err = zfs.ResumeSendSupported(ctx)
|
||||
check(err)
|
||||
if !supported {
|
||||
ctx.SkipNow()
|
||||
}
|
||||
|
||||
platformtest.Run(ctx, platformtest.PanicErr, ctx.RootDataset, `
|
||||
DESTROYROOT
|
||||
CREATEROOT
|
||||
+ "send er1"
|
||||
+ "send er2"
|
||||
`)
|
||||
|
||||
sendFS1 := fmt.Sprintf("%s/send er1", ctx.RootDataset)
|
||||
sendFS2 := fmt.Sprintf("%s/send er2", ctx.RootDataset)
|
||||
recvFS := fmt.Sprintf("%s/unenc recv", ctx.RootDataset)
|
||||
|
||||
src1 := makeDummyDataSnapshots(ctx, sendFS1)
|
||||
src2 := makeDummyDataSnapshots(ctx, sendFS2)
|
||||
|
||||
rs := makeResumeSituation(ctx, src1, recvFS, zfs.ZFSSendArgsUnvalidated{
|
||||
FS: sendFS1,
|
||||
To: src1.snapA,
|
||||
Encrypted: &zfs.NilBool{B: false},
|
||||
}, zfs.RecvOptions{
|
||||
RollbackAndForceRecv: false,
|
||||
SavePartialRecvState: true,
|
||||
})
|
||||
|
||||
// threat model: forged resume token tries to steal a full send of snapA on fs2 by
|
||||
// presenting a resume token for full send of snapA on fs1
|
||||
var maliciousSend zfs.ZFSSendArgsUnvalidated = zfs.ZFSSendArgsUnvalidated{
|
||||
FS: sendFS2,
|
||||
To: &zfs.ZFSSendArgVersion{
|
||||
RelName: src2.snapA.RelName,
|
||||
GUID: src2.snapA.GUID,
|
||||
},
|
||||
Encrypted: &zfs.NilBool{B: false},
|
||||
ResumeToken: rs.recvErrDecoded.ResumeTokenRaw,
|
||||
}
|
||||
_, err = maliciousSend.Validate(ctx)
|
||||
require.Error(ctx, err)
|
||||
ctx.Logf("send err: %T %s", err, err)
|
||||
validationErr, ok := err.(*zfs.ZFSSendArgsValidationError)
|
||||
require.True(ctx, ok)
|
||||
require.Equal(ctx, validationErr.What, zfs.ZFSSendArgsResumeTokenMismatch)
|
||||
ctx.Logf("%s", validationErr)
|
||||
|
||||
mismatchError, ok := validationErr.Msg.(*zfs.ZFSSendArgsResumeTokenMismatchError)
|
||||
require.True(ctx, ok)
|
||||
require.Equal(ctx, mismatchError.What, zfs.ZFSSendArgsResumeTokenMismatchFilesystem)
|
||||
}
|
||||
@@ -18,4 +18,17 @@ var Cases = []Case{
|
||||
UndestroyableSnapshotParsing,
|
||||
GetNonexistent,
|
||||
ReplicationCursor,
|
||||
IdempotentHold,
|
||||
IdempotentBookmark,
|
||||
IdempotentDestroy,
|
||||
ResumeTokenParsing,
|
||||
ResumableRecvAndTokenHandling,
|
||||
SendArgsValidationEncryptedSendOfUnencryptedDatasetForbidden,
|
||||
SendArgsValidationResumeTokenEncryptionMismatchForbidden,
|
||||
SendArgsValidationResumeTokenDifferentFilesystemForbidden,
|
||||
ListFilesystemVersionsTypeFilteringAndPrefix,
|
||||
ListFilesystemVersionsFilesystemNotExist,
|
||||
ListFilesystemVersionsFilesystemNotExist,
|
||||
ListFilesystemVersionsUserrefs,
|
||||
ListFilesystemsNoFilter,
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/zrepl/zrepl/platformtest"
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
@@ -19,7 +20,7 @@ func UndestroyableSnapshotParsing(t *platformtest.Context) {
|
||||
R zfs hold zrepl_platformtest "${ROOTDS}/foo bar@4 5 6"
|
||||
`)
|
||||
|
||||
err := zfs.ZFSDestroy(fmt.Sprintf("%s/foo bar@1 2 3,4 5 6,7 8 9", t.RootDataset))
|
||||
err := zfs.ZFSDestroy(t, fmt.Sprintf("%s/foo bar@1 2 3,4 5 6,7 8 9", t.RootDataset))
|
||||
if err == nil {
|
||||
panic("expecting destroy error due to hold")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,367 @@
|
||||
The goal of this document is to describe what **logical steps** zrepl takes to replicate ZFS filesystems.
|
||||
Note that the actual code executing these steps is spread over the `endpoint.{Sender,Receiver}` RPC endpoint implementations. The code in `replication/{driver,logic}` (mostly `logic.Step.doReplication`) drives the replication and invokes the `endpoint` methods (locally or via RPC).
|
||||
Hence, when trying to map algorithm to implementation, use the code in package `replication` as the entrypoint and follow the RPC calls.
|
||||
|
||||
|
||||
* The [Single Replication Step](#zrepl-algo-single-step) algorithm is implemented as described
|
||||
* step holds are implemented as described
|
||||
* step bookmarks rely on bookmark cloning, which is WIP in OpenZFS (see respective TODO comments)
|
||||
* the feature to hold receive-side `to` immediately after replication is used to move the `last-received-hold` forward
|
||||
* this is a little more than what we allow in the algorithm (we only allow specifying a hold tag whereas moving the `last-received-hold` is a little more complex)
|
||||
* the algorithm's sender-side callback is used move the `zrepl_CURSOR` bookmark to point to `to`
|
||||
* The `zrepl_CURSOR` bookmark and the `last-received-hold` ensure that future replication steps can always be done using incremental replication:
|
||||
* Both reference / hold the last successfully received snapshot `to`.
|
||||
* Thus, `to` or `#zrepl_CURSOR_G_${to.GUID}_J_${jobid}` can always be used for a future incremental replication step `to => future-to`:
|
||||
* incremental send will be possible because `to` doesn't go away because we claim ownership of the `#zrepl_CURSOR` namespace
|
||||
* incremental recv will be possible iff
|
||||
* `to` will still be present on the recv-side because of `last-received-hold`
|
||||
* the recv-side fs doesn't get newer snapshots than `to` in the meantime
|
||||
* guaranteed because the zrepl model of the receiver assumes ownership of the filesystems it receives into
|
||||
* if that assumption is broken, future replication attempts will fail with a conflict
|
||||
* The [Algorithm for Planning and Executing Replication of an Filesystems](#zrepl-algo-filesystem) is a design draft and not used.
|
||||
However, there were some noteworthy lessons learned when implementing the algorithm for a single step:
|
||||
* In order to avoid leaking `step-hold`s and `step-bookmarks`, if the replication planner is invoked a second time after a replication step (either initial or incremental) has been attempted but failed to completed, the replication planner must
|
||||
* A) either guarantee that it will resume that replication step, and continue as if nothing happened or
|
||||
* B) release the step holds and bookmarks and clear the partially received state on the sending side.
|
||||
* Option A is what we want to do: we use the step algorithm to achieve resumability in the first place!
|
||||
* Option B is not done by zrepl except if the sending side doesn't support resuming.
|
||||
In that case however, we need not release any holds since the behavior is to re-start the send
|
||||
from the beginning.
|
||||
* However, there is one **edge-case to Option A for initial replication**:
|
||||
* If initial replication "`full a`" fails without leaving resumable state, the step holds on the sending side are still present, which makes sense because
|
||||
* a) we want resumability and
|
||||
* b) **the sending side cannot immediately be informed post-failure whether the initial replication left any state that would mandate keeping the step hold**, because the network connection might have failed.
|
||||
* Thus, the sending side must keep the step hold for "`full a`" until it knows more.
|
||||
* In the current implementation, it knows more when the next replication attempt is made, the planner is invoked, the diffing algorithm run, and the `HintMostRecentCommonAncestor` RPC is sent by the active side, communicating the most recent common version shared betwen sender and receiver.
|
||||
* At this point, the sender can safely throw away any step holds with CreateTXG's older than that version.
|
||||
* **The `step-hold`, `step-bookmark`, `last-received-hold` and `replication-cursor` abstractions are currently local concepts of package `endpoint` and not part of the replication protocol**
|
||||
* This is not necessarilty the best design decision and should be revisited some point:
|
||||
* The (quite expensive) `HintMostRecentCommonAncestor` RPC impl on the sender would not be necessary if step holds were part of the replication protocol:
|
||||
* We only need the `HintMostRecentCommonAncestor` info for the aforementioned edge-case during initial replication, where the receive is aborted without any partial received state being stored on the receiver (due to network failure, wrong zfs invocation, bad permissions, etc):
|
||||
* **The replication planner does not know about the step holds, thus it cannot deterministically pick up where it left of (right at the start of the last failing initial replication).**
|
||||
* Instead, it will seem like no prior invocation happened at all, and it will apply its policy for initial replication to pick a new `full b != full a`, **thereby leaking the step holds of `full a`**.
|
||||
* In contrast, if the replication planner created the step holds and knew about them, it could use the step holds as an indicator where it left off and re-start from there (of course asserting that the thereby inferred step is compatible with the state of the receiving side).
|
||||
* (What we do in zrepl right now is to hard-code the initial replication policy, and hard-code that assumption in `endpoint.ListStale` as well.)
|
||||
* The cummulative cleanup done in `HintMostRecentCommonAncestor` provides a nice self-healing aspect, though.
|
||||
|
||||
* We also have [Notes on Planning and Executing Replication of Multiple Filesystems](#zrepl-algo-multiple-filesystems-notes)
|
||||
|
||||
---
|
||||
|
||||
<a id="zrepl-algo-single-step"></a>
|
||||
## Algorithm for a Single Replication Step
|
||||
|
||||
The algorithm described below describes how a single replication step must be implemented.
|
||||
A *replication step* is a full send of a snapshot `to` for initial replication, or an incremental send (`from => to`) for incremental replication.
|
||||
|
||||
The algorithm **ensures resumability** of the replication step in presence of
|
||||
|
||||
* uncoordinated or unsynchronized destruction of the filesystem, snapshots, or bookmarks involved in the replication step
|
||||
* network failures at any time
|
||||
* other instances of this algorithm executing the same step in parallel (e.g. concurrent replication to different destinations)
|
||||
|
||||
To accomplish this goal, the algorithm **assumes ownership of parts of the ZFS hold tag namespace and the bookmark namespace**:
|
||||
* holds with prefix `zrepl_STEP` on any snapshot are reserved for zrepl
|
||||
* bookmarks with prefix `zrepl_STEP` are reserved for zrepl
|
||||
|
||||
Resumability of a step cannot be guaranteed if these sub-namespaces are manipulated through software other than zrepl.
|
||||
|
||||
Note that the algorithm **does not ensure** that a replication *plan*, which describes a *set* of replication steps, can be carried out successfully.
|
||||
If that is desirable, additional measures outside of this algorithm must be taken.
|
||||
|
||||
---
|
||||
|
||||
### Definitions:
|
||||
|
||||
#### Step Completion & Invariants
|
||||
|
||||
The replication step (full `to` send or `from => to` send) is *complete* iff the algorithm ran to completion without errors or a permanent non-network error is reported by sender or receiver.
|
||||
|
||||
Specifically, the algorithm may be invoked with the same `from` and `to` arguments, and potentially a `resume_token`, after a temporary (like network-related) failure:
|
||||
**Unless permanent errors occur, repeated invocations of the algorithm with updated resume token will converge monotonically (but not strictly monotonically) toward completion.**
|
||||
|
||||
Note that the mere existence of `to` on the receiving side does not constitute completion, since there may still be post-recv actions to be performed on sender and receiver.
|
||||
|
||||
#### Job and Job ID
|
||||
This algorithm supports that *multiple* instance of it run in parallel on the *same* step (full `to` / `from => to` pair).
|
||||
An exemplary use case for this feature are concurrent replication jobs that replicate the same dataset to different receivers.
|
||||
|
||||
**We require that all parallel invocations of this algorithm provide different and unique `jobid`s.**
|
||||
Violation of `jobid` uniqueness across parallel jobs may result in interference between instances of this algorithm, resulting in potential compromise of resumability.
|
||||
|
||||
After a step is *completed*, `jobid` is guaranteed to not be encoded in on-disk state.
|
||||
Before a step is completed, there is no such guarantee.
|
||||
Changing the `jobid` before step completion may compromise resumability and may leak the underlying ZFS holds or step bookmarks (i.e. zrepl won't clean them up)
|
||||
Note the definition of *complete* above.
|
||||
|
||||
#### Step Bookmark
|
||||
|
||||
A step bookmark is our equivalent of ZFS holds, but for bookmarks.<br/>
|
||||
A step bookmark is a ZFS bookmark whose name matches the following regex:
|
||||
|
||||
```
|
||||
STEP_BOOKMARK = #zrepl_STEP_bm_G_([0-9a-f]+)_J_(.+)
|
||||
```
|
||||
|
||||
- capture group `1` must be the guid of `zfs get guid ${STEP_BOOKMARK}`, encoded hexadecimal (without leading `0x`) and fixed-length (i.e. padded with leading zeroes)
|
||||
- capture group `2` must be equal to the `jobid`
|
||||
|
||||
### Algorithm
|
||||
|
||||
INPUT:
|
||||
|
||||
* `jobid`
|
||||
* `from`: snapshot or bookmark: may be nil for full send
|
||||
* `to`: snapshot, must never be nil
|
||||
* `resume_token` (may be nil)
|
||||
* (`from` and `to` must be on the same filesystem)
|
||||
|
||||
#### Prepare-Phase
|
||||
|
||||
Send-side: make sure `to` and `from` don't go away
|
||||
|
||||
- hold `to` using `idempotent_hold(to, zrepl_STEP_J_${jobid})`
|
||||
- make sure `from` doesn't go away:
|
||||
- if `from` is a snapshot: hold `from` using `idempotent_hold(from, zrepl_STEP_J_${jobid})`
|
||||
- else `idempotent_step_bookmark(from)` (`from` is a bookmark)
|
||||
- PERMANENT ERROR if this fails (e.g. because `from` is a bookmark whose snapshot no longer exists and we don't have bookmark copying yet (ZFS feature is in development) )
|
||||
- Why? we must assume the given bookmark is externally managed (i.e. not in the )
|
||||
- this means the bookmark is externally created bookmark and cannot be trusted to persist until the replication step succeeds
|
||||
- Maybe provide an override-knob for this behavior
|
||||
|
||||
Recv-side: no-op
|
||||
|
||||
- `from` cannot go away once we received enough data for the step to be resumable:
|
||||
```text
|
||||
# do a partial incremental recv @a=>@b (i.e. recv with -s, and abort the incremental recv in the middle)
|
||||
# try destroying the incremental source on the receiving side
|
||||
zfs destroy p1/recv@a
|
||||
cannot destroy 'p1/recv@a': snapshot has dependent clones
|
||||
use '-R' to destroy the following datasets:
|
||||
p1/recv/%recv
|
||||
# => doesn't work, because zfs recv is implemented as a `clone` internally, that's exactly what we want
|
||||
```
|
||||
|
||||
- if recv-side `to` exists, goto cleanup-phase (no replication to do)
|
||||
|
||||
- `to` cannot be destroyed while being received, because it isn't visible as a snapshot yet (it isn't yet one after all)
|
||||
|
||||
#### Replication Phase
|
||||
|
||||
Attempt the replication step:
|
||||
start `zfs send` and pipe its output into `zfs recv` until an error occurs or `recv` returns without error.
|
||||
|
||||
Let us now think about interferences that could occur during this phase, and ensure that none of them compromise the goals of this algorithm, i.e., monotonic convergence toward step completion using resumable send & recv.
|
||||
|
||||
**Safety from External Pruning**
|
||||
We are safe from pruning during the replication step because we have guarantees that no external action will destroy send-side `from` and `to`, and recv-side `to` (for both snapshot and bookmark `from`s)<br/>
|
||||
|
||||
**Safety In Presence of Network Failures During Replication**
|
||||
Network failures during replication can be recovered from using resumable send & recv:
|
||||
|
||||
- Network failure before the receive-side could stored data:
|
||||
- Send-side `from` and `to` are guaranteed to be still present due to zfs holds
|
||||
- recv-side `from` may no longer exist because we don't `hold` it explicitly
|
||||
- if that is the case, ERROR OUT, step can never complete
|
||||
- If the step planning algorithm does not include the step, for example because a snapshot filter configuration was changed by the user inbetween which hides `from` or `to` from the second planning attempt: **tough luck, we're leaking all holds**
|
||||
- Network failure during the replication
|
||||
- send-side `from` and `to` are still present due to zfs holds
|
||||
- recv-side `from` is still present because the partial receive state prevents its destruction (see prepare-phase)
|
||||
- if recv-side has a resume token, the resume token will continue to work on the sender because `from`s and `to` are still present
|
||||
- Network failure at the end of the replication step stream transmission
|
||||
- Variant A: failure from the sender's perspective, success from the receiver's perspective
|
||||
- receive-side `to` doesn't have a hold and could be destroyed anytime
|
||||
- receive-side `from` doesn't have a hold and could be destroyed anytime
|
||||
- thus, when the step is invoked again, pattern match `(from_exists, to_exists)`
|
||||
- `(true, true)`: prepare-phase will early-exit
|
||||
- `(false,true)`: prepare-phase will error out bc. `from` does not exist
|
||||
- `(true, false)`: entire step will be re-done
|
||||
- FIXME monotonicity requirement does not hold
|
||||
- `(false, false)`: prepare-phase will error out bc. `from` does not exist
|
||||
- Variant B: success from the sender's perspective, failure from the receiver's perspective
|
||||
- No idea how this would happen except for bugs in error reporting in the replication protocol
|
||||
- Misclassification by the sender, most likely broken error handling in the sender or replication protocol
|
||||
- => the sender will release holds and move the replication cursor while the receiver won't => tough luck
|
||||
|
||||
If the RPC used for `zfs recv` returned without error, this phase is done.
|
||||
(Although the *replication step* (this algorithm) is not yet *complete*, see definition of complete).
|
||||
|
||||
|
||||
#### Cleanup-Phase
|
||||
|
||||
At the end of this phase, all intermediate state we built up to support the resumable replication of the step is destroyed.
|
||||
However, consumers of this algorithm might want to take advantage of the fact that we currently still have holds / step bookmarks.
|
||||
|
||||
##### Recv-side: Optionally hold `to` with a caller-defined tag
|
||||
|
||||
Users of the algorithm might want to depend on `to` being available on the receiving side for a longer duration than the lifetime of the current step's algorithm invocation.
|
||||
For reasons explained in the next paragraph, we cannot guarantee that we have a `hold` when `recv` exists. If that were the case, we could take our time to provide a generalized callback to the user of the algorithm, and have them do whatever they want with `to` while we guarantee that `to` doesn't go away through the hold. But that functionality isn't available, so we only provide a fixed operation right after receive: **take a `hold` with a tag of the algorithm user's choice**. That's what's needed to guarantee that a replication plan, consisting of multiple consecutive steps, can be carried out without a receive-side prune job interfering by destroying `to`. Technically, this step is racy, i.e., it could happen that `to` is destroyed between `recv` and `hold`. But this is a) unlikely and b) not fatal because we can detect that hold fails because the 'dataset does not exist` and re-do the entire transmission since we still hold send-side `from` and `to`, i.e. we just lose a lot of work in rare cases.
|
||||
|
||||
So why can't we have a `hold` at the time `recv` exits?
|
||||
It seems like [`zfs send --holds`](https://github.com/zfsonlinux/zfs/commit/9c5e88b1ded19cb4b19b9d767d5c71b34c189540) could be used to send the send-side's holds to the receive-side. But that flag always sends all holds, and `--holds` is implemented in user-space libzfs, i.e., doesn't happen in the same txg as the recv completion. Thus, the race window is in fact only smaller (unless we oversaw some synchronization in userland zfs).
|
||||
But if we're willing to entertain the idea a little further, we still hit the problem that `--holds` sends _all_ holds, whereas our goal is to _temporarily_ have >= 1 hold that we own until the callback is done, and then release all of the received holds so that no holds created by us exist after this algorithm completes.
|
||||
Since there could be concurrent `zfs hold` invocations on the sender and receiver while this algorithm runs, and because `--holds` doesn't provide info about which holds were sent, we cannot correctly destroy _exactly_ those holds that we received.
|
||||
|
||||
##### Send-side: callback to consumer, then cleanup
|
||||
We can provide the algorithm user with a generic callback because we have holds / step bookmarks for `to` and `from`, respectively.
|
||||
|
||||
Example use case for the sender-side callback is the replication cursor, see algorithm for filesystem replication below.
|
||||
|
||||
After the callback is done: cleanup holds & step bookmark:
|
||||
|
||||
- `idempotent_release(to, zrepl_STEP_J_${jobid})`
|
||||
- make sure `from` can now go away:
|
||||
- if `from` is a snapshot: `idempotent_release(from, zrepl_STEP_J_${jobid})`
|
||||
- else `idempotent_step_bookmark_destroy(from)`
|
||||
- if `from` fulfills the properties of a step bookmark: destroy it
|
||||
- otherwise: it's a bookmark not created by this algorithm that happened to be used for replication: leave it alone
|
||||
- that can only happen if user used the override knob
|
||||
|
||||
Note that "make sure `from` can now go away" is the inverse of "Send-side: make sure `to` and `from` don't go away". Those are relatively complex operations and should be implemented in the same file next to each other to ease maintainability.
|
||||
|
||||
---
|
||||
|
||||
### Notes & Pseudo-APIs used in the algorithm
|
||||
|
||||
- `idempotent_hold(snapshot s, string tag)` like zfs hold, but doesn't error if hold already exists
|
||||
- `idempotent_release(snapshot s, string tag)` like zfs hold, but doesn't error if hold already exists
|
||||
- `idempotent_step_bookmark(snapshot_or_bookmark b)` creates a *step bookmark* of b
|
||||
- determine step bookmark name N
|
||||
- if `N` already exists, verify it's a correct step bookmark => DONE or ERROR
|
||||
- if `b` is a snapshot, issue `zfs bookmark`
|
||||
- if `b` is a bookmark:
|
||||
- if bookmark cloning supported, use it to duplicate the bookmark
|
||||
- else ERROR OUT, with an error that upper layers can identify as such, so that they are able to ignore the fact that we couldn't create a step bookmark
|
||||
- `idempotent_destroy(bookmark #b_$GUID, of a snapshot s)` must atomically check that `$GUID == s.guid` before destroying s
|
||||
- `idempotent_bookmark(snapshot s, $GUID, name #b_$GUID)` must atomically check that `$GUID == s.guid` at the time of bookmark creation
|
||||
- `idempotent_destroy(snapshot s)` must atomically check that zrepl's `s.guid` matches the current `guid` of the snapshot (i.e. destroy by guid)
|
||||
|
||||
|
||||
<a id="zrepl-algo-filesystem"></a>
|
||||
## Algorithm for Planning and Executing Replication of a Filesystems (planned, not implemented yet)
|
||||
|
||||
This algorithm describes how a filesystem or zvol is replicated in zrepl.
|
||||
The algorithm is invoked with a `jobid`, `sender`, `receiver`, a sender-side `filesystem path`.
|
||||
It builds a diff between the sender and receiver filesystem bookmarks+snapshots and determines whether a replication conflict exists or whether fast-forward replication using full or incremental sends is possible.
|
||||
In case of conflict, the algorithm errors out with a conflict description that can be used to manually or automatically resolve the conflict.
|
||||
Otherwise, the algorithm builds a list of replication steps that are then worked on sequentially by the "Algorithm for a Single Replication Step".
|
||||
|
||||
The algorithm ensures that a plan can be executed exactly as planned by acquiring appropriate zfs holds.
|
||||
The algorithm can be configured to retry a plan when encountering non-permanent errors (e.g. network errors).
|
||||
However, permanent errors result in the plan being cancelled.
|
||||
|
||||
Regardless of whether a plan can be executed to completion or is cancelled, the algorithm guarantees that leftover artifacts (i.e. holds) of its invocation are cleaned up.
|
||||
However, since network failure can occur at any point, there might be stale holds on sending or receiving side after a crash or other error.
|
||||
These will be cleaned up on a subsequent invocation of this algorithm with the same `jobid`.
|
||||
|
||||
The algorithm is fit to be executed in parallel from the same sender to different receivers.
|
||||
To be clear: replicating in parallel from the same sender to different receivers is supported.
|
||||
But one instance of the algorithm assumes ownership of the `filesystem path` on the receiver.
|
||||
|
||||
The algorithm reserves the following sub-namespaces:
|
||||
* zfs hold: `zrepl_FS`
|
||||
* bookmark names: `zrepl_FS`
|
||||
|
||||
### Definitions
|
||||
|
||||
#### ZFS Filesystem Path
|
||||
We refer to the name of a ZFS filesystem or volume as a *filesystem path*, sometimes just *path*.
|
||||
The name includes the pool name.
|
||||
|
||||
#### Sender and Receiver
|
||||
Sender and Receiver are passed as RPC stubs to the algorithm.
|
||||
One of those RPC stubs will typically be local, i.e., call methods on a struct in the same address space.
|
||||
The other RPC stub may invoke actual calls over the network (unless local replication is used).
|
||||
The algorithm does not make any assumption about which object is local or an actual stub.
|
||||
This design decouples the replication logic (described in this algorithm) from the question which side (sender or receiver) initiates the replication.
|
||||
|
||||
### Algorithm
|
||||
|
||||
**Cleanup Previous Invocations With Same `jobid`** by scanning the filesystem's list of snapshots and step bookmarks, and destroying any which was created by this algorithm when it was invoked with `jobid`.<br/>
|
||||
TODO HOW
|
||||
|
||||
**Build a fast-forward list of replication steps** `STEPS`.
|
||||
`STEPS` may contain an optional initial full send, and subsequent incremental send steps.
|
||||
`STEPS[0]` may also have a resume token.
|
||||
If fast-forward is not possible, produce a conflict description and ERROR OUT.<br/>
|
||||
TODOs:
|
||||
- make it configurable what snapshots are included in the list (i.e. every one we see, only most recent, at least one every X hours, ...)
|
||||
|
||||
**Ensure that we will be able to carry out all steps** by acquiring holds or fsstep bookmarks on the sending side
|
||||
- `idempotent_hold([s.to for s in STEPS], zrepl_FS_J_${jobid})`
|
||||
- `if STEPS[0].from != nil: idempotent_FSSTEP_bookmark(STEPS[0].from, zrepl_FSSTEP_bm_G_${STEPS[0].from.guid}_J_${jobid})`
|
||||
|
||||
**Determine which steps have not been completed (`uncompleted_steps`)** (we might be in an second invocation of this algorithm after a network failure and some steps might already be done):
|
||||
- `res_tok := receiver.ResumeToken(fs)`
|
||||
- `rmrfsv := receiver.MostRecentFilesystemVersion(fs)`
|
||||
- if `res_tok != nil`: ensure that `res_tok` has a corresponding step in `STEPS`, otherwise ERROR OUT
|
||||
- if `rmrfsv != nil`: ensure that `res_tok` has a corresponding step in `STEPS`, otherwise ERROR OUT
|
||||
- if `(res_token != nil && rmrfsv != nil)`: ensure that `res_tok` is the subsequent step to the one we found for `rmrfsv`
|
||||
- if both are nil, we are at the beginning, `uncompleted_steps = STEPS` and goto next block
|
||||
- `rstep := if res_tok != nil { res_tok } else { rmrfsv }`
|
||||
- `uncompleted_steps := STEPS[find_step_idx(STEPS, rstep).expect("must exist, checked above"):]`
|
||||
- Note that we do not explicitly check for the completion of prior replication steps.
|
||||
All we care about is what needs to be done from `rstep`.
|
||||
- This is intentional and necessary because we cumulatively release all holds and step bookmarks made for steps that precede a just-completed step (see next paragraph)
|
||||
|
||||
**Execute uncompleted steps**<br/>
|
||||
Invoke the "Algorithm for a Single Replication Step" for each step in `uncompleted_steps`.
|
||||
Remember to pass the resume token if one exists.
|
||||
|
||||
In the wind-down phase of each replication step `from => to`, while the per-step algorithm still holds send-side `from` and `to`, as well as recv-side `to`:
|
||||
|
||||
- Sending side: **Idempotently move the replication cursor to `to`.**
|
||||
- Why: replication cursor tracks the sender's last known replication state, outlives the current plan, but is required to guarantee that future invocations of this algorithm find an incremental path.
|
||||
- Impl for 'atomic' move: have two cursors (fixes [#177](https://github.com/zrepl/zrepl/issues/177))
|
||||
- Idempotently cursor-bookmark `to` using `idempotent_bookmark(to, to.guid, #zrepl_replication_cursor_G_${to.guid}_J_${jobid})`
|
||||
- Idempotently destroy old cursor-bookmark of `from` `idempotent_destroy(#zrepl_replication_cursor_G_${from.guid}_J_${jobid}, from)`
|
||||
- If `from` is a snapshot, release the hold on it using `idempotent_release(from, zrepl_J_${jobid})`
|
||||
- FIXME: resumability if we crash inbetween those operations (or scrap it and wait for channel programs to bring atomicity)
|
||||
|
||||
- Receiving side: **`idempotent_hold(to, zrepl_FS_J_${jobid})` because its going to be the next step's `from`**
|
||||
- As discussed in the section on the per-step algorithm, this is a feature provided to us by the per-step algorithm.
|
||||
|
||||
- Receiving side + Sending side (order doesn't matter): Make sure all holds & step bookmarks made by this plan on already replicated snapshots are released / destroyed:
|
||||
- `idempotent_release_prior_and_including(from, zrepl_FS_TODO)`
|
||||
- `idempotent_step_bookmark_destroy_prior_and_including(from, zrepl_FS_TODO)`
|
||||
|
||||
**Cleanup** receiving and sending side (order doesn't matter)
|
||||
|
||||
- `idempotent_release_prior_and_including(STEPS[-1].to, zrepl_FS_TODO)`
|
||||
- `idempotent_step_bookmark_destroy_prior_and_including(STEPS[-1].from, zrepl_FS_TODO)`
|
||||
|
||||
### Notes
|
||||
|
||||
- `idempotent_FSSTEP_bookmark` is like `idempotent_STEP_bookmark`, but with prefix `zrepl_FSSTEP` instead of `zrepl_STEP`
|
||||
|
||||
|
||||
<a id="zrepl-algo-multiple-filesystems-notes"></a>
|
||||
|
||||
## Notes on Planning and Executing Replication of Multiple Filesystems
|
||||
|
||||
### The RPC Interfaces Uses Send-side Filesystem Names to Identify a Filesystem
|
||||
|
||||
The model of the receiver includes the assumption that it will `Receive` all snapshot streams sent to it into filesystems with paths prefixed with `root_fs`.
|
||||
This is useful for separating filesystem path namespaces for multiple clients.
|
||||
The receiver hides this prefixing in its RPC interface, i.e., when responding to `ListFilesystems` rpcs, the prefix is removed before sending the response.
|
||||
This behavior is useful to achieve symmetry in this algorithm: we do not need to take the prefixing into consideration when computing diffs.
|
||||
For the receiver, it has the advantage that `Receive` rpc can enforce the namespacing and need not trust this algorithm to apply it correctly.
|
||||
|
||||
The receiver is also allowed (and may need to) do implement other filtering / namespace transformations.
|
||||
For example, when only `foo/a/b` has been received, but not `foo` nor `foo/a`, the receiver must not include the latter two in its `ListFilesystems` response.
|
||||
The current zrepl receiver endpoint implementation uses the `zrepl:placeholder` property to mark filesystems `foo` and `foo/a` as placeholders, and filters out such filesystems in the `ListFilesystems` response.
|
||||
Another approach would be to have a flat list of received filesystems per sender, and have a separate table that associates send-side names and receive-side filesystem paths.
|
||||
|
||||
Similarly, the sender is allowed to filter the output of its RPC interface to hide certain filesystems or snapshots.
|
||||
|
||||
#### Consequences of the Above Design
|
||||
|
||||
Namespace filtering and transformations of filesystem paths on sender and receiver are user-configurable.
|
||||
Both ends of the replication setup have their own config, and do not coordinate config changes.
|
||||
This results in a number of challenges:
|
||||
|
||||
- A sender might change its filter to allow additional filesystems to be replicated.
|
||||
For example, where `foo/a/b` was initially allowed, the new filter might allow `foo` and `foo/a` as well.
|
||||
If the receiver uses the `zrepl:placeholder` approach as sketched out above, this means that the receiver will need to replace the placeholder filesystems `$root_fs/foo` and `$root_fs/foo/a` with the incoming full sends of `foo` and `foo/a`.
|
||||
|
||||
- Send-side renames cannot be handled efficiently because send-side rename effectively changes the filesystem identity because we use its name.
|
||||
@@ -146,6 +146,12 @@ type fs struct {
|
||||
|
||||
l *chainlock.L
|
||||
|
||||
// ordering relationship that must be maintained for initial replication
|
||||
initialRepOrd struct {
|
||||
parents, children []*fs
|
||||
parentDidUpdate chan struct{}
|
||||
}
|
||||
|
||||
planning struct {
|
||||
done bool
|
||||
err *timedError
|
||||
@@ -296,6 +302,7 @@ func (a *attempt) do(ctx context.Context, prev *attempt) {
|
||||
fs: pfs,
|
||||
l: a.l,
|
||||
}
|
||||
fs.initialRepOrd.parentDidUpdate = make(chan struct{}, 1)
|
||||
a.fss = append(a.fss, fs)
|
||||
}
|
||||
|
||||
@@ -352,10 +359,22 @@ func (a *attempt) do(ctx context.Context, prev *attempt) {
|
||||
}
|
||||
}
|
||||
}
|
||||
// invariant: prevs contains an entry for each unambigious correspondence
|
||||
// invariant: prevs contains an entry for each unambiguous correspondence
|
||||
|
||||
// build up parent-child relationship (FIXME (O(n^2), but who's going to have that many filesystems...))
|
||||
for _, f1 := range a.fss {
|
||||
fs1 := f1.fs.ReportInfo().Name
|
||||
for _, f2 := range a.fss {
|
||||
fs2 := f2.fs.ReportInfo().Name
|
||||
if strings.HasPrefix(fs1, fs2) && fs1 != fs2 {
|
||||
f1.initialRepOrd.parents = append(f1.initialRepOrd.parents, f2)
|
||||
f2.initialRepOrd.children = append(f2.initialRepOrd.children, f1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stepQueue := newStepQueue()
|
||||
defer stepQueue.Start(1)() // TODO parallel replication
|
||||
defer stepQueue.Start(envconst.Int("ZREPL_REPLICATION_EXPERIMENTAL_REPLICATION_CONCURRENCY", 1))() // TODO parallel replication
|
||||
var fssesDone sync.WaitGroup
|
||||
for _, f := range a.fss {
|
||||
fssesDone.Add(1)
|
||||
@@ -370,42 +389,76 @@ func (a *attempt) do(ctx context.Context, prev *attempt) {
|
||||
a.finishedAt = time.Now()
|
||||
}
|
||||
|
||||
func (fs *fs) do(ctx context.Context, pq *stepQueue, prev *fs) {
|
||||
psteps, err := fs.fs.PlanFS(ctx)
|
||||
errTime := time.Now()
|
||||
defer fs.l.Lock().Unlock()
|
||||
debug := debugPrefix("fs=%s", fs.fs.ReportInfo().Name)
|
||||
fs.planning.done = true
|
||||
func (f *fs) debug(format string, args ...interface{}) {
|
||||
debugPrefix("fs=%s", f.fs.ReportInfo().Name)(format, args...)
|
||||
}
|
||||
|
||||
// wake up children that watch for f.{planning.{err,done},planned.{step,stepErr}}
|
||||
func (f *fs) initialRepOrdWakeupChildren() {
|
||||
var children []string
|
||||
for _, c := range f.initialRepOrd.children {
|
||||
// no locking required, c.fs does not change
|
||||
children = append(children, c.fs.ReportInfo().Name)
|
||||
}
|
||||
f.debug("wakeup children %s", children)
|
||||
for _, child := range f.initialRepOrd.children {
|
||||
select {
|
||||
// no locking required, child.initialRepOrd does not change
|
||||
case child.initialRepOrd.parentDidUpdate <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (f *fs) do(ctx context.Context, pq *stepQueue, prev *fs) {
|
||||
|
||||
defer f.l.Lock().Unlock()
|
||||
defer f.initialRepOrdWakeupChildren()
|
||||
|
||||
// get planned steps from replication logic
|
||||
var psteps []Step
|
||||
var errTime time.Time
|
||||
var err error
|
||||
f.l.DropWhile(func() {
|
||||
// TODO hacky
|
||||
// choose target time that is earlier than any snapshot, so fs planning is always prioritized
|
||||
targetDate := time.Unix(0, 0)
|
||||
defer pq.WaitReady(f, targetDate)()
|
||||
psteps, err = f.fs.PlanFS(ctx) // no shadow
|
||||
errTime = time.Now() // no shadow
|
||||
})
|
||||
if err != nil {
|
||||
fs.planning.err = newTimedError(err, errTime)
|
||||
f.planning.err = newTimedError(err, errTime)
|
||||
return
|
||||
}
|
||||
for _, pstep := range psteps {
|
||||
step := &step{
|
||||
l: fs.l,
|
||||
l: f.l,
|
||||
step: pstep,
|
||||
}
|
||||
fs.planned.steps = append(fs.planned.steps, step)
|
||||
f.planned.steps = append(f.planned.steps, step)
|
||||
}
|
||||
debug("iniital len(fs.planned.steps) = %d", len(fs.planned.steps))
|
||||
// we're not done planning yet, f.planned.steps might still be changed by next block
|
||||
// => don't set f.planning.done just yet
|
||||
f.debug("initial len(fs.planned.steps) = %d", len(f.planned.steps))
|
||||
|
||||
// for not-first attempts, only allow fs.planned.steps
|
||||
// up to including the originally planned target snapshot
|
||||
if prev != nil && prev.planning.done && prev.planning.err == nil {
|
||||
prevUncompleted := prev.planned.steps[prev.planned.step:]
|
||||
if len(prevUncompleted) == 0 {
|
||||
debug("prevUncompleted is empty")
|
||||
f.debug("prevUncompleted is empty")
|
||||
return
|
||||
}
|
||||
if len(fs.planned.steps) == 0 {
|
||||
debug("fs.planned.steps is empty")
|
||||
if len(f.planned.steps) == 0 {
|
||||
f.debug("fs.planned.steps is empty")
|
||||
return
|
||||
}
|
||||
prevFailed := prevUncompleted[0]
|
||||
curFirst := fs.planned.steps[0]
|
||||
curFirst := f.planned.steps[0]
|
||||
// we assume that PlanFS retries prevFailed (using curFirst)
|
||||
if !prevFailed.step.TargetEquals(curFirst.step) {
|
||||
debug("Targets don't match")
|
||||
f.debug("Targets don't match")
|
||||
// Two options:
|
||||
// A: planning algorithm is broken
|
||||
// B: manual user intervention inbetween
|
||||
@@ -421,44 +474,130 @@ func (fs *fs) do(ctx context.Context, pq *stepQueue, prev *fs) {
|
||||
}
|
||||
msg := fmt.Sprintf("last attempt's uncompleted step %s does not correspond to this attempt's first planned step %s",
|
||||
stepFmt(prevFailed), stepFmt(curFirst))
|
||||
fs.planned.stepErr = newTimedError(errors.New(msg), time.Now())
|
||||
f.planned.stepErr = newTimedError(errors.New(msg), time.Now())
|
||||
return
|
||||
}
|
||||
// only allow until step targets diverge
|
||||
min := len(prevUncompleted)
|
||||
if min > len(fs.planned.steps) {
|
||||
min = len(fs.planned.steps)
|
||||
if min > len(f.planned.steps) {
|
||||
min = len(f.planned.steps)
|
||||
}
|
||||
diverge := 0
|
||||
for ; diverge < min; diverge++ {
|
||||
debug("diverge compare iteration %d", diverge)
|
||||
if !fs.planned.steps[diverge].step.TargetEquals(prevUncompleted[diverge].step) {
|
||||
f.debug("diverge compare iteration %d", diverge)
|
||||
if !f.planned.steps[diverge].step.TargetEquals(prevUncompleted[diverge].step) {
|
||||
break
|
||||
}
|
||||
}
|
||||
debug("diverge is %d", diverge)
|
||||
fs.planned.steps = fs.planned.steps[0:diverge]
|
||||
f.debug("diverge is %d", diverge)
|
||||
f.planned.steps = f.planned.steps[0:diverge]
|
||||
}
|
||||
debug("post-prev-merge len(fs.planned.steps) = %d", len(fs.planned.steps))
|
||||
f.debug("post-prev-merge len(fs.planned.steps) = %d", len(f.planned.steps))
|
||||
|
||||
for i, s := range fs.planned.steps {
|
||||
var (
|
||||
err error
|
||||
errTime time.Time
|
||||
)
|
||||
// lock must not be held while executing step in order for reporting to work
|
||||
fs.l.DropWhile(func() {
|
||||
targetDate := s.step.TargetDate()
|
||||
defer pq.WaitReady(fs, targetDate)()
|
||||
err = s.step.Step(ctx) // no shadow
|
||||
errTime = time.Now() // no shadow
|
||||
// now we are done planning (f.planned.steps won't change from now on)
|
||||
f.planning.done = true
|
||||
|
||||
// wait for parents' initial replication
|
||||
var parents []string
|
||||
for _, p := range f.initialRepOrd.parents {
|
||||
parents = append(parents, p.fs.ReportInfo().Name)
|
||||
}
|
||||
f.debug("wait for parents %s", parents)
|
||||
for {
|
||||
var initialReplicatingParentsWithErrors []string
|
||||
allParentsPresentOnReceiver := true
|
||||
f.l.DropWhile(func() {
|
||||
for _, p := range f.initialRepOrd.parents {
|
||||
p.l.HoldWhile(func() {
|
||||
// (get the preconditions that allow us to inspect p.planned)
|
||||
parentHasPlanningDone := p.planning.done && p.planning.err == nil
|
||||
if !parentHasPlanningDone {
|
||||
// if the parent couldn't be planned, we cannot know whether it needs initial replication
|
||||
// or incremental replication => be conservative and assume it was initial replication
|
||||
allParentsPresentOnReceiver = false
|
||||
if p.planning.err != nil {
|
||||
initialReplicatingParentsWithErrors = append(initialReplicatingParentsWithErrors, p.fs.ReportInfo().Name)
|
||||
}
|
||||
return
|
||||
}
|
||||
// now allowed to inspect p.planned
|
||||
|
||||
// if there are no steps to be done, the filesystem must exist on the receiving side
|
||||
// (otherwise we'd replicate it, and there would be a step for that)
|
||||
// (FIXME hardcoded initial replication policy, assuming the policy will always do _some_ initial replication)
|
||||
parentHasNoSteps := len(p.planned.steps) == 0
|
||||
|
||||
// OR if it has completed at least one step
|
||||
// (remember that .step points to the next step to be done)
|
||||
// (TODO technically, we could make this step ready in the moment the recv-side
|
||||
// dataset exists, i.e. after the first few megabytes of transferred data, but we'd have to ask the receiver for that -> poll ListFilesystems RPC)
|
||||
parentHasTakenAtLeastOneSuccessfulStep := !parentHasNoSteps && p.planned.step >= 1
|
||||
|
||||
parentFirstStepIsIncremental := // no need to lock for .report() because step.l == it's fs.l
|
||||
len(p.planned.steps) > 0 && p.planned.steps[0].report().IsIncremental()
|
||||
|
||||
f.debug("parentHasNoSteps=%v parentFirstStepIsIncremental=%v parentHasTakenAtLeastOneSuccessfulStep=%v",
|
||||
parentHasNoSteps, parentFirstStepIsIncremental, parentHasTakenAtLeastOneSuccessfulStep)
|
||||
|
||||
parentPresentOnReceiver := parentHasNoSteps || parentFirstStepIsIncremental || parentHasTakenAtLeastOneSuccessfulStep
|
||||
|
||||
allParentsPresentOnReceiver = allParentsPresentOnReceiver && parentPresentOnReceiver // no shadow
|
||||
|
||||
if !parentPresentOnReceiver && p.planned.stepErr != nil {
|
||||
initialReplicatingParentsWithErrors = append(initialReplicatingParentsWithErrors, p.fs.ReportInfo().Name)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
if len(initialReplicatingParentsWithErrors) > 0 {
|
||||
f.planned.stepErr = newTimedError(fmt.Errorf("parent(s) failed during initial replication: %s", initialReplicatingParentsWithErrors), time.Now())
|
||||
return
|
||||
}
|
||||
|
||||
if allParentsPresentOnReceiver {
|
||||
break // good to go
|
||||
}
|
||||
|
||||
// wait for wakeups from parents, then check again
|
||||
// lock must not be held while waiting in order for reporting to work
|
||||
f.l.DropWhile(func() {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
f.planned.stepErr = newTimedError(ctx.Err(), time.Now())
|
||||
return
|
||||
case <-f.initialRepOrd.parentDidUpdate:
|
||||
// loop
|
||||
}
|
||||
})
|
||||
if f.planned.stepErr != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
f.debug("all parents ready, start replication %s", parents)
|
||||
|
||||
// do our steps
|
||||
for i, s := range f.planned.steps {
|
||||
// lock must not be held while executing step in order for reporting to work
|
||||
f.l.DropWhile(func() {
|
||||
// wait for parallel replication
|
||||
targetDate := s.step.TargetDate()
|
||||
defer pq.WaitReady(f, targetDate)()
|
||||
// do the step
|
||||
err, errTime = s.step.Step(ctx), time.Now() // no shadow
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
fs.planned.stepErr = newTimedError(err, errTime)
|
||||
f.planned.stepErr = newTimedError(err, errTime)
|
||||
break
|
||||
}
|
||||
fs.planned.step = i + 1 // fs.planned.step must be == len(fs.planned.steps) if all went OK
|
||||
f.planned.step = i + 1 // fs.planned.step must be == len(fs.planned.steps) if all went OK
|
||||
|
||||
f.initialRepOrdWakeupChildren()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// caller must hold lock l
|
||||
|
||||
@@ -26,6 +26,6 @@ type debugFunc func(format string, args ...interface{})
|
||||
func debugPrefix(prefixFormat string, prefixFormatArgs ...interface{}) debugFunc {
|
||||
prefix := fmt.Sprintf(prefixFormat, prefixFormatArgs...)
|
||||
return func(format string, args ...interface{}) {
|
||||
debug("%s: %s", prefix, fmt.Sprintf(format, args))
|
||||
debug("%s: %s", prefix, fmt.Sprintf(format, args...))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,12 +8,12 @@ import (
|
||||
|
||||
type Logger = logger.Logger
|
||||
|
||||
type contexKey int
|
||||
type contextKey int
|
||||
|
||||
const contexKeyLogger contexKey = iota + 1
|
||||
const contextKeyLogger contextKey = iota + 1
|
||||
|
||||
func getLog(ctx context.Context) Logger {
|
||||
l, ok := ctx.Value(contexKeyLogger).(Logger)
|
||||
l, ok := ctx.Value(contextKeyLogger).(Logger)
|
||||
if !ok {
|
||||
l = logger.NewNullLogger()
|
||||
}
|
||||
@@ -21,5 +21,5 @@ func getLog(ctx context.Context) Logger {
|
||||
}
|
||||
|
||||
func WithLogger(ctx context.Context, log Logger) context.Context {
|
||||
return context.WithValue(ctx, contexKeyLogger, log)
|
||||
return context.WithValue(ctx, contextKeyLogger, log)
|
||||
}
|
||||
|
||||
@@ -174,7 +174,7 @@ func TestReplication(t *testing.T) {
|
||||
waitBegin := time.Now()
|
||||
wait(true)
|
||||
waitDuration := time.Since(waitBegin)
|
||||
assert.True(t, waitDuration < 10*time.Millisecond, "%v", waitDuration) // and that's gratious
|
||||
assert.True(t, waitDuration < 10*time.Millisecond, "%v", waitDuration) // and that's gracious
|
||||
|
||||
prev, err := json.Marshal(reports[0])
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
)
|
||||
|
||||
// FIXME: this test relies on timing and is thus rather flaky
|
||||
// (relies on scheduler responsivity of < 500ms)
|
||||
// (relies on scheduler responsiveness of < 500ms)
|
||||
func TestPqNotconcurrent(t *testing.T) {
|
||||
var ctr uint32
|
||||
q := newStepQueue()
|
||||
|
||||
@@ -17,7 +17,7 @@ func fsvlist(fsv ...string) (r []*FilesystemVersion) {
|
||||
r = make([]*FilesystemVersion, len(fsv))
|
||||
for i, f := range fsv {
|
||||
|
||||
// parse the id from fsvlist. it is used to derivce Guid,CreateTXG and Creation attrs
|
||||
// parse the id from fsvlist. it is used to derive Guid,CreateTXG and Creation attrs
|
||||
split := strings.Split(f, ",")
|
||||
if len(split) != 2 {
|
||||
panic("invalid fsv spec")
|
||||
@@ -114,7 +114,7 @@ func TestIncrementalPath_BookmarkSupport(t *testing.T) {
|
||||
assert.Equal(t, l("#a,1", "@b,2"), path)
|
||||
})
|
||||
|
||||
// boomarks are stripped from IncrementalPath (cannot send incrementally)
|
||||
// bookmarks are stripped from IncrementalPath (cannot send incrementally)
|
||||
doTest(l("@a,1"), l("#a,1", "#b,2", "@c,3"), func(path []*FilesystemVersion, conflict error) {
|
||||
assert.Equal(t, l("#a,1", "@c,3"), path)
|
||||
})
|
||||
|
||||
+371
-291
@@ -23,6 +23,32 @@ var _ = math.Inf
|
||||
// proto package needs to be updated.
|
||||
const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package
|
||||
|
||||
type Tri int32
|
||||
|
||||
const (
|
||||
Tri_DontCare Tri = 0
|
||||
Tri_False Tri = 1
|
||||
Tri_True Tri = 2
|
||||
)
|
||||
|
||||
var Tri_name = map[int32]string{
|
||||
0: "DontCare",
|
||||
1: "False",
|
||||
2: "True",
|
||||
}
|
||||
var Tri_value = map[string]int32{
|
||||
"DontCare": 0,
|
||||
"False": 1,
|
||||
"True": 2,
|
||||
}
|
||||
|
||||
func (x Tri) String() string {
|
||||
return proto.EnumName(Tri_name, int32(x))
|
||||
}
|
||||
func (Tri) EnumDescriptor() ([]byte, []int) {
|
||||
return fileDescriptor_pdu_e59763dc61674a79, []int{0}
|
||||
}
|
||||
|
||||
type FilesystemVersion_VersionType int32
|
||||
|
||||
const (
|
||||
@@ -43,7 +69,7 @@ func (x FilesystemVersion_VersionType) String() string {
|
||||
return proto.EnumName(FilesystemVersion_VersionType_name, int32(x))
|
||||
}
|
||||
func (FilesystemVersion_VersionType) EnumDescriptor() ([]byte, []int) {
|
||||
return fileDescriptor_pdu_83b7e2a28d820622, []int{5, 0}
|
||||
return fileDescriptor_pdu_e59763dc61674a79, []int{5, 0}
|
||||
}
|
||||
|
||||
type ListFilesystemReq struct {
|
||||
@@ -56,7 +82,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_83b7e2a28d820622, []int{0}
|
||||
return fileDescriptor_pdu_e59763dc61674a79, []int{0}
|
||||
}
|
||||
func (m *ListFilesystemReq) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_ListFilesystemReq.Unmarshal(m, b)
|
||||
@@ -87,7 +113,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_83b7e2a28d820622, []int{1}
|
||||
return fileDescriptor_pdu_e59763dc61674a79, []int{1}
|
||||
}
|
||||
func (m *ListFilesystemRes) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_ListFilesystemRes.Unmarshal(m, b)
|
||||
@@ -118,6 +144,7 @@ type Filesystem struct {
|
||||
Path string `protobuf:"bytes,1,opt,name=Path,proto3" json:"Path,omitempty"`
|
||||
ResumeToken string `protobuf:"bytes,2,opt,name=ResumeToken,proto3" json:"ResumeToken,omitempty"`
|
||||
IsPlaceholder bool `protobuf:"varint,3,opt,name=IsPlaceholder,proto3" json:"IsPlaceholder,omitempty"`
|
||||
IsEncrypted bool `protobuf:"varint,4,opt,name=IsEncrypted,proto3" json:"IsEncrypted,omitempty"`
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
XXX_sizecache int32 `json:"-"`
|
||||
@@ -127,7 +154,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_83b7e2a28d820622, []int{2}
|
||||
return fileDescriptor_pdu_e59763dc61674a79, []int{2}
|
||||
}
|
||||
func (m *Filesystem) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_Filesystem.Unmarshal(m, b)
|
||||
@@ -168,6 +195,13 @@ func (m *Filesystem) GetIsPlaceholder() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *Filesystem) GetIsEncrypted() bool {
|
||||
if m != nil {
|
||||
return m.IsEncrypted
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type ListFilesystemVersionsReq struct {
|
||||
Filesystem string `protobuf:"bytes,1,opt,name=Filesystem,proto3" json:"Filesystem,omitempty"`
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
@@ -179,7 +213,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_83b7e2a28d820622, []int{3}
|
||||
return fileDescriptor_pdu_e59763dc61674a79, []int{3}
|
||||
}
|
||||
func (m *ListFilesystemVersionsReq) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_ListFilesystemVersionsReq.Unmarshal(m, b)
|
||||
@@ -217,7 +251,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_83b7e2a28d820622, []int{4}
|
||||
return fileDescriptor_pdu_e59763dc61674a79, []int{4}
|
||||
}
|
||||
func (m *ListFilesystemVersionsRes) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_ListFilesystemVersionsRes.Unmarshal(m, b)
|
||||
@@ -259,7 +293,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_83b7e2a28d820622, []int{5}
|
||||
return fileDescriptor_pdu_e59763dc61674a79, []int{5}
|
||||
}
|
||||
func (m *FilesystemVersion) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_FilesystemVersion.Unmarshal(m, b)
|
||||
@@ -316,20 +350,18 @@ func (m *FilesystemVersion) GetCreation() string {
|
||||
|
||||
type SendReq struct {
|
||||
Filesystem string `protobuf:"bytes,1,opt,name=Filesystem,proto3" json:"Filesystem,omitempty"`
|
||||
From string `protobuf:"bytes,2,opt,name=From,proto3" json:"From,omitempty"`
|
||||
// May be empty / null to request a full transfer of From
|
||||
To string `protobuf:"bytes,3,opt,name=To,proto3" json:"To,omitempty"`
|
||||
// If ResumeToken is not empty, the resume token that CAN be tried for 'zfs send' by the sender.
|
||||
// The sender MUST indicate in SendRes.UsedResumeToken
|
||||
// If it does not work, the sender 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.
|
||||
// May be empty / null to request a full transfer of To
|
||||
From *FilesystemVersion `protobuf:"bytes,2,opt,name=From,proto3" json:"From,omitempty"`
|
||||
To *FilesystemVersion `protobuf:"bytes,3,opt,name=To,proto3" json:"To,omitempty"`
|
||||
// If ResumeToken is not empty, the resume token that CAN be used for 'zfs
|
||||
// send' by the sender. The sender MUST indicate use of ResumeToken in the
|
||||
// reply message SendRes.UsedResumeToken If it does not work, the sender
|
||||
// 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"`
|
||||
Compress bool `protobuf:"varint,5,opt,name=Compress,proto3" json:"Compress,omitempty"`
|
||||
Dedup bool `protobuf:"varint,6,opt,name=Dedup,proto3" json:"Dedup,omitempty"`
|
||||
DryRun bool `protobuf:"varint,7,opt,name=DryRun,proto3" json:"DryRun,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:"-"`
|
||||
@@ -339,7 +371,7 @@ 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_83b7e2a28d820622, []int{6}
|
||||
return fileDescriptor_pdu_e59763dc61674a79, []int{6}
|
||||
}
|
||||
func (m *SendReq) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_SendReq.Unmarshal(m, b)
|
||||
@@ -366,18 +398,18 @@ func (m *SendReq) GetFilesystem() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *SendReq) GetFrom() string {
|
||||
func (m *SendReq) GetFrom() *FilesystemVersion {
|
||||
if m != nil {
|
||||
return m.From
|
||||
}
|
||||
return ""
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *SendReq) GetTo() string {
|
||||
func (m *SendReq) GetTo() *FilesystemVersion {
|
||||
if m != nil {
|
||||
return m.To
|
||||
}
|
||||
return ""
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *SendReq) GetResumeToken() string {
|
||||
@@ -387,18 +419,11 @@ func (m *SendReq) GetResumeToken() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *SendReq) GetCompress() bool {
|
||||
func (m *SendReq) GetEncrypted() Tri {
|
||||
if m != nil {
|
||||
return m.Compress
|
||||
return m.Encrypted
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *SendReq) GetDedup() bool {
|
||||
if m != nil {
|
||||
return m.Dedup
|
||||
}
|
||||
return false
|
||||
return Tri_DontCare
|
||||
}
|
||||
|
||||
func (m *SendReq) GetDryRun() bool {
|
||||
@@ -420,7 +445,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_83b7e2a28d820622, []int{7}
|
||||
return fileDescriptor_pdu_e59763dc61674a79, []int{7}
|
||||
}
|
||||
func (m *Property) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_Property.Unmarshal(m, b)
|
||||
@@ -456,6 +481,7 @@ func (m *Property) GetValue() string {
|
||||
|
||||
type SendRes struct {
|
||||
// Whether the resume token provided in the request has been used or not.
|
||||
// If the SendReq.ResumeToken == "", this field has no meaning.
|
||||
UsedResumeToken bool `protobuf:"varint,2,opt,name=UsedResumeToken,proto3" json:"UsedResumeToken,omitempty"`
|
||||
// Expected stream size determined by dry run, not exact.
|
||||
// 0 indicates that for the given SendReq, no size estimate could be made.
|
||||
@@ -470,7 +496,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_83b7e2a28d820622, []int{8}
|
||||
return fileDescriptor_pdu_e59763dc61674a79, []int{8}
|
||||
}
|
||||
func (m *SendRes) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_SendRes.Unmarshal(m, b)
|
||||
@@ -511,10 +537,80 @@ func (m *SendRes) GetProperties() []*Property {
|
||||
return nil
|
||||
}
|
||||
|
||||
type SendCompletedReq struct {
|
||||
OriginalReq *SendReq `protobuf:"bytes,2,opt,name=OriginalReq,proto3" json:"OriginalReq,omitempty"`
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
XXX_sizecache int32 `json:"-"`
|
||||
}
|
||||
|
||||
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}
|
||||
}
|
||||
func (m *SendCompletedReq) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_SendCompletedReq.Unmarshal(m, b)
|
||||
}
|
||||
func (m *SendCompletedReq) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
|
||||
return xxx_messageInfo_SendCompletedReq.Marshal(b, m, deterministic)
|
||||
}
|
||||
func (dst *SendCompletedReq) XXX_Merge(src proto.Message) {
|
||||
xxx_messageInfo_SendCompletedReq.Merge(dst, src)
|
||||
}
|
||||
func (m *SendCompletedReq) XXX_Size() int {
|
||||
return xxx_messageInfo_SendCompletedReq.Size(m)
|
||||
}
|
||||
func (m *SendCompletedReq) XXX_DiscardUnknown() {
|
||||
xxx_messageInfo_SendCompletedReq.DiscardUnknown(m)
|
||||
}
|
||||
|
||||
var xxx_messageInfo_SendCompletedReq proto.InternalMessageInfo
|
||||
|
||||
func (m *SendCompletedReq) GetOriginalReq() *SendReq {
|
||||
if m != nil {
|
||||
return m.OriginalReq
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type SendCompletedRes struct {
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
XXX_sizecache int32 `json:"-"`
|
||||
}
|
||||
|
||||
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}
|
||||
}
|
||||
func (m *SendCompletedRes) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_SendCompletedRes.Unmarshal(m, b)
|
||||
}
|
||||
func (m *SendCompletedRes) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
|
||||
return xxx_messageInfo_SendCompletedRes.Marshal(b, m, deterministic)
|
||||
}
|
||||
func (dst *SendCompletedRes) XXX_Merge(src proto.Message) {
|
||||
xxx_messageInfo_SendCompletedRes.Merge(dst, src)
|
||||
}
|
||||
func (m *SendCompletedRes) XXX_Size() int {
|
||||
return xxx_messageInfo_SendCompletedRes.Size(m)
|
||||
}
|
||||
func (m *SendCompletedRes) XXX_DiscardUnknown() {
|
||||
xxx_messageInfo_SendCompletedRes.DiscardUnknown(m)
|
||||
}
|
||||
|
||||
var xxx_messageInfo_SendCompletedRes proto.InternalMessageInfo
|
||||
|
||||
type ReceiveReq struct {
|
||||
Filesystem string `protobuf:"bytes,1,opt,name=Filesystem,proto3" json:"Filesystem,omitempty"`
|
||||
// If true, the receiver should clear the resume token before perfoming the zfs recv of the stream in the request
|
||||
ClearResumeToken bool `protobuf:"varint,2,opt,name=ClearResumeToken,proto3" json:"ClearResumeToken,omitempty"`
|
||||
Filesystem string `protobuf:"bytes,1,opt,name=Filesystem,proto3" json:"Filesystem,omitempty"`
|
||||
To *FilesystemVersion `protobuf:"bytes,2,opt,name=To,proto3" json:"To,omitempty"`
|
||||
// If true, the receiver should clear the resume token before performing the
|
||||
// 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:"-"`
|
||||
@@ -524,7 +620,7 @@ 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_83b7e2a28d820622, []int{9}
|
||||
return fileDescriptor_pdu_e59763dc61674a79, []int{11}
|
||||
}
|
||||
func (m *ReceiveReq) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_ReceiveReq.Unmarshal(m, b)
|
||||
@@ -551,6 +647,13 @@ func (m *ReceiveReq) GetFilesystem() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *ReceiveReq) GetTo() *FilesystemVersion {
|
||||
if m != nil {
|
||||
return m.To
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *ReceiveReq) GetClearResumeToken() bool {
|
||||
if m != nil {
|
||||
return m.ClearResumeToken
|
||||
@@ -568,7 +671,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_83b7e2a28d820622, []int{10}
|
||||
return fileDescriptor_pdu_e59763dc61674a79, []int{12}
|
||||
}
|
||||
func (m *ReceiveRes) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_ReceiveRes.Unmarshal(m, b)
|
||||
@@ -601,7 +704,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_83b7e2a28d820622, []int{11}
|
||||
return fileDescriptor_pdu_e59763dc61674a79, []int{13}
|
||||
}
|
||||
func (m *DestroySnapshotsReq) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_DestroySnapshotsReq.Unmarshal(m, b)
|
||||
@@ -647,7 +750,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_83b7e2a28d820622, []int{12}
|
||||
return fileDescriptor_pdu_e59763dc61674a79, []int{14}
|
||||
}
|
||||
func (m *DestroySnapshotRes) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_DestroySnapshotRes.Unmarshal(m, b)
|
||||
@@ -692,7 +795,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_83b7e2a28d820622, []int{13}
|
||||
return fileDescriptor_pdu_e59763dc61674a79, []int{15}
|
||||
}
|
||||
func (m *DestroySnapshotsRes) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_DestroySnapshotsRes.Unmarshal(m, b)
|
||||
@@ -720,21 +823,17 @@ func (m *DestroySnapshotsRes) GetResults() []*DestroySnapshotRes {
|
||||
}
|
||||
|
||||
type ReplicationCursorReq struct {
|
||||
Filesystem string `protobuf:"bytes,1,opt,name=Filesystem,proto3" json:"Filesystem,omitempty"`
|
||||
// Types that are valid to be assigned to Op:
|
||||
// *ReplicationCursorReq_Get
|
||||
// *ReplicationCursorReq_Set
|
||||
Op isReplicationCursorReq_Op `protobuf_oneof:"op"`
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
XXX_sizecache int32 `json:"-"`
|
||||
Filesystem string `protobuf:"bytes,1,opt,name=Filesystem,proto3" json:"Filesystem,omitempty"`
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
XXX_sizecache int32 `json:"-"`
|
||||
}
|
||||
|
||||
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_83b7e2a28d820622, []int{14}
|
||||
return fileDescriptor_pdu_e59763dc61674a79, []int{16}
|
||||
}
|
||||
func (m *ReplicationCursorReq) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_ReplicationCursorReq.Unmarshal(m, b)
|
||||
@@ -761,185 +860,6 @@ func (m *ReplicationCursorReq) GetFilesystem() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
type isReplicationCursorReq_Op interface {
|
||||
isReplicationCursorReq_Op()
|
||||
}
|
||||
|
||||
type ReplicationCursorReq_Get struct {
|
||||
Get *ReplicationCursorReq_GetOp `protobuf:"bytes,2,opt,name=get,proto3,oneof"`
|
||||
}
|
||||
|
||||
type ReplicationCursorReq_Set struct {
|
||||
Set *ReplicationCursorReq_SetOp `protobuf:"bytes,3,opt,name=set,proto3,oneof"`
|
||||
}
|
||||
|
||||
func (*ReplicationCursorReq_Get) isReplicationCursorReq_Op() {}
|
||||
|
||||
func (*ReplicationCursorReq_Set) isReplicationCursorReq_Op() {}
|
||||
|
||||
func (m *ReplicationCursorReq) GetOp() isReplicationCursorReq_Op {
|
||||
if m != nil {
|
||||
return m.Op
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *ReplicationCursorReq) GetGet() *ReplicationCursorReq_GetOp {
|
||||
if x, ok := m.GetOp().(*ReplicationCursorReq_Get); ok {
|
||||
return x.Get
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *ReplicationCursorReq) GetSet() *ReplicationCursorReq_SetOp {
|
||||
if x, ok := m.GetOp().(*ReplicationCursorReq_Set); ok {
|
||||
return x.Set
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// XXX_OneofFuncs is for the internal use of the proto package.
|
||||
func (*ReplicationCursorReq) XXX_OneofFuncs() (func(msg proto.Message, b *proto.Buffer) error, func(msg proto.Message, tag, wire int, b *proto.Buffer) (bool, error), func(msg proto.Message) (n int), []interface{}) {
|
||||
return _ReplicationCursorReq_OneofMarshaler, _ReplicationCursorReq_OneofUnmarshaler, _ReplicationCursorReq_OneofSizer, []interface{}{
|
||||
(*ReplicationCursorReq_Get)(nil),
|
||||
(*ReplicationCursorReq_Set)(nil),
|
||||
}
|
||||
}
|
||||
|
||||
func _ReplicationCursorReq_OneofMarshaler(msg proto.Message, b *proto.Buffer) error {
|
||||
m := msg.(*ReplicationCursorReq)
|
||||
// op
|
||||
switch x := m.Op.(type) {
|
||||
case *ReplicationCursorReq_Get:
|
||||
b.EncodeVarint(2<<3 | proto.WireBytes)
|
||||
if err := b.EncodeMessage(x.Get); err != nil {
|
||||
return err
|
||||
}
|
||||
case *ReplicationCursorReq_Set:
|
||||
b.EncodeVarint(3<<3 | proto.WireBytes)
|
||||
if err := b.EncodeMessage(x.Set); err != nil {
|
||||
return err
|
||||
}
|
||||
case nil:
|
||||
default:
|
||||
return fmt.Errorf("ReplicationCursorReq.Op has unexpected type %T", x)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func _ReplicationCursorReq_OneofUnmarshaler(msg proto.Message, tag, wire int, b *proto.Buffer) (bool, error) {
|
||||
m := msg.(*ReplicationCursorReq)
|
||||
switch tag {
|
||||
case 2: // op.get
|
||||
if wire != proto.WireBytes {
|
||||
return true, proto.ErrInternalBadWireType
|
||||
}
|
||||
msg := new(ReplicationCursorReq_GetOp)
|
||||
err := b.DecodeMessage(msg)
|
||||
m.Op = &ReplicationCursorReq_Get{msg}
|
||||
return true, err
|
||||
case 3: // op.set
|
||||
if wire != proto.WireBytes {
|
||||
return true, proto.ErrInternalBadWireType
|
||||
}
|
||||
msg := new(ReplicationCursorReq_SetOp)
|
||||
err := b.DecodeMessage(msg)
|
||||
m.Op = &ReplicationCursorReq_Set{msg}
|
||||
return true, err
|
||||
default:
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
func _ReplicationCursorReq_OneofSizer(msg proto.Message) (n int) {
|
||||
m := msg.(*ReplicationCursorReq)
|
||||
// op
|
||||
switch x := m.Op.(type) {
|
||||
case *ReplicationCursorReq_Get:
|
||||
s := proto.Size(x.Get)
|
||||
n += 1 // tag and wire
|
||||
n += proto.SizeVarint(uint64(s))
|
||||
n += s
|
||||
case *ReplicationCursorReq_Set:
|
||||
s := proto.Size(x.Set)
|
||||
n += 1 // tag and wire
|
||||
n += proto.SizeVarint(uint64(s))
|
||||
n += s
|
||||
case nil:
|
||||
default:
|
||||
panic(fmt.Sprintf("proto: unexpected type %T in oneof", x))
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
type ReplicationCursorReq_GetOp struct {
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
XXX_sizecache int32 `json:"-"`
|
||||
}
|
||||
|
||||
func (m *ReplicationCursorReq_GetOp) Reset() { *m = ReplicationCursorReq_GetOp{} }
|
||||
func (m *ReplicationCursorReq_GetOp) String() string { return proto.CompactTextString(m) }
|
||||
func (*ReplicationCursorReq_GetOp) ProtoMessage() {}
|
||||
func (*ReplicationCursorReq_GetOp) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_pdu_83b7e2a28d820622, []int{14, 0}
|
||||
}
|
||||
func (m *ReplicationCursorReq_GetOp) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_ReplicationCursorReq_GetOp.Unmarshal(m, b)
|
||||
}
|
||||
func (m *ReplicationCursorReq_GetOp) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
|
||||
return xxx_messageInfo_ReplicationCursorReq_GetOp.Marshal(b, m, deterministic)
|
||||
}
|
||||
func (dst *ReplicationCursorReq_GetOp) XXX_Merge(src proto.Message) {
|
||||
xxx_messageInfo_ReplicationCursorReq_GetOp.Merge(dst, src)
|
||||
}
|
||||
func (m *ReplicationCursorReq_GetOp) XXX_Size() int {
|
||||
return xxx_messageInfo_ReplicationCursorReq_GetOp.Size(m)
|
||||
}
|
||||
func (m *ReplicationCursorReq_GetOp) XXX_DiscardUnknown() {
|
||||
xxx_messageInfo_ReplicationCursorReq_GetOp.DiscardUnknown(m)
|
||||
}
|
||||
|
||||
var xxx_messageInfo_ReplicationCursorReq_GetOp proto.InternalMessageInfo
|
||||
|
||||
type ReplicationCursorReq_SetOp struct {
|
||||
Snapshot string `protobuf:"bytes,2,opt,name=Snapshot,proto3" json:"Snapshot,omitempty"`
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
XXX_sizecache int32 `json:"-"`
|
||||
}
|
||||
|
||||
func (m *ReplicationCursorReq_SetOp) Reset() { *m = ReplicationCursorReq_SetOp{} }
|
||||
func (m *ReplicationCursorReq_SetOp) String() string { return proto.CompactTextString(m) }
|
||||
func (*ReplicationCursorReq_SetOp) ProtoMessage() {}
|
||||
func (*ReplicationCursorReq_SetOp) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_pdu_83b7e2a28d820622, []int{14, 1}
|
||||
}
|
||||
func (m *ReplicationCursorReq_SetOp) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_ReplicationCursorReq_SetOp.Unmarshal(m, b)
|
||||
}
|
||||
func (m *ReplicationCursorReq_SetOp) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
|
||||
return xxx_messageInfo_ReplicationCursorReq_SetOp.Marshal(b, m, deterministic)
|
||||
}
|
||||
func (dst *ReplicationCursorReq_SetOp) XXX_Merge(src proto.Message) {
|
||||
xxx_messageInfo_ReplicationCursorReq_SetOp.Merge(dst, src)
|
||||
}
|
||||
func (m *ReplicationCursorReq_SetOp) XXX_Size() int {
|
||||
return xxx_messageInfo_ReplicationCursorReq_SetOp.Size(m)
|
||||
}
|
||||
func (m *ReplicationCursorReq_SetOp) XXX_DiscardUnknown() {
|
||||
xxx_messageInfo_ReplicationCursorReq_SetOp.DiscardUnknown(m)
|
||||
}
|
||||
|
||||
var xxx_messageInfo_ReplicationCursorReq_SetOp proto.InternalMessageInfo
|
||||
|
||||
func (m *ReplicationCursorReq_SetOp) GetSnapshot() string {
|
||||
if m != nil {
|
||||
return m.Snapshot
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type ReplicationCursorRes struct {
|
||||
// Types that are valid to be assigned to Result:
|
||||
// *ReplicationCursorRes_Guid
|
||||
@@ -954,7 +874,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_83b7e2a28d820622, []int{15}
|
||||
return fileDescriptor_pdu_e59763dc61674a79, []int{17}
|
||||
}
|
||||
func (m *ReplicationCursorRes) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_ReplicationCursorRes.Unmarshal(m, b)
|
||||
@@ -1090,7 +1010,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_83b7e2a28d820622, []int{16}
|
||||
return fileDescriptor_pdu_e59763dc61674a79, []int{18}
|
||||
}
|
||||
func (m *PingReq) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_PingReq.Unmarshal(m, b)
|
||||
@@ -1129,7 +1049,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_83b7e2a28d820622, []int{17}
|
||||
return fileDescriptor_pdu_e59763dc61674a79, []int{19}
|
||||
}
|
||||
func (m *PingRes) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_PingRes.Unmarshal(m, b)
|
||||
@@ -1156,6 +1076,91 @@ 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")
|
||||
@@ -1166,17 +1171,20 @@ func init() {
|
||||
proto.RegisterType((*SendReq)(nil), "SendReq")
|
||||
proto.RegisterType((*Property)(nil), "Property")
|
||||
proto.RegisterType((*SendRes)(nil), "SendRes")
|
||||
proto.RegisterType((*SendCompletedReq)(nil), "SendCompletedReq")
|
||||
proto.RegisterType((*SendCompletedRes)(nil), "SendCompletedRes")
|
||||
proto.RegisterType((*ReceiveReq)(nil), "ReceiveReq")
|
||||
proto.RegisterType((*ReceiveRes)(nil), "ReceiveRes")
|
||||
proto.RegisterType((*DestroySnapshotsReq)(nil), "DestroySnapshotsReq")
|
||||
proto.RegisterType((*DestroySnapshotRes)(nil), "DestroySnapshotRes")
|
||||
proto.RegisterType((*DestroySnapshotsRes)(nil), "DestroySnapshotsRes")
|
||||
proto.RegisterType((*ReplicationCursorReq)(nil), "ReplicationCursorReq")
|
||||
proto.RegisterType((*ReplicationCursorReq_GetOp)(nil), "ReplicationCursorReq.GetOp")
|
||||
proto.RegisterType((*ReplicationCursorReq_SetOp)(nil), "ReplicationCursorReq.SetOp")
|
||||
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("FilesystemVersion_VersionType", FilesystemVersion_VersionType_name, FilesystemVersion_VersionType_value)
|
||||
}
|
||||
|
||||
@@ -1197,6 +1205,8 @@ type ReplicationClient interface {
|
||||
ListFilesystemVersions(ctx context.Context, in *ListFilesystemVersionsReq, opts ...grpc.CallOption) (*ListFilesystemVersionsRes, error)
|
||||
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 {
|
||||
@@ -1252,6 +1262,24 @@ func (c *replicationClient) ReplicationCursor(ctx context.Context, in *Replicati
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *replicationClient) SendCompleted(ctx context.Context, in *SendCompletedReq, opts ...grpc.CallOption) (*SendCompletedRes, error) {
|
||||
out := new(SendCompletedRes)
|
||||
err := c.cc.Invoke(ctx, "/Replication/SendCompleted", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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)
|
||||
@@ -1259,6 +1287,8 @@ type ReplicationServer interface {
|
||||
ListFilesystemVersions(context.Context, *ListFilesystemVersionsReq) (*ListFilesystemVersionsRes, error)
|
||||
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) {
|
||||
@@ -1355,6 +1385,42 @@ func _Replication_ReplicationCursor_Handler(srv interface{}, ctx context.Context
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Replication_SendCompleted_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(SendCompletedReq)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(ReplicationServer).SendCompleted(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/Replication/SendCompleted",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ReplicationServer).SendCompleted(ctx, req.(*SendCompletedReq))
|
||||
}
|
||||
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),
|
||||
@@ -1379,63 +1445,77 @@ var _Replication_serviceDesc = grpc.ServiceDesc{
|
||||
MethodName: "ReplicationCursor",
|
||||
Handler: _Replication_ReplicationCursor_Handler,
|
||||
},
|
||||
{
|
||||
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_83b7e2a28d820622) }
|
||||
func init() { proto.RegisterFile("pdu.proto", fileDescriptor_pdu_e59763dc61674a79) }
|
||||
|
||||
var fileDescriptor_pdu_83b7e2a28d820622 = []byte{
|
||||
// 785 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x55, 0xd1, 0x8e, 0xe3, 0x34,
|
||||
0x14, 0x9d, 0xb4, 0x69, 0x9b, 0xde, 0x0e, 0xbb, 0x1d, 0x4f, 0x59, 0x85, 0x00, 0xab, 0xca, 0xcb,
|
||||
0x43, 0x17, 0x89, 0x80, 0x0a, 0x2f, 0x08, 0x09, 0x89, 0x4e, 0x67, 0x67, 0x10, 0xb0, 0x54, 0x6e,
|
||||
0x59, 0xad, 0xf6, 0x2d, 0x34, 0x57, 0x6d, 0x34, 0x6d, 0x9d, 0xb5, 0x13, 0xb4, 0xe5, 0x91, 0xbf,
|
||||
0x9a, 0x7f, 0xe0, 0x91, 0x0f, 0x42, 0x76, 0xe3, 0x34, 0x6d, 0x52, 0xa9, 0x4f, 0xf1, 0x39, 0xf7,
|
||||
0xda, 0x3e, 0xf7, 0xd8, 0xd7, 0x81, 0x76, 0x1c, 0xa6, 0x7e, 0x2c, 0x78, 0xc2, 0xe9, 0x35, 0x5c,
|
||||
0xfd, 0x1a, 0xc9, 0xe4, 0x55, 0xb4, 0x42, 0xb9, 0x95, 0x09, 0xae, 0x19, 0xbe, 0xa7, 0xa3, 0x32,
|
||||
0x29, 0xc9, 0x57, 0xd0, 0xd9, 0x13, 0xd2, 0xb5, 0xfa, 0xf5, 0x41, 0x67, 0xd8, 0xf1, 0x0b, 0x49,
|
||||
0xc5, 0x38, 0x5d, 0x02, 0xec, 0x21, 0x21, 0x60, 0x4f, 0x82, 0x64, 0xe9, 0x5a, 0x7d, 0x6b, 0xd0,
|
||||
0x66, 0x7a, 0x4c, 0xfa, 0xd0, 0x61, 0x28, 0xd3, 0x35, 0xce, 0xf8, 0x03, 0x6e, 0xdc, 0x9a, 0x0e,
|
||||
0x15, 0x29, 0xf2, 0x05, 0x7c, 0xf4, 0xb3, 0x9c, 0xac, 0x82, 0x39, 0x2e, 0xf9, 0x2a, 0x44, 0xe1,
|
||||
0xd6, 0xfb, 0xd6, 0xc0, 0x61, 0x87, 0x24, 0xfd, 0x01, 0x3e, 0x39, 0x54, 0xfb, 0x06, 0x85, 0x8c,
|
||||
0xf8, 0x46, 0x32, 0x7c, 0x4f, 0x9e, 0x17, 0x65, 0x64, 0xdb, 0x17, 0x18, 0xfa, 0xcb, 0xe9, 0xc9,
|
||||
0x92, 0xf8, 0xe0, 0x18, 0x98, 0xd5, 0x4b, 0xfc, 0x52, 0x26, 0xcb, 0x73, 0xe8, 0x7f, 0x16, 0x5c,
|
||||
0x95, 0xe2, 0x64, 0x08, 0xf6, 0x6c, 0x1b, 0xa3, 0xde, 0xfc, 0xc9, 0xf0, 0x79, 0x79, 0x05, 0x3f,
|
||||
0xfb, 0xaa, 0x2c, 0xa6, 0x73, 0x95, 0x5f, 0xaf, 0x83, 0x35, 0x66, 0xa6, 0xe8, 0xb1, 0xe2, 0xee,
|
||||
0xd2, 0x28, 0xd4, 0x26, 0xd8, 0x4c, 0x8f, 0xc9, 0x67, 0xd0, 0xbe, 0x11, 0x18, 0x24, 0x38, 0x7b,
|
||||
0x7b, 0xe7, 0xda, 0x3a, 0xb0, 0x27, 0x88, 0x07, 0x8e, 0x06, 0x11, 0xdf, 0xb8, 0x0d, 0xbd, 0x52,
|
||||
0x8e, 0xe9, 0x4b, 0xe8, 0x14, 0xb6, 0x25, 0x97, 0xe0, 0x4c, 0x37, 0x41, 0x2c, 0x97, 0x3c, 0xe9,
|
||||
0x5e, 0x28, 0x34, 0xe2, 0xfc, 0x61, 0x1d, 0x88, 0x87, 0xae, 0x45, 0x1f, 0x2d, 0x68, 0x4d, 0x71,
|
||||
0x13, 0x9e, 0xe1, 0xa7, 0x12, 0xf9, 0x4a, 0xf0, 0xb5, 0x11, 0xae, 0xc6, 0xe4, 0x09, 0xd4, 0x66,
|
||||
0x5c, 0xcb, 0x6e, 0xb3, 0xda, 0x8c, 0x1f, 0x1f, 0xbc, 0x5d, 0x3e, 0x78, 0x25, 0x9c, 0xaf, 0x63,
|
||||
0x81, 0x52, 0x6a, 0xe1, 0x0e, 0xcb, 0x31, 0xe9, 0x41, 0x63, 0x8c, 0x61, 0x1a, 0xbb, 0x4d, 0x1d,
|
||||
0xd8, 0x01, 0xf2, 0x0c, 0x9a, 0x63, 0xb1, 0x65, 0xe9, 0xc6, 0x6d, 0x69, 0x3a, 0x43, 0xf4, 0x3b,
|
||||
0x70, 0x26, 0x82, 0xc7, 0x28, 0x92, 0x6d, 0x6e, 0xaa, 0x55, 0x30, 0xb5, 0x07, 0x8d, 0x37, 0xc1,
|
||||
0x2a, 0x35, 0x4e, 0xef, 0x00, 0xfd, 0x27, 0xaf, 0x58, 0x92, 0x01, 0x3c, 0xfd, 0x43, 0x62, 0x78,
|
||||
0x7c, 0x55, 0x1d, 0x76, 0x4c, 0x13, 0x0a, 0x97, 0xb7, 0x1f, 0x62, 0x9c, 0x27, 0x18, 0x4e, 0xa3,
|
||||
0xbf, 0x51, 0x57, 0x5c, 0x67, 0x07, 0x1c, 0x79, 0x09, 0x90, 0xe9, 0x89, 0x50, 0xba, 0xb6, 0xbe,
|
||||
0x54, 0x6d, 0xdf, 0x48, 0x64, 0x85, 0x20, 0x7d, 0x0b, 0xc0, 0x70, 0x8e, 0xd1, 0x5f, 0x78, 0x8e,
|
||||
0xf1, 0x5f, 0x42, 0xf7, 0x66, 0x85, 0x81, 0x28, 0xeb, 0x2c, 0xf1, 0xf4, 0xb2, 0xb0, 0xb2, 0xa4,
|
||||
0x0b, 0xb8, 0x1e, 0xa3, 0x4c, 0x04, 0xdf, 0x9a, 0x1b, 0x70, 0x4e, 0xe7, 0x90, 0x6f, 0xa0, 0x9d,
|
||||
0xe7, 0xbb, 0xb5, 0x93, 0xdd, 0xb1, 0x4f, 0xa2, 0xef, 0x80, 0x1c, 0x6d, 0x94, 0x35, 0x99, 0x81,
|
||||
0x7a, 0x97, 0x13, 0x4d, 0x66, 0x72, 0xd4, 0x89, 0xdd, 0x0a, 0xc1, 0x85, 0x39, 0x31, 0x0d, 0xe8,
|
||||
0xb8, 0xaa, 0x08, 0xf5, 0x68, 0xb5, 0x54, 0xe1, 0xab, 0xc4, 0x34, 0xf0, 0xb5, 0x5f, 0x96, 0xc0,
|
||||
0x4c, 0x0e, 0xfd, 0xd7, 0x82, 0x1e, 0xc3, 0x78, 0x15, 0xcd, 0x75, 0x93, 0xdc, 0xa4, 0x42, 0x72,
|
||||
0x71, 0x8e, 0x19, 0x5f, 0x43, 0x7d, 0x81, 0x89, 0x96, 0xd4, 0x19, 0x7e, 0xea, 0x57, 0xad, 0xe1,
|
||||
0xdf, 0x61, 0xf2, 0x7b, 0x7c, 0x7f, 0xc1, 0x54, 0xa6, 0x9a, 0x20, 0x31, 0xd1, 0x57, 0xe4, 0xe4,
|
||||
0x84, 0xa9, 0x99, 0x20, 0x31, 0xf1, 0x5a, 0xd0, 0xd0, 0x0b, 0x78, 0x2f, 0xa0, 0xa1, 0x03, 0xaa,
|
||||
0x49, 0x72, 0xe3, 0x76, 0x5e, 0xe4, 0x78, 0x64, 0x43, 0x8d, 0xc7, 0x74, 0x56, 0x59, 0x8d, 0x6a,
|
||||
0xa1, 0xdd, 0x4b, 0xa2, 0xea, 0xb0, 0xef, 0x2f, 0xf2, 0xb7, 0xc4, 0x79, 0xcd, 0x13, 0xfc, 0x10,
|
||||
0xc9, 0xdd, 0x7a, 0xce, 0xfd, 0x05, 0xcb, 0x99, 0x91, 0x03, 0xcd, 0x9d, 0x4b, 0xf4, 0x05, 0xb4,
|
||||
0x26, 0xd1, 0x66, 0xa1, 0x6c, 0x71, 0xa1, 0xf5, 0x1b, 0x4a, 0x19, 0x2c, 0x4c, 0x53, 0x19, 0x48,
|
||||
0x3f, 0x37, 0x49, 0x52, 0xb5, 0xdd, 0xed, 0x7c, 0xc9, 0x4d, 0xdb, 0xa9, 0xf1, 0xf0, 0xb1, 0xa6,
|
||||
0xde, 0x80, 0x5c, 0x1a, 0xf1, 0xc0, 0x56, 0xe9, 0xc4, 0xf1, 0xb3, 0xa5, 0x3d, 0x33, 0x92, 0xe4,
|
||||
0x7b, 0x78, 0x7a, 0xf8, 0x44, 0x4b, 0x42, 0xfc, 0xd2, 0x4f, 0xcb, 0x2b, 0x73, 0x92, 0x4c, 0xe0,
|
||||
0x59, 0xf5, 0xeb, 0x4e, 0x3c, 0xff, 0xe4, 0x3f, 0xc3, 0x3b, 0x1d, 0x93, 0xe4, 0x47, 0xe8, 0x1e,
|
||||
0xdf, 0x33, 0xd2, 0xf3, 0x2b, 0xfa, 0xc7, 0xab, 0x62, 0x25, 0xf9, 0x09, 0xae, 0x4a, 0x47, 0x42,
|
||||
0x3e, 0xae, 0x3c, 0x7f, 0xaf, 0x92, 0x96, 0xa3, 0xc6, 0xbb, 0x7a, 0x1c, 0xa6, 0x7f, 0x36, 0xf5,
|
||||
0x0f, 0xfc, 0xdb, 0xff, 0x03, 0x00, 0x00, 0xff, 0xff, 0x37, 0x0e, 0xf2, 0xe4, 0xcd, 0x07, 0x00,
|
||||
0x00,
|
||||
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,
|
||||
}
|
||||
|
||||
@@ -2,131 +2,143 @@ syntax = "proto3";
|
||||
option go_package = "pdu";
|
||||
|
||||
service Replication {
|
||||
rpc Ping (PingReq) returns (PingRes);
|
||||
rpc ListFilesystems (ListFilesystemReq) returns (ListFilesystemRes);
|
||||
rpc ListFilesystemVersions (ListFilesystemVersionsReq) returns (ListFilesystemVersionsRes);
|
||||
rpc DestroySnapshots (DestroySnapshotsReq) returns (DestroySnapshotsRes);
|
||||
rpc ReplicationCursor (ReplicationCursorReq) returns (ReplicationCursorRes);
|
||||
// for Send and Recv, see package rpc
|
||||
rpc Ping(PingReq) returns (PingRes);
|
||||
rpc ListFilesystems(ListFilesystemReq) returns (ListFilesystemRes);
|
||||
rpc ListFilesystemVersions(ListFilesystemVersionsReq)
|
||||
returns (ListFilesystemVersionsRes);
|
||||
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
|
||||
}
|
||||
|
||||
message ListFilesystemReq {}
|
||||
|
||||
message ListFilesystemRes {
|
||||
repeated Filesystem Filesystems = 1;
|
||||
}
|
||||
message ListFilesystemRes { repeated Filesystem Filesystems = 1; }
|
||||
|
||||
message Filesystem {
|
||||
string Path = 1;
|
||||
string ResumeToken = 2;
|
||||
bool IsPlaceholder = 3;
|
||||
string Path = 1;
|
||||
string ResumeToken = 2;
|
||||
bool IsPlaceholder = 3;
|
||||
bool IsEncrypted = 4;
|
||||
}
|
||||
|
||||
message ListFilesystemVersionsReq {
|
||||
string Filesystem = 1;
|
||||
}
|
||||
message ListFilesystemVersionsReq { string Filesystem = 1; }
|
||||
|
||||
message ListFilesystemVersionsRes {
|
||||
repeated FilesystemVersion Versions = 1;
|
||||
}
|
||||
message ListFilesystemVersionsRes { repeated FilesystemVersion Versions = 1; }
|
||||
|
||||
message FilesystemVersion {
|
||||
enum VersionType {
|
||||
Snapshot = 0;
|
||||
Bookmark = 1;
|
||||
}
|
||||
VersionType Type = 1;
|
||||
string Name = 2;
|
||||
uint64 Guid = 3;
|
||||
uint64 CreateTXG = 4;
|
||||
string Creation = 5; // RFC 3339
|
||||
enum VersionType {
|
||||
Snapshot = 0;
|
||||
Bookmark = 1;
|
||||
}
|
||||
VersionType Type = 1;
|
||||
string Name = 2;
|
||||
uint64 Guid = 3;
|
||||
uint64 CreateTXG = 4;
|
||||
string Creation = 5; // RFC 3339
|
||||
}
|
||||
|
||||
enum Tri {
|
||||
DontCare = 0;
|
||||
False = 1;
|
||||
True = 2;
|
||||
}
|
||||
|
||||
message SendReq {
|
||||
string Filesystem = 1;
|
||||
string From = 2;
|
||||
// May be empty / null to request a full transfer of From
|
||||
string To = 3;
|
||||
string Filesystem = 1;
|
||||
// May be empty / null to request a full transfer of To
|
||||
FilesystemVersion From = 2;
|
||||
FilesystemVersion To = 3;
|
||||
|
||||
// If ResumeToken is not empty, the resume token that CAN be tried for 'zfs send' by the sender.
|
||||
// The sender MUST indicate in SendRes.UsedResumeToken
|
||||
// If it does not work, the sender 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.
|
||||
string ResumeToken = 4;
|
||||
bool Compress = 5;
|
||||
bool Dedup = 6;
|
||||
// If ResumeToken is not empty, the resume token that CAN be used for 'zfs
|
||||
// send' by the sender. The sender MUST indicate use of ResumeToken in the
|
||||
// reply message SendRes.UsedResumeToken If it does not work, the sender
|
||||
// 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.
|
||||
string ResumeToken = 4;
|
||||
Tri Encrypted = 5;
|
||||
|
||||
bool DryRun = 7;
|
||||
bool DryRun = 6;
|
||||
}
|
||||
|
||||
message Property {
|
||||
string Name = 1;
|
||||
string Value = 2;
|
||||
string Name = 1;
|
||||
string Value = 2;
|
||||
}
|
||||
|
||||
message SendRes {
|
||||
// Whether the resume token provided in the request has been used or not.
|
||||
bool UsedResumeToken = 2;
|
||||
// Whether the resume token provided in the request has been used or not.
|
||||
// If the SendReq.ResumeToken == "", this field has no meaning.
|
||||
bool UsedResumeToken = 2;
|
||||
|
||||
// Expected stream size determined by dry run, not exact.
|
||||
// 0 indicates that for the given SendReq, no size estimate could be made.
|
||||
int64 ExpectedSize = 3;
|
||||
// Expected stream size determined by dry run, not exact.
|
||||
// 0 indicates that for the given SendReq, no size estimate could be made.
|
||||
int64 ExpectedSize = 3;
|
||||
|
||||
repeated Property Properties = 4;
|
||||
repeated Property Properties = 4;
|
||||
}
|
||||
|
||||
message ReceiveReq {
|
||||
string Filesystem = 1; // FIXME should be snapshot name, we can enforce that on recv
|
||||
message SendCompletedReq {
|
||||
SendReq OriginalReq = 2;
|
||||
}
|
||||
|
||||
// If true, the receiver should clear the resume token before perfoming the zfs recv of the stream in the request
|
||||
bool ClearResumeToken = 2;
|
||||
message SendCompletedRes {}
|
||||
|
||||
message ReceiveReq {
|
||||
string Filesystem = 1;
|
||||
FilesystemVersion To = 2;
|
||||
|
||||
// If true, the receiver should clear the resume token before performing the
|
||||
// zfs recv of the stream in the request
|
||||
bool ClearResumeToken = 3;
|
||||
}
|
||||
|
||||
message ReceiveRes {}
|
||||
|
||||
message DestroySnapshotsReq {
|
||||
string Filesystem = 1;
|
||||
// Path to filesystem, snapshot or bookmark to be destroyed
|
||||
repeated FilesystemVersion Snapshots = 2;
|
||||
string Filesystem = 1;
|
||||
// Path to filesystem, snapshot or bookmark to be destroyed
|
||||
repeated FilesystemVersion Snapshots = 2;
|
||||
}
|
||||
|
||||
message DestroySnapshotRes {
|
||||
FilesystemVersion Snapshot = 1;
|
||||
string Error = 2;
|
||||
FilesystemVersion Snapshot = 1;
|
||||
string Error = 2;
|
||||
}
|
||||
|
||||
message DestroySnapshotsRes {
|
||||
repeated DestroySnapshotRes Results = 1;
|
||||
}
|
||||
message DestroySnapshotsRes { repeated DestroySnapshotRes Results = 1; }
|
||||
|
||||
message ReplicationCursorReq {
|
||||
string Filesystem = 1;
|
||||
message GetOp {}
|
||||
message SetOp {
|
||||
string Snapshot = 2;
|
||||
}
|
||||
oneof op {
|
||||
GetOp get = 2;
|
||||
SetOp set = 3;
|
||||
}
|
||||
}
|
||||
message ReplicationCursorReq { string Filesystem = 1; }
|
||||
|
||||
message ReplicationCursorRes {
|
||||
oneof Result {
|
||||
uint64 Guid = 1;
|
||||
bool Notexist = 2;
|
||||
}
|
||||
oneof Result {
|
||||
uint64 Guid = 1;
|
||||
bool Notexist = 2;
|
||||
}
|
||||
}
|
||||
|
||||
message PingReq {
|
||||
string Message = 1;
|
||||
}
|
||||
message PingReq { string Message = 1; }
|
||||
|
||||
message PingRes {
|
||||
// Echo must be PingReq.Message
|
||||
string Echo = 1;
|
||||
}
|
||||
// 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 {}
|
||||
@@ -7,6 +7,14 @@ import (
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
|
||||
func (v *FilesystemVersion) GetRelName() string {
|
||||
zv, err := v.ZFSFilesystemVersion()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return zv.String()
|
||||
}
|
||||
|
||||
func (v *FilesystemVersion) RelName() string {
|
||||
zv, err := v.ZFSFilesystemVersion()
|
||||
if err != nil {
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -30,6 +31,7 @@ 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 {
|
||||
@@ -37,7 +39,8 @@ type Sender interface {
|
||||
// If a non-nil io.ReadCloser is returned, it is guaranteed to be closed before
|
||||
// any next call to the parent github.com/zrepl/zrepl/replication.Endpoint.
|
||||
// If the send request is for dry run the io.ReadCloser will be nil
|
||||
Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, zfs.StreamCopier, error)
|
||||
Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, io.ReadCloser, error)
|
||||
SendCompleted(ctx context.Context, r *pdu.SendCompletedReq) (*pdu.SendCompletedRes, error)
|
||||
ReplicationCursor(ctx context.Context, req *pdu.ReplicationCursorReq) (*pdu.ReplicationCursorRes, error)
|
||||
}
|
||||
|
||||
@@ -45,12 +48,17 @@ type Receiver interface {
|
||||
Endpoint
|
||||
// Receive sends r and sendStream (the latter containing a ZFS send stream)
|
||||
// to the parent github.com/zrepl/zrepl/replication.Endpoint.
|
||||
Receive(ctx context.Context, req *pdu.ReceiveReq, receive zfs.StreamCopier) (*pdu.ReceiveRes, error)
|
||||
Receive(ctx context.Context, req *pdu.ReceiveReq, receive io.ReadCloser) (*pdu.ReceiveRes, error)
|
||||
}
|
||||
|
||||
type PlannerPolicy struct {
|
||||
EncryptedSend tri // all sends must be encrypted (send -w, and encryption!=off)
|
||||
}
|
||||
|
||||
type Planner struct {
|
||||
sender Sender
|
||||
receiver Receiver
|
||||
policy PlannerPolicy
|
||||
|
||||
promSecsPerState *prometheus.HistogramVec // labels: state
|
||||
promBytesReplicated *prometheus.CounterVec // labels: filesystem
|
||||
@@ -109,10 +117,11 @@ func (p *Planner) WaitForConnectivity(ctx context.Context) error {
|
||||
type Filesystem struct {
|
||||
sender Sender
|
||||
receiver Receiver
|
||||
policy PlannerPolicy
|
||||
|
||||
Path string // compat
|
||||
receiverFS *pdu.Filesystem
|
||||
promBytesReplicated prometheus.Counter // compat
|
||||
Path string // compat
|
||||
receiverFS, senderFS *pdu.Filesystem // receiverFS may be nil, senderFS never nil
|
||||
promBytesReplicated prometheus.Counter // compat
|
||||
|
||||
sizeEstimateRequestSem *semaphore.S
|
||||
}
|
||||
@@ -145,14 +154,16 @@ type Step struct {
|
||||
sender Sender
|
||||
receiver Receiver
|
||||
|
||||
parent *Filesystem
|
||||
from, to *pdu.FilesystemVersion // compat
|
||||
parent *Filesystem
|
||||
from, to *pdu.FilesystemVersion // from may be nil, indicating full send
|
||||
encrypt tri
|
||||
resumeToken string // empty means no resume token shall be used
|
||||
|
||||
expectedSize int64 // 0 means no size estimate present / possible
|
||||
|
||||
// byteCounter is nil initially, and set later in Step.doReplication
|
||||
// => concurrent read of that pointer from Step.ReportInfo must be protected
|
||||
byteCounter bytecounter.StreamCopier
|
||||
byteCounter bytecounter.ReadCloser
|
||||
byteCounterMtx chainlock.L
|
||||
}
|
||||
|
||||
@@ -186,23 +197,36 @@ func (s *Step) ReportInfo() *report.StepInfo {
|
||||
}
|
||||
s.byteCounterMtx.Unlock()
|
||||
|
||||
// FIXME stick to zfs convention of from and to
|
||||
from := ""
|
||||
if s.from != nil {
|
||||
from = s.from.RelName()
|
||||
}
|
||||
var encrypted report.EncryptedEnum
|
||||
switch s.encrypt {
|
||||
case DontCare:
|
||||
encrypted = report.EncryptedSenderDependent
|
||||
case True:
|
||||
encrypted = report.EncryptedTrue
|
||||
case False:
|
||||
encrypted = report.EncryptedFalse
|
||||
default:
|
||||
panic(fmt.Sprintf("unknown variant %s", s.encrypt))
|
||||
}
|
||||
return &report.StepInfo{
|
||||
From: from,
|
||||
To: s.to.RelName(),
|
||||
Resumed: s.resumeToken != "",
|
||||
Encrypted: encrypted,
|
||||
BytesExpected: s.expectedSize,
|
||||
BytesReplicated: byteCounter,
|
||||
}
|
||||
}
|
||||
|
||||
func NewPlanner(secsPerState *prometheus.HistogramVec, bytesReplicated *prometheus.CounterVec, sender Sender, receiver Receiver) *Planner {
|
||||
func NewPlanner(secsPerState *prometheus.HistogramVec, bytesReplicated *prometheus.CounterVec, sender Sender, receiver Receiver, policy PlannerPolicy) *Planner {
|
||||
return &Planner{
|
||||
sender: sender,
|
||||
receiver: receiver,
|
||||
policy: policy,
|
||||
promSecsPerState: secsPerState,
|
||||
promBytesReplicated: bytesReplicated,
|
||||
}
|
||||
@@ -211,6 +235,7 @@ func resolveConflict(conflict error) (path []*pdu.FilesystemVersion, msg string)
|
||||
if noCommonAncestor, ok := conflict.(*ConflictNoCommonAncestor); ok {
|
||||
if len(noCommonAncestor.SortedReceiverVersions) == 0 {
|
||||
// TODO this is hard-coded replication policy: most recent snapshot as source
|
||||
// NOTE: Keep in sync with listStaleFiltering, it depends on this hard-coded assumption
|
||||
var mostRecentSnap *pdu.FilesystemVersion
|
||||
for n := len(noCommonAncestor.SortedSenderVersions) - 1; n >= 0; n-- {
|
||||
if noCommonAncestor.SortedSenderVersions[n].Type == pdu.FilesystemVersion_Snapshot {
|
||||
@@ -239,7 +264,6 @@ func (p *Planner) doPlanning(ctx context.Context) ([]*Filesystem, error) {
|
||||
return nil, err
|
||||
}
|
||||
sfss := slfssres.GetFilesystems()
|
||||
// no progress here since we could run in a live-lock on connectivity issues
|
||||
|
||||
rlfssres, err := p.receiver.ListFilesystems(ctx, &pdu.ListFilesystemReq{})
|
||||
if err != nil {
|
||||
@@ -265,7 +289,9 @@ func (p *Planner) doPlanning(ctx context.Context) ([]*Filesystem, error) {
|
||||
q = append(q, &Filesystem{
|
||||
sender: p.sender,
|
||||
receiver: p.receiver,
|
||||
policy: p.policy,
|
||||
Path: fs.Path,
|
||||
senderFS: fs,
|
||||
receiverFS: receiverFS,
|
||||
promBytesReplicated: ctr,
|
||||
sizeEstimateRequestSem: sizeEstimateRequestSem,
|
||||
@@ -281,6 +307,10 @@ func (fs *Filesystem) doPlanning(ctx context.Context) ([]*Step, error) {
|
||||
|
||||
log.Debug("assessing filesystem")
|
||||
|
||||
if fs.policy.EncryptedSend == True && !fs.senderFS.GetIsEncrypted() {
|
||||
return nil, fmt.Errorf("sender filesystem is not encrypted but policy mandates encrypted send")
|
||||
}
|
||||
|
||||
sfsvsres, err := fs.sender.ListFilesystemVersions(ctx, &pdu.ListFilesystemVersionsReq{Filesystem: fs.Path})
|
||||
if err != nil {
|
||||
log.WithError(err).Error("cannot get remote filesystem versions")
|
||||
@@ -306,42 +336,193 @@ func (fs *Filesystem) doPlanning(ctx context.Context) ([]*Step, error) {
|
||||
rfsvs = []*pdu.FilesystemVersion{}
|
||||
}
|
||||
|
||||
path, conflict := IncrementalPath(rfsvs, sfsvs)
|
||||
if conflict != nil {
|
||||
var msg string
|
||||
path, msg = resolveConflict(conflict) // no shadowing allowed!
|
||||
if path != nil {
|
||||
log.WithField("conflict", conflict).Info("conflict")
|
||||
log.WithField("resolution", msg).Info("automatically resolved")
|
||||
} else {
|
||||
log.WithField("conflict", conflict).Error("conflict")
|
||||
log.WithField("problem", msg).Error("cannot resolve conflict")
|
||||
var resumeToken *zfs.ResumeToken
|
||||
var resumeTokenRaw string
|
||||
if fs.receiverFS != nil && fs.receiverFS.ResumeToken != "" {
|
||||
resumeTokenRaw = fs.receiverFS.ResumeToken // shadow
|
||||
log.WithField("receiverFS.ResumeToken", resumeTokenRaw).Debug("decode receiver fs resume token")
|
||||
resumeToken, err = zfs.ParseResumeToken(ctx, resumeTokenRaw) // shadow
|
||||
if err != nil {
|
||||
// TODO in theory, we could do replication without resume token, but that would mean that
|
||||
// we need to discard the resumable state on the receiver's side.
|
||||
// Would be easy by setting UsedResumeToken=false in the RecvReq ...
|
||||
// FIXME / CHECK semantics UsedResumeToken if SendReq.ResumeToken == ""
|
||||
log.WithError(err).Error("cannot decode resume token, aborting")
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if len(path) == 0 {
|
||||
return nil, conflict
|
||||
log.WithField("token", resumeToken).Debug("decode resume token")
|
||||
}
|
||||
|
||||
steps := make([]*Step, 0, len(path))
|
||||
// FIXME unify struct declarations => initializer?
|
||||
if len(path) == 1 {
|
||||
steps = append(steps, &Step{
|
||||
// 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()
|
||||
log := log.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
|
||||
//
|
||||
// prefer to resume any started replication instead of starting over with a normal IncrementalPath
|
||||
//
|
||||
// look for the step encoded in the resume token in the sender's version
|
||||
// if we find that step:
|
||||
// 1. use it as first step (including resume token)
|
||||
// 2. compute subsequent steps by computing incremental path from the token.To version on
|
||||
// ...
|
||||
// that's actually equivalent to simply cutting off earlier versions from rfsvs and sfsvs
|
||||
if resumeToken != nil {
|
||||
|
||||
sfsvs := SortVersionListByCreateTXGThenBookmarkLTSnapshot(sfsvs)
|
||||
|
||||
var fromVersion, toVersion *pdu.FilesystemVersion
|
||||
var toVersionIdx int
|
||||
for idx, sfsv := range sfsvs {
|
||||
if resumeToken.HasFromGUID && sfsv.Guid == resumeToken.FromGUID {
|
||||
if fromVersion != nil && fromVersion.Type == pdu.FilesystemVersion_Snapshot {
|
||||
// prefer snapshots over bookmarks for size estimation
|
||||
} else {
|
||||
fromVersion = sfsv
|
||||
}
|
||||
}
|
||||
if resumeToken.HasToGUID && sfsv.Guid == resumeToken.ToGUID && sfsv.Type == pdu.FilesystemVersion_Snapshot {
|
||||
// `toversion` must always be a snapshot
|
||||
toVersion, toVersionIdx = sfsv, idx
|
||||
}
|
||||
}
|
||||
|
||||
encryptionMatches := false
|
||||
switch fs.policy.EncryptedSend {
|
||||
case True:
|
||||
encryptionMatches = resumeToken.RawOK && resumeToken.CompressOK
|
||||
case False:
|
||||
encryptionMatches = !resumeToken.RawOK && !resumeToken.CompressOK
|
||||
case DontCare:
|
||||
encryptionMatches = true
|
||||
}
|
||||
|
||||
log.WithField("fromVersion", fromVersion).
|
||||
WithField("toVersion", toVersion).
|
||||
WithField("encryptionMatches", encryptionMatches).
|
||||
Debug("result of resume-token-matching to sender's versions")
|
||||
|
||||
if !encryptionMatches {
|
||||
return nil, fmt.Errorf("resume token `rawok`=%v and `compressok`=%v are incompatible with encryption policy=%v", resumeToken.RawOK, resumeToken.CompressOK, fs.policy.EncryptedSend)
|
||||
} else if toVersion == nil {
|
||||
return nil, fmt.Errorf("resume token `toguid` = %v not found on sender (`toname` = %q)", resumeToken.ToGUID, resumeToken.ToName)
|
||||
} else if fromVersion == toVersion {
|
||||
return nil, fmt.Errorf("resume token `fromguid` and `toguid` match same version on sener")
|
||||
}
|
||||
// fromVersion may be nil, toVersion is no nil, encryption matches
|
||||
// good to go this one step!
|
||||
resumeStep := &Step{
|
||||
parent: fs,
|
||||
sender: fs.sender,
|
||||
receiver: fs.receiver,
|
||||
from: nil,
|
||||
to: path[0],
|
||||
})
|
||||
} else {
|
||||
for i := 0; i < len(path)-1; i++ {
|
||||
|
||||
from: fromVersion,
|
||||
to: toVersion,
|
||||
encrypt: fs.policy.EncryptedSend,
|
||||
|
||||
resumeToken: resumeTokenRaw,
|
||||
}
|
||||
|
||||
// by definition, the resume token _must_ be the receiver's most recent version, if they have any
|
||||
// don't bother checking, zfs recv will produce an error if above assumption is wrong
|
||||
//
|
||||
// thus, subsequent steps are just incrementals on the sender's remaining _snapshots_ (not bookmarks)
|
||||
|
||||
var remainingSFSVs []*pdu.FilesystemVersion
|
||||
for _, sfsv := range sfsvs[toVersionIdx:] {
|
||||
if sfsv.Type == pdu.FilesystemVersion_Snapshot {
|
||||
remainingSFSVs = append(remainingSFSVs, sfsv)
|
||||
}
|
||||
}
|
||||
|
||||
steps = make([]*Step, 0, len(remainingSFSVs)) // shadow
|
||||
steps = append(steps, resumeStep)
|
||||
for i := 0; i < len(remainingSFSVs)-1; i++ {
|
||||
steps = append(steps, &Step{
|
||||
parent: fs,
|
||||
sender: fs.sender,
|
||||
receiver: fs.receiver,
|
||||
from: path[i],
|
||||
to: path[i+1],
|
||||
from: remainingSFSVs[i],
|
||||
to: remainingSFSVs[i+1],
|
||||
encrypt: fs.policy.EncryptedSend,
|
||||
})
|
||||
}
|
||||
} else { // resumeToken == nil
|
||||
path, conflict := IncrementalPath(rfsvs, sfsvs)
|
||||
if conflict != nil {
|
||||
var msg string
|
||||
path, msg = resolveConflict(conflict) // no shadowing allowed!
|
||||
if path != nil {
|
||||
log.WithField("conflict", conflict).Info("conflict")
|
||||
log.WithField("resolution", msg).Info("automatically resolved")
|
||||
} else {
|
||||
log.WithField("conflict", conflict).Error("conflict")
|
||||
log.WithField("problem", msg).Error("cannot resolve conflict")
|
||||
}
|
||||
}
|
||||
if len(path) == 0 {
|
||||
return nil, conflict
|
||||
}
|
||||
|
||||
steps = make([]*Step, 0, len(path)) // shadow
|
||||
if len(path) == 1 {
|
||||
steps = append(steps, &Step{
|
||||
parent: fs,
|
||||
sender: fs.sender,
|
||||
receiver: fs.receiver,
|
||||
|
||||
from: nil,
|
||||
to: path[0],
|
||||
encrypt: fs.policy.EncryptedSend,
|
||||
})
|
||||
} else {
|
||||
for i := 0; i < len(path)-1; i++ {
|
||||
steps = append(steps, &Step{
|
||||
parent: fs,
|
||||
sender: fs.sender,
|
||||
receiver: fs.receiver,
|
||||
|
||||
from: path[i],
|
||||
to: path[i+1],
|
||||
encrypt: fs.policy.EncryptedSend,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(steps) == 0 {
|
||||
log.Info("planning determined that no replication steps are required")
|
||||
}
|
||||
|
||||
log.Debug("compute send size estimate")
|
||||
@@ -389,29 +570,6 @@ func (fs *Filesystem) doPlanning(ctx context.Context) ([]*Step, error) {
|
||||
return steps, nil
|
||||
}
|
||||
|
||||
// type FilesystemsReplicationFailedError struct {
|
||||
// FilesystemsWithError []*fsrep.Replication
|
||||
// }
|
||||
|
||||
// func (e FilesystemsReplicationFailedError) Error() string {
|
||||
// allSame := true
|
||||
// lastErr := e.FilesystemsWithError[0].Err().Error()
|
||||
// for _, fs := range e.FilesystemsWithError {
|
||||
// fsErr := fs.Err().Error()
|
||||
// allSame = allSame && lastErr == fsErr
|
||||
// }
|
||||
|
||||
// fsstr := "multiple filesystems"
|
||||
// if len(e.FilesystemsWithError) == 1 {
|
||||
// fsstr = fmt.Sprintf("filesystem %s", e.FilesystemsWithError[0].FS())
|
||||
// }
|
||||
// errorStr := lastErr
|
||||
// if !allSame {
|
||||
// errorStr = "multiple different errors"
|
||||
// }
|
||||
// return fmt.Sprintf("%s could not be replicated: %s", fsstr, errorStr)
|
||||
// }
|
||||
|
||||
func (s *Step) updateSizeEstimate(ctx context.Context) error {
|
||||
|
||||
log := getLogger(ctx)
|
||||
@@ -430,19 +588,13 @@ func (s *Step) updateSizeEstimate(ctx context.Context) error {
|
||||
|
||||
func (s *Step) buildSendRequest(dryRun bool) (sr *pdu.SendReq) {
|
||||
fs := s.parent.Path
|
||||
if s.from == nil {
|
||||
sr = &pdu.SendReq{
|
||||
Filesystem: fs,
|
||||
To: s.to.RelName(),
|
||||
DryRun: dryRun,
|
||||
}
|
||||
} else {
|
||||
sr = &pdu.SendReq{
|
||||
Filesystem: fs,
|
||||
From: s.from.RelName(),
|
||||
To: s.to.RelName(),
|
||||
DryRun: dryRun,
|
||||
}
|
||||
sr = &pdu.SendReq{
|
||||
Filesystem: fs,
|
||||
From: s.from, // may be nil
|
||||
To: s.to,
|
||||
Encrypted: s.encrypt.ToPDU(),
|
||||
ResumeToken: s.resumeToken,
|
||||
DryRun: dryRun,
|
||||
}
|
||||
return sr
|
||||
}
|
||||
@@ -455,19 +607,19 @@ func (s *Step) doReplication(ctx context.Context) error {
|
||||
sr := s.buildSendRequest(false)
|
||||
|
||||
log.Debug("initiate send request")
|
||||
sres, sstreamCopier, err := s.sender.Send(ctx, sr)
|
||||
sres, stream, err := s.sender.Send(ctx, sr)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("send request failed")
|
||||
return err
|
||||
}
|
||||
if sstreamCopier == nil {
|
||||
if stream == nil {
|
||||
err := errors.New("send request did not return a stream, broken endpoint implementation")
|
||||
return err
|
||||
}
|
||||
defer sstreamCopier.Close()
|
||||
defer stream.Close()
|
||||
|
||||
// Install a byte counter to track progress + for status report
|
||||
byteCountingStream := bytecounter.NewStreamCopier(sstreamCopier)
|
||||
byteCountingStream := bytecounter.NewReadCloser(stream)
|
||||
s.byteCounterMtx.Lock()
|
||||
s.byteCounter = byteCountingStream
|
||||
s.byteCounterMtx.Unlock()
|
||||
@@ -478,6 +630,7 @@ func (s *Step) doReplication(ctx context.Context) error {
|
||||
|
||||
rr := &pdu.ReceiveReq{
|
||||
Filesystem: fs,
|
||||
To: sr.GetTo(),
|
||||
ClearResumeToken: !sres.UsedResumeToken,
|
||||
}
|
||||
log.Debug("initiate receive request")
|
||||
@@ -486,6 +639,7 @@ func (s *Step) doReplication(ctx context.Context) error {
|
||||
log.
|
||||
WithError(err).
|
||||
WithField("errType", fmt.Sprintf("%T", err)).
|
||||
WithField("rr", fmt.Sprintf("%v", rr)).
|
||||
Error("receive request failed (might also be error on sender)")
|
||||
// This failure could be due to
|
||||
// - an unexpected exit of ZFS on the sending side
|
||||
@@ -495,22 +649,12 @@ func (s *Step) doReplication(ctx context.Context) error {
|
||||
}
|
||||
log.Debug("receive finished")
|
||||
|
||||
log.Debug("advance replication cursor")
|
||||
req := &pdu.ReplicationCursorReq{
|
||||
Filesystem: fs,
|
||||
Op: &pdu.ReplicationCursorReq_Set{
|
||||
Set: &pdu.ReplicationCursorReq_SetOp{
|
||||
Snapshot: s.to.GetName(),
|
||||
},
|
||||
},
|
||||
}
|
||||
_, err = s.sender.ReplicationCursor(ctx, req)
|
||||
log.Debug("tell sender replication completed")
|
||||
_, err = s.sender.SendCompleted(ctx, &pdu.SendCompletedReq{
|
||||
OriginalReq: sr,
|
||||
})
|
||||
if err != nil {
|
||||
log.WithError(err).Error("error advancing replication cursor")
|
||||
// If this fails and replication planning restarts, the diff algorithm will find
|
||||
// that cursor out of place. This is not a problem because then, it would just use another FS
|
||||
// However, we FIXME have no means to just update the cursor in a
|
||||
// second replication attempt right after this one where we don't have new snaps yet
|
||||
log.WithError(err).Error("error telling sender that replication completed successfully")
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package logic
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/zrepl/zrepl/replication/logic/pdu"
|
||||
)
|
||||
|
||||
type tri int
|
||||
|
||||
const (
|
||||
DontCare = 0x0
|
||||
False = 0x1
|
||||
True = 0x2
|
||||
)
|
||||
|
||||
func (t tri) String() string {
|
||||
switch t {
|
||||
case DontCare:
|
||||
return "dontcare"
|
||||
case False:
|
||||
return "false"
|
||||
case True:
|
||||
return "true"
|
||||
}
|
||||
panic(fmt.Sprintf("unknown variant %v", int(t)))
|
||||
}
|
||||
|
||||
func (t tri) ToPDU() pdu.Tri {
|
||||
switch t {
|
||||
case DontCare:
|
||||
return pdu.Tri_DontCare
|
||||
case False:
|
||||
return pdu.Tri_False
|
||||
case True:
|
||||
return pdu.Tri_True
|
||||
}
|
||||
panic(fmt.Sprintf("unknown variant %v", int(t)))
|
||||
}
|
||||
|
||||
func TriFromBool(b bool) tri {
|
||||
if b {
|
||||
return True
|
||||
}
|
||||
return False
|
||||
}
|
||||
@@ -85,25 +85,37 @@ type StepReport struct {
|
||||
Info *StepInfo
|
||||
}
|
||||
|
||||
type EncryptedEnum string
|
||||
|
||||
const (
|
||||
EncryptedTrue EncryptedEnum = "yes"
|
||||
EncryptedFalse EncryptedEnum = "no"
|
||||
EncryptedSenderDependent EncryptedEnum = "sender-dependent"
|
||||
)
|
||||
|
||||
type StepInfo struct {
|
||||
From, To string
|
||||
Resumed bool
|
||||
Encrypted EncryptedEnum
|
||||
BytesExpected int64
|
||||
BytesReplicated int64
|
||||
}
|
||||
|
||||
func (a *AttemptReport) BytesSum() (expected, replicated int64) {
|
||||
func (a *AttemptReport) BytesSum() (expected, replicated int64, containsInvalidSizeEstimates bool) {
|
||||
for _, fs := range a.Filesystems {
|
||||
e, r := fs.BytesSum()
|
||||
e, r, fsContainsInvalidEstimate := fs.BytesSum()
|
||||
containsInvalidSizeEstimates = containsInvalidSizeEstimates || fsContainsInvalidEstimate
|
||||
expected += e
|
||||
replicated += r
|
||||
}
|
||||
return expected, replicated
|
||||
return expected, replicated, containsInvalidSizeEstimates
|
||||
}
|
||||
|
||||
func (f *FilesystemReport) BytesSum() (expected, replicated int64) {
|
||||
func (f *FilesystemReport) BytesSum() (expected, replicated int64, containsInvalidSizeEstimates bool) {
|
||||
for _, step := range f.Steps {
|
||||
expected += step.Info.BytesExpected
|
||||
replicated += step.Info.BytesReplicated
|
||||
containsInvalidSizeEstimates = containsInvalidSizeEstimates || step.Info.BytesExpected == 0
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -148,5 +160,5 @@ func (f *FilesystemReport) NextStep() *StepReport {
|
||||
}
|
||||
|
||||
func (f *StepReport) IsIncremental() bool {
|
||||
return f.Info.From != "" // FIXME change to ZFS semantics (To != "")
|
||||
return f.Info.From != ""
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/golang/protobuf/proto"
|
||||
@@ -11,7 +12,6 @@ import (
|
||||
"github.com/zrepl/zrepl/replication/logic/pdu"
|
||||
"github.com/zrepl/zrepl/rpc/dataconn/stream"
|
||||
"github.com/zrepl/zrepl/transport"
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
@@ -26,7 +26,7 @@ func NewClient(connecter transport.Connecter, log Logger) *Client {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) send(ctx context.Context, conn *stream.Conn, endpoint string, req proto.Message, streamCopier zfs.StreamCopier) error {
|
||||
func (c *Client) send(ctx context.Context, conn *stream.Conn, endpoint string, req proto.Message, stream io.ReadCloser) error {
|
||||
|
||||
var buf bytes.Buffer
|
||||
_, memErr := buf.WriteString(endpoint)
|
||||
@@ -46,8 +46,8 @@ func (c *Client) send(ctx context.Context, conn *stream.Conn, endpoint string, r
|
||||
return err
|
||||
}
|
||||
|
||||
if streamCopier != nil {
|
||||
return conn.SendStream(ctx, streamCopier, ZFSStream)
|
||||
if stream != nil {
|
||||
return conn.SendStream(ctx, stream, ZFSStream)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
@@ -109,7 +109,7 @@ func (c *Client) putWire(conn *stream.Conn) {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) ReqSend(ctx context.Context, req *pdu.SendReq) (*pdu.SendRes, zfs.StreamCopier, error) {
|
||||
func (c *Client) ReqSend(ctx context.Context, req *pdu.SendReq) (*pdu.SendRes, io.ReadCloser, error) {
|
||||
conn, err := c.getWire(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
@@ -130,16 +130,19 @@ func (c *Client) ReqSend(ctx context.Context, req *pdu.SendReq) (*pdu.SendRes, z
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
var copier zfs.StreamCopier = nil
|
||||
var stream io.ReadCloser
|
||||
if !req.DryRun {
|
||||
putWireOnReturn = false
|
||||
copier = &streamCopier{streamConn: conn, closeStreamOnClose: true}
|
||||
stream, err = conn.ReadStream(ZFSStream, true) // no shadow
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &res, copier, nil
|
||||
return &res, stream, nil
|
||||
}
|
||||
|
||||
func (c *Client) ReqRecv(ctx context.Context, req *pdu.ReceiveReq, streamCopier zfs.StreamCopier) (*pdu.ReceiveRes, error) {
|
||||
func (c *Client) ReqRecv(ctx context.Context, req *pdu.ReceiveReq, stream io.ReadCloser) (*pdu.ReceiveRes, error) {
|
||||
|
||||
defer c.log.Debug("ReqRecv returns")
|
||||
conn, err := c.getWire(ctx)
|
||||
@@ -166,7 +169,7 @@ func (c *Client) ReqRecv(ctx context.Context, req *pdu.ReceiveReq, streamCopier
|
||||
|
||||
sendErrChan := make(chan error)
|
||||
go func() {
|
||||
if err := c.send(ctx, conn, EndpointRecv, req, streamCopier); err != nil {
|
||||
if err := c.send(ctx, conn, EndpointRecv, req, stream); err != nil {
|
||||
sendErrChan <- err
|
||||
} else {
|
||||
sendErrChan <- nil
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/golang/protobuf/proto"
|
||||
|
||||
@@ -11,7 +12,6 @@ import (
|
||||
"github.com/zrepl/zrepl/replication/logic/pdu"
|
||||
"github.com/zrepl/zrepl/rpc/dataconn/stream"
|
||||
"github.com/zrepl/zrepl/transport"
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
|
||||
// WireInterceptor has a chance to exchange the context and connection on each client connection.
|
||||
@@ -21,11 +21,11 @@ type WireInterceptor func(ctx context.Context, rawConn *transport.AuthConn) (con
|
||||
type Handler interface {
|
||||
// Send handles a SendRequest.
|
||||
// The returned io.ReadCloser is allowed to be nil, for example if the requested Send is a dry-run.
|
||||
Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, zfs.StreamCopier, error)
|
||||
Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, io.ReadCloser, error)
|
||||
// Receive handles a ReceiveRequest.
|
||||
// It is guaranteed that Server calls Receive with a stream that holds the IdleConnTimeout
|
||||
// configured in ServerConfig.Shared.IdleConnTimeout.
|
||||
Receive(ctx context.Context, r *pdu.ReceiveReq, receive zfs.StreamCopier) (*pdu.ReceiveRes, error)
|
||||
Receive(ctx context.Context, r *pdu.ReceiveReq, receive io.ReadCloser) (*pdu.ReceiveRes, error)
|
||||
// PingDataconn handles a PingReq
|
||||
PingDataconn(ctx context.Context, r *pdu.PingReq) (*pdu.PingRes, error)
|
||||
}
|
||||
@@ -111,7 +111,7 @@ func (s *Server) serveConn(nc *transport.AuthConn) {
|
||||
s.log.WithField("endpoint", endpoint).Debug("calling handler")
|
||||
|
||||
var res proto.Message
|
||||
var sendStream zfs.StreamCopier
|
||||
var sendStream io.ReadCloser
|
||||
var handlerErr error
|
||||
switch endpoint {
|
||||
case EndpointSend:
|
||||
@@ -127,7 +127,12 @@ func (s *Server) serveConn(nc *transport.AuthConn) {
|
||||
s.log.WithError(err).Error("cannot unmarshal receive request")
|
||||
return
|
||||
}
|
||||
res, handlerErr = s.h.Receive(ctx, &req, &streamCopier{streamConn: c, closeStreamOnClose: false}) // SHADOWING
|
||||
stream, err := c.ReadStream(ZFSStream, false)
|
||||
if err != nil {
|
||||
s.log.WithError(err).Error("cannot open stream in receive request")
|
||||
return
|
||||
}
|
||||
res, handlerErr = s.h.Receive(ctx, &req, stream) // SHADOWING
|
||||
case EndpointPing:
|
||||
var req pdu.PingReq
|
||||
if err := proto.Unmarshal(reqStructured, &req); err != nil {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user