Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b86bbda7fe | |||
| 2fbd9d8f8c | |||
| 18e101a04e | |||
| e594421322 | |||
| 0d4bfda2fb | |||
| b83c026cdc | |||
| 46caf31075 | |||
| 2b9d696b49 | |||
| 70afff6e3b | |||
| 01bbda13e5 | |||
| c5a8f6635f | |||
| 6f441c55dc | |||
| 7b34d6cba5 | |||
| 70f9c6482f | |||
| aed6149c8c | |||
| 0834a184b8 | |||
| f61295f76c | |||
| 0920a40751 | |||
| e0b5bd75f8 | |||
| 96e188d7c4 | |||
| 1336c91865 | |||
| 9568e46f05 | |||
| 3187129672 | |||
| 44bd354eae | |||
| 94caf8b8db | |||
| 3ff1966cab | |||
| a3842155c5 | |||
| 02b3b4f80c | |||
| 0882290595 |
@@ -6,6 +6,7 @@ workflows:
|
|||||||
- build-1.11
|
- build-1.11
|
||||||
- build-1.12
|
- build-1.12
|
||||||
- build-1.13
|
- build-1.13
|
||||||
|
- build-1.14
|
||||||
- build-latest
|
- build-latest
|
||||||
- test-build-in-docker
|
- test-build-in-docker
|
||||||
jobs:
|
jobs:
|
||||||
@@ -111,6 +112,11 @@ jobs:
|
|||||||
docker:
|
docker:
|
||||||
- image: circleci/golang:1.13
|
- 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
|
# this job tries to mimic the build-in-docker instructions
|
||||||
# given in docs/installation.rst
|
# given in docs/installation.rst
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ GOHOSTARCH ?= $(shell bash -c 'source <($(GO) env) && echo "$$GOHOSTARCH"')
|
|||||||
GO_ENV_VARS := GO111MODULE=on
|
GO_ENV_VARS := GO111MODULE=on
|
||||||
GO_LDFLAGS := "-X github.com/zrepl/zrepl/version.zreplVersion=$(_ZREPL_VERSION)"
|
GO_LDFLAGS := "-X github.com/zrepl/zrepl/version.zreplVersion=$(_ZREPL_VERSION)"
|
||||||
GO_MOD_READONLY := -mod=readonly
|
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)
|
GO_BUILD := $(GO_ENV_VARS) $(GO) build $(GO_BUILDFLAGS) -ldflags $(GO_LDFLAGS)
|
||||||
GOLANGCI_LINT := golangci-lint
|
GOLANGCI_LINT := golangci-lint
|
||||||
ifneq ($(GOARM),)
|
ifneq ($(GOARM),)
|
||||||
@@ -65,6 +66,7 @@ wrapup-and-checksum:
|
|||||||
-acf $(NOARCH_TARBALL) \
|
-acf $(NOARCH_TARBALL) \
|
||||||
$(ARTIFACTDIR)/docs/html \
|
$(ARTIFACTDIR)/docs/html \
|
||||||
$(ARTIFACTDIR)/bash_completion \
|
$(ARTIFACTDIR)/bash_completion \
|
||||||
|
$(ARTIFACTDIR)/_zrepl.zsh_completion \
|
||||||
$(ARTIFACTDIR)/go_env.txt \
|
$(ARTIFACTDIR)/go_env.txt \
|
||||||
dist \
|
dist \
|
||||||
config/samples
|
config/samples
|
||||||
@@ -159,7 +161,7 @@ platformtest: # do not track dependency on platformtest-bin to allow build of pl
|
|||||||
$(ZREPL_PLATFORMTEST_ARGS)
|
$(ZREPL_PLATFORMTEST_ARGS)
|
||||||
|
|
||||||
##################### NOARCH #####################
|
##################### 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):
|
$(ARTIFACTDIR):
|
||||||
@@ -167,12 +169,16 @@ $(ARTIFACTDIR):
|
|||||||
$(ARTIFACTDIR)/docs: $(ARTIFACTDIR)
|
$(ARTIFACTDIR)/docs: $(ARTIFACTDIR)
|
||||||
mkdir -p "$@"
|
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
|
# pass
|
||||||
|
|
||||||
$(ARTIFACTDIR)/bash_completion:
|
$(ARTIFACTDIR)/bash_completion:
|
||||||
$(MAKE) zrepl-bin GOOS=$(GOHOSTOS) GOARCH=$(GOHOSTARCH)
|
$(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:
|
$(ARTIFACTDIR)/go_env.txt:
|
||||||
$(GO_ENV_VARS) $(GO) env > $@
|
$(GO_ENV_VARS) $(GO) env > $@
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
[](https://github.com/zrepl/zrepl/blob/master/LICENSE)
|
[](https://github.com/zrepl/zrepl/blob/master/LICENSE)
|
||||||
[](https://golang.org/)
|
[](https://golang.org/)
|
||||||
[](https://zrepl.github.io)
|
[](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://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)
|
[](https://twitter.com/intent/tweet?text=Wow:&url=https%3A%2F%2Fgithub.com%2Fzrepl%2Fzrepl)
|
||||||
|
|
||||||
# zrepl
|
# zrepl
|
||||||
@@ -23,6 +24,7 @@ zrepl is a one-stop ZFS backup & replication solution.
|
|||||||
If so, think of an expressive configuration example.
|
If so, think of an expressive configuration example.
|
||||||
2. Think of at least one use case that generalizes from your concrete application.
|
2. Think of at least one use case that generalizes from your concrete application.
|
||||||
3. Open an issue on GitHub with example conf & use case attached.
|
3. Open an issue on GitHub with example conf & use case attached.
|
||||||
|
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.
|
The above does not apply if you already implemented everything.
|
||||||
Check out the *Coding Workflow* section below for details.
|
Check out the *Coding Workflow* section below for details.
|
||||||
@@ -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.
|
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`.
|
To get started, run `./lazy.sh devsetup` to easily install build dependencies and read `docs/installation.rst -> Compiling from Source`.
|
||||||
|
`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).
|
||||||
### Overall Architecture
|
If you just want to install the Go dependencies, run `./lazy.sh godep`.
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
### Project Structure
|
### Project Structure
|
||||||
|
|
||||||
@@ -62,14 +61,15 @@ Make sure to develop an understanding how zrepl is typically used by studying th
|
|||||||
│ └── samples
|
│ └── samples
|
||||||
├── daemon # the implementation of `zrepl daemon` subcommand
|
├── daemon # the implementation of `zrepl daemon` subcommand
|
||||||
│ ├── filters
|
│ ├── filters
|
||||||
|
│ ├── hooks # snapshot hooks
|
||||||
│ ├── job # job implementations
|
│ ├── job # job implementations
|
||||||
│ ├── logging # logging outlets + formatters
|
│ ├── logging # logging outlets + formatters
|
||||||
│ ├── nethelpers
|
│ ├── nethelpers
|
||||||
│ ├── prometheus
|
│ ├── prometheus
|
||||||
│ ├── pruner # pruner implementation
|
│ ├── pruner # pruner implementation
|
||||||
│ ├── snapper # snapshotter implementation
|
│ ├── snapper # snapshotter implementation
|
||||||
├── docs # sphinx-based documentation
|
|
||||||
├── dist # supplemental material for users & package maintainers
|
├── dist # supplemental material for users & package maintainers
|
||||||
|
├── docs # sphinx-based documentation
|
||||||
│ ├── **/*.rst # documentation in reStructuredText
|
│ ├── **/*.rst # documentation in reStructuredText
|
||||||
│ ├── sphinxconf
|
│ ├── sphinxconf
|
||||||
│ │ └── conf.py # sphinx config (see commit 445a280 why its not in docs/)
|
│ │ └── 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
|
│ └── public_git # checkout of zrepl.github.io managed by above shell script
|
||||||
├── endpoint # implementation of replication endpoints (=> package replication)
|
├── endpoint # implementation of replication endpoints (=> package replication)
|
||||||
├── logger # our own logger package
|
├── logger # our own logger package
|
||||||
|
├── platformtest # test suite for our zfs abstractions (error classification, etc)
|
||||||
├── pruning # pruning rules (the logic, not the actual execution)
|
├── pruning # pruning rules (the logic, not the actual execution)
|
||||||
│ └── retentiongrid
|
│ └── retentiongrid
|
||||||
├── replication
|
├── 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
|
│ ├── transportmux # TCP connecter and listener used to split control & data traffic
|
||||||
│ └── versionhandshake # replication protocol version handshake perfomed on newly established connections
|
│ └── versionhandshake # replication protocol version handshake perfomed on newly established connections
|
||||||
├── tlsconf # abstraction for Go TLS server + client config
|
├── tlsconf # abstraction for Go TLS server + client config
|
||||||
├── transport # transports implementation
|
├── transport # transport implementations
|
||||||
│ ├── fromconfig
|
│ ├── fromconfig
|
||||||
│ ├── local
|
│ ├── local
|
||||||
│ ├── ssh
|
│ ├── ssh
|
||||||
│ ├── tcp
|
│ ├── tcp
|
||||||
│ └── tls
|
│ └── tls
|
||||||
├── util
|
├── util
|
||||||
├── vendor # managed by dep
|
|
||||||
├── version # abstraction for versions (filled during build by Makefile)
|
├── version # abstraction for versions (filled during build by Makefile)
|
||||||
└── zfs # zfs(8) wrappers
|
└── 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.
|
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
|
||||||
|
|
||||||
|
|||||||
+37
-11
@@ -19,9 +19,38 @@ var rootCmd = &cobra.Command{
|
|||||||
Short: "One-stop ZFS replication solution",
|
Short: "One-stop ZFS replication solution",
|
||||||
}
|
}
|
||||||
|
|
||||||
var bashcompCmd = &cobra.Command{
|
func init() {
|
||||||
Use: "bashcomp path/to/out/file",
|
rootCmd.PersistentFlags().StringVar(&rootArgs.configPath, "config", "", "config file path")
|
||||||
Short: "generate bash completions",
|
}
|
||||||
|
|
||||||
|
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",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
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) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
if len(args) != 1 {
|
if len(args) != 1 {
|
||||||
fmt.Fprintf(os.Stderr, "specify exactly one positional agument\n")
|
fmt.Fprintf(os.Stderr, "specify exactly one positional agument\n")
|
||||||
@@ -31,17 +60,14 @@ var bashcompCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
if err := rootCmd.GenBashCompletionFile(args[0]); err != nil {
|
if err := info.genFunc(args[0]); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "error generating bash completion: %s", err)
|
fmt.Fprintf(os.Stderr, "error generating %s completion: %s", sh, err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Hidden: true,
|
})
|
||||||
}
|
}
|
||||||
|
rootCmd.AddCommand(genCompletionCmd)
|
||||||
func init() {
|
|
||||||
rootCmd.PersistentFlags().StringVar(&rootArgs.configPath, "config", "", "config file path")
|
|
||||||
rootCmd.AddCommand(bashcompCmd)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Subcommand struct {
|
type Subcommand struct {
|
||||||
|
|||||||
@@ -1,87 +0,0 @@
|
|||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/cli"
|
|
||||||
"github.com/zrepl/zrepl/daemon/filters"
|
|
||||||
"github.com/zrepl/zrepl/endpoint"
|
|
||||||
"github.com/zrepl/zrepl/zfs"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
HoldsCmd = &cli.Subcommand{
|
|
||||||
Use: "holds",
|
|
||||||
Short: "manage holds & step bookmarks",
|
|
||||||
SetupSubcommands: func() []*cli.Subcommand {
|
|
||||||
return holdsList
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
var holdsList = []*cli.Subcommand{
|
|
||||||
&cli.Subcommand{
|
|
||||||
Use: "list [FSFILTER]",
|
|
||||||
Run: doHoldsList,
|
|
||||||
NoRequireConfig: true,
|
|
||||||
Short: `
|
|
||||||
FSFILTER SYNTAX:
|
|
||||||
representation of a 'filesystems' filter statement on the command line
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func fsfilterFromCliArg(arg string) (zfs.DatasetFilter, error) {
|
|
||||||
mappings := strings.Split(arg, ",")
|
|
||||||
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 nil, thisMappingErr
|
|
||||||
}
|
|
||||||
err := f.Add(lhsrhs[0], lhsrhs[1])
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("%s: %s", thisMappingErr, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return f.AsFilter(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func doHoldsList(sc *cli.Subcommand, args []string) error {
|
|
||||||
var err error
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
if len(args) > 1 {
|
|
||||||
return errors.New("this subcommand takes at most one argument")
|
|
||||||
}
|
|
||||||
|
|
||||||
var filter zfs.DatasetFilter
|
|
||||||
if len(args) == 0 {
|
|
||||||
filter = zfs.NoFilter()
|
|
||||||
} else {
|
|
||||||
filter, err = fsfilterFromCliArg(args[0])
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "cannot parse filesystem filter args")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
listing, err := endpoint.ListZFSHoldsAndBookmarks(ctx, filter)
|
|
||||||
if err != nil {
|
|
||||||
return err // context clear by invocation of command
|
|
||||||
}
|
|
||||||
|
|
||||||
enc := json.NewEncoder(os.Stdout)
|
|
||||||
enc.SetIndent(" ", " ")
|
|
||||||
if err := enc.Encode(listing); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
+8
-10
@@ -99,7 +99,7 @@ func doMigratePlaceholder0_1(sc *cli.Subcommand, args []string) error {
|
|||||||
}
|
}
|
||||||
for _, fs := range wi.fss {
|
for _, fs := range wi.fss {
|
||||||
fmt.Printf("\t%q ... ", fs.ToString())
|
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 {
|
if err != nil {
|
||||||
fmt.Printf("error: %s\n", err)
|
fmt.Printf("error: %s\n", err)
|
||||||
} else if !r.NeedsModification {
|
} else if !r.NeedsModification {
|
||||||
@@ -165,7 +165,7 @@ func doMigrateReplicationCursor(sc *cli.Subcommand, args []string) error {
|
|||||||
var hadError bool
|
var hadError bool
|
||||||
for _, fs := range fss {
|
for _, fs := range fss {
|
||||||
|
|
||||||
bold.Printf("INSPECT FILESYTEM %q\n", fs.ToString())
|
bold.Printf("INSPECT FILESYSTEM %q\n", fs.ToString())
|
||||||
|
|
||||||
err := doMigrateReplicationCursorFS(ctx, v1cursorJobs, fs)
|
err := doMigrateReplicationCursorFS(ctx, v1cursorJobs, fs)
|
||||||
if err == migrateReplicationCursorSkipSentinel {
|
if err == migrateReplicationCursorSkipSentinel {
|
||||||
@@ -211,17 +211,15 @@ func doMigrateReplicationCursorFS(ctx context.Context, v1CursorJobs []job.Job, f
|
|||||||
}
|
}
|
||||||
fmt.Printf("identified owning job %q\n", owningJob.Name())
|
fmt.Printf("identified owning job %q\n", owningJob.Name())
|
||||||
|
|
||||||
versions, err := zfs.ZFSListFilesystemVersions(fs, nil)
|
bookmarks, err := zfs.ZFSListFilesystemVersions(ctx, fs, zfs.ListFilesystemVersionsOptions{
|
||||||
|
Types: zfs.Bookmarks,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrapf(err, "list filesystem versions of %q", fs.ToString())
|
return errors.Wrapf(err, "list filesystem versions of %q", fs.ToString())
|
||||||
}
|
}
|
||||||
|
|
||||||
var oldCursor *zfs.FilesystemVersion
|
var oldCursor *zfs.FilesystemVersion
|
||||||
for i, fsv := range versions {
|
for i, fsv := range bookmarks {
|
||||||
if fsv.Type != zfs.Bookmark {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
_, _, err := endpoint.ParseReplicationCursorBookmarkName(fsv.ToAbsPath(fs))
|
_, _, err := endpoint.ParseReplicationCursorBookmarkName(fsv.ToAbsPath(fs))
|
||||||
if err != endpoint.ErrV1ReplicationCursor {
|
if err != endpoint.ErrV1ReplicationCursor {
|
||||||
continue
|
continue
|
||||||
@@ -232,7 +230,7 @@ func doMigrateReplicationCursorFS(ctx context.Context, v1CursorJobs []job.Job, f
|
|||||||
return errors.Wrap(err, "multiple filesystem versions identified as v1 replication cursors")
|
return errors.Wrap(err, "multiple filesystem versions identified as v1 replication cursors")
|
||||||
}
|
}
|
||||||
|
|
||||||
oldCursor = &versions[i]
|
oldCursor = &bookmarks[i]
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,7 +262,7 @@ func doMigrateReplicationCursorFS(ctx context.Context, v1CursorJobs []job.Job, f
|
|||||||
if migrateReplicationCursorArgs.dryRun {
|
if migrateReplicationCursorArgs.dryRun {
|
||||||
succ.Printf("DRY RUN\n")
|
succ.Printf("DRY RUN\n")
|
||||||
} else {
|
} else {
|
||||||
if err := zfs.ZFSDestroyFilesystemVersion(fs, oldCursor); err != nil {
|
if err := zfs.ZFSDestroyFilesystemVersion(ctx, fs, oldCursor); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-7
@@ -11,7 +11,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"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
|
// as of tcell#252, the number of default distributed terminals is relatively limited
|
||||||
// additional terminal definitions can be included via side-effect import
|
// additional terminal definitions can be included via side-effect import
|
||||||
// See https://github.com/gdamore/tcell/blob/master/terminfo/base/base.go
|
// See https://github.com/gdamore/tcell/blob/master/terminfo/base/base.go
|
||||||
@@ -81,7 +81,7 @@ type tui struct {
|
|||||||
indent int
|
indent int
|
||||||
|
|
||||||
lock sync.Mutex //For report and error
|
lock sync.Mutex //For report and error
|
||||||
report map[string]job.Status
|
report map[string]*job.Status
|
||||||
err error
|
err error
|
||||||
|
|
||||||
jobFilter string
|
jobFilter string
|
||||||
@@ -219,7 +219,7 @@ func runStatus(s *cli.Subcommand, args []string) error {
|
|||||||
defer termbox.Close()
|
defer termbox.Close()
|
||||||
|
|
||||||
update := func() {
|
update := func() {
|
||||||
m := make(map[string]job.Status)
|
var m daemon.Status
|
||||||
|
|
||||||
err2 := jsonRequestResponse(httpc, daemon.ControlJobEndpointStatus,
|
err2 := jsonRequestResponse(httpc, daemon.ControlJobEndpointStatus,
|
||||||
struct{}{},
|
struct{}{},
|
||||||
@@ -228,7 +228,7 @@ func runStatus(s *cli.Subcommand, args []string) error {
|
|||||||
|
|
||||||
t.lock.Lock()
|
t.lock.Lock()
|
||||||
t.err = err2
|
t.err = err2
|
||||||
t.report = m
|
t.report = m.Jobs
|
||||||
t.lock.Unlock()
|
t.lock.Unlock()
|
||||||
t.draw()
|
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]
|
p, ok := t.replicationProgress[jobName]
|
||||||
if !ok {
|
if !ok {
|
||||||
p = &bytesProgressHistory{}
|
p = &bytesProgressHistory{}
|
||||||
@@ -329,7 +329,7 @@ func (t *tui) draw() {
|
|||||||
t.printf("Replication:")
|
t.printf("Replication:")
|
||||||
t.newline()
|
t.newline()
|
||||||
t.addIndent(1)
|
t.addIndent(1)
|
||||||
t.renderReplicationReport(activeStatus.Replication, t.getReplicationProgresHistory(k))
|
t.renderReplicationReport(activeStatus.Replication, t.getReplicationProgressHistory(k))
|
||||||
t.addIndent(-1)
|
t.addIndent(-1)
|
||||||
|
|
||||||
t.printf("Pruning Sender:")
|
t.printf("Pruning Sender:")
|
||||||
@@ -697,7 +697,7 @@ func rightPad(str string, length int, pad string) string {
|
|||||||
|
|
||||||
var arrowPositions = `>\|/`
|
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) {
|
func (t *tui) drawBar(length int, bytes, totalBytes int64, changeCount int) {
|
||||||
var completedLength int
|
var completedLength int
|
||||||
if totalBytes > 0 {
|
if totalBytes > 0 {
|
||||||
|
|||||||
+7
-3
@@ -49,6 +49,7 @@ func runTestFilterCmd(subcommand *cli.Subcommand, args []string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
conf := subcommand.Config()
|
conf := subcommand.Config()
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
var confFilter config.FilesystemsFilter
|
var confFilter config.FilesystemsFilter
|
||||||
job, err := conf.Job(testFilterArgs.job)
|
job, err := conf.Job(testFilterArgs.job)
|
||||||
@@ -60,6 +61,8 @@ func runTestFilterCmd(subcommand *cli.Subcommand, args []string) error {
|
|||||||
confFilter = j.Filesystems
|
confFilter = j.Filesystems
|
||||||
case *config.PushJob:
|
case *config.PushJob:
|
||||||
confFilter = j.Filesystems
|
confFilter = j.Filesystems
|
||||||
|
case *config.SnapJob:
|
||||||
|
confFilter = j.Filesystems
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("job type %T does not have filesystems filter", j)
|
return fmt.Errorf("job type %T does not have filesystems filter", j)
|
||||||
}
|
}
|
||||||
@@ -73,7 +76,7 @@ func runTestFilterCmd(subcommand *cli.Subcommand, args []string) error {
|
|||||||
if testFilterArgs.input != "" {
|
if testFilterArgs.input != "" {
|
||||||
fsnames = []string{testFilterArgs.input}
|
fsnames = []string{testFilterArgs.input}
|
||||||
} else {
|
} else {
|
||||||
out, err := zfs.ZFSList([]string{"name"})
|
out, err := zfs.ZFSList(ctx, []string{"name"})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("could not list ZFS filesystems: %s", err)
|
return fmt.Errorf("could not list ZFS filesystems: %s", err)
|
||||||
}
|
}
|
||||||
@@ -137,10 +140,11 @@ var testPlaceholder = &cli.Subcommand{
|
|||||||
func runTestPlaceholder(subcommand *cli.Subcommand, args []string) error {
|
func runTestPlaceholder(subcommand *cli.Subcommand, args []string) error {
|
||||||
|
|
||||||
var checkDPs []*zfs.DatasetPath
|
var checkDPs []*zfs.DatasetPath
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
// all actions first
|
// all actions first
|
||||||
if testPlaceholderArgs.all {
|
if testPlaceholderArgs.all {
|
||||||
out, err := zfs.ZFSList([]string{"name"})
|
out, err := zfs.ZFSList(ctx, []string{"name"})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "could not list ZFS filesystems")
|
return errors.Wrap(err, "could not list ZFS filesystems")
|
||||||
}
|
}
|
||||||
@@ -164,7 +168,7 @@ func runTestPlaceholder(subcommand *cli.Subcommand, args []string) error {
|
|||||||
|
|
||||||
fmt.Printf("IS_PLACEHOLDER\tDATASET\tzrepl:placeholder\n")
|
fmt.Printf("IS_PLACEHOLDER\tDATASET\tzrepl:placeholder\n")
|
||||||
for _, dp := range checkDPs {
|
for _, dp := range checkDPs {
|
||||||
ph, err := zfs.ZFSGetFilesystemPlaceholderState(dp)
|
ph, err := zfs.ZFSGetFilesystemPlaceholderState(ctx, dp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "cannot get placeholder state")
|
return errors.Wrap(err, "cannot get placeholder state")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -620,7 +620,7 @@ func ParseConfigBytes(bytes []byte) (*Config, error) {
|
|||||||
|
|
||||||
var durationStringRegex *regexp.Regexp = regexp.MustCompile(`^\s*(\d+)\s*(s|m|h|d|w)\s*$`)
|
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)
|
comps := durationStringRegex.FindStringSubmatch(e)
|
||||||
if len(comps) != 3 {
|
if len(comps) != 3 {
|
||||||
err = fmt.Errorf("does not match regex: %s %#v", e, comps)
|
err = fmt.Errorf("does not match regex: %s %#v", e, comps)
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ jobs:
|
|||||||
clients: {
|
clients: {
|
||||||
"10.0.0.1":"foo"
|
"10.0.0.1":"foo"
|
||||||
}
|
}
|
||||||
root_fs: zoot/foo
|
root_fs: zroot/foo
|
||||||
`
|
`
|
||||||
_, err := ParseConfigBytes([]byte(jobdef))
|
_, err := ParseConfigBytes([]byte(jobdef))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|||||||
@@ -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]
|
//nolint[:deadcode,unused]
|
||||||
func testValidConfigTemplate(t *testing.T, tmpl string, val string) *Config {
|
func testValidConfigTemplate(t *testing.T, tmpl string, val string) *Config {
|
||||||
tmp, err := template.New("master").Parse(tmpl)
|
tmp, err := template.New("master").Parse(tmpl)
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ func parseRetentionGridIntervalString(e string) (intervals []RetentionInterval,
|
|||||||
return nil, fmt.Errorf("contains factor <= 0")
|
return nil, fmt.Errorf("contains factor <= 0")
|
||||||
}
|
}
|
||||||
|
|
||||||
duration, err := parsePostitiveDuration(comps[2])
|
duration, err := parsePositiveDuration(comps[2])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,11 @@ jobs:
|
|||||||
type: tcp
|
type: tcp
|
||||||
listen: "0.0.0.0:8888"
|
listen: "0.0.0.0:8888"
|
||||||
clients: {
|
clients: {
|
||||||
"192.168.122.123" : "client1"
|
"192.168.122.123" : "mysql01",
|
||||||
|
"192.168.122.42" : "mx01",
|
||||||
|
"2001:0db8:85a3::8a2e:0370:7334": "gateway",
|
||||||
|
"10.23.42.0/24": "cluster-*",
|
||||||
|
"fde4:8dba:82e1::/64": "san-*",
|
||||||
}
|
}
|
||||||
filesystems: {
|
filesystems: {
|
||||||
"<": true,
|
"<": true,
|
||||||
|
|||||||
+6
-3
@@ -20,6 +20,7 @@ import (
|
|||||||
"github.com/zrepl/zrepl/util/envconst"
|
"github.com/zrepl/zrepl/util/envconst"
|
||||||
"github.com/zrepl/zrepl/version"
|
"github.com/zrepl/zrepl/version"
|
||||||
"github.com/zrepl/zrepl/zfs"
|
"github.com/zrepl/zrepl/zfs"
|
||||||
|
"github.com/zrepl/zrepl/zfs/zfscmd"
|
||||||
)
|
)
|
||||||
|
|
||||||
type controlJob struct {
|
type controlJob struct {
|
||||||
@@ -64,7 +65,7 @@ func (j *controlJob) RegisterMetrics(registerer prometheus.Registerer) {
|
|||||||
Namespace: "zrepl",
|
Namespace: "zrepl",
|
||||||
Subsystem: "control",
|
Subsystem: "control",
|
||||||
Name: "request_finished",
|
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},
|
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"})
|
}, []string{"endpoint"})
|
||||||
registerer.MustRegister(promControl.requestBegin)
|
registerer.MustRegister(promControl.requestBegin)
|
||||||
@@ -117,7 +118,9 @@ func (j *controlJob) Run(ctx context.Context) {
|
|||||||
mux.Handle(ControlJobEndpointStatus,
|
mux.Handle(ControlJobEndpointStatus,
|
||||||
// don't log requests to status endpoint, too spammy
|
// don't log requests to status endpoint, too spammy
|
||||||
jsonResponder{log, func() (interface{}, error) {
|
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
|
return s, nil
|
||||||
}})
|
}})
|
||||||
|
|
||||||
@@ -250,7 +253,7 @@ func (j jsonRequestResponder) ServeHTTP(w http.ResponseWriter, r *http.Request)
|
|||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
encodeErr := json.NewEncoder(&buf).Encode(res)
|
encodeErr := json.NewEncoder(&buf).Encode(res)
|
||||||
if encodeErr != nil {
|
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)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
_, err := io.WriteString(w, encodeErr.Error())
|
_, err := io.WriteString(w, encodeErr.Error())
|
||||||
logIoErr(err)
|
logIoErr(err)
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import (
|
|||||||
"github.com/zrepl/zrepl/daemon/logging"
|
"github.com/zrepl/zrepl/daemon/logging"
|
||||||
"github.com/zrepl/zrepl/logger"
|
"github.com/zrepl/zrepl/logger"
|
||||||
"github.com/zrepl/zrepl/version"
|
"github.com/zrepl/zrepl/version"
|
||||||
|
"github.com/zrepl/zrepl/zfs/zfscmd"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Run(conf *config.Config) error {
|
func Run(conf *config.Config) error {
|
||||||
@@ -81,6 +82,9 @@ func Run(conf *config.Config) error {
|
|||||||
jobs.start(ctx, job, true)
|
jobs.start(ctx, job, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// register global (=non job-local) metrics
|
||||||
|
zfscmd.RegisterMetrics(prometheus.DefaultRegisterer)
|
||||||
|
|
||||||
log.Info("starting daemon")
|
log.Info("starting daemon")
|
||||||
|
|
||||||
// start regular jobs
|
// start regular jobs
|
||||||
@@ -128,6 +132,15 @@ func (s *jobs) wait() <-chan struct{} {
|
|||||||
return ch
|
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 {
|
func (s *jobs) status() map[string]*job.Status {
|
||||||
s.m.RLock()
|
s.m.RLock()
|
||||||
defer s.m.RUnlock()
|
defer s.m.RUnlock()
|
||||||
@@ -207,6 +220,7 @@ func (s *jobs) start(ctx context.Context, j job.Job, internal bool) {
|
|||||||
|
|
||||||
s.jobs[jobName] = j
|
s.jobs[jobName] = j
|
||||||
ctx = job.WithLogger(ctx, jobLog)
|
ctx = job.WithLogger(ctx, jobLog)
|
||||||
|
ctx = zfscmd.WithJobID(ctx, j.Name())
|
||||||
ctx, wakeup := wakeup.Context(ctx)
|
ctx, wakeup := wakeup.Context(ctx)
|
||||||
ctx, resetFunc := reset.Context(ctx)
|
ctx, resetFunc := reset.Context(ctx)
|
||||||
s.wakeups[jobName] = wakeup
|
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
|
// 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) {
|
func (m DatasetMapFilter) InvertedFilter() (inv *DatasetMapFilter, err error) {
|
||||||
|
|
||||||
if m.filterMode {
|
if m.filterMode {
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -147,7 +147,7 @@ func modePushFromConfig(g *config.Global, in *config.PushJob, jobID endpoint.Job
|
|||||||
|
|
||||||
fsf, err := filters.DatasetMapFilterFromConfig(in.Filesystems)
|
fsf, err := filters.DatasetMapFilterFromConfig(in.Filesystems)
|
||||||
if err != nil {
|
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{
|
m.senderConfig = &endpoint.SenderConfig{
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ func modeSourceFromConfig(g *config.Global, in *config.SourceJob, jobID endpoint
|
|||||||
m = &modeSource{}
|
m = &modeSource{}
|
||||||
fsf, err := filters.DatasetMapFilterFromConfig(in.Filesystems)
|
fsf, err := filters.DatasetMapFilterFromConfig(in.Filesystems)
|
||||||
if err != nil {
|
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{
|
m.senderConfig = &endpoint.SenderConfig{
|
||||||
FSF: fsf,
|
FSF: fsf,
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import (
|
|||||||
"github.com/zrepl/zrepl/rpc/transportmux"
|
"github.com/zrepl/zrepl/rpc/transportmux"
|
||||||
"github.com/zrepl/zrepl/tlsconf"
|
"github.com/zrepl/zrepl/tlsconf"
|
||||||
"github.com/zrepl/zrepl/transport"
|
"github.com/zrepl/zrepl/transport"
|
||||||
|
"github.com/zrepl/zrepl/zfs/zfscmd"
|
||||||
)
|
)
|
||||||
|
|
||||||
func OutletsFromConfig(in config.LoggingOutletEnumList) (*logger.Outlets, error) {
|
func OutletsFromConfig(in config.LoggingOutletEnumList) (*logger.Outlets, error) {
|
||||||
@@ -70,7 +71,7 @@ type Subsystem string
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
SubsysReplication Subsystem = "repl"
|
SubsysReplication Subsystem = "repl"
|
||||||
SubsyEndpoint Subsystem = "endpoint"
|
SubsysEndpoint Subsystem = "endpoint"
|
||||||
SubsysPruning Subsystem = "pruning"
|
SubsysPruning Subsystem = "pruning"
|
||||||
SubsysSnapshot Subsystem = "snapshot"
|
SubsysSnapshot Subsystem = "snapshot"
|
||||||
SubsysHooks Subsystem = "hook"
|
SubsysHooks Subsystem = "hook"
|
||||||
@@ -79,17 +80,19 @@ const (
|
|||||||
SubsysRPC Subsystem = "rpc"
|
SubsysRPC Subsystem = "rpc"
|
||||||
SubsysRPCControl Subsystem = "rpc.ctrl"
|
SubsysRPCControl Subsystem = "rpc.ctrl"
|
||||||
SubsysRPCData Subsystem = "rpc.data"
|
SubsysRPCData Subsystem = "rpc.data"
|
||||||
|
SubsysZFSCmd Subsystem = "zfs.cmd"
|
||||||
)
|
)
|
||||||
|
|
||||||
func WithSubsystemLoggers(ctx context.Context, log logger.Logger) context.Context {
|
func WithSubsystemLoggers(ctx context.Context, log logger.Logger) context.Context {
|
||||||
ctx = logic.WithLogger(ctx, log.WithField(SubsysField, SubsysReplication))
|
ctx = logic.WithLogger(ctx, log.WithField(SubsysField, SubsysReplication))
|
||||||
ctx = driver.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 = pruner.WithLogger(ctx, log.WithField(SubsysField, SubsysPruning))
|
||||||
ctx = snapper.WithLogger(ctx, log.WithField(SubsysField, SubsysSnapshot))
|
ctx = snapper.WithLogger(ctx, log.WithField(SubsysField, SubsysSnapshot))
|
||||||
ctx = hooks.WithLogger(ctx, log.WithField(SubsysField, SubsysHooks))
|
ctx = hooks.WithLogger(ctx, log.WithField(SubsysField, SubsysHooks))
|
||||||
ctx = transport.WithLogger(ctx, log.WithField(SubsysField, SubsysTransport))
|
ctx = transport.WithLogger(ctx, log.WithField(SubsysField, SubsysTransport))
|
||||||
ctx = transportmux.WithLogger(ctx, log.WithField(SubsysField, SubsysTransportMux))
|
ctx = transportmux.WithLogger(ctx, log.WithField(SubsysField, SubsysTransportMux))
|
||||||
|
ctx = zfscmd.WithLogger(ctx, log.WithField(SubsysField, SubsysZFSCmd))
|
||||||
ctx = rpc.WithLoggers(ctx,
|
ctx = rpc.WithLoggers(ctx,
|
||||||
rpc.Loggers{
|
rpc.Loggers{
|
||||||
General: log.WithField(SubsysField, SubsysRPC),
|
General: log.WithField(SubsysField, SubsysRPC),
|
||||||
|
|||||||
@@ -211,7 +211,7 @@ func logfmtTryEncodeKeyval(enc *logfmt.Encoder, field, value interface{}) error
|
|||||||
case logfmt.ErrUnsupportedValueType:
|
case logfmt.ErrUnsupportedValueType:
|
||||||
err := enc.EncodeKeyval(field, fmt.Sprintf("<%T>", value))
|
err := enc.EncodeKeyval(field, fmt.Sprintf("<%T>", value))
|
||||||
if err != nil {
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ func NewTCPOutlet(formatter EntryFormatter, network, address string, tlsConfig *
|
|||||||
return
|
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{
|
o := &TCPOutlet{
|
||||||
formatter: formatter,
|
formatter: formatter,
|
||||||
|
|||||||
@@ -18,13 +18,13 @@ import (
|
|||||||
"github.com/zrepl/zrepl/util/envconst"
|
"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 {
|
type History interface {
|
||||||
ReplicationCursor(ctx context.Context, req *pdu.ReplicationCursorReq) (*pdu.ReplicationCursorRes, error)
|
ReplicationCursor(ctx context.Context, req *pdu.ReplicationCursorReq) (*pdu.ReplicationCursorRes, error)
|
||||||
ListFilesystems(ctx context.Context, req *pdu.ListFilesystemReq) (*pdu.ListFilesystemRes, 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 {
|
type Target interface {
|
||||||
ListFilesystems(ctx context.Context, req *pdu.ListFilesystemReq) (*pdu.ListFilesystemRes, error)
|
ListFilesystems(ctx context.Context, req *pdu.ListFilesystemReq) (*pdu.ListFilesystemRes, error)
|
||||||
ListFilesystemVersions(ctx context.Context, req *pdu.ListFilesystemVersionsReq) (*pdu.ListFilesystemVersionsRes, error)
|
ListFilesystemVersions(ctx context.Context, req *pdu.ListFilesystemVersionsReq) (*pdu.ListFilesystemVersionsRes, error)
|
||||||
|
|||||||
@@ -277,7 +277,7 @@ func snapshot(a args, u updater) state {
|
|||||||
|
|
||||||
jobCallback := hooks.NewCallbackHookForFilesystem("snapshot", fs, func(_ context.Context) (err error) {
|
jobCallback := hooks.NewCallbackHookForFilesystem("snapshot", fs, func(_ context.Context) (err error) {
|
||||||
l.Debug("create snapshot")
|
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 {
|
if err != nil {
|
||||||
l.WithError(err).Error("cannot create snapshot")
|
l.WithError(err).Error("cannot create snapshot")
|
||||||
}
|
}
|
||||||
@@ -485,7 +485,10 @@ 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) {
|
func findSyncPointFSNextOptimalSnapshotTime(l Logger, now time.Time, interval time.Duration, prefix string, d *zfs.DatasetPath) (time.Time, error) {
|
||||||
|
|
||||||
fsvs, err := zfs.ZFSListFilesystemVersions(d, filters.NewTypedPrefixFilter(prefix, zfs.Snapshot))
|
fsvs, err := zfs.ZFSListFilesystemVersions(context.TODO(), d, zfs.ListFilesystemVersionsOptions{
|
||||||
|
Types: zfs.Snapshots,
|
||||||
|
ShortnamePrefix: prefix,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return time.Time{}, errors.Wrap(err, "list filesystem versions")
|
return time.Time{}, errors.Wrap(err, "list filesystem versions")
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-1
@@ -62,7 +62,9 @@ Actual changelog:
|
|||||||
* |bugfix| |docs| snapshotting: clarify sync-up behavior and warn about filesystems
|
* |bugfix| |docs| snapshotting: clarify sync-up behavior and warn about filesystems
|
||||||
that will not be snapshotted until the sync-up phase is over
|
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`.
|
* |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]** 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
|
0.2.1
|
||||||
-----
|
-----
|
||||||
@@ -105,7 +107,7 @@ Actual changelog:
|
|||||||
| You can support maintenance and feature development through one of the following services:
|
| You can support maintenance and feature development through one of the following services:
|
||||||
| |Donate via Patreon| |Donate via Liberapay| |Donate via PayPal|
|
| |Donate via Patreon| |Donate via Liberapay| |Donate via PayPal|
|
||||||
| Note that PayPal processing fees are relatively high for small donations.
|
| 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
|
0.1.1
|
||||||
|
|||||||
@@ -202,7 +202,7 @@ The latter is particularly useful in combination with log aggregation services.
|
|||||||
.. WARNING::
|
.. WARNING::
|
||||||
|
|
||||||
zrepl drops log messages to the TCP outlet if the underlying connection is not fast enough.
|
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
|
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>` ).
|
with the TCP outlet (see :ref:`above <logging-error-outlet>` ).
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ Prometheus & Grafana
|
|||||||
zrepl can expose `Prometheus metrics <https://prometheus.io/docs/instrumenting/exposition_formats/>`_ via HTTP.
|
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 ``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 ``listen_freebind`` attribute is :ref:`explained here <listen-freebind-explanation>`.
|
The ``listen_freebind`` attribute is :ref:`explained here <listen-freebind-explanation>`.
|
||||||
The Prometheues monitoring job appears in the ``zrepl control`` job list and may be specified **at most once**.
|
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:
|
zrepl also ships with an importable `Grafana <https://grafana.com>`_ dashboard that consumes the Prometheus metrics:
|
||||||
see :repomasterlink:`dist/grafana`.
|
see :repomasterlink:`dist/grafana`.
|
||||||
|
|||||||
@@ -26,9 +26,9 @@ Config File Structure
|
|||||||
type: push
|
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 ``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:
|
.. _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**.
|
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 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**.
|
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.
|
Note that snapshot-creation denoted by "(snap)" is orthogonal to whether a job is active or passive.
|
||||||
@@ -120,7 +120,7 @@ The following steps take place during replication and can be monitored using the
|
|||||||
* Perform replication steps in the following order:
|
* 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.
|
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}``.
|
* Create placeholder filesystems on the receiving side to mirror the dataset paths on the sender to ``root_fs/${client_identity}``.
|
||||||
* Aquire send-side step-holds on the step's `from` and `to` snapshots.
|
* Acquire send-side *step-holds* on the step's `from` and `to` snapshots.
|
||||||
* Perform the replication step.
|
* Perform the replication step.
|
||||||
* Move the **replication cursor** bookmark on the sending side (see below).
|
* Move the **replication cursor** bookmark on the sending side (see below).
|
||||||
* Move the **last-received-hold** on the receiving side (see below).
|
* Move the **last-received-hold** on the receiving side (see below).
|
||||||
@@ -141,7 +141,7 @@ The ``zrepl holds list`` provides a listing of all bookmarks and holds managed b
|
|||||||
.. _replication-placeholder-property:
|
.. _replication-placeholder-property:
|
||||||
|
|
||||||
**Placeholder filesystems** on the receiving side are regular ZFS filesystems with the placeholder property ``zrepl:placeholder=on``.
|
**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.
|
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``.
|
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.
|
Thus, zrepl creates the parent filesystems as placeholders on the receiving side.
|
||||||
@@ -181,7 +181,7 @@ No Overlapping
|
|||||||
|
|
||||||
Jobs run independently of each other.
|
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.
|
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.
|
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.
|
However, the next replication attempt will re-examine the situation from scratch and should work.
|
||||||
|
|
||||||
N push jobs to 1 sink
|
N push jobs to 1 sink
|
||||||
@@ -198,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.
|
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.
|
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.
|
||||||
|
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ Policy ``regex``
|
|||||||
negate: true
|
negate: true
|
||||||
regex: "^zrepl_.*"
|
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>`_).
|
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.
|
The optional `negate` boolean field inverts the semantics: Use it if you want to keep all snapshots that *do not* match the given regex.
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ Send Options
|
|||||||
---------------------
|
---------------------
|
||||||
|
|
||||||
The ``encryption`` variable controls whether the matched filesystems are sent as `OpenZFS native encryption <http://open-zfs.org/wiki/ZFS-Native_Encryption>`_ raw sends.
|
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 specificially, if ``encryption=true``, zrepl
|
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
|
* 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
|
* invokes the ``zfs send`` subcommand with the ``-w`` option (raw sends) and
|
||||||
|
|||||||
@@ -52,8 +52,15 @@ Serve
|
|||||||
listen: ":8888"
|
listen: ":8888"
|
||||||
listen_freebind: true # optional, default false
|
listen_freebind: true # optional, default false
|
||||||
clients: {
|
clients: {
|
||||||
"192.168.122.123" : "mysql01"
|
"192.168.122.123" : "mysql01",
|
||||||
"192.168.122.123" : "mx01"
|
"192.168.122.42" : "mx01",
|
||||||
|
"2001:0db8:85a3::8a2e:0370:7334": "gateway",
|
||||||
|
|
||||||
|
# CIDR masks require a '*' in the client identity string
|
||||||
|
# that is expanded to the client's IP address
|
||||||
|
|
||||||
|
"10.23.42.0/24": "cluster-*"
|
||||||
|
"fde4:8dba:82e1::/64": "san-*"
|
||||||
}
|
}
|
||||||
...
|
...
|
||||||
|
|
||||||
|
|||||||
+10
-129
@@ -1,138 +1,19 @@
|
|||||||
.. _binary releases: https://github.com/zrepl/zrepl/releases
|
|
||||||
|
|
||||||
.. _installation:
|
.. _installation_toc:
|
||||||
|
|
||||||
|
************
|
||||||
Installation
|
Installation
|
||||||
============
|
************
|
||||||
|
|
||||||
.. TIP::
|
.. TIP::
|
||||||
|
|
||||||
Note: check out the :ref:`tutorial` if you want a first impression of zrepl.
|
Note: check out the :ref:`tutorial` if you want a first impression of zrepl.
|
||||||
|
|
||||||
User Privileges
|
.. toctree::
|
||||||
---------------
|
|
||||||
|
|
||||||
It is possible to run zrepl as an unprivileged user in combination with
|
installation/user-privileges
|
||||||
`ZFS delegation <https://www.freebsd.org/doc/handbook/zfs-zfs-allow.html>`_.
|
installation/packages
|
||||||
Also, there is the possibility to run it in a jail on FreeBSD by delegating a dataset to the jail.
|
installation/apt-repos
|
||||||
However, until we get around documenting those setups, you will have to run zrepl as root or experiment yourself :)
|
installation/compile-from-source
|
||||||
|
installation/freebsd-jail-with-iocage
|
||||||
Packages
|
installation/what-next
|
||||||
--------
|
|
||||||
|
|
||||||
zrepl source releases are signed & tagged by the author in the git repository.
|
|
||||||
Your OS vendor may provide binary packages of zrepl through the package manager.
|
|
||||||
Additionally, `binary releases`_ are provided on GitHub.
|
|
||||||
The following list may be incomplete, feel free to submit a PR with an update:
|
|
||||||
|
|
||||||
.. list-table::
|
|
||||||
:header-rows: 1
|
|
||||||
|
|
||||||
* - OS / Distro
|
|
||||||
- Install Command
|
|
||||||
- Link
|
|
||||||
* - FreeBSD
|
|
||||||
- ``pkg install zrepl``
|
|
||||||
- `<https://www.freshports.org/sysutils/zrepl/>`_
|
|
||||||
* - MacOS
|
|
||||||
- ``brew install zrepl``
|
|
||||||
- Available on `homebrew <https://brew.sh>`_
|
|
||||||
* - Arch Linux
|
|
||||||
- ``yay install zrepl``
|
|
||||||
- Available on `AUR <https://aur.archlinux.org/packages/zrepl>`_
|
|
||||||
* - Fedora
|
|
||||||
- ``dnf install zrepl``
|
|
||||||
- Available on `COPR <https://copr.fedorainfracloud.org/coprs/poettlerric/zrepl/>`_
|
|
||||||
* - CentOS/RHEL
|
|
||||||
- ``yum install zrepl``
|
|
||||||
- Available on `COPR <https://copr.fedorainfracloud.org/coprs/poettlerric/zrepl/>`_
|
|
||||||
* - Debian + Ubuntu
|
|
||||||
- ``apt install zrepl``
|
|
||||||
- APT repository config :ref:`see below <installation-apt-repos>`
|
|
||||||
* - OmniOS
|
|
||||||
- ``pkg install zrepl``
|
|
||||||
- Available since `r151030 <https://pkg.omniosce.org/r151030/extra/en/search.shtml?token=zrepl&action=Search>`_
|
|
||||||
* - Void Linux
|
|
||||||
- ``xbps-install zrepl``
|
|
||||||
- Available since `a88a2a4 <https://github.com/void-linux/void-packages/commit/a88a2a4d7bf56072dadf61ab56b8424e39155890>`_
|
|
||||||
* - Others
|
|
||||||
-
|
|
||||||
- Use `binary releases`_ or build from source.
|
|
||||||
|
|
||||||
.. _installation-apt-repos:
|
|
||||||
|
|
||||||
Debian / Ubuntu APT repositories
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
We maintain APT repositories for Debian, Ubuntu and derivatives.
|
|
||||||
The fingerprint of the signing key is ``E101 418F D3D6 FBCB 9D65 A62D 7086 99FC 5F2E BF16``.
|
|
||||||
It is available at `<https://zrepl.cschwarz.com/apt/apt-key.asc>`_ .
|
|
||||||
Please open an issue `in the packaging repository <https://github.com/zrepl/debian-binary-packaging>`_ if you encounter any issues with the repository.
|
|
||||||
|
|
||||||
The following snippet configure the repository for your Debian or Ubuntu release:
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
apt update && apt install curl gnupg lsb-release; \
|
|
||||||
ARCH="$(dpkg --print-architecture)"; \
|
|
||||||
CODENAME="$(lsb_release -i -s | tr '[:upper:]' '[:lower:]') $(lsb_release -c -s | tr '[:upper:]' '[:lower:]')"; \
|
|
||||||
echo "Using Distro and Codename: $CODENAME"; \
|
|
||||||
(curl https://zrepl.cschwarz.com/apt/apt-key.asc | apt-key add -) && \
|
|
||||||
(echo "deb [arch=$ARCH] https://zrepl.cschwarz.com/apt/$CODENAME main" > /etc/apt/sources.list.d/zrepl.list) && \
|
|
||||||
apt update
|
|
||||||
|
|
||||||
|
|
||||||
.. NOTE::
|
|
||||||
|
|
||||||
Until zrepl reaches 1.0, all APT repositories will be updated to the latest zrepl release immediately.
|
|
||||||
This includes breaking changes between zrepl versions.
|
|
||||||
Use ``apt-mark hold zrepl`` to prevent upgrades of zrepl.
|
|
||||||
|
|
||||||
Compile From Source
|
|
||||||
~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
Producing a release requires **Go 1.11** or newer and **Python 3** + **pip3** + ``docs/requirements.txt`` for the Sphinx documentation.
|
|
||||||
A tutorial to install Go is available over at `golang.org <https://golang.org/doc/install>`_.
|
|
||||||
Python and pip3 should probably be installed via your distro's package manager.
|
|
||||||
|
|
||||||
Alternatively, you can use the Docker build process:
|
|
||||||
it is used to produce the official zrepl `binary releases`_
|
|
||||||
and serves as a reference for build dependencies and procedure:
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
git clone https://github.com/zrepl/zrepl.git
|
|
||||||
cd zrepl
|
|
||||||
sudo docker build -t zrepl_build -f build.Dockerfile .
|
|
||||||
sudo docker run -it --rm \
|
|
||||||
-v "${PWD}:/src" \
|
|
||||||
--user "$(id -u):$(id -g)" \
|
|
||||||
zrepl_build make release
|
|
||||||
|
|
||||||
Alternatively, you can install build dependencies on your local system and then build in your ``$GOPATH``:
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
mkdir -p "${GOPATH}/src/github.com/zrepl/zrepl"
|
|
||||||
git clone https://github.com/zrepl/zrepl.git "${GOPATH}/src/github.com/zrepl/zrepl"
|
|
||||||
cd "${GOPATH}/src/github.com/zrepl/zrepl"
|
|
||||||
python3 -m venv3
|
|
||||||
source venv3/bin/activate
|
|
||||||
./lazy.sh devsetup
|
|
||||||
make release
|
|
||||||
|
|
||||||
The Python venv is used for the documentation build dependencies.
|
|
||||||
If you just want to build the zrepl binary, leave it out and use `./lazy.sh godep` instead.
|
|
||||||
Either way, all build results are located in the ``artifacts/`` directory.
|
|
||||||
|
|
||||||
.. NOTE::
|
|
||||||
|
|
||||||
It is your job to install the apropriate binary in the zrepl users's ``$PATH``, e.g. ``/usr/local/bin/zrepl``.
|
|
||||||
Otherwise, the examples in the :ref:`tutorial` may need to be adjusted.
|
|
||||||
|
|
||||||
What next?
|
|
||||||
----------
|
|
||||||
|
|
||||||
Read the :ref:`configuration chapter<configuration_toc>` and then continue with the :ref:`usage chapter<usage>`.
|
|
||||||
|
|
||||||
**Reminder**: If you want a quick introduction, please read the :ref:`tutorial`.
|
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
|
||||||
|
.. _installation-apt-repos:
|
||||||
|
|
||||||
|
Debian / Ubuntu APT repositories
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
We maintain APT repositories for Debian, Ubuntu and derivatives.
|
||||||
|
The fingerprint of the signing key is ``E101 418F D3D6 FBCB 9D65 A62D 7086 99FC 5F2E BF16``.
|
||||||
|
It is available at `<https://zrepl.cschwarz.com/apt/apt-key.asc>`_ .
|
||||||
|
Please open an issue `in the packaging repository <https://github.com/zrepl/debian-binary-packaging>`_ if you encounter any issues with the repository.
|
||||||
|
|
||||||
|
The following snippet configure the repository for your Debian or Ubuntu release:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
apt update && apt install curl gnupg lsb-release; \
|
||||||
|
ARCH="$(dpkg --print-architecture)"; \
|
||||||
|
CODENAME="$(lsb_release -i -s | tr '[:upper:]' '[:lower:]') $(lsb_release -c -s | tr '[:upper:]' '[:lower:]')"; \
|
||||||
|
echo "Using Distro and Codename: $CODENAME"; \
|
||||||
|
(curl https://zrepl.cschwarz.com/apt/apt-key.asc | apt-key add -) && \
|
||||||
|
(echo "deb [arch=$ARCH] https://zrepl.cschwarz.com/apt/$CODENAME main" > /etc/apt/sources.list.d/zrepl.list) && \
|
||||||
|
apt update
|
||||||
|
|
||||||
|
|
||||||
|
.. NOTE::
|
||||||
|
|
||||||
|
Until zrepl reaches 1.0, all APT repositories will be updated to the latest zrepl release immediately.
|
||||||
|
This includes breaking changes between zrepl versions.
|
||||||
|
Use ``apt-mark hold zrepl`` to prevent upgrades of zrepl.
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
.. _binary releases: https://github.com/zrepl/zrepl/releases
|
||||||
|
|
||||||
|
.. _installation-compile-from-source:
|
||||||
|
|
||||||
|
Compile From Source
|
||||||
|
~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Producing a release requires **Go 1.11** or newer and **Python 3** + **pip3** + ``docs/requirements.txt`` for the Sphinx documentation.
|
||||||
|
A tutorial to install Go is available over at `golang.org <https://golang.org/doc/install>`_.
|
||||||
|
Python and pip3 should probably be installed via your distro's package manager.
|
||||||
|
|
||||||
|
Alternatively, you can use the Docker build process:
|
||||||
|
it is used to produce the official zrepl `binary releases`_
|
||||||
|
and serves as a reference for build dependencies and procedure:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
git clone https://github.com/zrepl/zrepl.git && \
|
||||||
|
cd zrepl && \
|
||||||
|
sudo docker build -t zrepl_build -f build.Dockerfile . && \
|
||||||
|
sudo docker run -it --rm \
|
||||||
|
-v "${PWD}:/src" \
|
||||||
|
--user "$(id -u):$(id -g)" \
|
||||||
|
zrepl_build make release
|
||||||
|
|
||||||
|
Alternatively, you can install build dependencies on your local system and then build in your ``$GOPATH``:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
mkdir -p "${GOPATH}/src/github.com/zrepl/zrepl"
|
||||||
|
git clone https://github.com/zrepl/zrepl.git "${GOPATH}/src/github.com/zrepl/zrepl"
|
||||||
|
cd "${GOPATH}/src/github.com/zrepl/zrepl"
|
||||||
|
python3 -m venv3
|
||||||
|
source venv3/bin/activate
|
||||||
|
./lazy.sh devsetup
|
||||||
|
make release
|
||||||
|
|
||||||
|
The Python venv is used for the documentation build dependencies.
|
||||||
|
If you just want to build the zrepl binary, leave it out and use `./lazy.sh godep` instead.
|
||||||
|
Either way, all build results are located in the ``artifacts/`` directory.
|
||||||
|
|
||||||
|
.. NOTE::
|
||||||
|
|
||||||
|
It is your job to install the appropriate binary in the zrepl users's ``$PATH``, e.g. ``/usr/local/bin/zrepl``.
|
||||||
|
Otherwise, the examples in the :ref:`tutorial` may need to be adjusted.
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
.. include:: ../global.rst.inc
|
||||||
|
|
||||||
|
.. _installation-freebsd-jail-with-iocage:
|
||||||
|
|
||||||
|
FreeBSD Jail With iocage
|
||||||
|
========================
|
||||||
|
|
||||||
|
|
||||||
|
This tutorial shows how zrepl can be installed on FreeBSD, or FreeNAS in a jail using iocage.
|
||||||
|
While this tutorial focuses on using iocage, much of the setup would be similar
|
||||||
|
using a different jail manager.
|
||||||
|
|
||||||
|
.. NOTE::
|
||||||
|
|
||||||
|
From a security perspective, just keep in mind that ``zfs send``/``recv`` was never designed with
|
||||||
|
jails in mind, an attacker could probably crash the receive-side kernel or worse induce stateful
|
||||||
|
damage to the receive-side pool if they were able to get access to the jail.
|
||||||
|
|
||||||
|
The jail doesn't provide security benefits, but only management ones.
|
||||||
|
|
||||||
|
Requirements
|
||||||
|
------------
|
||||||
|
|
||||||
|
A dataset that will be delegated to the jail needs to be created if one does not already exist.
|
||||||
|
For the tutorial ``tank/zrepl`` will be used.
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
zfs create -o mountpoint=none tank/zrepl
|
||||||
|
|
||||||
|
The only software requirements on the host system are ``iocage``, which can be installed
|
||||||
|
from ports or packages.
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
pkg install py37-iocage
|
||||||
|
|
||||||
|
.. NOTE::
|
||||||
|
|
||||||
|
By default ``iocage`` will "activate" on first use which will set up some defaults such as
|
||||||
|
which pool will be used. To activate ``iocage`` manually the ``iocage activate`` command can be used.
|
||||||
|
|
||||||
|
Jail Creation
|
||||||
|
-------------
|
||||||
|
|
||||||
|
There are two options for jail creation using FreeBSD.
|
||||||
|
|
||||||
|
1. Manually set up the jail from scratch
|
||||||
|
2. Create the jail using the ``zrepl`` plugin. On FreeNAS this is possible from the user interface using the community index.
|
||||||
|
|
||||||
|
Manual Jail
|
||||||
|
###########
|
||||||
|
|
||||||
|
Create a jail, using the same release as the host, called ``zrepl`` that will be automatically started at boot.
|
||||||
|
The jail will have ``tank/zrepl`` delegated into it.
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
iocage create --release "$(freebsd-version -k | cut -d '-' -f '1,2')" --name zrepl \
|
||||||
|
boot=on nat=1 \
|
||||||
|
jail_zfs=on \
|
||||||
|
jail_zfs_dataset=zrepl \
|
||||||
|
jail_zfs_mountpoint='none'
|
||||||
|
|
||||||
|
Enter the jail:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
iocage console zrepl
|
||||||
|
|
||||||
|
Install ``zrepl``
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
pkg update && pkg upgrade
|
||||||
|
pkg install zrepl
|
||||||
|
|
||||||
|
Create the log file ``/var/log/zrepl.log``
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
touch /var/log/zrepl.log && service newsyslog restart
|
||||||
|
|
||||||
|
Tell syslogd to redirect facility local0 to the ``zrepl.log`` file:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
service syslogd reload
|
||||||
|
|
||||||
|
Enable the zrepl daemon to start automatically at boot:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
sysrc zrepl_enable="YES"
|
||||||
|
|
||||||
|
|
||||||
|
Plugin
|
||||||
|
######
|
||||||
|
|
||||||
|
When using the plugin, ``zrepl`` will be installed for you in a jail using the following ``iocage`` properties.
|
||||||
|
|
||||||
|
* ``nat=1``
|
||||||
|
* ``jail_zfs=on``
|
||||||
|
* ``jail_zfs_mountpoint=none``
|
||||||
|
|
||||||
|
Additionally the delegated dataset should be specified upon creation, and optionally start on boot can be set.
|
||||||
|
This can also be done from the FreeNAS webui.
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
fetch https://raw.githubusercontent.com/ix-plugin-hub/iocage-plugin-index/master/zrepl.json -o /tmp/zrepl.json
|
||||||
|
iocage fetch -P /tmp/zrepl.json --name zrepl jail_zfs_dataset=zrepl boot=on
|
||||||
|
|
||||||
|
Configuration
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Now ``zrepl`` can be configured.
|
||||||
|
|
||||||
|
Enter the jail.
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
iocage console zrepl
|
||||||
|
|
||||||
|
Modify the ``/usr/local/etc/zrepl/zrepl.yml`` configuration file.
|
||||||
|
|
||||||
|
.. TIP::
|
||||||
|
|
||||||
|
Note: check out the :ref:`tutorial` for examples of a ``sink`` job.
|
||||||
|
|
||||||
|
Now ``zrepl`` can be started.
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
service zrepl start
|
||||||
|
|
||||||
|
Summary
|
||||||
|
-------
|
||||||
|
|
||||||
|
Congratulations, you have a working jail!
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
.. _installation-packages:
|
||||||
|
|
||||||
|
.. _binary releases: https://github.com/zrepl/zrepl/releases
|
||||||
|
|
||||||
|
Packages
|
||||||
|
--------
|
||||||
|
|
||||||
|
zrepl source releases are signed & tagged by the author in the git repository.
|
||||||
|
Your OS vendor may provide binary packages of zrepl through the package manager.
|
||||||
|
Additionally, `binary releases`_ are provided on GitHub.
|
||||||
|
The following list may be incomplete, feel free to submit a PR with an update:
|
||||||
|
|
||||||
|
.. list-table::
|
||||||
|
:header-rows: 1
|
||||||
|
|
||||||
|
* - OS / Distro
|
||||||
|
- Install Command
|
||||||
|
- Link
|
||||||
|
* - FreeBSD
|
||||||
|
- ``pkg install zrepl``
|
||||||
|
- `<https://www.freshports.org/sysutils/zrepl/>`_
|
||||||
|
|
||||||
|
:ref:`installation-freebsd-jail-with-iocage`
|
||||||
|
* - FreeNAS
|
||||||
|
-
|
||||||
|
- :ref:`installation-freebsd-jail-with-iocage`
|
||||||
|
* - MacOS
|
||||||
|
- ``brew install zrepl``
|
||||||
|
- Available on `homebrew <https://brew.sh>`_
|
||||||
|
* - Arch Linux
|
||||||
|
- ``yay install zrepl``
|
||||||
|
- Available on `AUR <https://aur.archlinux.org/packages/zrepl>`_
|
||||||
|
* - Fedora
|
||||||
|
- ``dnf install zrepl``
|
||||||
|
- Available on `COPR <https://copr.fedorainfracloud.org/coprs/poettlerric/zrepl/>`_
|
||||||
|
* - CentOS/RHEL
|
||||||
|
- ``yum install zrepl``
|
||||||
|
- Available on `COPR <https://copr.fedorainfracloud.org/coprs/poettlerric/zrepl/>`_
|
||||||
|
* - Debian + Ubuntu
|
||||||
|
- ``apt install zrepl``
|
||||||
|
- APT repository config :ref:`see below <installation-apt-repos>`
|
||||||
|
* - OmniOS
|
||||||
|
- ``pkg install zrepl``
|
||||||
|
- Available since `r151030 <https://pkg.omniosce.org/r151030/extra/en/search.shtml?token=zrepl&action=Search>`_
|
||||||
|
* - Void Linux
|
||||||
|
- ``xbps-install zrepl``
|
||||||
|
- Available since `a88a2a4 <https://github.com/void-linux/void-packages/commit/a88a2a4d7bf56072dadf61ab56b8424e39155890>`_
|
||||||
|
* - Others
|
||||||
|
-
|
||||||
|
- Use `binary releases`_ or build from source.
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
.. _installation-user-privileges:
|
||||||
|
|
||||||
|
User Privileges
|
||||||
|
---------------
|
||||||
|
|
||||||
|
It is possible to run zrepl as an unprivileged user in combination with
|
||||||
|
`ZFS delegation <https://www.freebsd.org/doc/handbook/zfs-zfs-allow.html>`_.
|
||||||
|
Also, there is the possibility to run it in a jail on FreeBSD by delegating a dataset to the jail.
|
||||||
|
|
||||||
|
.. TIP::
|
||||||
|
|
||||||
|
Note: check out the :ref:`installation-freebsd-jail-with-iocage` for FreeBSD jail setup instructions.
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
.. _installation-what-next:
|
||||||
|
|
||||||
|
What next?
|
||||||
|
----------
|
||||||
|
|
||||||
|
Read the :ref:`configuration chapter<configuration_toc>` and then continue with the :ref:`usage chapter<usage>`.
|
||||||
|
|
||||||
|
**Reminder**: If you want a quick introduction, please read the :ref:`tutorial`.
|
||||||
+1
-1
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
zrepl is a spare-time project primarily developed by `Christian Schwarz <https://cschwarz.com>`_.
|
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.
|
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!**
|
**Thanks for your support!**
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -47,7 +47,7 @@ We can model this situation as two jobs:
|
|||||||
Install zrepl
|
Install zrepl
|
||||||
-------------
|
-------------
|
||||||
|
|
||||||
Follow the :ref:`OS-specific installation instructions <installation>` and come back here.
|
Follow the :ref:`OS-specific installation instructions <installation_toc>` and come back here.
|
||||||
|
|
||||||
Generate TLS Certificates
|
Generate TLS Certificates
|
||||||
-------------------------
|
-------------------------
|
||||||
|
|||||||
+4
-2
@@ -38,6 +38,8 @@ CLI Overview
|
|||||||
* - ``zrepl migrate``
|
* - ``zrepl migrate``
|
||||||
- | perform on-disk state / ZFS property migrations
|
- | perform on-disk state / ZFS property migrations
|
||||||
| (see :ref:`changelog <changelog>` for details)
|
| (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:
|
.. _usage-zrepl-daemon:
|
||||||
|
|
||||||
@@ -48,7 +50,7 @@ zrepl daemon
|
|||||||
All actual work zrepl does is performed by a daemon process.
|
All actual work zrepl does is performed by a daemon process.
|
||||||
The daemon supports structured :ref:`logging <logging>` and provides :ref:`monitoring endpoints <monitoring>`.
|
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.
|
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``.
|
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
|
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.
|
Note that some of the options only work on recent versions of systemd.
|
||||||
Any help & improvements are very welcome, see :issue:`145`.
|
Any help & improvements are very welcome, see :issue:`145`.
|
||||||
+143
-55
@@ -96,7 +96,7 @@ func (s *Sender) ListFilesystemVersions(ctx context.Context, r *pdu.ListFilesyst
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
fsvs, err := zfs.ZFSListFilesystemVersions(lp, nil)
|
fsvs, err := zfs.ZFSListFilesystemVersions(ctx, lp, zfs.ListFilesystemVersionsOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -110,34 +110,116 @@ func (s *Sender) ListFilesystemVersions(ctx context.Context, r *pdu.ListFilesyst
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *Sender) HintMostRecentCommonAncestor(ctx context.Context, r *pdu.HintMostRecentCommonAncestorReq) (*pdu.HintMostRecentCommonAncestorRes, error) {
|
func (p *Sender) HintMostRecentCommonAncestor(ctx context.Context, r *pdu.HintMostRecentCommonAncestorReq) (*pdu.HintMostRecentCommonAncestorRes, error) {
|
||||||
var err error
|
|
||||||
|
|
||||||
fs := r.GetFilesystem()
|
fsp, err := p.filterCheckFS(r.GetFilesystem())
|
||||||
mostRecent, err := sendArgsFromPDUAndValidateExists(ctx, fs, r.GetSenderVersion())
|
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 {
|
if err != nil {
|
||||||
msg := "HintMostRecentCommonAncestor rpc with nonexistent most recent version"
|
msg := "HintMostRecentCommonAncestor rpc with nonexistent most recent version"
|
||||||
getLogger(ctx).WithField("fs", fs).WithField("hinted_most_recent", fmt.Sprintf("%#v", mostRecent)).
|
log.Warn(msg)
|
||||||
Warn(msg)
|
|
||||||
return nil, errors.Wrap(err, msg)
|
return nil, errors.Wrap(err, msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// move replication cursor to this position
|
// move replication cursor to this position
|
||||||
_, err = MoveReplicationCursor(ctx, fs, mostRecent, p.jobId)
|
destroyedCursors, err := MoveReplicationCursor(ctx, fs, mostRecentVersion, p.jobId)
|
||||||
if err == zfs.ErrBookmarkCloningNotSupported {
|
if err == zfs.ErrBookmarkCloningNotSupported {
|
||||||
getLogger(ctx).Debug("not creating replication cursor from bookmark because ZFS does not support it")
|
log.Debug("not creating replication cursor from bookmark because ZFS does not support it")
|
||||||
// fallthrough
|
// fallthrough
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return nil, errors.Wrap(err, "cannot set replication cursor to hinted version")
|
return nil, errors.Wrap(err, "cannot set replication cursor to hinted version")
|
||||||
}
|
}
|
||||||
|
|
||||||
// cleanup previous steps
|
// take care of stale step holds
|
||||||
if err := ReleaseStepAll(ctx, fs, mostRecent, p.jobId); err != nil {
|
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")
|
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
|
return &pdu.HintMostRecentCommonAncestorRes{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type HintMostRecentCommonAncestorStepCleanupMode struct{ string }
|
||||||
|
|
||||||
|
var (
|
||||||
|
StepCleanupRangeSinceReplicationCursor = HintMostRecentCommonAncestorStepCleanupMode{"range-since-replication-cursor"}
|
||||||
|
StepCleanupRangeSinceUnbounded = HintMostRecentCommonAncestorStepCleanupMode{"range-since-unbounded"}
|
||||||
|
StepCleanupNoCleanup = HintMostRecentCommonAncestorStepCleanupMode{"no-cleanup"}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (m HintMostRecentCommonAncestorStepCleanupMode) String() string { return string(m.string) }
|
||||||
|
func (m *HintMostRecentCommonAncestorStepCleanupMode) Set(s string) error {
|
||||||
|
switch s {
|
||||||
|
case StepCleanupRangeSinceReplicationCursor.String():
|
||||||
|
*m = StepCleanupRangeSinceReplicationCursor
|
||||||
|
case StepCleanupRangeSinceUnbounded.String():
|
||||||
|
*m = StepCleanupRangeSinceUnbounded
|
||||||
|
case StepCleanupNoCleanup.String():
|
||||||
|
*m = StepCleanupNoCleanup
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unknown step cleanup mode %q", s)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var senderHintMostRecentCommonAncestorStepCleanupMode = *envconst.Var("ZREPL_ENDPOINT_SENDER_HINT_MOST_RECENT_STEP_HOLD_CLEANUP_MODE", &StepCleanupRangeSinceReplicationCursor).(*HintMostRecentCommonAncestorStepCleanupMode)
|
||||||
|
|
||||||
var maxConcurrentZFSSendSemaphore = semaphore.New(envconst.Int64("ZREPL_ENDPOINT_MAX_CONCURRENT_SEND", 10))
|
var maxConcurrentZFSSendSemaphore = semaphore.New(envconst.Int64("ZREPL_ENDPOINT_MAX_CONCURRENT_SEND", 10))
|
||||||
|
|
||||||
func uncheckedSendArgsFromPDU(fsv *pdu.FilesystemVersion) *zfs.ZFSSendArgVersion {
|
func uncheckedSendArgsFromPDU(fsv *pdu.FilesystemVersion) *zfs.ZFSSendArgVersion {
|
||||||
@@ -147,15 +229,16 @@ func uncheckedSendArgsFromPDU(fsv *pdu.FilesystemVersion) *zfs.ZFSSendArgVersion
|
|||||||
return &zfs.ZFSSendArgVersion{RelName: fsv.GetRelName(), GUID: fsv.Guid}
|
return &zfs.ZFSSendArgVersion{RelName: fsv.GetRelName(), GUID: fsv.Guid}
|
||||||
}
|
}
|
||||||
|
|
||||||
func sendArgsFromPDUAndValidateExists(ctx context.Context, fs string, fsv *pdu.FilesystemVersion) (*zfs.ZFSSendArgVersion, error) {
|
func sendArgsFromPDUAndValidateExistsAndGetVersion(ctx context.Context, fs string, fsv *pdu.FilesystemVersion) (v zfs.FilesystemVersion, err error) {
|
||||||
v := uncheckedSendArgsFromPDU(fsv)
|
sendArgs := uncheckedSendArgsFromPDU(fsv)
|
||||||
if v == nil {
|
if sendArgs == nil {
|
||||||
return nil, errors.New("must not be nil")
|
return v, errors.New("must not be nil")
|
||||||
}
|
}
|
||||||
if err := v.ValidateExists(ctx, fs); err != nil {
|
version, err := sendArgs.ValidateExistsAndGetVersion(ctx, fs)
|
||||||
return nil, err
|
if err != nil {
|
||||||
|
return v, err
|
||||||
}
|
}
|
||||||
return v, nil
|
return version, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Sender) Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, zfs.StreamCopier, error) {
|
func (s *Sender) Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, zfs.StreamCopier, error) {
|
||||||
@@ -170,7 +253,7 @@ func (s *Sender) Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, zfs.St
|
|||||||
// ok, fallthrough outer
|
// ok, fallthrough outer
|
||||||
case pdu.Tri_False:
|
case pdu.Tri_False:
|
||||||
if s.encrypt.B {
|
if s.encrypt.B {
|
||||||
return nil, nil, errors.New("only encrytped sends allowed (send -w + encryption!= off), but unencrytped send requested")
|
return nil, nil, errors.New("only encrypted sends allowed (send -w + encryption!= off), but unencrypted send requested")
|
||||||
}
|
}
|
||||||
// fallthrough outer
|
// fallthrough outer
|
||||||
case pdu.Tri_True:
|
case pdu.Tri_True:
|
||||||
@@ -182,7 +265,7 @@ func (s *Sender) Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, zfs.St
|
|||||||
return nil, nil, fmt.Errorf("unknown pdu.Tri variant %q", r.Encrypted)
|
return nil, nil, fmt.Errorf("unknown pdu.Tri variant %q", r.Encrypted)
|
||||||
}
|
}
|
||||||
|
|
||||||
sendArgs := zfs.ZFSSendArgs{
|
sendArgsUnvalidated := zfs.ZFSSendArgsUnvalidated{
|
||||||
FS: r.Filesystem,
|
FS: r.Filesystem,
|
||||||
From: uncheckedSendArgsFromPDU(r.GetFrom()), // validated by zfs.ZFSSendDry / zfs.ZFSSend
|
From: uncheckedSendArgsFromPDU(r.GetFrom()), // validated by zfs.ZFSSendDry / zfs.ZFSSend
|
||||||
To: uncheckedSendArgsFromPDU(r.GetTo()), // validated by zfs.ZFSSendDry / zfs.ZFSSend
|
To: uncheckedSendArgsFromPDU(r.GetTo()), // validated by zfs.ZFSSendDry / zfs.ZFSSend
|
||||||
@@ -190,6 +273,11 @@ func (s *Sender) Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, zfs.St
|
|||||||
ResumeToken: r.ResumeToken, // nil or not nil, depending on decoding success
|
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")
|
getLogger(ctx).Debug("acquire concurrent send semaphore")
|
||||||
// TODO use try-acquire and fail with resource-exhaustion rpc status
|
// TODO use try-acquire and fail with resource-exhaustion rpc status
|
||||||
// => would require handling on the client-side
|
// => would require handling on the client-side
|
||||||
@@ -206,7 +294,7 @@ func (s *Sender) Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, zfs.St
|
|||||||
}
|
}
|
||||||
|
|
||||||
// From now on, assume that sendArgs has been validated by ZFSSendDry
|
// From now on, assume that sendArgs has been validated by ZFSSendDry
|
||||||
// (because validation invovles shelling out, it's actually a little expensive)
|
// (because validation involves shelling out, it's actually a little expensive)
|
||||||
|
|
||||||
var expSize int64 = 0 // protocol says 0 means no estimate
|
var expSize int64 = 0 // protocol says 0 means no estimate
|
||||||
if si.SizeEstimate != -1 { // but si returns -1 for no size estimate
|
if si.SizeEstimate != -1 { // but si returns -1 for no size estimate
|
||||||
@@ -224,7 +312,7 @@ func (s *Sender) Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, zfs.St
|
|||||||
// update replication cursor
|
// update replication cursor
|
||||||
if sendArgs.From != nil {
|
if sendArgs.From != nil {
|
||||||
// For all but the first replication, this should always be a no-op because SendCompleted already moved the cursor
|
// 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.From, s.jobId)
|
_, err = MoveReplicationCursor(ctx, sendArgs.FS, sendArgs.FromVersion, s.jobId)
|
||||||
if err == zfs.ErrBookmarkCloningNotSupported {
|
if err == zfs.ErrBookmarkCloningNotSupported {
|
||||||
getLogger(ctx).Debug("not creating replication cursor from bookmark because ZFS does not support it")
|
getLogger(ctx).Debug("not creating replication cursor from bookmark because ZFS does not support it")
|
||||||
// fallthrough
|
// fallthrough
|
||||||
@@ -235,18 +323,18 @@ func (s *Sender) Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, zfs.St
|
|||||||
|
|
||||||
// make sure `From` doesn't go away in order to make this step resumable
|
// make sure `From` doesn't go away in order to make this step resumable
|
||||||
if sendArgs.From != nil {
|
if sendArgs.From != nil {
|
||||||
err := HoldStep(ctx, sendArgs.FS, sendArgs.From, s.jobId)
|
_, err := HoldStep(ctx, sendArgs.FS, *sendArgs.FromVersion, s.jobId)
|
||||||
if err == zfs.ErrBookmarkCloningNotSupported {
|
if err == zfs.ErrBookmarkCloningNotSupported {
|
||||||
getLogger(ctx).Debug("not creating step bookmark because ZFS does not support it")
|
getLogger(ctx).Debug("not creating step bookmark because ZFS does not support it")
|
||||||
// fallthrough
|
// fallthrough
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return nil, nil, errors.Wrap(err, "cannot create step bookmark")
|
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
|
// make sure `To` doesn't go away in order to make this step resumable
|
||||||
err = HoldStep(ctx, sendArgs.FS, sendArgs.To, s.jobId)
|
_, err = HoldStep(ctx, sendArgs.FS, sendArgs.ToVersion, s.jobId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, errors.Wrapf(err, "cannot hold `to` version %q before starting send", sendArgs.To.RelName)
|
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
|
// step holds & replication cursor released / moved forward in s.SendCompleted => s.moveCursorAndReleaseSendHolds
|
||||||
@@ -259,27 +347,32 @@ func (s *Sender) Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, zfs.St
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *Sender) SendCompleted(ctx context.Context, r *pdu.SendCompletedReq) (*pdu.SendCompletedRes, error) {
|
func (p *Sender) SendCompleted(ctx context.Context, r *pdu.SendCompletedReq) (*pdu.SendCompletedRes, error) {
|
||||||
orig := r.GetOriginalReq() // may be nil, always use proto getters
|
|
||||||
fs := orig.GetFilesystem()
|
|
||||||
|
|
||||||
var err error
|
orig := r.GetOriginalReq() // may be nil, always use proto getters
|
||||||
var from *zfs.ZFSSendArgVersion
|
fsp, err := p.filterCheckFS(orig.GetFilesystem())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
fs := fsp.ToString()
|
||||||
|
|
||||||
|
var from *zfs.FilesystemVersion
|
||||||
if orig.GetFrom() != nil {
|
if orig.GetFrom() != nil {
|
||||||
from, err = sendArgsFromPDUAndValidateExists(ctx, fs, orig.GetFrom()) // no shadow
|
f, err := sendArgsFromPDUAndValidateExistsAndGetVersion(ctx, fs, orig.GetFrom()) // no shadow
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "validate `from` exists")
|
return nil, errors.Wrap(err, "validate `from` exists")
|
||||||
}
|
}
|
||||||
|
from = &f
|
||||||
}
|
}
|
||||||
to, err := sendArgsFromPDUAndValidateExists(ctx, fs, orig.GetTo())
|
to, err := sendArgsFromPDUAndValidateExistsAndGetVersion(ctx, fs, orig.GetTo())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "validate `to` exists")
|
return nil, errors.Wrap(err, "validate `to` exists")
|
||||||
}
|
}
|
||||||
|
|
||||||
log := getLogger(ctx).WithField("to_guid", to.GUID).
|
log := getLogger(ctx).WithField("to_guid", to.Guid).
|
||||||
WithField("fs", fs).
|
WithField("fs", fs).
|
||||||
WithField("to", to.RelName)
|
WithField("to", to.RelName)
|
||||||
if from != nil {
|
if from != nil {
|
||||||
log = log.WithField("from", from.RelName).WithField("from_guid", from.GUID)
|
log = log.WithField("from", from.RelName).WithField("from_guid", from.Guid)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug("move replication cursor to most recent common version")
|
log.Debug("move replication cursor to most recent common version")
|
||||||
@@ -320,18 +413,14 @@ func (p *Sender) SendCompleted(ctx context.Context, r *pdu.SendCompletedReq) (*p
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Debug("release step-hold of or step-bookmark on `from`")
|
log.Debug("release step-hold of or step-bookmark on `from`")
|
||||||
err := ReleaseStep(ctx, fs, from, p.jobId)
|
err := ReleaseStep(ctx, fs, *from, p.jobId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if dne, ok := err.(*zfs.DatasetDoesNotExist); ok {
|
if dne, ok := err.(*zfs.DatasetDoesNotExist); ok {
|
||||||
// If bookmark cloning is not supported, `from` might be the old replication cursor
|
// If bookmark cloning is not supported, `from` might be the old replication cursor
|
||||||
// and thus have already been destroyed by MoveReplicationCursor above
|
// and thus have already been destroyed by MoveReplicationCursor above
|
||||||
// In that case, nonexistence of `from` is not an error, otherwise it is.
|
// In that case, nonexistence of `from` is not an error, otherwise it is.
|
||||||
fsp, err := zfs.NewDatasetPath(fs)
|
for _, c := range destroyedCursors {
|
||||||
if err != nil {
|
if c.GetFullPath() == dne.Path {
|
||||||
panic(err) // fs has been validated multiple times above
|
|
||||||
}
|
|
||||||
for _, fsv := range destroyedCursors {
|
|
||||||
if fsv.ToAbsPath(fsp) == dne.Path {
|
|
||||||
log.Info("`from` was a replication cursor and has already been destroyed")
|
log.Info("`from` was a replication cursor and has already been destroyed")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -511,7 +600,7 @@ func (s *Receiver) ListFilesystems(ctx context.Context, req *pdu.ListFilesystemR
|
|||||||
fss := make([]*pdu.Filesystem, 0, len(filtered))
|
fss := make([]*pdu.Filesystem, 0, len(filtered))
|
||||||
for _, a := range filtered {
|
for _, a := range filtered {
|
||||||
l := getLogger(ctx).WithField("fs", a)
|
l := getLogger(ctx).WithField("fs", a)
|
||||||
ph, err := zfs.ZFSGetFilesystemPlaceholderState(a)
|
ph, err := zfs.ZFSGetFilesystemPlaceholderState(ctx, a)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.WithError(err).Error("error getting placeholder state")
|
l.WithError(err).Error("error getting placeholder state")
|
||||||
return nil, errors.Wrapf(err, "cannot get placeholder state for fs %q", a)
|
return nil, errors.Wrapf(err, "cannot get placeholder state for fs %q", a)
|
||||||
@@ -557,8 +646,9 @@ func (s *Receiver) ListFilesystemVersions(ctx context.Context, req *pdu.ListFile
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -610,9 +700,6 @@ func (s *Receiver) Receive(ctx context.Context, req *pdu.ReceiveReq, receive zfs
|
|||||||
if to == nil {
|
if to == nil {
|
||||||
return nil, errors.New("`To` must not be nil")
|
return nil, errors.New("`To` must not be nil")
|
||||||
}
|
}
|
||||||
if err := to.ValidateInMemory(lp.ToString()); err != nil {
|
|
||||||
return nil, errors.Wrap(err, "`To` invalid")
|
|
||||||
}
|
|
||||||
if !to.IsSnapshot() {
|
if !to.IsSnapshot() {
|
||||||
return nil, errors.New("`To` must be a snapshot")
|
return nil, errors.New("`To` must be a snapshot")
|
||||||
}
|
}
|
||||||
@@ -625,9 +712,9 @@ func (s *Receiver) Receive(ctx context.Context, req *pdu.ReceiveReq, receive zfs
|
|||||||
// ZFS dataset hierarchy subtrees.
|
// ZFS dataset hierarchy subtrees.
|
||||||
var visitErr error
|
var visitErr error
|
||||||
func() {
|
func() {
|
||||||
getLogger(ctx).Debug("begin aquire recvParentCreationMtx")
|
getLogger(ctx).Debug("begin acquire recvParentCreationMtx")
|
||||||
defer s.recvParentCreationMtx.Lock().Unlock()
|
defer s.recvParentCreationMtx.Lock().Unlock()
|
||||||
getLogger(ctx).Debug("end aquire recvParentCreationMtx")
|
getLogger(ctx).Debug("end acquire recvParentCreationMtx")
|
||||||
defer getLogger(ctx).Debug("release recvParentCreationMtx")
|
defer getLogger(ctx).Debug("release recvParentCreationMtx")
|
||||||
|
|
||||||
f := zfs.NewDatasetPathForest()
|
f := zfs.NewDatasetPathForest()
|
||||||
@@ -637,7 +724,7 @@ func (s *Receiver) Receive(ctx context.Context, req *pdu.ReceiveReq, receive zfs
|
|||||||
if v.Path.Equal(lp) {
|
if v.Path.Equal(lp) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
ph, err := zfs.ZFSGetFilesystemPlaceholderState(v.Path)
|
ph, err := zfs.ZFSGetFilesystemPlaceholderState(ctx, v.Path)
|
||||||
getLogger(ctx).
|
getLogger(ctx).
|
||||||
WithField("fs", v.Path.ToString()).
|
WithField("fs", v.Path.ToString()).
|
||||||
WithField("placeholder_state", fmt.Sprintf("%#v", ph)).
|
WithField("placeholder_state", fmt.Sprintf("%#v", ph)).
|
||||||
@@ -661,7 +748,7 @@ func (s *Receiver) Receive(ctx context.Context, req *pdu.ReceiveReq, receive zfs
|
|||||||
}
|
}
|
||||||
l := getLogger(ctx).WithField("placeholder_fs", v.Path)
|
l := getLogger(ctx).WithField("placeholder_fs", v.Path)
|
||||||
l.Debug("create placeholder filesystem")
|
l.Debug("create placeholder filesystem")
|
||||||
err := zfs.ZFSCreatePlaceholderFilesystem(v.Path)
|
err := zfs.ZFSCreatePlaceholderFilesystem(ctx, v.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.WithError(err).Error("cannot create placeholder filesystem")
|
l.WithError(err).Error("cannot create placeholder filesystem")
|
||||||
visitErr = err
|
visitErr = err
|
||||||
@@ -681,19 +768,19 @@ func (s *Receiver) Receive(ctx context.Context, req *pdu.ReceiveReq, receive zfs
|
|||||||
// determine whether we need to rollback the filesystem / change its placeholder state
|
// determine whether we need to rollback the filesystem / change its placeholder state
|
||||||
var clearPlaceholderProperty bool
|
var clearPlaceholderProperty bool
|
||||||
var recvOpts zfs.RecvOptions
|
var recvOpts zfs.RecvOptions
|
||||||
ph, err := zfs.ZFSGetFilesystemPlaceholderState(lp)
|
ph, err := zfs.ZFSGetFilesystemPlaceholderState(ctx, lp)
|
||||||
if err == nil && ph.FSExists && ph.IsPlaceholder {
|
if err == nil && ph.FSExists && ph.IsPlaceholder {
|
||||||
recvOpts.RollbackAndForceRecv = true
|
recvOpts.RollbackAndForceRecv = true
|
||||||
clearPlaceholderProperty = true
|
clearPlaceholderProperty = true
|
||||||
}
|
}
|
||||||
if clearPlaceholderProperty {
|
if clearPlaceholderProperty {
|
||||||
if err := zfs.ZFSSetPlaceholder(lp, false); err != nil {
|
if err := zfs.ZFSSetPlaceholder(ctx, lp, false); err != nil {
|
||||||
return nil, fmt.Errorf("cannot clear placeholder property for forced receive: %s", err)
|
return nil, fmt.Errorf("cannot clear placeholder property for forced receive: %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.ClearResumeToken && ph.FSExists {
|
if req.ClearResumeToken && ph.FSExists {
|
||||||
if err := zfs.ZFSRecvClearResumeToken(lp.ToString()); err != nil {
|
if err := zfs.ZFSRecvClearResumeToken(ctx, lp.ToString()); err != nil {
|
||||||
return nil, errors.Wrap(err, "cannot clear resume token")
|
return nil, errors.Wrap(err, "cannot clear resume token")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -719,13 +806,14 @@ func (s *Receiver) Receive(ctx context.Context, req *pdu.ReceiveReq, receive zfs
|
|||||||
if err := zfs.ZFSRecv(ctx, lp.ToString(), to, receive, recvOpts); err != nil {
|
if err := zfs.ZFSRecv(ctx, lp.ToString(), to, receive, recvOpts); err != nil {
|
||||||
getLogger(ctx).
|
getLogger(ctx).
|
||||||
WithError(err).
|
WithError(err).
|
||||||
WithField("opts", recvOpts).
|
WithField("opts", fmt.Sprintf("%#v", recvOpts)).
|
||||||
Error("zfs receive failed")
|
Error("zfs receive failed")
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// validate that we actually received what the sender claimed
|
// validate that we actually received what the sender claimed
|
||||||
if err := to.ValidateExists(ctx, lp.ToString()); err != nil {
|
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"
|
msg := "receive request's `To` version does not match what we received in the stream"
|
||||||
getLogger(ctx).WithError(err).WithField("snap", snapFullPath).Error(msg)
|
getLogger(ctx).WithError(err).WithField("snap", snapFullPath).Error(msg)
|
||||||
getLogger(ctx).Error("aborting recv request, but keeping received snapshot for inspection")
|
getLogger(ctx).Error("aborting recv request, but keeping received snapshot for inspection")
|
||||||
@@ -734,7 +822,7 @@ func (s *Receiver) Receive(ctx context.Context, req *pdu.ReceiveReq, receive zfs
|
|||||||
|
|
||||||
if s.conf.UpdateLastReceivedHold {
|
if s.conf.UpdateLastReceivedHold {
|
||||||
getLogger(ctx).Debug("move last-received-hold")
|
getLogger(ctx).Debug("move last-received-hold")
|
||||||
if err := MoveLastReceivedHold(ctx, lp.ToString(), *to, s.conf.JobID); err != nil {
|
if err := MoveLastReceivedHold(ctx, lp.ToString(), toRecvd, s.conf.JobID); err != nil {
|
||||||
return nil, errors.Wrap(err, "cannot move last-received-hold")
|
return nil, errors.Wrap(err, "cannot move last-received-hold")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,503 +0,0 @@
|
|||||||
package endpoint
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"regexp"
|
|
||||||
"sort"
|
|
||||||
|
|
||||||
"github.com/kr/pretty"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/zfs"
|
|
||||||
)
|
|
||||||
|
|
||||||
var stepHoldTagRE = regexp.MustCompile("^zrepl_STEP_J_(.+)")
|
|
||||||
|
|
||||||
func StepHoldTag(jobid JobID) (string, error) {
|
|
||||||
return stepHoldTagImpl(jobid.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
func stepHoldTagImpl(jobid string) (string, error) {
|
|
||||||
t := fmt.Sprintf("zrepl_STEP_J_%s", jobid)
|
|
||||||
if err := zfs.ValidHoldTag(t); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return t, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// err != nil always means that the bookmark is not a step bookmark
|
|
||||||
func ParseStepHoldTag(tag string) (JobID, error) {
|
|
||||||
match := stepHoldTagRE.FindStringSubmatch(tag)
|
|
||||||
if match == nil {
|
|
||||||
return JobID{}, fmt.Errorf("parse hold tag: match regex %q", stepHoldTagRE)
|
|
||||||
}
|
|
||||||
jobID, err := MakeJobID(match[1])
|
|
||||||
if err != nil {
|
|
||||||
return JobID{}, errors.Wrap(err, "parse hold tag: invalid job id field")
|
|
||||||
}
|
|
||||||
return jobID, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const stepBookmarkNamePrefix = "zrepl_STEP"
|
|
||||||
|
|
||||||
// v must be validated by caller
|
|
||||||
func StepBookmarkName(fs string, guid uint64, id JobID) (string, error) {
|
|
||||||
return stepBookmarkNameImpl(fs, guid, id.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
func stepBookmarkNameImpl(fs string, guid uint64, jobid string) (string, error) {
|
|
||||||
return makeJobAndGuidBookmarkName(stepBookmarkNamePrefix, fs, guid, jobid)
|
|
||||||
}
|
|
||||||
|
|
||||||
// name is the full bookmark name, including dataset path
|
|
||||||
//
|
|
||||||
// err != nil always means that the bookmark is not a step bookmark
|
|
||||||
func ParseStepBookmarkName(fullname string) (guid uint64, jobID JobID, err error) {
|
|
||||||
guid, jobID, err = parseJobAndGuidBookmarkName(fullname, stepBookmarkNamePrefix)
|
|
||||||
if err != nil {
|
|
||||||
err = errors.Wrap(err, "parse step bookmark name") // no shadow!
|
|
||||||
}
|
|
||||||
return guid, jobID, err
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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, fs *zfs.DatasetPath, jobID JobID) ([]zfs.FilesystemVersion, error) {
|
|
||||||
|
|
||||||
listOut := &ListHoldsAndBookmarksOutput{}
|
|
||||||
if err := listZFSHoldsAndBookmarksImplFS(ctx, listOut, fs); err != nil {
|
|
||||||
return nil, errors.Wrap(err, "get replication cursor: list bookmarks and holds")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(listOut.V1ReplicationCursors) > 0 {
|
|
||||||
getLogger(ctx).WithField("bookmark", pretty.Sprint(listOut.V1ReplicationCursors)).
|
|
||||||
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 listOut.ReplicationCursorBookmarks {
|
|
||||||
zv := zfs.ZFSSendArgVersion{
|
|
||||||
RelName: "#" + v.Name,
|
|
||||||
GUID: v.Guid,
|
|
||||||
}
|
|
||||||
if err := zv.ValidateExists(ctx, v.FS); err != nil {
|
|
||||||
getLogger(ctx).WithError(err).WithField("bookmark", zv.FullPath(v.FS)).
|
|
||||||
Error("found invalid replication cursor bookmark")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
candidates = append(candidates, v.v)
|
|
||||||
}
|
|
||||||
|
|
||||||
return candidates, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// `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 *zfs.ZFSSendArgVersion, jobID JobID) (destroyedCursors []zfs.FilesystemVersion, err error) {
|
|
||||||
|
|
||||||
if !target.IsSnapshot() {
|
|
||||||
return nil, zfs.ErrBookmarkCloningNotSupported
|
|
||||||
}
|
|
||||||
|
|
||||||
snapProps, err := target.ValidateExistsAndGetCheckedProps(ctx, fs)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrapf(err, "invalid replication cursor target %q (guid=%v)", target.RelName, target.GUID)
|
|
||||||
}
|
|
||||||
|
|
||||||
bookmarkname, err := ReplicationCursorBookmarkName(fs, snapProps.Guid, 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(fs, *target, 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
|
|
||||||
}
|
|
||||||
|
|
||||||
func DestroyObsoleteReplicationCursors(ctx context.Context, fs string, target *zfs.ZFSSendArgVersion, jobID JobID) (destroyed []zfs.FilesystemVersion, err error) {
|
|
||||||
return destroyBookmarksOlderThan(ctx, fs, target, jobID, func(shortname string) (accept bool) {
|
|
||||||
_, parsedID, err := ParseReplicationCursorBookmarkName(fs + "#" + shortname)
|
|
||||||
return err == nil && parsedID == jobID
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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.ZFSSendArgVersion, jobID JobID) error {
|
|
||||||
if err := v.ValidateExists(ctx, fs); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if v.IsSnapshot() {
|
|
||||||
|
|
||||||
tag, err := StepHoldTag(jobID)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "step hold tag")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := zfs.ZFSHold(ctx, fs, *v, tag); err != nil {
|
|
||||||
return errors.Wrap(err, "step hold: zfs")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
v.MustBeBookmark()
|
|
||||||
|
|
||||||
bmname, err := StepBookmarkName(fs, v.GUID, jobID)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "create step bookmark: determine bookmark name")
|
|
||||||
}
|
|
||||||
// idempotently create bookmark
|
|
||||||
err = zfs.ZFSBookmark(fs, *v, bmname)
|
|
||||||
if err != nil {
|
|
||||||
if err == zfs.ErrBookmarkCloningNotSupported {
|
|
||||||
// TODO we could actually try to find a local snapshot that has the requested GUID
|
|
||||||
// however, the replication algorithm prefers snapshots anyways, so this quest
|
|
||||||
// is most likely not going to be successful. Also, there's the possibility that
|
|
||||||
// the caller might want to filter what snapshots are eligibile, and this would
|
|
||||||
// complicate things even further.
|
|
||||||
return err // TODO go1.13 use wrapping
|
|
||||||
}
|
|
||||||
return errors.Wrap(err, "create step bookmark: zfs")
|
|
||||||
}
|
|
||||||
return 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.ZFSSendArgVersion, jobID JobID) error {
|
|
||||||
|
|
||||||
if err := v.ValidateExists(ctx, fs); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
v.MustBeBookmark()
|
|
||||||
|
|
||||||
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(bmname); err != nil {
|
|
||||||
return errors.Wrap(err, "step release: bookmark destroy: zfs")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// release {step holds, step bookmarks} earlier and including `mostRecent`
|
|
||||||
func ReleaseStepAll(ctx context.Context, fs string, mostRecent *zfs.ZFSSendArgVersion, jobID JobID) error {
|
|
||||||
|
|
||||||
if err := mostRecent.ValidateInMemory(fs); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
tag, err := StepHoldTag(jobID)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "step release all: tag")
|
|
||||||
}
|
|
||||||
|
|
||||||
err = zfs.ZFSReleaseAllOlderAndIncludingGUID(ctx, fs, mostRecent.GUID, tag)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrapf(err, "step release all: release holds older and including %q", mostRecent.FullPath(fs))
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = destroyBookmarksOlderThan(ctx, fs, mostRecent, jobID, func(shortname string) bool {
|
|
||||||
_, parsedId, parseErr := ParseStepBookmarkName(fs + "#" + shortname)
|
|
||||||
return parseErr == nil && parsedId == jobID
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrapf(err, "step release all: destroy bookmarks older than %q", mostRecent.FullPath(fs))
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
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.ZFSSendArgVersion, jobID JobID) error {
|
|
||||||
if err := to.ValidateExists(ctx, fs); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := zfs.EntityNamecheck(to.FullPath(fs), zfs.EntityTypeSnapshot); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
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")
|
|
||||||
}
|
|
||||||
|
|
||||||
err = zfs.ZFSReleaseAllOlderThanGUID(ctx, fs, to.GUID, tag)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "last-received-hold: release older holds")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type ListHoldsAndBookmarksOutputBookmarkV1ReplicationCursor struct {
|
|
||||||
FS string
|
|
||||||
Name string
|
|
||||||
}
|
|
||||||
|
|
||||||
type ListHoldsAndBookmarksOutput struct {
|
|
||||||
StepBookmarks []*ListHoldsAndBookmarksOutputBookmark
|
|
||||||
StepHolds []*ListHoldsAndBookmarksOutputHold
|
|
||||||
|
|
||||||
ReplicationCursorBookmarks []*ListHoldsAndBookmarksOutputBookmark
|
|
||||||
V1ReplicationCursors []*ListHoldsAndBookmarksOutputBookmarkV1ReplicationCursor
|
|
||||||
LastReceivedHolds []*ListHoldsAndBookmarksOutputHold
|
|
||||||
}
|
|
||||||
|
|
||||||
type ListHoldsAndBookmarksOutputBookmark struct {
|
|
||||||
FS, Name string
|
|
||||||
Guid uint64
|
|
||||||
JobID JobID
|
|
||||||
v zfs.FilesystemVersion
|
|
||||||
}
|
|
||||||
|
|
||||||
type ListHoldsAndBookmarksOutputHold struct {
|
|
||||||
FS string
|
|
||||||
Snap string
|
|
||||||
SnapGuid uint64
|
|
||||||
SnapCreateTXG uint64
|
|
||||||
Tag string
|
|
||||||
JobID JobID
|
|
||||||
}
|
|
||||||
|
|
||||||
// List all holds and bookmarks managed by endpoint
|
|
||||||
func ListZFSHoldsAndBookmarks(ctx context.Context, fsfilter zfs.DatasetFilter) (*ListHoldsAndBookmarksOutput, error) {
|
|
||||||
|
|
||||||
// initialize all fields so that JSON serializion of output looks pretty (see client/holds.go)
|
|
||||||
// however, listZFSHoldsAndBookmarksImplFS shouldn't rely on it
|
|
||||||
out := &ListHoldsAndBookmarksOutput{
|
|
||||||
StepBookmarks: make([]*ListHoldsAndBookmarksOutputBookmark, 0),
|
|
||||||
StepHolds: make([]*ListHoldsAndBookmarksOutputHold, 0),
|
|
||||||
ReplicationCursorBookmarks: make([]*ListHoldsAndBookmarksOutputBookmark, 0),
|
|
||||||
V1ReplicationCursors: make([]*ListHoldsAndBookmarksOutputBookmarkV1ReplicationCursor, 0),
|
|
||||||
LastReceivedHolds: make([]*ListHoldsAndBookmarksOutputHold, 0),
|
|
||||||
}
|
|
||||||
|
|
||||||
fss, err := zfs.ZFSListMapping(ctx, fsfilter)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "list filesystems")
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, fs := range fss {
|
|
||||||
err := listZFSHoldsAndBookmarksImplFS(ctx, out, fs)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrapf(err, "list holds and bookmarks on %q", fs.ToString())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func listZFSHoldsAndBookmarksImplFS(ctx context.Context, out *ListHoldsAndBookmarksOutput, fs *zfs.DatasetPath) error {
|
|
||||||
fsvs, err := zfs.ZFSListFilesystemVersions(fs, nil)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrapf(err, "list filesystem versions of %q", fs)
|
|
||||||
}
|
|
||||||
for _, v := range fsvs {
|
|
||||||
switch v.Type {
|
|
||||||
case zfs.Bookmark:
|
|
||||||
listZFSHoldsAndBookmarksImplTryParseBookmark(ctx, out, fs, v)
|
|
||||||
case zfs.Snapshot:
|
|
||||||
holds, err := zfs.ZFSHolds(ctx, fs.ToString(), v.Name)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrapf(err, "get holds of %q", v.ToAbsPath(fs))
|
|
||||||
}
|
|
||||||
for _, tag := range holds {
|
|
||||||
listZFSHoldsAndBookmarksImplSnapshotTryParseHold(ctx, out, fs, v, tag)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// pure function, err != nil always indicates parsing error
|
|
||||||
func listZFSHoldsAndBookmarksImplTryParseBookmark(ctx context.Context, out *ListHoldsAndBookmarksOutput, fs *zfs.DatasetPath, v zfs.FilesystemVersion) {
|
|
||||||
var err error
|
|
||||||
|
|
||||||
if v.Type != zfs.Bookmark {
|
|
||||||
panic("impl error")
|
|
||||||
}
|
|
||||||
|
|
||||||
fullname := v.ToAbsPath(fs)
|
|
||||||
|
|
||||||
bm := &ListHoldsAndBookmarksOutputBookmark{
|
|
||||||
FS: fs.ToString(), Name: v.Name, v: v,
|
|
||||||
}
|
|
||||||
bm.Guid, bm.JobID, err = ParseStepBookmarkName(fullname)
|
|
||||||
if err == nil {
|
|
||||||
out.StepBookmarks = append(out.StepBookmarks, bm)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
bm.Guid, bm.JobID, err = ParseReplicationCursorBookmarkName(fullname)
|
|
||||||
if err == nil {
|
|
||||||
out.ReplicationCursorBookmarks = append(out.ReplicationCursorBookmarks, bm)
|
|
||||||
return
|
|
||||||
} else if err == ErrV1ReplicationCursor {
|
|
||||||
v1rc := &ListHoldsAndBookmarksOutputBookmarkV1ReplicationCursor{
|
|
||||||
FS: fs.ToString(), Name: v.Name,
|
|
||||||
}
|
|
||||||
out.V1ReplicationCursors = append(out.V1ReplicationCursors, v1rc)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// pure function, err != nil always indicates parsing error
|
|
||||||
func listZFSHoldsAndBookmarksImplSnapshotTryParseHold(ctx context.Context, out *ListHoldsAndBookmarksOutput, fs *zfs.DatasetPath, v zfs.FilesystemVersion, holdTag string) {
|
|
||||||
var err error
|
|
||||||
|
|
||||||
if v.Type != zfs.Snapshot {
|
|
||||||
panic("impl error")
|
|
||||||
}
|
|
||||||
|
|
||||||
hold := &ListHoldsAndBookmarksOutputHold{
|
|
||||||
FS: fs.ToString(),
|
|
||||||
Snap: v.Name,
|
|
||||||
SnapGuid: v.Guid,
|
|
||||||
SnapCreateTXG: v.CreateTXG,
|
|
||||||
Tag: holdTag,
|
|
||||||
}
|
|
||||||
hold.JobID, err = ParseStepHoldTag(holdTag)
|
|
||||||
if err == nil {
|
|
||||||
out.StepHolds = append(out.StepHolds, hold)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
hold.JobID, err = ParseLastReceivedHoldTag(holdTag)
|
|
||||||
if err == nil {
|
|
||||||
out.LastReceivedHolds = append(out.LastReceivedHolds, hold)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,838 @@
|
|||||||
|
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[i].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 {
|
||||||
|
if a.GetJobID() == nil {
|
||||||
|
continue // already put those into always-live list noJobId in above loop
|
||||||
|
}
|
||||||
|
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))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
package endpoint
|
package endpoint
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -57,53 +56,3 @@ func parseJobAndGuidBookmarkName(fullname string, prefix string) (guid uint64, j
|
|||||||
|
|
||||||
return guid, jobID, nil
|
return guid, jobID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func destroyBookmarksOlderThan(ctx context.Context, fs string, mostRecent *zfs.ZFSSendArgVersion, jobID JobID, filter func(shortname string) (accept bool)) (destroyed []zfs.FilesystemVersion, err error) {
|
|
||||||
if filter == nil {
|
|
||||||
panic(filter)
|
|
||||||
}
|
|
||||||
|
|
||||||
fsp, err := zfs.NewDatasetPath(fs)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "invalid filesystem path")
|
|
||||||
}
|
|
||||||
|
|
||||||
mostRecentProps, err := mostRecent.ValidateExistsAndGetCheckedProps(ctx, fs)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "validate most recent version argument")
|
|
||||||
}
|
|
||||||
|
|
||||||
stepBookmarks, err := zfs.ZFSListFilesystemVersions(fsp, zfs.FilterFromClosure(
|
|
||||||
func(t zfs.VersionType, name string) (accept bool, err error) {
|
|
||||||
if t != zfs.Bookmark {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
return filter(name), nil
|
|
||||||
}))
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "list bookmarks")
|
|
||||||
}
|
|
||||||
|
|
||||||
// cut off all bookmarks prior to mostRecent's CreateTXG
|
|
||||||
var destroy []zfs.FilesystemVersion
|
|
||||||
for _, v := range stepBookmarks {
|
|
||||||
if v.Type != zfs.Bookmark {
|
|
||||||
panic("implementation error")
|
|
||||||
}
|
|
||||||
if !filter(v.Name) {
|
|
||||||
panic("inconsistent filter result")
|
|
||||||
}
|
|
||||||
if v.CreateTXG < mostRecentProps.CreateTXG {
|
|
||||||
destroy = append(destroy, v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME use batch destroy, must adopt code to handle bookmarks
|
|
||||||
for _, v := range destroy {
|
|
||||||
if err := zfs.ZFSDestroyIdempotent(v.ToAbsPath(fsp)); err != nil {
|
|
||||||
return nil, errors.Wrap(err, "destroy bookmark")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return destroy, nil
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
|
}
|
||||||
+5
-5
@@ -9,8 +9,8 @@ import (
|
|||||||
"github.com/zrepl/zrepl/zfs"
|
"github.com/zrepl/zrepl/zfs"
|
||||||
)
|
)
|
||||||
|
|
||||||
// An instance of this type returned by MakeJobID guarantees
|
// JobID instances returned by MakeJobID() guarantee their JobID.String()
|
||||||
// that that instance's JobID.String() can be used in a ZFS dataset name and hold tag.
|
// can be used in ZFS dataset names and hold tags.
|
||||||
type JobID struct {
|
type JobID struct {
|
||||||
jid string
|
jid string
|
||||||
}
|
}
|
||||||
@@ -21,7 +21,7 @@ func MakeJobID(s string) (JobID, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := zfs.ComponentNamecheck(s); err != nil {
|
if err := zfs.ComponentNamecheck(s); err != nil {
|
||||||
return JobID{}, errors.Wrap(err, "muse be usable as a dataset path component")
|
return JobID{}, errors.Wrap(err, "must be usable as a dataset path component")
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := stepBookmarkNameImpl("pool/ds", 0xface601d, s); err != nil {
|
if _, err := stepBookmarkNameImpl("pool/ds", 0xface601d, s); err != nil {
|
||||||
@@ -34,7 +34,7 @@ func MakeJobID(s string) (JobID, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if _, err := lastReceivedHoldImpl(s); err != nil {
|
if _, err := lastReceivedHoldImpl(s); err != nil {
|
||||||
return JobID{}, errors.Wrap(err, "must be usabel as a last-recieved-hold tag")
|
return JobID{}, errors.Wrap(err, "must be usable as a last-received-hold tag")
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME replication cursor bookmark name
|
// FIXME replication cursor bookmark name
|
||||||
@@ -57,7 +57,7 @@ func MustMakeJobID(s string) JobID {
|
|||||||
|
|
||||||
func (j JobID) expectInitialized() {
|
func (j JobID) expectInitialized() {
|
||||||
if j.jid == "" {
|
if j.jid == "" {
|
||||||
panic("use of unitialized JobID")
|
panic("use of uninitialized JobID")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ require (
|
|||||||
github.com/onsi/gomega v1.7.0 // indirect
|
github.com/onsi/gomega v1.7.0 // indirect
|
||||||
github.com/pkg/errors v0.8.1
|
github.com/pkg/errors v0.8.1
|
||||||
github.com/pkg/profile v1.2.1
|
github.com/pkg/profile v1.2.1
|
||||||
github.com/problame/go-netssh v0.0.0-20191209123953-18d8aa6923c7
|
github.com/problame/go-netssh v0.0.0-20200601114649-26439f9f0dc5
|
||||||
github.com/prometheus/client_golang v1.2.1
|
github.com/prometheus/client_golang v1.2.1
|
||||||
github.com/sergi/go-diff v1.0.1-0.20180205163309-da645544ed44 // go1.12 thinks it needs this
|
github.com/sergi/go-diff v1.0.1-0.20180205163309-da645544ed44 // go1.12 thinks it needs this
|
||||||
github.com/spf13/cobra v0.0.2
|
github.com/spf13/cobra v0.0.2
|
||||||
@@ -33,5 +33,6 @@ require (
|
|||||||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980
|
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58
|
||||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037
|
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037
|
||||||
|
gonum.org/v1/gonum v0.7.0 // indirect
|
||||||
google.golang.org/grpc v1.17.0
|
google.golang.org/grpc v1.17.0
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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-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/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/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-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/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=
|
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.6.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||||
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
|
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
|
||||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
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 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
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=
|
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/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.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||||
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
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 h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
|
||||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
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=
|
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.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||||
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
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/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 h1:uC1QfSlInpQF+M0ao65imhwqKnz3Q2z/d8PWZRMQvDM=
|
||||||
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k=
|
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=
|
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||||
@@ -204,6 +208,8 @@ github.com/problame/go-netssh v0.0.0-20191026123024-f34099f4f6b1 h1:HH8yzlaZq/A8
|
|||||||
github.com/problame/go-netssh v0.0.0-20191026123024-f34099f4f6b1/go.mod h1:JRs3nyBjvOv/9YHC+Vlw9z1Jfzl5s5t0BNnTzmclj1c=
|
github.com/problame/go-netssh v0.0.0-20191026123024-f34099f4f6b1/go.mod h1:JRs3nyBjvOv/9YHC+Vlw9z1Jfzl5s5t0BNnTzmclj1c=
|
||||||
github.com/problame/go-netssh v0.0.0-20191209123953-18d8aa6923c7 h1:Oik8tLrLhoMq4fduDRuNNOAQ+q0M0dXkjAYLUf4+mAc=
|
github.com/problame/go-netssh v0.0.0-20191209123953-18d8aa6923c7 h1:Oik8tLrLhoMq4fduDRuNNOAQ+q0M0dXkjAYLUf4+mAc=
|
||||||
github.com/problame/go-netssh v0.0.0-20191209123953-18d8aa6923c7/go.mod h1:Ba6RaFpK1+IHWs1wLZ6sCBuXkt4iAVLeOG5GinXHusM=
|
github.com/problame/go-netssh v0.0.0-20191209123953-18d8aa6923c7/go.mod h1:Ba6RaFpK1+IHWs1wLZ6sCBuXkt4iAVLeOG5GinXHusM=
|
||||||
|
github.com/problame/go-netssh v0.0.0-20200601114649-26439f9f0dc5 h1:1eSrO2WAeSXjMol8WRKW9LekL1252VIMaKQwWkmEdvs=
|
||||||
|
github.com/problame/go-netssh v0.0.0-20200601114649-26439f9f0dc5/go.mod h1:Ba6RaFpK1+IHWs1wLZ6sCBuXkt4iAVLeOG5GinXHusM=
|
||||||
github.com/prometheus/client_golang v0.0.0-20180410130117-e11c6ff8170b/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
github.com/prometheus/client_golang v0.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 v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||||
@@ -259,6 +265,7 @@ github.com/spf13/viper v1.0.2/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7Sr
|
|||||||
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
|
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
@@ -289,6 +296,11 @@ golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnf
|
|||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
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-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/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-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/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=
|
golang.org/x/net v0.0.0-20170915142106-8351a756f30f/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
@@ -328,17 +340,24 @@ 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/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-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-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-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-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-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-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-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-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-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-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-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-20190521203540-521d6ed310dd/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||||
golang.org/x/tools v0.0.0-20190524210228-3d17549cdc6b/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 h1:igQkv0AAhEIvTEpD5LIpAfav2eeVO9HBTjvKHVJPRSs=
|
||||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc=
|
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc=
|
||||||
@@ -350,6 +369,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=
|
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/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/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 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 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
@@ -368,5 +388,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/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-20190209190245-fbb59629db34/go.mod h1:H6SUd1XjIs+qQCyskXg5OFSrilMRUkD8ePJpHKDPaeY=
|
||||||
mvdan.cc/unparam v0.0.0-20190310220240-1b9ccfa71afe/go.mod h1:BnhuWBAqxH3+J5bDybdxgw5ZfS+DsVd4iylsKQePN8o=
|
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 v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0=
|
||||||
sourcegraph.com/sqs/pbtypes v1.0.0/go.mod h1:3AciMUv4qUuRHRHhOG4TZOB+72GdPVz5k+c648qsFS4=
|
sourcegraph.com/sqs/pbtypes v1.0.0/go.mod h1:3AciMUv4qUuRHRHhOG4TZOB+72GdPVz5k+c648qsFS4=
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ func init() {
|
|||||||
cli.AddSubcommand(client.PprofCmd)
|
cli.AddSubcommand(client.PprofCmd)
|
||||||
cli.AddSubcommand(client.TestCmd)
|
cli.AddSubcommand(client.TestCmd)
|
||||||
cli.AddSubcommand(client.MigrateCmd)
|
cli.AddSubcommand(client.MigrateCmd)
|
||||||
cli.AddSubcommand(client.HoldsCmd)
|
cli.AddSubcommand(client.ZFSAbstractionsCmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|||||||
@@ -172,21 +172,21 @@ type testCaseResult struct {
|
|||||||
func runTestCase(ctx *platformtest.Context, ex platformtest.Execer, c tests.Case) *testCaseResult {
|
func runTestCase(ctx *platformtest.Context, ex platformtest.Execer, c tests.Case) *testCaseResult {
|
||||||
|
|
||||||
// run case
|
// run case
|
||||||
var paniced = false
|
var panicked = false
|
||||||
var panicValue interface{} = nil
|
var panicValue interface{} = nil
|
||||||
var panicStack error
|
var panicStack error
|
||||||
func() {
|
func() {
|
||||||
defer func() {
|
defer func() {
|
||||||
if item := recover(); item != nil {
|
if item := recover(); item != nil {
|
||||||
panicValue = item
|
panicValue = item
|
||||||
paniced = true
|
panicked = true
|
||||||
panicStack = errors.Errorf("panic while running test: %v", panicValue)
|
panicStack = errors.Errorf("panic while running test: %v", panicValue)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
c(ctx)
|
c(ctx)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if paniced {
|
if panicked {
|
||||||
switch panicValue {
|
switch panicValue {
|
||||||
case platformtest.SkipNowSentinel:
|
case platformtest.SkipNowSentinel:
|
||||||
return &testCaseResult{skipped: true}
|
return &testCaseResult{skipped: true}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ func (c *Context) Logf(format string, args ...interface{}) {
|
|||||||
|
|
||||||
func (c *Context) Errorf(format string, args ...interface{}) {
|
func (c *Context) Errorf(format string, args ...interface{}) {
|
||||||
GetLog(c).Printf(format, args...)
|
GetLog(c).Printf(format, args...)
|
||||||
|
c.FailNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Context) FailNow() {
|
func (c *Context) FailNow() {
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ type Stmt interface {
|
|||||||
type Op string
|
type Op string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
Comment Op = "#"
|
||||||
AssertExists Op = "!E"
|
AssertExists Op = "!E"
|
||||||
AssertNotExists Op = "!N"
|
AssertNotExists Op = "!N"
|
||||||
Add Op = "+"
|
Add Op = "+"
|
||||||
@@ -102,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 {
|
type RunOp struct {
|
||||||
RootDS string
|
RootDS string
|
||||||
Script string
|
Script string
|
||||||
@@ -180,8 +201,8 @@ func splitQuotedWords(data []byte, atEOF bool) (advance int, token []byte, err e
|
|||||||
// unescaped quote, end of this string
|
// unescaped quote, end of this string
|
||||||
// remove backslash-escapes
|
// remove backslash-escapes
|
||||||
withBackslash := data[begin+1 : end]
|
withBackslash := data[begin+1 : end]
|
||||||
withoutBaskslash := bytes.Replace(withBackslash, []byte("\\\""), []byte("\""), -1)
|
withoutBackslash := bytes.Replace(withBackslash, []byte("\\\""), []byte("\""), -1)
|
||||||
return end + 1, withoutBaskslash, nil
|
return end + 1, withoutBackslash, nil
|
||||||
} else {
|
} else {
|
||||||
// continue to next quote
|
// continue to next quote
|
||||||
end += 1
|
end += 1
|
||||||
@@ -255,16 +276,26 @@ nextLine:
|
|||||||
op = AssertExists
|
op = AssertExists
|
||||||
case string(AssertNotExists):
|
case string(AssertNotExists):
|
||||||
op = AssertNotExists
|
op = AssertNotExists
|
||||||
|
case string(Comment):
|
||||||
|
op = Comment
|
||||||
|
continue
|
||||||
default:
|
default:
|
||||||
return nil, &LineError{scan.Text(), fmt.Sprintf("invalid op %q", comps.Text())}
|
return nil, &LineError{scan.Text(), fmt.Sprintf("invalid op %q", comps.Text())}
|
||||||
}
|
}
|
||||||
|
|
||||||
// FS / SNAP
|
// FS / SNAP / BOOKMARK
|
||||||
if err := expectMoreTokens(); err != nil {
|
if err := expectMoreTokens(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if strings.ContainsAny(comps.Text(), "@") {
|
if strings.ContainsAny(comps.Text(), "@") {
|
||||||
stmts = append(stmts, &SnapOp{Op: op, Path: fmt.Sprintf("%s/%s", rootds, 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 {
|
} else {
|
||||||
// FS
|
// FS
|
||||||
fs := comps.Text()
|
fs := comps.Text()
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ func (a ZpoolCreateArgs) Validate() error {
|
|||||||
return errors.Errorf("Mountpoint must be an absolute path to a directory")
|
return errors.Errorf("Mountpoint must be an absolute path to a directory")
|
||||||
}
|
}
|
||||||
if a.PoolName == "" {
|
if a.PoolName == "" {
|
||||||
return errors.Errorf("PoolName must not be emtpy")
|
return errors.Errorf("PoolName must not be empty")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -45,7 +45,7 @@ func CreateOrReplaceZpool(ctx context.Context, e Execer, args ZpoolCreateArgs) (
|
|||||||
}
|
}
|
||||||
|
|
||||||
// export pool if it already exists (idempotence)
|
// 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 {
|
if _, ok := err.(*zfs.DatasetDoesNotExist); ok {
|
||||||
// we'll create it shortly
|
// we'll create it shortly
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,154 +0,0 @@
|
|||||||
package tests
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/platformtest"
|
|
||||||
"github.com/zrepl/zrepl/zfs"
|
|
||||||
)
|
|
||||||
|
|
||||||
type rollupReleaseExpectTags struct {
|
|
||||||
Snap string
|
|
||||||
Holds map[string]bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func rollupReleaseTest(ctx *platformtest.Context, cb func(fs string) []rollupReleaseExpectTags) {
|
|
||||||
|
|
||||||
platformtest.Run(ctx, platformtest.PanicErr, ctx.RootDataset, `
|
|
||||||
DESTROYROOT
|
|
||||||
CREATEROOT
|
|
||||||
+ "foo bar"
|
|
||||||
+ "foo bar@1"
|
|
||||||
+ "foo bar@2"
|
|
||||||
+ "foo bar@3"
|
|
||||||
+ "foo bar@4"
|
|
||||||
+ "foo bar@5"
|
|
||||||
+ "foo bar@6"
|
|
||||||
R zfs hold zrepl_platformtest "${ROOTDS}/foo bar@1"
|
|
||||||
R zfs hold zrepl_platformtest_2 "${ROOTDS}/foo bar@2"
|
|
||||||
R zfs hold zrepl_platformtest "${ROOTDS}/foo bar@3"
|
|
||||||
R zfs hold zrepl_platformtest "${ROOTDS}/foo bar@5"
|
|
||||||
R zfs hold zrepl_platformtest "${ROOTDS}/foo bar@6"
|
|
||||||
R zfs bookmark "${ROOTDS}/foo bar@5" "${ROOTDS}/foo bar#5"
|
|
||||||
`)
|
|
||||||
|
|
||||||
fs := fmt.Sprintf("%s/foo bar", ctx.RootDataset)
|
|
||||||
|
|
||||||
expTags := cb(fs)
|
|
||||||
|
|
||||||
for _, exp := range expTags {
|
|
||||||
holds, err := zfs.ZFSHolds(ctx, fs, exp.Snap)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
for _, h := range holds {
|
|
||||||
if e, ok := exp.Holds[h]; !ok || !e {
|
|
||||||
panic(fmt.Sprintf("tag %q on snap %q not expected", h, exp.Snap))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func RollupReleaseIncluding(ctx *platformtest.Context) {
|
|
||||||
rollupReleaseTest(ctx, func(fs string) []rollupReleaseExpectTags {
|
|
||||||
guid5, err := zfs.ZFSGetGUID(fs, "@5")
|
|
||||||
require.NoError(ctx, err)
|
|
||||||
|
|
||||||
err = zfs.ZFSReleaseAllOlderAndIncludingGUID(ctx, fs, guid5, "zrepl_platformtest")
|
|
||||||
require.NoError(ctx, err)
|
|
||||||
|
|
||||||
return []rollupReleaseExpectTags{
|
|
||||||
{"1", map[string]bool{}},
|
|
||||||
{"2", map[string]bool{"zrepl_platformtest_2": true}},
|
|
||||||
{"3", map[string]bool{}},
|
|
||||||
{"4", map[string]bool{}},
|
|
||||||
{"5", map[string]bool{}},
|
|
||||||
{"6", map[string]bool{"zrepl_platformtest": true}},
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func RollupReleaseExcluding(ctx *platformtest.Context) {
|
|
||||||
rollupReleaseTest(ctx, func(fs string) []rollupReleaseExpectTags {
|
|
||||||
guid5, err := zfs.ZFSGetGUID(fs, "@5")
|
|
||||||
require.NoError(ctx, err)
|
|
||||||
|
|
||||||
err = zfs.ZFSReleaseAllOlderThanGUID(ctx, fs, guid5, "zrepl_platformtest")
|
|
||||||
require.NoError(ctx, err)
|
|
||||||
|
|
||||||
return []rollupReleaseExpectTags{
|
|
||||||
{"1", map[string]bool{}},
|
|
||||||
{"2", map[string]bool{"zrepl_platformtest_2": true}},
|
|
||||||
{"3", map[string]bool{}},
|
|
||||||
{"4", map[string]bool{}},
|
|
||||||
{"5", map[string]bool{"zrepl_platformtest": true}},
|
|
||||||
{"6", map[string]bool{"zrepl_platformtest": true}},
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func RollupReleaseMostRecentIsBookmarkWithoutSnapshot(ctx *platformtest.Context) {
|
|
||||||
rollupReleaseTest(ctx, func(fs string) []rollupReleaseExpectTags {
|
|
||||||
guid5, err := zfs.ZFSGetGUID(fs, "#5")
|
|
||||||
require.NoError(ctx, err)
|
|
||||||
|
|
||||||
err = zfs.ZFSRelease(ctx, "zrepl_platformtest", fs+"@5")
|
|
||||||
require.NoError(ctx, err)
|
|
||||||
|
|
||||||
err = zfs.ZFSDestroy(fs + "@5")
|
|
||||||
require.NoError(ctx, err)
|
|
||||||
|
|
||||||
err = zfs.ZFSReleaseAllOlderAndIncludingGUID(ctx, fs, guid5, "zrepl_platformtest")
|
|
||||||
require.NoError(ctx, err)
|
|
||||||
|
|
||||||
return []rollupReleaseExpectTags{
|
|
||||||
{"1", map[string]bool{}},
|
|
||||||
{"2", map[string]bool{"zrepl_platformtest_2": true}},
|
|
||||||
{"3", map[string]bool{}},
|
|
||||||
{"4", map[string]bool{}},
|
|
||||||
// {"5", map[string]bool{}}, doesn't exist
|
|
||||||
{"6", map[string]bool{"zrepl_platformtest": true}},
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func RollupReleaseMostRecentIsBookmarkAndSnapshotStillExists(ctx *platformtest.Context) {
|
|
||||||
rollupReleaseTest(ctx, func(fs string) []rollupReleaseExpectTags {
|
|
||||||
guid5, err := zfs.ZFSGetGUID(fs, "#5")
|
|
||||||
require.NoError(ctx, err)
|
|
||||||
|
|
||||||
err = zfs.ZFSReleaseAllOlderAndIncludingGUID(ctx, fs, guid5, "zrepl_platformtest")
|
|
||||||
require.NoError(ctx, err)
|
|
||||||
|
|
||||||
return []rollupReleaseExpectTags{
|
|
||||||
{"1", map[string]bool{}},
|
|
||||||
{"2", map[string]bool{"zrepl_platformtest_2": true}},
|
|
||||||
{"3", map[string]bool{}},
|
|
||||||
{"4", map[string]bool{}},
|
|
||||||
{"5", map[string]bool{}},
|
|
||||||
{"6", map[string]bool{"zrepl_platformtest": true}},
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func RollupReleaseMostRecentDoesntExist(ctx *platformtest.Context) {
|
|
||||||
rollupReleaseTest(ctx, func(fs string) []rollupReleaseExpectTags {
|
|
||||||
|
|
||||||
const nonexistentGuid = 0 // let's take our chances...
|
|
||||||
err := zfs.ZFSReleaseAllOlderAndIncludingGUID(ctx, fs, nonexistentGuid, "zrepl_platformtest")
|
|
||||||
require.Error(ctx, err)
|
|
||||||
require.Contains(ctx, err.Error(), "cannot find snapshot or bookmark with guid 0")
|
|
||||||
|
|
||||||
return []rollupReleaseExpectTags{
|
|
||||||
{"1", map[string]bool{"zrepl_platformtest": true}},
|
|
||||||
{"2", map[string]bool{"zrepl_platformtest_2": true}},
|
|
||||||
{"3", map[string]bool{"zrepl_platformtest": true}},
|
|
||||||
{"4", map[string]bool{"zrepl_platformtest": true}},
|
|
||||||
{"5", map[string]bool{"zrepl_platformtest": true}},
|
|
||||||
{"6", map[string]bool{"zrepl_platformtest": true}},
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -17,14 +17,14 @@ func GetNonexistent(ctx *platformtest.Context) {
|
|||||||
`)
|
`)
|
||||||
|
|
||||||
// test raw
|
// 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 {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// test nonexistent filesystem
|
// test nonexistent filesystem
|
||||||
nonexistent := fmt.Sprintf("%s/nonexistent filesystem", ctx.RootDataset)
|
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 {
|
if err == nil {
|
||||||
panic(props)
|
panic(props)
|
||||||
}
|
}
|
||||||
@@ -37,7 +37,7 @@ func GetNonexistent(ctx *platformtest.Context) {
|
|||||||
|
|
||||||
// test nonexistent snapshot
|
// test nonexistent snapshot
|
||||||
nonexistent = fmt.Sprintf("%s/foo bar@non existent", ctx.RootDataset)
|
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 {
|
if err == nil {
|
||||||
panic(props)
|
panic(props)
|
||||||
}
|
}
|
||||||
@@ -50,7 +50,7 @@ func GetNonexistent(ctx *platformtest.Context) {
|
|||||||
|
|
||||||
// test nonexistent bookmark
|
// test nonexistent bookmark
|
||||||
nonexistent = fmt.Sprintf("%s/foo bar#non existent", ctx.RootDataset)
|
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 {
|
if err == nil {
|
||||||
panic(props)
|
panic(props)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"math/rand"
|
"math/rand"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@@ -14,8 +15,8 @@ import (
|
|||||||
"github.com/zrepl/zrepl/zfs"
|
"github.com/zrepl/zrepl/zfs"
|
||||||
)
|
)
|
||||||
|
|
||||||
func sendArgVersion(fs, relName string) zfs.ZFSSendArgVersion {
|
func sendArgVersion(ctx *platformtest.Context, fs, relName string) zfs.ZFSSendArgVersion {
|
||||||
guid, err := zfs.ZFSGetGUID(fs, relName)
|
guid, err := zfs.ZFSGetGUID(ctx, fs, relName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@@ -25,6 +26,14 @@ func sendArgVersion(fs, relName string) zfs.ZFSSendArgVersion {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
func mustDatasetPath(fs string) *zfs.DatasetPath {
|
||||||
p, err := zfs.NewDatasetPath(fs)
|
p, err := zfs.NewDatasetPath(fs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -33,7 +42,7 @@ func mustDatasetPath(fs string) *zfs.DatasetPath {
|
|||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
func mustSnapshot(snap string) {
|
func mustSnapshot(ctx *platformtest.Context, snap string) {
|
||||||
if err := zfs.EntityNamecheck(snap, zfs.EntityTypeSnapshot); err != nil {
|
if err := zfs.EntityNamecheck(snap, zfs.EntityTypeSnapshot); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@@ -41,16 +50,16 @@ func mustSnapshot(snap string) {
|
|||||||
if len(comps) != 2 {
|
if len(comps) != 2 {
|
||||||
panic(comps)
|
panic(comps)
|
||||||
}
|
}
|
||||||
err := zfs.ZFSSnapshot(mustDatasetPath(comps[0]), comps[1], false)
|
err := zfs.ZFSSnapshot(ctx, mustDatasetPath(comps[0]), comps[1], false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func mustGetProps(entity string) zfs.ZFSPropCreateTxgAndGuidProps {
|
func mustGetFilesystemVersion(ctx *platformtest.Context, snapOrBookmark string) zfs.FilesystemVersion {
|
||||||
props, err := zfs.ZFSGetCreateTXGAndGuid(entity)
|
v, err := zfs.ZFSGetFilesystemVersion(ctx, snapOrBookmark)
|
||||||
check(err)
|
check(err)
|
||||||
return props
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
func check(err error) {
|
func check(err error) {
|
||||||
@@ -78,7 +87,7 @@ type dummySnapshotSituation struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type resumeSituation struct {
|
type resumeSituation struct {
|
||||||
sendArgs zfs.ZFSSendArgs
|
sendArgs zfs.ZFSSendArgsUnvalidated
|
||||||
recvOpts zfs.RecvOptions
|
recvOpts zfs.RecvOptions
|
||||||
sendErr, recvErr error
|
sendErr, recvErr error
|
||||||
recvErrDecoded *zfs.RecvFailedWithResumeTokenErr
|
recvErrDecoded *zfs.RecvFailedWithResumeTokenErr
|
||||||
@@ -87,7 +96,7 @@ type resumeSituation struct {
|
|||||||
func makeDummyDataSnapshots(ctx *platformtest.Context, sendFS string) (situation dummySnapshotSituation) {
|
func makeDummyDataSnapshots(ctx *platformtest.Context, sendFS string) (situation dummySnapshotSituation) {
|
||||||
|
|
||||||
situation.sendFS = sendFS
|
situation.sendFS = sendFS
|
||||||
sendFSMount, err := zfs.ZFSGetMountpoint(sendFS)
|
sendFSMount, err := zfs.ZFSGetMountpoint(ctx, sendFS)
|
||||||
require.NoError(ctx, err)
|
require.NoError(ctx, err)
|
||||||
require.True(ctx, sendFSMount.Mounted)
|
require.True(ctx, sendFSMount.Mounted)
|
||||||
|
|
||||||
@@ -95,28 +104,33 @@ func makeDummyDataSnapshots(ctx *platformtest.Context, sendFS string) (situation
|
|||||||
situation.dummyDataLen = dummyLen
|
situation.dummyDataLen = dummyLen
|
||||||
|
|
||||||
writeDummyData(path.Join(sendFSMount.Mountpoint, "dummy_data"), dummyLen)
|
writeDummyData(path.Join(sendFSMount.Mountpoint, "dummy_data"), dummyLen)
|
||||||
mustSnapshot(sendFS + "@a snapshot")
|
mustSnapshot(ctx, sendFS+"@a snapshot")
|
||||||
snapA := sendArgVersion(sendFS, "@a snapshot")
|
snapA := sendArgVersion(ctx, sendFS, "@a snapshot")
|
||||||
situation.snapA = &snapA
|
situation.snapA = &snapA
|
||||||
|
|
||||||
writeDummyData(path.Join(sendFSMount.Mountpoint, "dummy_data"), dummyLen)
|
writeDummyData(path.Join(sendFSMount.Mountpoint, "dummy_data"), dummyLen)
|
||||||
mustSnapshot(sendFS + "@b snapshot")
|
mustSnapshot(ctx, sendFS+"@b snapshot")
|
||||||
snapB := sendArgVersion(sendFS, "@b snapshot")
|
snapB := sendArgVersion(ctx, sendFS, "@b snapshot")
|
||||||
situation.snapB = &snapB
|
situation.snapB = &snapB
|
||||||
|
|
||||||
return situation
|
return situation
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeResumeSituation(ctx *platformtest.Context, src dummySnapshotSituation, recvFS string, sendArgs zfs.ZFSSendArgs, recvOptions zfs.RecvOptions) *resumeSituation {
|
func makeResumeSituation(ctx *platformtest.Context, src dummySnapshotSituation, recvFS string, sendArgs zfs.ZFSSendArgsUnvalidated, recvOptions zfs.RecvOptions) *resumeSituation {
|
||||||
|
|
||||||
situation := &resumeSituation{}
|
situation := &resumeSituation{}
|
||||||
|
|
||||||
situation.sendArgs = sendArgs
|
situation.sendArgs = sendArgs
|
||||||
situation.recvOpts = recvOptions
|
situation.recvOpts = recvOptions
|
||||||
require.True(ctx, recvOptions.SavePartialRecvState, "this method would be pointeless otherwise")
|
require.True(ctx, recvOptions.SavePartialRecvState, "this method would be pointless otherwise")
|
||||||
require.Equal(ctx, sendArgs.FS, src.sendFS)
|
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, sendArgs)
|
copier, err := zfs.ZFSSend(ctx, sendArgsValidated)
|
||||||
situation.sendErr = err
|
situation.sendErr = err
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return situation
|
return situation
|
||||||
@@ -137,3 +151,26 @@ func makeResumeSituation(ctx *platformtest.Context, src dummySnapshotSituation,
|
|||||||
|
|
||||||
return situation
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,22 +19,22 @@ func IdempotentBookmark(ctx *platformtest.Context) {
|
|||||||
|
|
||||||
fs := fmt.Sprintf("%s/foo bar", ctx.RootDataset)
|
fs := fmt.Sprintf("%s/foo bar", ctx.RootDataset)
|
||||||
|
|
||||||
asnap := sendArgVersion(fs, "@a snap")
|
asnap := sendArgVersion(ctx, fs, "@a snap")
|
||||||
anotherSnap := sendArgVersion(fs, "@another snap")
|
anotherSnap := sendArgVersion(ctx, fs, "@another snap")
|
||||||
|
|
||||||
err := zfs.ZFSBookmark(fs, asnap, "a bookmark")
|
err := zfs.ZFSBookmark(ctx, fs, asnap, "a bookmark")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// do it again, should be idempotent
|
// do it again, should be idempotent
|
||||||
err = zfs.ZFSBookmark(fs, asnap, "a bookmark")
|
err = zfs.ZFSBookmark(ctx, fs, asnap, "a bookmark")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// should fail for another snapshot
|
// should fail for another snapshot
|
||||||
err = zfs.ZFSBookmark(fs, anotherSnap, "a bookmark")
|
err = zfs.ZFSBookmark(ctx, fs, anotherSnap, "a bookmark")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@@ -43,12 +43,12 @@ func IdempotentBookmark(ctx *platformtest.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// destroy the snapshot
|
// destroy the snapshot
|
||||||
if err := zfs.ZFSDestroy(fmt.Sprintf("%s@a snap", fs)); err != nil {
|
if err := zfs.ZFSDestroy(ctx, fmt.Sprintf("%s@a snap", fs)); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// do it again, should fail with special error type
|
// do it again, should fail with special error type
|
||||||
err = zfs.ZFSBookmark(fs, asnap, "a bookmark")
|
err = zfs.ZFSBookmark(ctx, fs, asnap, "a bookmark")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ func IdempotentDestroy(ctx *platformtest.Context) {
|
|||||||
`)
|
`)
|
||||||
|
|
||||||
fs := fmt.Sprintf("%s/foo bar", ctx.RootDataset)
|
fs := fmt.Sprintf("%s/foo bar", ctx.RootDataset)
|
||||||
asnap := sendArgVersion(fs, "@a snap")
|
asnap := sendArgVersion(ctx, fs, "@a snap")
|
||||||
err := zfs.ZFSBookmark(fs, asnap, "a bookmark")
|
err := zfs.ZFSBookmark(ctx, fs, asnap, "a bookmark")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@@ -41,17 +41,17 @@ func IdempotentDestroy(ctx *platformtest.Context) {
|
|||||||
log.Printf("SUBBEGIN testing idempotent destroy %q for path %q", c.description, c.path)
|
log.Printf("SUBBEGIN testing idempotent destroy %q for path %q", c.description, c.path)
|
||||||
|
|
||||||
log.Println("destroy existing")
|
log.Println("destroy existing")
|
||||||
err = zfs.ZFSDestroy(c.path)
|
err = zfs.ZFSDestroy(ctx, c.path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
log.Println("destroy again, non-idempotently, must error")
|
log.Println("destroy again, non-idempotently, must error")
|
||||||
err = zfs.ZFSDestroy(c.path)
|
err = zfs.ZFSDestroy(ctx, c.path)
|
||||||
if _, ok := err.(*zfs.DatasetDoesNotExist); !ok {
|
if _, ok := err.(*zfs.DatasetDoesNotExist); !ok {
|
||||||
panic(fmt.Sprintf("%T: %s", err, err))
|
panic(fmt.Sprintf("%T: %s", err, err))
|
||||||
}
|
}
|
||||||
log.Println("destroy again, idempotently, must not error")
|
log.Println("destroy again, idempotently, must not error")
|
||||||
err = zfs.ZFSDestroyIdempotent(c.path)
|
err = zfs.ZFSDestroyIdempotent(ctx, c.path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@@ -62,12 +62,12 @@ func IdempotentDestroy(ctx *platformtest.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// also test idempotent destroy for cases where the parent dataset does not exist
|
// also test idempotent destroy for cases where the parent dataset does not exist
|
||||||
err = zfs.ZFSDestroyIdempotent(fmt.Sprintf("%s/not foo bar@nonexistent snapshot", ctx.RootDataset))
|
err = zfs.ZFSDestroyIdempotent(ctx, fmt.Sprintf("%s/not foo bar@nonexistent snapshot", ctx.RootDataset))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = zfs.ZFSDestroyIdempotent(fmt.Sprintf("%s/not foo bar#nonexistent bookmark", ctx.RootDataset))
|
err = zfs.ZFSDestroyIdempotent(ctx, fmt.Sprintf("%s/not foo bar#nonexistent bookmark", ctx.RootDataset))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ func IdempotentHold(ctx *platformtest.Context) {
|
|||||||
`)
|
`)
|
||||||
|
|
||||||
fs := fmt.Sprintf("%s/foo bar", ctx.RootDataset)
|
fs := fmt.Sprintf("%s/foo bar", ctx.RootDataset)
|
||||||
v1 := sendArgVersion(fs, "@1")
|
v1 := fsversion(ctx, fs, "@1")
|
||||||
|
|
||||||
tag := "zrepl_platformtest"
|
tag := "zrepl_platformtest"
|
||||||
err := zfs.ZFSHold(ctx, fs, v1, tag)
|
err := zfs.ZFSHold(ctx, fs, v1, tag)
|
||||||
@@ -34,14 +34,4 @@ func IdempotentHold(ctx *platformtest.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
vnonexistent := zfs.ZFSSendArgVersion{
|
|
||||||
RelName: "@nonexistent",
|
|
||||||
GUID: 0xbadf00d,
|
|
||||||
}
|
|
||||||
err = zfs.ZFSHold(ctx, fs, vnonexistent, tag)
|
|
||||||
if err == nil {
|
|
||||||
panic("still expecting error for nonexistent snapshot")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/endpoint"
|
"github.com/zrepl/zrepl/endpoint"
|
||||||
"github.com/zrepl/zrepl/platformtest"
|
"github.com/zrepl/zrepl/platformtest"
|
||||||
"github.com/zrepl/zrepl/zfs"
|
"github.com/zrepl/zrepl/zfs"
|
||||||
@@ -31,7 +32,7 @@ func ReplicationCursor(ctx *platformtest.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fs := ds.ToString()
|
fs := ds.ToString()
|
||||||
snap := sendArgVersion(fs, "@1 with space")
|
snap := fsversion(ctx, fs, "@1 with space")
|
||||||
|
|
||||||
destroyed, err := endpoint.MoveReplicationCursor(ctx, fs, &snap, jobid)
|
destroyed, err := endpoint.MoveReplicationCursor(ctx, fs, &snap, jobid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -39,7 +40,7 @@ func ReplicationCursor(ctx *platformtest.Context) {
|
|||||||
}
|
}
|
||||||
assert.Empty(ctx, destroyed)
|
assert.Empty(ctx, destroyed)
|
||||||
|
|
||||||
snapProps, err := zfs.ZFSGetCreateTXGAndGuid(snap.FullPath(fs))
|
snapProps, err := zfs.ZFSGetFilesystemVersion(ctx, snap.FullPath(fs))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@@ -56,13 +57,13 @@ func ReplicationCursor(ctx *platformtest.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// try moving
|
// try moving
|
||||||
cursor1BookmarkName, err := endpoint.ReplicationCursorBookmarkName(fs, snap.GUID, jobid)
|
cursor1BookmarkName, err := endpoint.ReplicationCursorBookmarkName(fs, snap.Guid, jobid)
|
||||||
require.NoError(ctx, err)
|
require.NoError(ctx, err)
|
||||||
|
|
||||||
snap2 := sendArgVersion(fs, "@2 with space")
|
snap2 := fsversion(ctx, fs, "@2 with space")
|
||||||
destroyed, err = endpoint.MoveReplicationCursor(ctx, fs, &snap2, jobid)
|
destroyed, err = endpoint.MoveReplicationCursor(ctx, fs, &snap2, jobid)
|
||||||
require.NoError(ctx, err)
|
require.NoError(ctx, err)
|
||||||
require.Equal(ctx, 1, len(destroyed))
|
require.Equal(ctx, 1, len(destroyed))
|
||||||
require.Equal(ctx, zfs.Bookmark, destroyed[0].Type)
|
require.Equal(ctx, endpoint.AbstractionReplicationCursorBookmarkV2, destroyed[0].GetType())
|
||||||
require.Equal(ctx, cursor1BookmarkName, destroyed[0].Name)
|
require.Equal(ctx, cursor1BookmarkName, destroyed[0].GetName())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ func ResumableRecvAndTokenHandling(ctx *platformtest.Context) {
|
|||||||
|
|
||||||
src := makeDummyDataSnapshots(ctx, sendFS)
|
src := makeDummyDataSnapshots(ctx, sendFS)
|
||||||
|
|
||||||
s := makeResumeSituation(ctx, src, recvFS, zfs.ZFSSendArgs{
|
s := makeResumeSituation(ctx, src, recvFS, zfs.ZFSSendArgsUnvalidated{
|
||||||
FS: sendFS,
|
FS: sendFS,
|
||||||
To: src.snapA,
|
To: src.snapA,
|
||||||
Encrypted: &zfs.NilBool{B: false},
|
Encrypted: &zfs.NilBool{B: false},
|
||||||
@@ -40,7 +40,7 @@ func ResumableRecvAndTokenHandling(ctx *platformtest.Context) {
|
|||||||
require.True(ctx, ok)
|
require.True(ctx, ok)
|
||||||
|
|
||||||
// we know that support on sendFS implies support on recvFS
|
// we know that support on sendFS implies support on recvFS
|
||||||
// => asser that if we don't support resumed recv, the method returns ""
|
// => assert that if we don't support resumed recv, the method returns ""
|
||||||
tok, err := zfs.ZFSGetReceiveResumeTokenOrEmptyStringIfNotSupported(ctx, mustDatasetPath(recvFS))
|
tok, err := zfs.ZFSGetReceiveResumeTokenOrEmptyStringIfNotSupported(ctx, mustDatasetPath(recvFS))
|
||||||
check(err)
|
check(err)
|
||||||
require.Equal(ctx, "", tok)
|
require.Equal(ctx, "", tok)
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ type resumeTokenTest struct {
|
|||||||
|
|
||||||
func (rtt *resumeTokenTest) Test(t *platformtest.Context) {
|
func (rtt *resumeTokenTest) Test(t *platformtest.Context) {
|
||||||
|
|
||||||
resumeSendSupported, err := zfs.ResumeSendSupported()
|
resumeSendSupported, err := zfs.ResumeSendSupported(t)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("cannot determine whether resume supported: %T %s", err, err)
|
t.Errorf("cannot determine whether resume supported: %T %s", err, err)
|
||||||
t.FailNow()
|
t.FailNow()
|
||||||
|
|||||||
@@ -23,9 +23,9 @@ func SendArgsValidationEncryptedSendOfUnencryptedDatasetForbidden(ctx *platformt
|
|||||||
`)
|
`)
|
||||||
|
|
||||||
fs := fmt.Sprintf("%s/send er", ctx.RootDataset)
|
fs := fmt.Sprintf("%s/send er", ctx.RootDataset)
|
||||||
props := mustGetProps(fs + "@a snap")
|
props := mustGetFilesystemVersion(ctx, fs+"@a snap")
|
||||||
|
|
||||||
sendArgs := zfs.ZFSSendArgs{
|
sendArgs, err := zfs.ZFSSendArgsUnvalidated{
|
||||||
FS: fs,
|
FS: fs,
|
||||||
To: &zfs.ZFSSendArgVersion{
|
To: &zfs.ZFSSendArgVersion{
|
||||||
RelName: "@a snap",
|
RelName: "@a snap",
|
||||||
@@ -33,11 +33,16 @@ func SendArgsValidationEncryptedSendOfUnencryptedDatasetForbidden(ctx *platformt
|
|||||||
},
|
},
|
||||||
Encrypted: &zfs.NilBool{B: true},
|
Encrypted: &zfs.NilBool{B: true},
|
||||||
ResumeToken: "",
|
ResumeToken: "",
|
||||||
}
|
}.Validate(ctx)
|
||||||
stream, err := zfs.ZFSSend(ctx, sendArgs)
|
|
||||||
|
var stream *zfs.ReadCloserCopier
|
||||||
|
if err == nil {
|
||||||
|
stream, err = zfs.ZFSSend(ctx, sendArgs) // no shadow
|
||||||
if err == nil {
|
if err == nil {
|
||||||
defer stream.Close()
|
defer stream.Close()
|
||||||
}
|
}
|
||||||
|
// fallthrough
|
||||||
|
}
|
||||||
|
|
||||||
if expectNotSupportedErr {
|
if expectNotSupportedErr {
|
||||||
require.Error(ctx, err)
|
require.Error(ctx, err)
|
||||||
@@ -58,7 +63,7 @@ func SendArgsValidationResumeTokenEncryptionMismatchForbidden(ctx *platformtest.
|
|||||||
if !supported {
|
if !supported {
|
||||||
ctx.SkipNow()
|
ctx.SkipNow()
|
||||||
}
|
}
|
||||||
supported, err = zfs.ResumeSendSupported()
|
supported, err = zfs.ResumeSendSupported(ctx)
|
||||||
check(err)
|
check(err)
|
||||||
if !supported {
|
if !supported {
|
||||||
ctx.SkipNow()
|
ctx.SkipNow()
|
||||||
@@ -76,7 +81,7 @@ func SendArgsValidationResumeTokenEncryptionMismatchForbidden(ctx *platformtest.
|
|||||||
|
|
||||||
src := makeDummyDataSnapshots(ctx, sendFS)
|
src := makeDummyDataSnapshots(ctx, sendFS)
|
||||||
|
|
||||||
unencS := makeResumeSituation(ctx, src, unencRecvFS, zfs.ZFSSendArgs{
|
unencS := makeResumeSituation(ctx, src, unencRecvFS, zfs.ZFSSendArgsUnvalidated{
|
||||||
FS: sendFS,
|
FS: sendFS,
|
||||||
To: src.snapA,
|
To: src.snapA,
|
||||||
Encrypted: &zfs.NilBool{B: false}, // !
|
Encrypted: &zfs.NilBool{B: false}, // !
|
||||||
@@ -85,7 +90,7 @@ func SendArgsValidationResumeTokenEncryptionMismatchForbidden(ctx *platformtest.
|
|||||||
SavePartialRecvState: true,
|
SavePartialRecvState: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
encS := makeResumeSituation(ctx, src, encRecvFS, zfs.ZFSSendArgs{
|
encS := makeResumeSituation(ctx, src, encRecvFS, zfs.ZFSSendArgsUnvalidated{
|
||||||
FS: sendFS,
|
FS: sendFS,
|
||||||
To: src.snapA,
|
To: src.snapA,
|
||||||
Encrypted: &zfs.NilBool{B: true}, // !
|
Encrypted: &zfs.NilBool{B: true}, // !
|
||||||
@@ -97,16 +102,10 @@ func SendArgsValidationResumeTokenEncryptionMismatchForbidden(ctx *platformtest.
|
|||||||
// threat model: use of a crafted resume token that requests an unencrypted send
|
// threat model: use of a crafted resume token that requests an unencrypted send
|
||||||
// but send args require encrypted send
|
// but send args require encrypted send
|
||||||
{
|
{
|
||||||
var maliciousSend zfs.ZFSSendArgs = encS.sendArgs
|
var maliciousSend zfs.ZFSSendArgsUnvalidated = encS.sendArgs
|
||||||
maliciousSend.ResumeToken = unencS.recvErrDecoded.ResumeTokenRaw
|
maliciousSend.ResumeToken = unencS.recvErrDecoded.ResumeTokenRaw
|
||||||
|
|
||||||
stream, err := zfs.ZFSSend(ctx, maliciousSend)
|
_, err := maliciousSend.Validate(ctx)
|
||||||
if err == nil {
|
|
||||||
defer stream.Close()
|
|
||||||
}
|
|
||||||
require.Nil(ctx, stream)
|
|
||||||
require.Error(ctx, err)
|
|
||||||
ctx.Logf("send err: %T %s", err, err)
|
|
||||||
validationErr, ok := err.(*zfs.ZFSSendArgsValidationError)
|
validationErr, ok := err.(*zfs.ZFSSendArgsValidationError)
|
||||||
require.True(ctx, ok)
|
require.True(ctx, ok)
|
||||||
require.Equal(ctx, validationErr.What, zfs.ZFSSendArgsResumeTokenMismatch)
|
require.Equal(ctx, validationErr.What, zfs.ZFSSendArgsResumeTokenMismatch)
|
||||||
@@ -117,17 +116,13 @@ func SendArgsValidationResumeTokenEncryptionMismatchForbidden(ctx *platformtest.
|
|||||||
require.Equal(ctx, mismatchError.What, zfs.ZFSSendArgsResumeTokenMismatchEncryptionNotSet)
|
require.Equal(ctx, mismatchError.What, zfs.ZFSSendArgsResumeTokenMismatchEncryptionNotSet)
|
||||||
}
|
}
|
||||||
|
|
||||||
// threat model: use of a crafted resume token that requests an encryped send
|
// threat model: use of a crafted resume token that requests an encrypted send
|
||||||
// but send args require unencrypted send
|
// but send args require unencrypted send
|
||||||
{
|
{
|
||||||
var maliciousSend zfs.ZFSSendArgs = unencS.sendArgs
|
var maliciousSend zfs.ZFSSendArgsUnvalidated = unencS.sendArgs
|
||||||
maliciousSend.ResumeToken = encS.recvErrDecoded.ResumeTokenRaw
|
maliciousSend.ResumeToken = encS.recvErrDecoded.ResumeTokenRaw
|
||||||
|
|
||||||
stream, err := zfs.ZFSSend(ctx, maliciousSend)
|
_, err := maliciousSend.Validate(ctx)
|
||||||
if err == nil {
|
|
||||||
defer stream.Close()
|
|
||||||
}
|
|
||||||
require.Nil(ctx, stream)
|
|
||||||
require.Error(ctx, err)
|
require.Error(ctx, err)
|
||||||
ctx.Logf("send err: %T %s", err, err)
|
ctx.Logf("send err: %T %s", err, err)
|
||||||
validationErr, ok := err.(*zfs.ZFSSendArgsValidationError)
|
validationErr, ok := err.(*zfs.ZFSSendArgsValidationError)
|
||||||
@@ -149,7 +144,7 @@ func SendArgsValidationResumeTokenDifferentFilesystemForbidden(ctx *platformtest
|
|||||||
if !supported {
|
if !supported {
|
||||||
ctx.SkipNow()
|
ctx.SkipNow()
|
||||||
}
|
}
|
||||||
supported, err = zfs.ResumeSendSupported()
|
supported, err = zfs.ResumeSendSupported(ctx)
|
||||||
check(err)
|
check(err)
|
||||||
if !supported {
|
if !supported {
|
||||||
ctx.SkipNow()
|
ctx.SkipNow()
|
||||||
@@ -169,7 +164,7 @@ func SendArgsValidationResumeTokenDifferentFilesystemForbidden(ctx *platformtest
|
|||||||
src1 := makeDummyDataSnapshots(ctx, sendFS1)
|
src1 := makeDummyDataSnapshots(ctx, sendFS1)
|
||||||
src2 := makeDummyDataSnapshots(ctx, sendFS2)
|
src2 := makeDummyDataSnapshots(ctx, sendFS2)
|
||||||
|
|
||||||
rs := makeResumeSituation(ctx, src1, recvFS, zfs.ZFSSendArgs{
|
rs := makeResumeSituation(ctx, src1, recvFS, zfs.ZFSSendArgsUnvalidated{
|
||||||
FS: sendFS1,
|
FS: sendFS1,
|
||||||
To: src1.snapA,
|
To: src1.snapA,
|
||||||
Encrypted: &zfs.NilBool{B: false},
|
Encrypted: &zfs.NilBool{B: false},
|
||||||
@@ -180,7 +175,7 @@ func SendArgsValidationResumeTokenDifferentFilesystemForbidden(ctx *platformtest
|
|||||||
|
|
||||||
// threat model: forged resume token tries to steal a full send of snapA on fs2 by
|
// 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
|
// presenting a resume token for full send of snapA on fs1
|
||||||
var maliciousSend zfs.ZFSSendArgs = zfs.ZFSSendArgs{
|
var maliciousSend zfs.ZFSSendArgsUnvalidated = zfs.ZFSSendArgsUnvalidated{
|
||||||
FS: sendFS2,
|
FS: sendFS2,
|
||||||
To: &zfs.ZFSSendArgVersion{
|
To: &zfs.ZFSSendArgVersion{
|
||||||
RelName: src2.snapA.RelName,
|
RelName: src2.snapA.RelName,
|
||||||
@@ -189,12 +184,7 @@ func SendArgsValidationResumeTokenDifferentFilesystemForbidden(ctx *platformtest
|
|||||||
Encrypted: &zfs.NilBool{B: false},
|
Encrypted: &zfs.NilBool{B: false},
|
||||||
ResumeToken: rs.recvErrDecoded.ResumeTokenRaw,
|
ResumeToken: rs.recvErrDecoded.ResumeTokenRaw,
|
||||||
}
|
}
|
||||||
|
_, err = maliciousSend.Validate(ctx)
|
||||||
stream, err := zfs.ZFSSend(ctx, maliciousSend)
|
|
||||||
if err == nil {
|
|
||||||
defer stream.Close()
|
|
||||||
}
|
|
||||||
require.Nil(ctx, stream)
|
|
||||||
require.Error(ctx, err)
|
require.Error(ctx, err)
|
||||||
ctx.Logf("send err: %T %s", err, err)
|
ctx.Logf("send err: %T %s", err, err)
|
||||||
validationErr, ok := err.(*zfs.ZFSSendArgsValidationError)
|
validationErr, ok := err.(*zfs.ZFSSendArgsValidationError)
|
||||||
|
|||||||
@@ -18,11 +18,6 @@ var Cases = []Case{
|
|||||||
UndestroyableSnapshotParsing,
|
UndestroyableSnapshotParsing,
|
||||||
GetNonexistent,
|
GetNonexistent,
|
||||||
ReplicationCursor,
|
ReplicationCursor,
|
||||||
RollupReleaseIncluding,
|
|
||||||
RollupReleaseExcluding,
|
|
||||||
RollupReleaseMostRecentIsBookmarkWithoutSnapshot,
|
|
||||||
RollupReleaseMostRecentIsBookmarkAndSnapshotStillExists,
|
|
||||||
RollupReleaseMostRecentDoesntExist,
|
|
||||||
IdempotentHold,
|
IdempotentHold,
|
||||||
IdempotentBookmark,
|
IdempotentBookmark,
|
||||||
IdempotentDestroy,
|
IdempotentDestroy,
|
||||||
@@ -31,4 +26,9 @@ var Cases = []Case{
|
|||||||
SendArgsValidationEncryptedSendOfUnencryptedDatasetForbidden,
|
SendArgsValidationEncryptedSendOfUnencryptedDatasetForbidden,
|
||||||
SendArgsValidationResumeTokenEncryptionMismatchForbidden,
|
SendArgsValidationResumeTokenEncryptionMismatchForbidden,
|
||||||
SendArgsValidationResumeTokenDifferentFilesystemForbidden,
|
SendArgsValidationResumeTokenDifferentFilesystemForbidden,
|
||||||
|
ListFilesystemVersionsTypeFilteringAndPrefix,
|
||||||
|
ListFilesystemVersionsFilesystemNotExist,
|
||||||
|
ListFilesystemVersionsFilesystemNotExist,
|
||||||
|
ListFilesystemVersionsUserrefs,
|
||||||
|
ListFilesystemsNoFilter,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ func UndestroyableSnapshotParsing(t *platformtest.Context) {
|
|||||||
R zfs hold zrepl_platformtest "${ROOTDS}/foo bar@4 5 6"
|
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 {
|
if err == nil {
|
||||||
panic("expecting destroy error due to hold")
|
panic("expecting destroy error due to hold")
|
||||||
}
|
}
|
||||||
|
|||||||
+35
-10
@@ -18,7 +18,32 @@ Hence, when trying to map algorithm to implementation, use the code in package `
|
|||||||
* the recv-side fs doesn't get newer snapshots than `to` in the meantime
|
* 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
|
* 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
|
* 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
|
* 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)
|
* We also have [Notes on Planning and Executing Replication of Multiple Filesystems](#zrepl-algo-multiple-filesystems-notes)
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -35,7 +60,7 @@ The algorithm **ensures resumability** of the replication step in presence of
|
|||||||
* network failures at any time
|
* network failures at any time
|
||||||
* other instances of this algorithm executing the same step in parallel (e.g. concurrent replication to different destinations)
|
* 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 ownersip of parts of the ZFS hold tag namespace and the bookmark namespace**:
|
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
|
* holds with prefix `zrepl_STEP` on any snapshot are reserved for zrepl
|
||||||
* bookmarks with prefix `zrepl_STEP` are reserved for zrepl
|
* bookmarks with prefix `zrepl_STEP` are reserved for zrepl
|
||||||
|
|
||||||
@@ -55,7 +80,7 @@ The replication step (full `to` send or `from => to` send) is *complete* iff the
|
|||||||
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:
|
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.**
|
**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 constitue completion, since there may still be post-recv actions to be performed on sender and receiver.
|
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
|
#### Job and Job ID
|
||||||
This algorithm supports that *multiple* instance of it run in parallel on the *same* step (full `to` / `from => to` pair).
|
This algorithm supports that *multiple* instance of it run in parallel on the *same* step (full `to` / `from => to` pair).
|
||||||
@@ -117,7 +142,7 @@ Recv-side: no-op
|
|||||||
# => doesn't work, because zfs recv is implemented as a `clone` internally, that's exactly what we want
|
# => doesn't work, because zfs recv is implemented as a `clone` internally, that's exactly what we want
|
||||||
```
|
```
|
||||||
|
|
||||||
- if recv-side `to` exists, goto cleaup-phase (no replication to do)
|
- 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)
|
- `to` cannot be destroyed while being received, because it isn't visible as a snapshot yet (it isn't yet one after all)
|
||||||
|
|
||||||
@@ -142,7 +167,7 @@ Network failures during replication can be recovered from using resumable send &
|
|||||||
- Network failure during the replication
|
- Network failure during the replication
|
||||||
- send-side `from` and `to` are still present due to zfs holds
|
- 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)
|
- recv-side `from` is still present because the partial receive state prevents its destruction (see prepare-phase)
|
||||||
- if recv-side hasa resume token, the resume token will continue to work on the sender because `from`s and `to` are still present
|
- 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
|
- 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
|
- 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 `to` doesn't have a hold and could be destroyed anytime
|
||||||
@@ -221,7 +246,7 @@ It builds a diff between the sender and receiver filesystem bookmarks+snapshots
|
|||||||
In case of conflict, the algorithm errors out with a conflict description that can be used to manually or automatically resolve the conflict.
|
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".
|
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 aquiring appropriate zfs holds.
|
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).
|
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.
|
However, permanent errors result in the plan being cancelled.
|
||||||
|
|
||||||
@@ -262,22 +287,22 @@ If fast-forward is not possible, produce a conflict description and ERROR OUT.<b
|
|||||||
TODOs:
|
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, ...)
|
- 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 aquiring holds or fsstep bookmarks on the sending side
|
**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})`
|
- `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})`
|
- `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):
|
**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)`
|
- `res_tok := receiver.ResumeToken(fs)`
|
||||||
- `rmrfsv := receiver.MostRecentFilesystemVersion(fs)`
|
- `rmrfsv := receiver.MostRecentFilesystemVersion(fs)`
|
||||||
- if `res_tok != nil`: ensure that `res_tok` has a correspondinng step in `STEPS`, otherwise ERROR OUT
|
- 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 correspondinng 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 `(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
|
- 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 }`
|
- `rstep := if res_tok != nil { res_tok } else { rmrfsv }`
|
||||||
- `uncompleted_steps := STEPS[find_step_idx(STEPS, rstep).expect("must exist, checked above"):]`
|
- `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.
|
- 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`.
|
All we care about is what needs to be done from `rstep`.
|
||||||
- This is intentional and necessary because we cummutatively release all holds and step bookmarks made for steps that preceed a just-completed step (see next paragraph)
|
- 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/>
|
**Execute uncompleted steps**<br/>
|
||||||
Invoke the "Algorithm for a Single Replication Step" for each step in `uncompleted_steps`.
|
Invoke the "Algorithm for a Single Replication Step" for each step in `uncompleted_steps`.
|
||||||
|
|||||||
@@ -352,10 +352,10 @@ 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
|
||||||
|
|
||||||
stepQueue := newStepQueue()
|
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
|
var fssesDone sync.WaitGroup
|
||||||
for _, f := range a.fss {
|
for _, f := range a.fss {
|
||||||
fssesDone.Add(1)
|
fssesDone.Add(1)
|
||||||
@@ -370,54 +370,57 @@ func (a *attempt) do(ctx context.Context, prev *attempt) {
|
|||||||
a.finishedAt = time.Now()
|
a.finishedAt = time.Now()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fs *fs) do(ctx context.Context, pq *stepQueue, prev *fs) {
|
func (f *fs) debug(format string, args ...interface{}) {
|
||||||
|
debugPrefix("fs=%s", f.fs.ReportInfo().Name)(format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
defer fs.l.Lock().Unlock()
|
func (f *fs) do(ctx context.Context, pq *stepQueue, prev *fs) {
|
||||||
|
|
||||||
|
defer f.l.Lock().Unlock()
|
||||||
|
|
||||||
// get planned steps from replication logic
|
// get planned steps from replication logic
|
||||||
var psteps []Step
|
var psteps []Step
|
||||||
var errTime time.Time
|
var errTime time.Time
|
||||||
var err error
|
var err error
|
||||||
fs.l.DropWhile(func() {
|
f.l.DropWhile(func() {
|
||||||
// TODO hacky
|
// TODO hacky
|
||||||
// choose target time that is earlier than any snapshot, so fs planning is always prioritized
|
// choose target time that is earlier than any snapshot, so fs planning is always prioritized
|
||||||
targetDate := time.Unix(0, 0)
|
targetDate := time.Unix(0, 0)
|
||||||
defer pq.WaitReady(fs, targetDate)()
|
defer pq.WaitReady(f, targetDate)()
|
||||||
psteps, err = fs.fs.PlanFS(ctx) // no shadow
|
psteps, err = f.fs.PlanFS(ctx) // no shadow
|
||||||
errTime = time.Now() // no shadow
|
errTime = time.Now() // no shadow
|
||||||
})
|
})
|
||||||
debug := debugPrefix("fs=%s", fs.fs.ReportInfo().Name)
|
f.planning.done = true
|
||||||
fs.planning.done = true
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fs.planning.err = newTimedError(err, errTime)
|
f.planning.err = newTimedError(err, errTime)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for _, pstep := range psteps {
|
for _, pstep := range psteps {
|
||||||
step := &step{
|
step := &step{
|
||||||
l: fs.l,
|
l: f.l,
|
||||||
step: pstep,
|
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))
|
f.debug("initial len(fs.planned.steps) = %d", len(f.planned.steps))
|
||||||
|
|
||||||
// for not-first attempts, only allow fs.planned.steps
|
// for not-first attempts, only allow fs.planned.steps
|
||||||
// up to including the originally planned target snapshot
|
// up to including the originally planned target snapshot
|
||||||
if prev != nil && prev.planning.done && prev.planning.err == nil {
|
if prev != nil && prev.planning.done && prev.planning.err == nil {
|
||||||
prevUncompleted := prev.planned.steps[prev.planned.step:]
|
prevUncompleted := prev.planned.steps[prev.planned.step:]
|
||||||
if len(prevUncompleted) == 0 {
|
if len(prevUncompleted) == 0 {
|
||||||
debug("prevUncompleted is empty")
|
f.debug("prevUncompleted is empty")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if len(fs.planned.steps) == 0 {
|
if len(f.planned.steps) == 0 {
|
||||||
debug("fs.planned.steps is empty")
|
f.debug("fs.planned.steps is empty")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
prevFailed := prevUncompleted[0]
|
prevFailed := prevUncompleted[0]
|
||||||
curFirst := fs.planned.steps[0]
|
curFirst := f.planned.steps[0]
|
||||||
// we assume that PlanFS retries prevFailed (using curFirst)
|
// we assume that PlanFS retries prevFailed (using curFirst)
|
||||||
if !prevFailed.step.TargetEquals(curFirst.step) {
|
if !prevFailed.step.TargetEquals(curFirst.step) {
|
||||||
debug("Targets don't match")
|
f.debug("Targets don't match")
|
||||||
// Two options:
|
// Two options:
|
||||||
// A: planning algorithm is broken
|
// A: planning algorithm is broken
|
||||||
// B: manual user intervention inbetween
|
// B: manual user intervention inbetween
|
||||||
@@ -433,43 +436,43 @@ 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",
|
msg := fmt.Sprintf("last attempt's uncompleted step %s does not correspond to this attempt's first planned step %s",
|
||||||
stepFmt(prevFailed), stepFmt(curFirst))
|
stepFmt(prevFailed), stepFmt(curFirst))
|
||||||
fs.planned.stepErr = newTimedError(errors.New(msg), time.Now())
|
f.planned.stepErr = newTimedError(errors.New(msg), time.Now())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// only allow until step targets diverge
|
// only allow until step targets diverge
|
||||||
min := len(prevUncompleted)
|
min := len(prevUncompleted)
|
||||||
if min > len(fs.planned.steps) {
|
if min > len(f.planned.steps) {
|
||||||
min = len(fs.planned.steps)
|
min = len(f.planned.steps)
|
||||||
}
|
}
|
||||||
diverge := 0
|
diverge := 0
|
||||||
for ; diverge < min; diverge++ {
|
for ; diverge < min; diverge++ {
|
||||||
debug("diverge compare iteration %d", diverge)
|
f.debug("diverge compare iteration %d", diverge)
|
||||||
if !fs.planned.steps[diverge].step.TargetEquals(prevUncompleted[diverge].step) {
|
if !f.planned.steps[diverge].step.TargetEquals(prevUncompleted[diverge].step) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
debug("diverge is %d", diverge)
|
f.debug("diverge is %d", diverge)
|
||||||
fs.planned.steps = fs.planned.steps[0: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 {
|
for i, s := range f.planned.steps {
|
||||||
var (
|
var (
|
||||||
err error
|
err error
|
||||||
errTime time.Time
|
errTime time.Time
|
||||||
)
|
)
|
||||||
// lock must not be held while executing step in order for reporting to work
|
// lock must not be held while executing step in order for reporting to work
|
||||||
fs.l.DropWhile(func() {
|
f.l.DropWhile(func() {
|
||||||
targetDate := s.step.TargetDate()
|
targetDate := s.step.TargetDate()
|
||||||
defer pq.WaitReady(fs, targetDate)()
|
defer pq.WaitReady(f, targetDate)()
|
||||||
err = s.step.Step(ctx) // no shadow
|
err = s.step.Step(ctx) // no shadow
|
||||||
errTime = time.Now() // no shadow
|
errTime = time.Now() // no shadow
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fs.planned.stepErr = newTimedError(err, errTime)
|
f.planned.stepErr = newTimedError(err, errTime)
|
||||||
break
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ import (
|
|||||||
|
|
||||||
type Logger = logger.Logger
|
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 {
|
func getLog(ctx context.Context) Logger {
|
||||||
l, ok := ctx.Value(contexKeyLogger).(Logger)
|
l, ok := ctx.Value(contextKeyLogger).(Logger)
|
||||||
if !ok {
|
if !ok {
|
||||||
l = logger.NewNullLogger()
|
l = logger.NewNullLogger()
|
||||||
}
|
}
|
||||||
@@ -21,5 +21,5 @@ func getLog(ctx context.Context) Logger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func WithLogger(ctx context.Context, log Logger) context.Context {
|
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()
|
waitBegin := time.Now()
|
||||||
wait(true)
|
wait(true)
|
||||||
waitDuration := time.Since(waitBegin)
|
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])
|
prev, err := json.Marshal(reports[0])
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// FIXME: this test relies on timing and is thus rather flaky
|
// 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) {
|
func TestPqNotconcurrent(t *testing.T) {
|
||||||
var ctr uint32
|
var ctr uint32
|
||||||
q := newStepQueue()
|
q := newStepQueue()
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ func fsvlist(fsv ...string) (r []*FilesystemVersion) {
|
|||||||
r = make([]*FilesystemVersion, len(fsv))
|
r = make([]*FilesystemVersion, len(fsv))
|
||||||
for i, f := range 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, ",")
|
split := strings.Split(f, ",")
|
||||||
if len(split) != 2 {
|
if len(split) != 2 {
|
||||||
panic("invalid fsv spec")
|
panic("invalid fsv spec")
|
||||||
@@ -114,7 +114,7 @@ func TestIncrementalPath_BookmarkSupport(t *testing.T) {
|
|||||||
assert.Equal(t, l("#a,1", "@b,2"), path)
|
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) {
|
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)
|
assert.Equal(t, l("#a,1", "@c,3"), path)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ func (x Tri) String() string {
|
|||||||
return proto.EnumName(Tri_name, int32(x))
|
return proto.EnumName(Tri_name, int32(x))
|
||||||
}
|
}
|
||||||
func (Tri) EnumDescriptor() ([]byte, []int) {
|
func (Tri) EnumDescriptor() ([]byte, []int) {
|
||||||
return fileDescriptor_pdu_0f43b713cd3bf056, []int{0}
|
return fileDescriptor_pdu_e59763dc61674a79, []int{0}
|
||||||
}
|
}
|
||||||
|
|
||||||
type FilesystemVersion_VersionType int32
|
type FilesystemVersion_VersionType int32
|
||||||
@@ -69,7 +69,7 @@ func (x FilesystemVersion_VersionType) String() string {
|
|||||||
return proto.EnumName(FilesystemVersion_VersionType_name, int32(x))
|
return proto.EnumName(FilesystemVersion_VersionType_name, int32(x))
|
||||||
}
|
}
|
||||||
func (FilesystemVersion_VersionType) EnumDescriptor() ([]byte, []int) {
|
func (FilesystemVersion_VersionType) EnumDescriptor() ([]byte, []int) {
|
||||||
return fileDescriptor_pdu_0f43b713cd3bf056, []int{5, 0}
|
return fileDescriptor_pdu_e59763dc61674a79, []int{5, 0}
|
||||||
}
|
}
|
||||||
|
|
||||||
type ListFilesystemReq struct {
|
type ListFilesystemReq struct {
|
||||||
@@ -82,7 +82,7 @@ func (m *ListFilesystemReq) Reset() { *m = ListFilesystemReq{} }
|
|||||||
func (m *ListFilesystemReq) String() string { return proto.CompactTextString(m) }
|
func (m *ListFilesystemReq) String() string { return proto.CompactTextString(m) }
|
||||||
func (*ListFilesystemReq) ProtoMessage() {}
|
func (*ListFilesystemReq) ProtoMessage() {}
|
||||||
func (*ListFilesystemReq) Descriptor() ([]byte, []int) {
|
func (*ListFilesystemReq) Descriptor() ([]byte, []int) {
|
||||||
return fileDescriptor_pdu_0f43b713cd3bf056, []int{0}
|
return fileDescriptor_pdu_e59763dc61674a79, []int{0}
|
||||||
}
|
}
|
||||||
func (m *ListFilesystemReq) XXX_Unmarshal(b []byte) error {
|
func (m *ListFilesystemReq) XXX_Unmarshal(b []byte) error {
|
||||||
return xxx_messageInfo_ListFilesystemReq.Unmarshal(m, b)
|
return xxx_messageInfo_ListFilesystemReq.Unmarshal(m, b)
|
||||||
@@ -113,7 +113,7 @@ func (m *ListFilesystemRes) Reset() { *m = ListFilesystemRes{} }
|
|||||||
func (m *ListFilesystemRes) String() string { return proto.CompactTextString(m) }
|
func (m *ListFilesystemRes) String() string { return proto.CompactTextString(m) }
|
||||||
func (*ListFilesystemRes) ProtoMessage() {}
|
func (*ListFilesystemRes) ProtoMessage() {}
|
||||||
func (*ListFilesystemRes) Descriptor() ([]byte, []int) {
|
func (*ListFilesystemRes) Descriptor() ([]byte, []int) {
|
||||||
return fileDescriptor_pdu_0f43b713cd3bf056, []int{1}
|
return fileDescriptor_pdu_e59763dc61674a79, []int{1}
|
||||||
}
|
}
|
||||||
func (m *ListFilesystemRes) XXX_Unmarshal(b []byte) error {
|
func (m *ListFilesystemRes) XXX_Unmarshal(b []byte) error {
|
||||||
return xxx_messageInfo_ListFilesystemRes.Unmarshal(m, b)
|
return xxx_messageInfo_ListFilesystemRes.Unmarshal(m, b)
|
||||||
@@ -154,7 +154,7 @@ func (m *Filesystem) Reset() { *m = Filesystem{} }
|
|||||||
func (m *Filesystem) String() string { return proto.CompactTextString(m) }
|
func (m *Filesystem) String() string { return proto.CompactTextString(m) }
|
||||||
func (*Filesystem) ProtoMessage() {}
|
func (*Filesystem) ProtoMessage() {}
|
||||||
func (*Filesystem) Descriptor() ([]byte, []int) {
|
func (*Filesystem) Descriptor() ([]byte, []int) {
|
||||||
return fileDescriptor_pdu_0f43b713cd3bf056, []int{2}
|
return fileDescriptor_pdu_e59763dc61674a79, []int{2}
|
||||||
}
|
}
|
||||||
func (m *Filesystem) XXX_Unmarshal(b []byte) error {
|
func (m *Filesystem) XXX_Unmarshal(b []byte) error {
|
||||||
return xxx_messageInfo_Filesystem.Unmarshal(m, b)
|
return xxx_messageInfo_Filesystem.Unmarshal(m, b)
|
||||||
@@ -213,7 +213,7 @@ func (m *ListFilesystemVersionsReq) Reset() { *m = ListFilesystemVersion
|
|||||||
func (m *ListFilesystemVersionsReq) String() string { return proto.CompactTextString(m) }
|
func (m *ListFilesystemVersionsReq) String() string { return proto.CompactTextString(m) }
|
||||||
func (*ListFilesystemVersionsReq) ProtoMessage() {}
|
func (*ListFilesystemVersionsReq) ProtoMessage() {}
|
||||||
func (*ListFilesystemVersionsReq) Descriptor() ([]byte, []int) {
|
func (*ListFilesystemVersionsReq) Descriptor() ([]byte, []int) {
|
||||||
return fileDescriptor_pdu_0f43b713cd3bf056, []int{3}
|
return fileDescriptor_pdu_e59763dc61674a79, []int{3}
|
||||||
}
|
}
|
||||||
func (m *ListFilesystemVersionsReq) XXX_Unmarshal(b []byte) error {
|
func (m *ListFilesystemVersionsReq) XXX_Unmarshal(b []byte) error {
|
||||||
return xxx_messageInfo_ListFilesystemVersionsReq.Unmarshal(m, b)
|
return xxx_messageInfo_ListFilesystemVersionsReq.Unmarshal(m, b)
|
||||||
@@ -251,7 +251,7 @@ func (m *ListFilesystemVersionsRes) Reset() { *m = ListFilesystemVersion
|
|||||||
func (m *ListFilesystemVersionsRes) String() string { return proto.CompactTextString(m) }
|
func (m *ListFilesystemVersionsRes) String() string { return proto.CompactTextString(m) }
|
||||||
func (*ListFilesystemVersionsRes) ProtoMessage() {}
|
func (*ListFilesystemVersionsRes) ProtoMessage() {}
|
||||||
func (*ListFilesystemVersionsRes) Descriptor() ([]byte, []int) {
|
func (*ListFilesystemVersionsRes) Descriptor() ([]byte, []int) {
|
||||||
return fileDescriptor_pdu_0f43b713cd3bf056, []int{4}
|
return fileDescriptor_pdu_e59763dc61674a79, []int{4}
|
||||||
}
|
}
|
||||||
func (m *ListFilesystemVersionsRes) XXX_Unmarshal(b []byte) error {
|
func (m *ListFilesystemVersionsRes) XXX_Unmarshal(b []byte) error {
|
||||||
return xxx_messageInfo_ListFilesystemVersionsRes.Unmarshal(m, b)
|
return xxx_messageInfo_ListFilesystemVersionsRes.Unmarshal(m, b)
|
||||||
@@ -293,7 +293,7 @@ func (m *FilesystemVersion) Reset() { *m = FilesystemVersion{} }
|
|||||||
func (m *FilesystemVersion) String() string { return proto.CompactTextString(m) }
|
func (m *FilesystemVersion) String() string { return proto.CompactTextString(m) }
|
||||||
func (*FilesystemVersion) ProtoMessage() {}
|
func (*FilesystemVersion) ProtoMessage() {}
|
||||||
func (*FilesystemVersion) Descriptor() ([]byte, []int) {
|
func (*FilesystemVersion) Descriptor() ([]byte, []int) {
|
||||||
return fileDescriptor_pdu_0f43b713cd3bf056, []int{5}
|
return fileDescriptor_pdu_e59763dc61674a79, []int{5}
|
||||||
}
|
}
|
||||||
func (m *FilesystemVersion) XXX_Unmarshal(b []byte) error {
|
func (m *FilesystemVersion) XXX_Unmarshal(b []byte) error {
|
||||||
return xxx_messageInfo_FilesystemVersion.Unmarshal(m, b)
|
return xxx_messageInfo_FilesystemVersion.Unmarshal(m, b)
|
||||||
@@ -371,7 +371,7 @@ func (m *SendReq) Reset() { *m = SendReq{} }
|
|||||||
func (m *SendReq) String() string { return proto.CompactTextString(m) }
|
func (m *SendReq) String() string { return proto.CompactTextString(m) }
|
||||||
func (*SendReq) ProtoMessage() {}
|
func (*SendReq) ProtoMessage() {}
|
||||||
func (*SendReq) Descriptor() ([]byte, []int) {
|
func (*SendReq) Descriptor() ([]byte, []int) {
|
||||||
return fileDescriptor_pdu_0f43b713cd3bf056, []int{6}
|
return fileDescriptor_pdu_e59763dc61674a79, []int{6}
|
||||||
}
|
}
|
||||||
func (m *SendReq) XXX_Unmarshal(b []byte) error {
|
func (m *SendReq) XXX_Unmarshal(b []byte) error {
|
||||||
return xxx_messageInfo_SendReq.Unmarshal(m, b)
|
return xxx_messageInfo_SendReq.Unmarshal(m, b)
|
||||||
@@ -445,7 +445,7 @@ func (m *Property) Reset() { *m = Property{} }
|
|||||||
func (m *Property) String() string { return proto.CompactTextString(m) }
|
func (m *Property) String() string { return proto.CompactTextString(m) }
|
||||||
func (*Property) ProtoMessage() {}
|
func (*Property) ProtoMessage() {}
|
||||||
func (*Property) Descriptor() ([]byte, []int) {
|
func (*Property) Descriptor() ([]byte, []int) {
|
||||||
return fileDescriptor_pdu_0f43b713cd3bf056, []int{7}
|
return fileDescriptor_pdu_e59763dc61674a79, []int{7}
|
||||||
}
|
}
|
||||||
func (m *Property) XXX_Unmarshal(b []byte) error {
|
func (m *Property) XXX_Unmarshal(b []byte) error {
|
||||||
return xxx_messageInfo_Property.Unmarshal(m, b)
|
return xxx_messageInfo_Property.Unmarshal(m, b)
|
||||||
@@ -496,7 +496,7 @@ func (m *SendRes) Reset() { *m = SendRes{} }
|
|||||||
func (m *SendRes) String() string { return proto.CompactTextString(m) }
|
func (m *SendRes) String() string { return proto.CompactTextString(m) }
|
||||||
func (*SendRes) ProtoMessage() {}
|
func (*SendRes) ProtoMessage() {}
|
||||||
func (*SendRes) Descriptor() ([]byte, []int) {
|
func (*SendRes) Descriptor() ([]byte, []int) {
|
||||||
return fileDescriptor_pdu_0f43b713cd3bf056, []int{8}
|
return fileDescriptor_pdu_e59763dc61674a79, []int{8}
|
||||||
}
|
}
|
||||||
func (m *SendRes) XXX_Unmarshal(b []byte) error {
|
func (m *SendRes) XXX_Unmarshal(b []byte) error {
|
||||||
return xxx_messageInfo_SendRes.Unmarshal(m, b)
|
return xxx_messageInfo_SendRes.Unmarshal(m, b)
|
||||||
@@ -548,7 +548,7 @@ func (m *SendCompletedReq) Reset() { *m = SendCompletedReq{} }
|
|||||||
func (m *SendCompletedReq) String() string { return proto.CompactTextString(m) }
|
func (m *SendCompletedReq) String() string { return proto.CompactTextString(m) }
|
||||||
func (*SendCompletedReq) ProtoMessage() {}
|
func (*SendCompletedReq) ProtoMessage() {}
|
||||||
func (*SendCompletedReq) Descriptor() ([]byte, []int) {
|
func (*SendCompletedReq) Descriptor() ([]byte, []int) {
|
||||||
return fileDescriptor_pdu_0f43b713cd3bf056, []int{9}
|
return fileDescriptor_pdu_e59763dc61674a79, []int{9}
|
||||||
}
|
}
|
||||||
func (m *SendCompletedReq) XXX_Unmarshal(b []byte) error {
|
func (m *SendCompletedReq) XXX_Unmarshal(b []byte) error {
|
||||||
return xxx_messageInfo_SendCompletedReq.Unmarshal(m, b)
|
return xxx_messageInfo_SendCompletedReq.Unmarshal(m, b)
|
||||||
@@ -585,7 +585,7 @@ func (m *SendCompletedRes) Reset() { *m = SendCompletedRes{} }
|
|||||||
func (m *SendCompletedRes) String() string { return proto.CompactTextString(m) }
|
func (m *SendCompletedRes) String() string { return proto.CompactTextString(m) }
|
||||||
func (*SendCompletedRes) ProtoMessage() {}
|
func (*SendCompletedRes) ProtoMessage() {}
|
||||||
func (*SendCompletedRes) Descriptor() ([]byte, []int) {
|
func (*SendCompletedRes) Descriptor() ([]byte, []int) {
|
||||||
return fileDescriptor_pdu_0f43b713cd3bf056, []int{10}
|
return fileDescriptor_pdu_e59763dc61674a79, []int{10}
|
||||||
}
|
}
|
||||||
func (m *SendCompletedRes) XXX_Unmarshal(b []byte) error {
|
func (m *SendCompletedRes) XXX_Unmarshal(b []byte) error {
|
||||||
return xxx_messageInfo_SendCompletedRes.Unmarshal(m, b)
|
return xxx_messageInfo_SendCompletedRes.Unmarshal(m, b)
|
||||||
@@ -608,7 +608,7 @@ var xxx_messageInfo_SendCompletedRes proto.InternalMessageInfo
|
|||||||
type ReceiveReq struct {
|
type ReceiveReq struct {
|
||||||
Filesystem string `protobuf:"bytes,1,opt,name=Filesystem,proto3" json:"Filesystem,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"`
|
To *FilesystemVersion `protobuf:"bytes,2,opt,name=To,proto3" json:"To,omitempty"`
|
||||||
// If true, the receiver should clear the resume token before perfoming the
|
// If true, the receiver should clear the resume token before performing the
|
||||||
// zfs recv of the stream in the request
|
// zfs recv of the stream in the request
|
||||||
ClearResumeToken bool `protobuf:"varint,3,opt,name=ClearResumeToken,proto3" json:"ClearResumeToken,omitempty"`
|
ClearResumeToken bool `protobuf:"varint,3,opt,name=ClearResumeToken,proto3" json:"ClearResumeToken,omitempty"`
|
||||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||||
@@ -620,7 +620,7 @@ func (m *ReceiveReq) Reset() { *m = ReceiveReq{} }
|
|||||||
func (m *ReceiveReq) String() string { return proto.CompactTextString(m) }
|
func (m *ReceiveReq) String() string { return proto.CompactTextString(m) }
|
||||||
func (*ReceiveReq) ProtoMessage() {}
|
func (*ReceiveReq) ProtoMessage() {}
|
||||||
func (*ReceiveReq) Descriptor() ([]byte, []int) {
|
func (*ReceiveReq) Descriptor() ([]byte, []int) {
|
||||||
return fileDescriptor_pdu_0f43b713cd3bf056, []int{11}
|
return fileDescriptor_pdu_e59763dc61674a79, []int{11}
|
||||||
}
|
}
|
||||||
func (m *ReceiveReq) XXX_Unmarshal(b []byte) error {
|
func (m *ReceiveReq) XXX_Unmarshal(b []byte) error {
|
||||||
return xxx_messageInfo_ReceiveReq.Unmarshal(m, b)
|
return xxx_messageInfo_ReceiveReq.Unmarshal(m, b)
|
||||||
@@ -671,7 +671,7 @@ func (m *ReceiveRes) Reset() { *m = ReceiveRes{} }
|
|||||||
func (m *ReceiveRes) String() string { return proto.CompactTextString(m) }
|
func (m *ReceiveRes) String() string { return proto.CompactTextString(m) }
|
||||||
func (*ReceiveRes) ProtoMessage() {}
|
func (*ReceiveRes) ProtoMessage() {}
|
||||||
func (*ReceiveRes) Descriptor() ([]byte, []int) {
|
func (*ReceiveRes) Descriptor() ([]byte, []int) {
|
||||||
return fileDescriptor_pdu_0f43b713cd3bf056, []int{12}
|
return fileDescriptor_pdu_e59763dc61674a79, []int{12}
|
||||||
}
|
}
|
||||||
func (m *ReceiveRes) XXX_Unmarshal(b []byte) error {
|
func (m *ReceiveRes) XXX_Unmarshal(b []byte) error {
|
||||||
return xxx_messageInfo_ReceiveRes.Unmarshal(m, b)
|
return xxx_messageInfo_ReceiveRes.Unmarshal(m, b)
|
||||||
@@ -704,7 +704,7 @@ func (m *DestroySnapshotsReq) Reset() { *m = DestroySnapshotsReq{} }
|
|||||||
func (m *DestroySnapshotsReq) String() string { return proto.CompactTextString(m) }
|
func (m *DestroySnapshotsReq) String() string { return proto.CompactTextString(m) }
|
||||||
func (*DestroySnapshotsReq) ProtoMessage() {}
|
func (*DestroySnapshotsReq) ProtoMessage() {}
|
||||||
func (*DestroySnapshotsReq) Descriptor() ([]byte, []int) {
|
func (*DestroySnapshotsReq) Descriptor() ([]byte, []int) {
|
||||||
return fileDescriptor_pdu_0f43b713cd3bf056, []int{13}
|
return fileDescriptor_pdu_e59763dc61674a79, []int{13}
|
||||||
}
|
}
|
||||||
func (m *DestroySnapshotsReq) XXX_Unmarshal(b []byte) error {
|
func (m *DestroySnapshotsReq) XXX_Unmarshal(b []byte) error {
|
||||||
return xxx_messageInfo_DestroySnapshotsReq.Unmarshal(m, b)
|
return xxx_messageInfo_DestroySnapshotsReq.Unmarshal(m, b)
|
||||||
@@ -750,7 +750,7 @@ func (m *DestroySnapshotRes) Reset() { *m = DestroySnapshotRes{} }
|
|||||||
func (m *DestroySnapshotRes) String() string { return proto.CompactTextString(m) }
|
func (m *DestroySnapshotRes) String() string { return proto.CompactTextString(m) }
|
||||||
func (*DestroySnapshotRes) ProtoMessage() {}
|
func (*DestroySnapshotRes) ProtoMessage() {}
|
||||||
func (*DestroySnapshotRes) Descriptor() ([]byte, []int) {
|
func (*DestroySnapshotRes) Descriptor() ([]byte, []int) {
|
||||||
return fileDescriptor_pdu_0f43b713cd3bf056, []int{14}
|
return fileDescriptor_pdu_e59763dc61674a79, []int{14}
|
||||||
}
|
}
|
||||||
func (m *DestroySnapshotRes) XXX_Unmarshal(b []byte) error {
|
func (m *DestroySnapshotRes) XXX_Unmarshal(b []byte) error {
|
||||||
return xxx_messageInfo_DestroySnapshotRes.Unmarshal(m, b)
|
return xxx_messageInfo_DestroySnapshotRes.Unmarshal(m, b)
|
||||||
@@ -795,7 +795,7 @@ func (m *DestroySnapshotsRes) Reset() { *m = DestroySnapshotsRes{} }
|
|||||||
func (m *DestroySnapshotsRes) String() string { return proto.CompactTextString(m) }
|
func (m *DestroySnapshotsRes) String() string { return proto.CompactTextString(m) }
|
||||||
func (*DestroySnapshotsRes) ProtoMessage() {}
|
func (*DestroySnapshotsRes) ProtoMessage() {}
|
||||||
func (*DestroySnapshotsRes) Descriptor() ([]byte, []int) {
|
func (*DestroySnapshotsRes) Descriptor() ([]byte, []int) {
|
||||||
return fileDescriptor_pdu_0f43b713cd3bf056, []int{15}
|
return fileDescriptor_pdu_e59763dc61674a79, []int{15}
|
||||||
}
|
}
|
||||||
func (m *DestroySnapshotsRes) XXX_Unmarshal(b []byte) error {
|
func (m *DestroySnapshotsRes) XXX_Unmarshal(b []byte) error {
|
||||||
return xxx_messageInfo_DestroySnapshotsRes.Unmarshal(m, b)
|
return xxx_messageInfo_DestroySnapshotsRes.Unmarshal(m, b)
|
||||||
@@ -833,7 +833,7 @@ func (m *ReplicationCursorReq) Reset() { *m = ReplicationCursorReq{} }
|
|||||||
func (m *ReplicationCursorReq) String() string { return proto.CompactTextString(m) }
|
func (m *ReplicationCursorReq) String() string { return proto.CompactTextString(m) }
|
||||||
func (*ReplicationCursorReq) ProtoMessage() {}
|
func (*ReplicationCursorReq) ProtoMessage() {}
|
||||||
func (*ReplicationCursorReq) Descriptor() ([]byte, []int) {
|
func (*ReplicationCursorReq) Descriptor() ([]byte, []int) {
|
||||||
return fileDescriptor_pdu_0f43b713cd3bf056, []int{16}
|
return fileDescriptor_pdu_e59763dc61674a79, []int{16}
|
||||||
}
|
}
|
||||||
func (m *ReplicationCursorReq) XXX_Unmarshal(b []byte) error {
|
func (m *ReplicationCursorReq) XXX_Unmarshal(b []byte) error {
|
||||||
return xxx_messageInfo_ReplicationCursorReq.Unmarshal(m, b)
|
return xxx_messageInfo_ReplicationCursorReq.Unmarshal(m, b)
|
||||||
@@ -874,7 +874,7 @@ func (m *ReplicationCursorRes) Reset() { *m = ReplicationCursorRes{} }
|
|||||||
func (m *ReplicationCursorRes) String() string { return proto.CompactTextString(m) }
|
func (m *ReplicationCursorRes) String() string { return proto.CompactTextString(m) }
|
||||||
func (*ReplicationCursorRes) ProtoMessage() {}
|
func (*ReplicationCursorRes) ProtoMessage() {}
|
||||||
func (*ReplicationCursorRes) Descriptor() ([]byte, []int) {
|
func (*ReplicationCursorRes) Descriptor() ([]byte, []int) {
|
||||||
return fileDescriptor_pdu_0f43b713cd3bf056, []int{17}
|
return fileDescriptor_pdu_e59763dc61674a79, []int{17}
|
||||||
}
|
}
|
||||||
func (m *ReplicationCursorRes) XXX_Unmarshal(b []byte) error {
|
func (m *ReplicationCursorRes) XXX_Unmarshal(b []byte) error {
|
||||||
return xxx_messageInfo_ReplicationCursorRes.Unmarshal(m, b)
|
return xxx_messageInfo_ReplicationCursorRes.Unmarshal(m, b)
|
||||||
@@ -1010,7 +1010,7 @@ func (m *PingReq) Reset() { *m = PingReq{} }
|
|||||||
func (m *PingReq) String() string { return proto.CompactTextString(m) }
|
func (m *PingReq) String() string { return proto.CompactTextString(m) }
|
||||||
func (*PingReq) ProtoMessage() {}
|
func (*PingReq) ProtoMessage() {}
|
||||||
func (*PingReq) Descriptor() ([]byte, []int) {
|
func (*PingReq) Descriptor() ([]byte, []int) {
|
||||||
return fileDescriptor_pdu_0f43b713cd3bf056, []int{18}
|
return fileDescriptor_pdu_e59763dc61674a79, []int{18}
|
||||||
}
|
}
|
||||||
func (m *PingReq) XXX_Unmarshal(b []byte) error {
|
func (m *PingReq) XXX_Unmarshal(b []byte) error {
|
||||||
return xxx_messageInfo_PingReq.Unmarshal(m, b)
|
return xxx_messageInfo_PingReq.Unmarshal(m, b)
|
||||||
@@ -1049,7 +1049,7 @@ func (m *PingRes) Reset() { *m = PingRes{} }
|
|||||||
func (m *PingRes) String() string { return proto.CompactTextString(m) }
|
func (m *PingRes) String() string { return proto.CompactTextString(m) }
|
||||||
func (*PingRes) ProtoMessage() {}
|
func (*PingRes) ProtoMessage() {}
|
||||||
func (*PingRes) Descriptor() ([]byte, []int) {
|
func (*PingRes) Descriptor() ([]byte, []int) {
|
||||||
return fileDescriptor_pdu_0f43b713cd3bf056, []int{19}
|
return fileDescriptor_pdu_e59763dc61674a79, []int{19}
|
||||||
}
|
}
|
||||||
func (m *PingRes) XXX_Unmarshal(b []byte) error {
|
func (m *PingRes) XXX_Unmarshal(b []byte) error {
|
||||||
return xxx_messageInfo_PingRes.Unmarshal(m, b)
|
return xxx_messageInfo_PingRes.Unmarshal(m, b)
|
||||||
@@ -1078,6 +1078,15 @@ func (m *PingRes) GetEcho() string {
|
|||||||
|
|
||||||
type HintMostRecentCommonAncestorReq struct {
|
type HintMostRecentCommonAncestorReq struct {
|
||||||
Filesystem string `protobuf:"bytes,1,opt,name=Filesystem,proto3" json:"Filesystem,omitempty"`
|
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"`
|
SenderVersion *FilesystemVersion `protobuf:"bytes,2,opt,name=SenderVersion,proto3" json:"SenderVersion,omitempty"`
|
||||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||||
XXX_unrecognized []byte `json:"-"`
|
XXX_unrecognized []byte `json:"-"`
|
||||||
@@ -1088,7 +1097,7 @@ func (m *HintMostRecentCommonAncestorReq) Reset() { *m = HintMostRecentC
|
|||||||
func (m *HintMostRecentCommonAncestorReq) String() string { return proto.CompactTextString(m) }
|
func (m *HintMostRecentCommonAncestorReq) String() string { return proto.CompactTextString(m) }
|
||||||
func (*HintMostRecentCommonAncestorReq) ProtoMessage() {}
|
func (*HintMostRecentCommonAncestorReq) ProtoMessage() {}
|
||||||
func (*HintMostRecentCommonAncestorReq) Descriptor() ([]byte, []int) {
|
func (*HintMostRecentCommonAncestorReq) Descriptor() ([]byte, []int) {
|
||||||
return fileDescriptor_pdu_0f43b713cd3bf056, []int{20}
|
return fileDescriptor_pdu_e59763dc61674a79, []int{20}
|
||||||
}
|
}
|
||||||
func (m *HintMostRecentCommonAncestorReq) XXX_Unmarshal(b []byte) error {
|
func (m *HintMostRecentCommonAncestorReq) XXX_Unmarshal(b []byte) error {
|
||||||
return xxx_messageInfo_HintMostRecentCommonAncestorReq.Unmarshal(m, b)
|
return xxx_messageInfo_HintMostRecentCommonAncestorReq.Unmarshal(m, b)
|
||||||
@@ -1132,7 +1141,7 @@ func (m *HintMostRecentCommonAncestorRes) Reset() { *m = HintMostRecentC
|
|||||||
func (m *HintMostRecentCommonAncestorRes) String() string { return proto.CompactTextString(m) }
|
func (m *HintMostRecentCommonAncestorRes) String() string { return proto.CompactTextString(m) }
|
||||||
func (*HintMostRecentCommonAncestorRes) ProtoMessage() {}
|
func (*HintMostRecentCommonAncestorRes) ProtoMessage() {}
|
||||||
func (*HintMostRecentCommonAncestorRes) Descriptor() ([]byte, []int) {
|
func (*HintMostRecentCommonAncestorRes) Descriptor() ([]byte, []int) {
|
||||||
return fileDescriptor_pdu_0f43b713cd3bf056, []int{21}
|
return fileDescriptor_pdu_e59763dc61674a79, []int{21}
|
||||||
}
|
}
|
||||||
func (m *HintMostRecentCommonAncestorRes) XXX_Unmarshal(b []byte) error {
|
func (m *HintMostRecentCommonAncestorRes) XXX_Unmarshal(b []byte) error {
|
||||||
return xxx_messageInfo_HintMostRecentCommonAncestorRes.Unmarshal(m, b)
|
return xxx_messageInfo_HintMostRecentCommonAncestorRes.Unmarshal(m, b)
|
||||||
@@ -1449,9 +1458,9 @@ var _Replication_serviceDesc = grpc.ServiceDesc{
|
|||||||
Metadata: "pdu.proto",
|
Metadata: "pdu.proto",
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() { proto.RegisterFile("pdu.proto", fileDescriptor_pdu_0f43b713cd3bf056) }
|
func init() { proto.RegisterFile("pdu.proto", fileDescriptor_pdu_e59763dc61674a79) }
|
||||||
|
|
||||||
var fileDescriptor_pdu_0f43b713cd3bf056 = []byte{
|
var fileDescriptor_pdu_e59763dc61674a79 = []byte{
|
||||||
// 892 bytes of a gzipped FileDescriptorProto
|
// 892 bytes of a gzipped FileDescriptorProto
|
||||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x56, 0xdf, 0x6f, 0xdb, 0x36,
|
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,
|
0x10, 0x8e, 0x6c, 0x39, 0x91, 0xcf, 0xe9, 0xea, 0x5c, 0xb2, 0x42, 0x13, 0xba, 0xce, 0xe3, 0x86,
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ message ReceiveReq {
|
|||||||
string Filesystem = 1;
|
string Filesystem = 1;
|
||||||
FilesystemVersion To = 2;
|
FilesystemVersion To = 2;
|
||||||
|
|
||||||
// If true, the receiver should clear the resume token before perfoming the
|
// If true, the receiver should clear the resume token before performing the
|
||||||
// zfs recv of the stream in the request
|
// zfs recv of the stream in the request
|
||||||
bool ClearResumeToken = 3;
|
bool ClearResumeToken = 3;
|
||||||
}
|
}
|
||||||
@@ -129,6 +129,16 @@ message PingRes {
|
|||||||
|
|
||||||
message HintMostRecentCommonAncestorReq {
|
message HintMostRecentCommonAncestorReq {
|
||||||
string Filesystem = 1;
|
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;
|
FilesystemVersion SenderVersion = 2;
|
||||||
}
|
}
|
||||||
message HintMostRecentCommonAncestorRes {}
|
message HintMostRecentCommonAncestorRes {}
|
||||||
@@ -234,6 +234,7 @@ func resolveConflict(conflict error) (path []*pdu.FilesystemVersion, msg string)
|
|||||||
if noCommonAncestor, ok := conflict.(*ConflictNoCommonAncestor); ok {
|
if noCommonAncestor, ok := conflict.(*ConflictNoCommonAncestor); ok {
|
||||||
if len(noCommonAncestor.SortedReceiverVersions) == 0 {
|
if len(noCommonAncestor.SortedReceiverVersions) == 0 {
|
||||||
// TODO this is hard-coded replication policy: most recent snapshot as source
|
// 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
|
var mostRecentSnap *pdu.FilesystemVersion
|
||||||
for n := len(noCommonAncestor.SortedSenderVersions) - 1; n >= 0; n-- {
|
for n := len(noCommonAncestor.SortedSenderVersions) - 1; n >= 0; n-- {
|
||||||
if noCommonAncestor.SortedSenderVersions[n].Type == pdu.FilesystemVersion_Snapshot {
|
if noCommonAncestor.SortedSenderVersions[n].Type == pdu.FilesystemVersion_Snapshot {
|
||||||
@@ -262,7 +263,6 @@ func (p *Planner) doPlanning(ctx context.Context) ([]*Filesystem, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
sfss := slfssres.GetFilesystems()
|
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{})
|
rlfssres, err := p.receiver.ListFilesystems(ctx, &pdu.ListFilesystemReq{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -352,18 +352,19 @@ func (fs *Filesystem) doPlanning(ctx context.Context) ([]*Step, error) {
|
|||||||
log.WithField("token", resumeToken).Debug("decode resume token")
|
log.WithField("token", resumeToken).Debug("decode resume token")
|
||||||
}
|
}
|
||||||
|
|
||||||
// give both sides a hint about how far the replication got
|
// give both sides a hint about how far prior replication attempts got
|
||||||
// This serves as a cummulative variant of SendCompleted and can be useful
|
// This serves as a cummulative variant of SendCompleted and can be useful
|
||||||
// for example to release stale holds from an earlier (interrupted) replication.
|
// 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
|
// 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
|
// then again, the step should run regardless of planning success
|
||||||
// so maybe a separate phase before PLANNING, then?
|
// so maybe a separate phase before PLANNING, then?
|
||||||
path, conflict := IncrementalPath(rfsvs, sfsvs)
|
path, conflict := IncrementalPath(rfsvs, sfsvs)
|
||||||
var sender_mrca *pdu.FilesystemVersion // from sfsvs
|
var sender_mrca *pdu.FilesystemVersion
|
||||||
if conflict == nil && len(path) > 0 {
|
if conflict == nil && len(path) > 0 {
|
||||||
sender_mrca = path[0] // shadow
|
sender_mrca = path[0] // shadow
|
||||||
}
|
}
|
||||||
if sender_mrca != nil {
|
// yes, sender_mrca may be nil, indicating that we do not have an mrca
|
||||||
|
{
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
doHint := func(ep Endpoint, name string) {
|
doHint := func(ep Endpoint, name string) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
@@ -383,8 +384,6 @@ func (fs *Filesystem) doPlanning(ctx context.Context) ([]*Step, error) {
|
|||||||
go doHint(fs.sender, "sender")
|
go doHint(fs.sender, "sender")
|
||||||
go doHint(fs.receiver, "receiver")
|
go doHint(fs.receiver, "receiver")
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
} else {
|
|
||||||
log.Debug("cannot identify most recent common ancestor, skipping hint")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var steps []*Step
|
var steps []*Step
|
||||||
@@ -603,7 +602,7 @@ func (s *Step) doReplication(ctx context.Context) error {
|
|||||||
|
|
||||||
fs := s.parent.Path
|
fs := s.parent.Path
|
||||||
|
|
||||||
log := getLogger(ctx)
|
log := getLogger(ctx).WithField("filesystem", fs)
|
||||||
sr := s.buildSendRequest(false)
|
sr := s.buildSendRequest(false)
|
||||||
|
|
||||||
log.Debug("initiate send request")
|
log.Debug("initiate send request")
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ func (s *streamCopier) WriteStreamTo(w io.Writer) zfs.StreamCopierError {
|
|||||||
s.mtx.Lock()
|
s.mtx.Lock()
|
||||||
defer s.mtx.Unlock()
|
defer s.mtx.Unlock()
|
||||||
if s.used {
|
if s.used {
|
||||||
panic("streamCopier used mulitple times")
|
panic("streamCopier used multiple times")
|
||||||
}
|
}
|
||||||
s.used = true
|
s.used = true
|
||||||
return s.streamConn.ReadStreamInto(w, ZFSStream)
|
return s.streamConn.ReadStreamInto(w, ZFSStream)
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ func (c *Conn) ReadFrame() (Frame, error) {
|
|||||||
return Frame{}, ErrShutdown
|
return Frame{}, ErrShutdown
|
||||||
}
|
}
|
||||||
|
|
||||||
// only aquire readMtx now to prioritize the draining in Shutdown()
|
// only acquire readMtx now to prioritize the draining in Shutdown()
|
||||||
// over external callers (= drain public callers)
|
// over external callers (= drain public callers)
|
||||||
|
|
||||||
c.readMtx.Lock()
|
c.readMtx.Lock()
|
||||||
@@ -148,7 +148,7 @@ func (c *Conn) readFrame() (Frame, error) {
|
|||||||
// | | | |
|
// | | | |
|
||||||
// | | | F3
|
// | | | F3
|
||||||
// | | |
|
// | | |
|
||||||
// | F2 |signficant time between frames because
|
// | F2 |significant time between frames because
|
||||||
// F1 the peer has nothing to say to us
|
// F1 the peer has nothing to say to us
|
||||||
//
|
//
|
||||||
// Assume we're at the point were F2's header is in c.readNext.
|
// Assume we're at the point were F2's header is in c.readNext.
|
||||||
@@ -246,7 +246,7 @@ func (c *Conn) Shutdown(deadline time.Time) error {
|
|||||||
//
|
//
|
||||||
// 1. Naive Option: We just call Close() right after CloseWrite.
|
// 1. Naive Option: We just call Close() right after CloseWrite.
|
||||||
// This yields the same race condition as explained above (DIF, first
|
// This yields the same race condition as explained above (DIF, first
|
||||||
// paragraph): The situation just becomae a little more unlikely because
|
// paragraph): The situation just became a little more unlikely because
|
||||||
// our rstFrameType + CloseWrite dance gave the client a full RTT worth of
|
// our rstFrameType + CloseWrite dance gave the client a full RTT worth of
|
||||||
// time to read the data from its TCP recv buffer.
|
// time to read the data from its TCP recv buffer.
|
||||||
//
|
//
|
||||||
@@ -295,7 +295,7 @@ func (c *Conn) Shutdown(deadline time.Time) error {
|
|||||||
defer prometheus.NewTimer(prom.ShutdownSeconds).ObserveDuration()
|
defer prometheus.NewTimer(prom.ShutdownSeconds).ObserveDuration()
|
||||||
|
|
||||||
closeWire := func(step string) error {
|
closeWire := func(step string) error {
|
||||||
// TODO SetLinger(0) or similiar (we want RST frames here, not FINS)
|
// TODO SetLinger(0) or similar (we want RST frames here, not FINS)
|
||||||
closeErr := c.nc.Close()
|
closeErr := c.nc.Close()
|
||||||
if closeErr == nil {
|
if closeErr == nil {
|
||||||
return nil
|
return nil
|
||||||
@@ -321,10 +321,10 @@ func (c *Conn) Shutdown(deadline time.Time) error {
|
|||||||
|
|
||||||
c.shutdown.Begin()
|
c.shutdown.Begin()
|
||||||
// new calls to c.ReadFrame and c.WriteFrame will now return ErrShutdown
|
// new calls to c.ReadFrame and c.WriteFrame will now return ErrShutdown
|
||||||
// Aquiring writeMtx and readMtx afterwards ensures that already-running calls exit successfully
|
// Acquiring writeMtx and readMtx afterwards ensures that already-running calls exit successfully
|
||||||
|
|
||||||
// disable renewing timeouts now, enforce the requested deadline instead
|
// disable renewing timeouts now, enforce the requested deadline instead
|
||||||
// we need to do this before aquiring locks to enforce the timeout on slow
|
// we need to do this before acquiring locks to enforce the timeout on slow
|
||||||
// clients / if something hangs (DoS mitigation)
|
// clients / if something hangs (DoS mitigation)
|
||||||
if err := c.nc.DisableTimeouts(); err != nil {
|
if err := c.nc.DisableTimeouts(); err != nil {
|
||||||
return hardclose(err, "disable_timeouts")
|
return hardclose(err, "disable_timeouts")
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// microbenchmark to manually test rpc/dataconn perforamnce
|
// microbenchmark to manually test rpc/dataconn performance
|
||||||
//
|
//
|
||||||
// With stdin / stdout on client and server, simulating zfs send|recv piping
|
// With stdin / stdout on client and server, simulating zfs send|recv piping
|
||||||
//
|
//
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
// ./microbenchmark -appmode client -direction recv < /dev/zero
|
// ./microbenchmark -appmode client -direction recv < /dev/zero
|
||||||
//
|
//
|
||||||
//
|
//
|
||||||
// Without the overhead of pipes (just protocol perforamnce, mostly useful with perf bc no bw measurement)
|
// Without the overhead of pipes (just protocol performance, mostly useful with perf bc no bw measurement)
|
||||||
//
|
//
|
||||||
// ./microbenchmark -appmode client -direction recv -devnoopWriter -devnoopReader
|
// ./microbenchmark -appmode client -direction recv -devnoopWriter -devnoopReader
|
||||||
// ./microbenchmark -appmode server -devnoopReader -devnoopWriter
|
// ./microbenchmark -appmode server -devnoopReader -devnoopWriter
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ type Wire interface {
|
|||||||
// No data that could otherwise be Read is lost as a consequence of this call.
|
// No data that could otherwise be Read is lost as a consequence of this call.
|
||||||
// The use case for this API is abortive connection shutdown.
|
// The use case for this API is abortive connection shutdown.
|
||||||
// To provide any value over draining Wire using io.Read, an implementation
|
// To provide any value over draining Wire using io.Read, an implementation
|
||||||
// will likely use out-of-bounds messaging mechanisms.
|
// will likely use out-of-band messaging mechanisms.
|
||||||
// TODO WaitForPeerClose() (supported bool, err error)
|
// TODO WaitForPeerClose() (supported bool, err error)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,7 +119,7 @@ restart:
|
|||||||
return n, err
|
return n, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Writes the given buffers to Conn, following the sematincs of io.Copy,
|
// Writes the given buffers to Conn, following the semantics of io.Copy,
|
||||||
// but is guaranteed to use the writev system call if the wrapped Wire
|
// but is guaranteed to use the writev system call if the wrapped Wire
|
||||||
// support it.
|
// support it.
|
||||||
// Note the Conn does not support writev through io.Copy(aConn, aNetBuffers).
|
// Note the Conn does not support writev through io.Copy(aConn, aNetBuffers).
|
||||||
@@ -158,9 +158,9 @@ var _ SyscallConner = (*net.TCPConn)(nil)
|
|||||||
// Think of io.ReadvFull, but for net.Buffers + using the readv syscall.
|
// Think of io.ReadvFull, but for net.Buffers + using the readv syscall.
|
||||||
//
|
//
|
||||||
// If the underlying Wire is not a SyscallConner, a fallback
|
// If the underlying Wire is not a SyscallConner, a fallback
|
||||||
// ipmlementation based on repeated Conn.Read invocations is used.
|
// implementation based on repeated Conn.Read invocations is used.
|
||||||
//
|
//
|
||||||
// If the connection returned io.EOF, the number of bytes up ritten until
|
// If the connection returned io.EOF, the number of bytes written until
|
||||||
// then + io.EOF is returned. This behavior is different to io.ReadFull
|
// then + io.EOF is returned. This behavior is different to io.ReadFull
|
||||||
// which returns io.ErrUnexpectedEOF.
|
// which returns io.ErrUnexpectedEOF.
|
||||||
func (c Conn) ReadvFull(buffers net.Buffers) (n int64, err error) {
|
func (c Conn) ReadvFull(buffers net.Buffers) (n int64, err error) {
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ func (c Conn) doOneReadv(rawConn syscall.RawConn, iovecs *[]syscall.Iovec) (n in
|
|||||||
// Update left, cannot go below 0 due to
|
// Update left, cannot go below 0 due to
|
||||||
// a) definition of thisIovecConsumedCompletely
|
// a) definition of thisIovecConsumedCompletely
|
||||||
// b) left > 0 due to loop invariant
|
// b) left > 0 due to loop invariant
|
||||||
// Convertion .Len to int64 is thus also safe now, because it is < left < INT_MAX
|
// Converting .Len to int64 is thus also safe now, because it is < left < INT_MAX
|
||||||
left -= int((*iovecs)[0].Len)
|
left -= int((*iovecs)[0].Len)
|
||||||
*iovecs = (*iovecs)[1:]
|
*iovecs = (*iovecs)[1:]
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ func (m *HandshakeMessage) DecodeReader(r io.Reader, maxLen int) error {
|
|||||||
|
|
||||||
func DoHandshakeCurrentVersion(conn net.Conn, deadline time.Time) *HandshakeError {
|
func DoHandshakeCurrentVersion(conn net.Conn, deadline time.Time) *HandshakeError {
|
||||||
// current protocol version is hardcoded here
|
// current protocol version is hardcoded here
|
||||||
return DoHandshakeVersion(conn, deadline, 2)
|
return DoHandshakeVersion(conn, deadline, 3)
|
||||||
}
|
}
|
||||||
|
|
||||||
const HandshakeMessageMaxLen = 16 * 4096
|
const HandshakeMessageMaxLen = 16 * 4096
|
||||||
|
|||||||
+1
-2
@@ -56,7 +56,7 @@ func NewClientAuthListener(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Accept() accepts a connection from the *net.TCPListener passed to the constructor
|
// Accept() accepts a connection from the *net.TCPListener passed to the constructor
|
||||||
// and sets up the TLS connection, including handshake and peer CommmonName validation
|
// and sets up the TLS connection, including handshake and peer CommonName validation
|
||||||
// within the specified handshakeTimeout.
|
// within the specified handshakeTimeout.
|
||||||
//
|
//
|
||||||
// It returns both the raw TCP connection (tcpConn) and the TLS connection (tlsConn) on top of it.
|
// It returns both the raw TCP connection (tcpConn) and the TLS connection (tlsConn) on top of it.
|
||||||
@@ -121,7 +121,6 @@ func ClientAuthClient(serverName string, rootCA *x509.CertPool, clientCert tls.C
|
|||||||
ServerName: serverName,
|
ServerName: serverName,
|
||||||
KeyLogWriter: keylogFromEnv(),
|
KeyLogWriter: keylogFromEnv(),
|
||||||
}
|
}
|
||||||
tlsConfig.BuildNameToCertificate()
|
|
||||||
return tlsConfig, nil
|
return tlsConfig, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ func (l *LocalListener) Accept(ctx context.Context) (*transport.AuthConn, error)
|
|||||||
WithField("res.conn", res.conn).WithField("res.err", res.err).
|
WithField("res.conn", res.conn).WithField("res.err", res.err).
|
||||||
Debug("responding to client request")
|
Debug("responding to client request")
|
||||||
|
|
||||||
// contract bewteen Connect and Accept is that Connect sends a req.callback
|
// contract between Connect and Accept is that Connect sends a req.callback
|
||||||
// into which we can send one result non-blockingly.
|
// into which we can send one result non-blockingly.
|
||||||
// We want to panic if that contract is violated (impl error)
|
// We want to panic if that contract is violated (impl error)
|
||||||
//
|
//
|
||||||
@@ -112,7 +112,7 @@ func (l *LocalListener) Accept(ctx context.Context) (*transport.AuthConn, error)
|
|||||||
defer func() {
|
defer func() {
|
||||||
errv := recover()
|
errv := recover()
|
||||||
if errv == clientCallbackBlocked {
|
if errv == clientCallbackBlocked {
|
||||||
// this would be a violation of contract betwee Connect and Accept, see above
|
// this would be a violation of contract between Connect and Accept, see above
|
||||||
panic(clientCallbackBlocked)
|
panic(clientCallbackBlocked)
|
||||||
} else {
|
} else {
|
||||||
transport.GetLogger(ctx).WithField("recover_err", errv).
|
transport.GetLogger(ctx).WithField("recover_err", errv).
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ func (m *MultiStdinserverListener) Close() error {
|
|||||||
return oneErr
|
return oneErr
|
||||||
}
|
}
|
||||||
|
|
||||||
// a single stdinserverListener (part of multiStinserverListener)
|
// a single stdinserverListener (part of multiStdinserverListener)
|
||||||
type stdinserverListener struct {
|
type stdinserverListener struct {
|
||||||
l *netssh.Listener
|
l *netssh.Listener
|
||||||
clientIdentity string
|
clientIdentity string
|
||||||
|
|||||||
@@ -11,39 +11,6 @@ import (
|
|||||||
"github.com/zrepl/zrepl/util/tcpsock"
|
"github.com/zrepl/zrepl/util/tcpsock"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ipMapEntry struct {
|
|
||||||
ip net.IP
|
|
||||||
ident string
|
|
||||||
}
|
|
||||||
|
|
||||||
type ipMap struct {
|
|
||||||
entries []ipMapEntry
|
|
||||||
}
|
|
||||||
|
|
||||||
func ipMapFromConfig(clients map[string]string) (*ipMap, error) {
|
|
||||||
entries := make([]ipMapEntry, 0, len(clients))
|
|
||||||
for clientIPString, clientIdent := range clients {
|
|
||||||
clientIP := net.ParseIP(clientIPString)
|
|
||||||
if clientIP == nil {
|
|
||||||
return nil, errors.Errorf("cannot parse client IP %q", clientIPString)
|
|
||||||
}
|
|
||||||
if err := transport.ValidateClientIdentity(clientIdent); err != nil {
|
|
||||||
return nil, errors.Wrapf(err, "invalid client identity for IP %q", clientIPString)
|
|
||||||
}
|
|
||||||
entries = append(entries, ipMapEntry{clientIP, clientIdent})
|
|
||||||
}
|
|
||||||
return &ipMap{entries: entries}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *ipMap) Get(ip net.IP) (string, error) {
|
|
||||||
for _, e := range m.entries {
|
|
||||||
if e.ip.Equal(ip) {
|
|
||||||
return e.ident, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "", errors.Errorf("no identity mapping for client IP %s", ip)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TCPListenerFactoryFromConfig(c *config.Global, in *config.TCPServe) (transport.AuthenticatedListenerFactory, error) {
|
func TCPListenerFactoryFromConfig(c *config.Global, in *config.TCPServe) (transport.AuthenticatedListenerFactory, error) {
|
||||||
clientMap, err := ipMapFromConfig(in.Clients)
|
clientMap, err := ipMapFromConfig(in.Clients)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -69,10 +36,13 @@ func (f *TCPAuthListener) Accept(ctx context.Context) (*transport.AuthConn, erro
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
clientIP := nc.RemoteAddr().(*net.TCPAddr).IP
|
clientAddr := &net.IPAddr{
|
||||||
clientIdent, err := f.clientMap.Get(clientIP)
|
IP: nc.RemoteAddr().(*net.TCPAddr).IP,
|
||||||
|
Zone: nc.RemoteAddr().(*net.TCPAddr).Zone,
|
||||||
|
}
|
||||||
|
clientIdent, err := f.clientMap.Get(clientAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
transport.GetLogger(ctx).WithField("ip", clientIP).Error("client IP not in client map")
|
transport.GetLogger(ctx).WithField("ipaddr", clientAddr).Error("client IP not in client map")
|
||||||
nc.Close()
|
nc.Close()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,166 @@
|
|||||||
|
package tcp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/zrepl/zrepl/transport"
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ipMapEntry struct {
|
||||||
|
subnet *net.IPNet
|
||||||
|
// ident is always not empty
|
||||||
|
ident string
|
||||||
|
// zone may be empty (e.g. for IPv4)
|
||||||
|
zone string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ipMap struct {
|
||||||
|
entries []*ipMapEntry
|
||||||
|
}
|
||||||
|
|
||||||
|
func ipMapFromConfig(clients map[string]string) (*ipMap, error) {
|
||||||
|
|
||||||
|
entries := make([]*ipMapEntry, 0, len(clients))
|
||||||
|
for clientInput, clientIdent := range clients {
|
||||||
|
userIPMapEntry, err := newIPMapEntry(clientInput, clientIdent)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "cannot not parse %q:%q", clientInput, clientIdent)
|
||||||
|
}
|
||||||
|
|
||||||
|
entries = append(entries, userIPMapEntry)
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Sort(byPrefixlen(entries))
|
||||||
|
|
||||||
|
return &ipMap{entries: entries}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *ipMap) Get(ipAddr *net.IPAddr) (string, error) {
|
||||||
|
for _, e := range m.entries {
|
||||||
|
if e.zone != ipAddr.Zone {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if e.subnet.Contains(ipAddr.IP) {
|
||||||
|
return zfsDatasetPathComponentCompatibleRepresentation(e.ident, ipAddr), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", errors.Errorf("no identity mapping for client IP: %s%%%s", ipAddr.IP, ipAddr.Zone)
|
||||||
|
}
|
||||||
|
|
||||||
|
var ipv6FullySpecifiedMask = bytes.Repeat([]byte{0xff}, net.IPv6len)
|
||||||
|
|
||||||
|
func newIPMapEntry(input string, ident string) (*ipMapEntry, error) {
|
||||||
|
ip := input
|
||||||
|
var zone string
|
||||||
|
|
||||||
|
ipZoneSplit := strings.SplitN(input, "%", 2)
|
||||||
|
if len(ipZoneSplit) > 1 {
|
||||||
|
ip = ipZoneSplit[0]
|
||||||
|
zone = ipZoneSplit[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
_, subnet, err := net.ParseCIDR(ip)
|
||||||
|
if err != nil {
|
||||||
|
// expect full IP, no '*' placeholder expansion
|
||||||
|
|
||||||
|
if strings.Count(ident, "*") != 0 {
|
||||||
|
return nil, fmt.Errorf("non-CIDR matches must not contain '*' placeholder")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := transport.ValidateClientIdentity(ident); err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "invalid client identity %q for IP %q", ident, ip)
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedIP := net.ParseIP(ip)
|
||||||
|
if parsedIP == nil {
|
||||||
|
return nil, errors.Wrapf(err, "invalid client address %q", ip)
|
||||||
|
}
|
||||||
|
parsedIP = parsedIP.To16()
|
||||||
|
|
||||||
|
return &ipMapEntry{
|
||||||
|
subnet: &net.IPNet{
|
||||||
|
IP: parsedIP,
|
||||||
|
Mask: ipv6FullySpecifiedMask,
|
||||||
|
},
|
||||||
|
zone: zone,
|
||||||
|
ident: ident,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// expect CIDR and '*' placeholder expansion
|
||||||
|
|
||||||
|
if strings.Count(ident, "*") != 1 {
|
||||||
|
err = fmt.Errorf("CIDRs require 1 IP placeholder")
|
||||||
|
return nil, errors.Wrapf(err, "invalid client identity %q for IP %q", ident, ip)
|
||||||
|
}
|
||||||
|
|
||||||
|
longestIPAddr := net.IPAddr{
|
||||||
|
IP: net.IP(bytes.Repeat([]byte{0xff}, net.IPv6len)),
|
||||||
|
Zone: strings.Repeat("i", unix.IFNAMSIZ),
|
||||||
|
}
|
||||||
|
longestIdent := zfsDatasetPathComponentCompatibleRepresentation(ident, &longestIPAddr)
|
||||||
|
|
||||||
|
if err := transport.ValidateClientIdentity(longestIdent); err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "invalid client identity for IP %q", ip)
|
||||||
|
}
|
||||||
|
|
||||||
|
ones, _ := subnet.Mask.Size()
|
||||||
|
preExpansionAddrlen := len(subnet.IP) * 8
|
||||||
|
expanded := subnet.IP.To16()
|
||||||
|
postExpansionAddrlen := len(expanded) * 8
|
||||||
|
return &ipMapEntry{
|
||||||
|
subnet: &net.IPNet{
|
||||||
|
IP: expanded,
|
||||||
|
Mask: net.CIDRMask(postExpansionAddrlen-preExpansionAddrlen+ones, postExpansionAddrlen),
|
||||||
|
},
|
||||||
|
zone: zone,
|
||||||
|
ident: ident,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func zfsDatasetPathComponentCompatibleRepresentation(identityWithWildcard string, addr *net.IPAddr) string {
|
||||||
|
// If a Zone exists we append it after the IP using a "-" because "%"
|
||||||
|
// is a zfs dataset forbidden char.
|
||||||
|
if addr.Zone != "" {
|
||||||
|
return strings.Replace(identityWithWildcard, "*", addr.IP.String()+"-"+addr.Zone, 1)
|
||||||
|
}
|
||||||
|
// newIPMapEntry validates that the line contains exactly one "*"
|
||||||
|
return strings.Replace(identityWithWildcard, "*", addr.IP.String(), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ipMapEntry) String() string {
|
||||||
|
return fmt.Sprintf("&ipMapEntry{subnet=%q, ident=%q, zone=%q}", e.subnet.String(), e.ident, e.zone)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ipMapEntry) PrefixLen() int {
|
||||||
|
ones, bits := e.subnet.Mask.Size()
|
||||||
|
if bits != net.IPv6len*8 {
|
||||||
|
panic(fmt.Sprintf("impl error: we represent all addresses as 16byte internally ones=%v bits=%v: %s", ones, bits, e))
|
||||||
|
}
|
||||||
|
return ones
|
||||||
|
}
|
||||||
|
|
||||||
|
// newtype to support sorting by prefixlength
|
||||||
|
type byPrefixlen []*ipMapEntry
|
||||||
|
|
||||||
|
func (m byPrefixlen) Len() int { return len(m) }
|
||||||
|
func (m byPrefixlen) Less(i, j int) bool {
|
||||||
|
|
||||||
|
if m[i].PrefixLen() != m[j].PrefixLen() {
|
||||||
|
return m[i].PrefixLen() > m[j].PrefixLen()
|
||||||
|
}
|
||||||
|
|
||||||
|
addrCmp := bytes.Compare(m[i].subnet.IP.To16(), m[j].subnet.IP.To16())
|
||||||
|
if addrCmp != 0 {
|
||||||
|
return addrCmp < 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Compare(m[i].zone, m[j].zone) < 0
|
||||||
|
}
|
||||||
|
func (m byPrefixlen) Swap(i, j int) { m[i], m[j] = m[j], m[i] }
|
||||||
@@ -0,0 +1,204 @@
|
|||||||
|
package tcp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/kr/pretty"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestIPMap(t *testing.T) {
|
||||||
|
type testCaseExpect struct {
|
||||||
|
expectNoMapping bool
|
||||||
|
expectIdent string
|
||||||
|
}
|
||||||
|
type testCase struct {
|
||||||
|
name string
|
||||||
|
config map[string]string
|
||||||
|
expectInitErr bool
|
||||||
|
expect map[string]testCaseExpect
|
||||||
|
}
|
||||||
|
|
||||||
|
cases := []testCase{
|
||||||
|
{
|
||||||
|
name: "regular ips",
|
||||||
|
expectInitErr: false,
|
||||||
|
config: map[string]string{
|
||||||
|
"192.168.123.234": "ident1",
|
||||||
|
"192.168.20.23": "ident2",
|
||||||
|
},
|
||||||
|
expect: map[string]testCaseExpect{
|
||||||
|
"192.168.123.234": {expectIdent: "ident1"},
|
||||||
|
"192.168.20.23": {expectIdent: "ident2"},
|
||||||
|
"192.168.20.10": {expectNoMapping: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "full wildcard",
|
||||||
|
expectInitErr: false,
|
||||||
|
config: map[string]string{
|
||||||
|
"0.0.0.0/0": "*",
|
||||||
|
"0::/0": "*",
|
||||||
|
},
|
||||||
|
expect: map[string]testCaseExpect{
|
||||||
|
"10.123.234.24": {expectIdent: "10.123.234.24"},
|
||||||
|
// '[' and ']' are forbiddenin dataset names
|
||||||
|
"fe80::23:42": {expectIdent: "fe80::23:42"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "longest prefix matching",
|
||||||
|
expectInitErr: false,
|
||||||
|
config: map[string]string{
|
||||||
|
"10.1.2.3": "specific-host",
|
||||||
|
"10.1.2.0/24": "subnet-one-two-*",
|
||||||
|
"10.1.1.0/24": "subnet-one-one-*",
|
||||||
|
"10.1.0.0/24": "subnet-one-zero-*",
|
||||||
|
"10.1.0.0/16": "subnet-one-*",
|
||||||
|
"fde4:8dba:82e1:1::1": "v6-48-1-specialhost",
|
||||||
|
"fde4:8dba:82e1::/48": "v6-48-*",
|
||||||
|
"fde4:8dba:82e1:1::/64": "v6-64-1-*",
|
||||||
|
"fde4:8dba:82e1:2::/64": "v6-64-2-*",
|
||||||
|
},
|
||||||
|
expect: map[string]testCaseExpect{
|
||||||
|
"10.1.2.3": {expectIdent: "specific-host"},
|
||||||
|
"10.1.2.1": {expectIdent: "subnet-one-two-10.1.2.1"},
|
||||||
|
"10.1.1.1": {expectIdent: "subnet-one-one-10.1.1.1"},
|
||||||
|
"10.1.0.23": {expectIdent: "subnet-one-zero-10.1.0.23"},
|
||||||
|
"10.1.3.1": {expectIdent: "subnet-one-10.1.3.1"},
|
||||||
|
"10.2.1.1": {expectNoMapping: true},
|
||||||
|
"fde4:8dba:82e1:1::1": {expectIdent: "v6-48-1-specialhost"},
|
||||||
|
"fde4:8dba:82e1:23::1": {expectIdent: "v6-48-fde4:8dba:82e1:23::1"},
|
||||||
|
"fde4:8dba:82e1:1::2": {expectIdent: "v6-64-1-fde4:8dba:82e1:1::2"},
|
||||||
|
"fde4:8dba:82e1:2::1": {expectIdent: "v6-64-2-fde4:8dba:82e1:2::1"},
|
||||||
|
"fde4:8dba:82e2::1": {expectNoMapping: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "different prefixes, mixed ipv4 ipv6, with interface ids",
|
||||||
|
expectInitErr: false,
|
||||||
|
config: map[string]string{
|
||||||
|
"192.168.23.0/24": "db-*",
|
||||||
|
"192.168.23.23": "db-twentythree",
|
||||||
|
"192.168.42.0/24": "web-*",
|
||||||
|
"10.1.4.0/24": "my-*-server",
|
||||||
|
"2001:0db8:85a3:0000:0000:8a2e:0370:7334": "aspecifichost",
|
||||||
|
"2001:0db8:85a3:0000:0000:8a2e:0370:7334%eth1": "aspecifichost",
|
||||||
|
"fe80::/16%eth1": "san-*",
|
||||||
|
"fde4:8dba:82e1::/64": "sub64-*",
|
||||||
|
},
|
||||||
|
expect: map[string]testCaseExpect{
|
||||||
|
"10.1.2.3": {expectNoMapping: true},
|
||||||
|
"192.168.23.1": {expectIdent: "db-192.168.23.1"},
|
||||||
|
"192.168.23.23": {expectIdent: "db-twentythree"},
|
||||||
|
"192.168.023.001": {expectIdent: "db-192.168.23.1"},
|
||||||
|
"10.1.4.5": {expectIdent: "my-10.1.4.5-server"},
|
||||||
|
|
||||||
|
// normalization
|
||||||
|
"192.168.42.1": {expectIdent: "web-192.168.42.1"},
|
||||||
|
"192.168.042.001": {expectIdent: "web-192.168.42.1"},
|
||||||
|
// v6 matching
|
||||||
|
"fe80::23:42%eth1": {expectIdent: "san-fe80::23:42-eth1"},
|
||||||
|
"fe80::23:42%eth2": {expectNoMapping: true},
|
||||||
|
|
||||||
|
// v6 subnet matching
|
||||||
|
"fde4:8dba:82e1::1": {expectIdent: "sub64-fde4:8dba:82e1::1"},
|
||||||
|
// v6 subnet matching with suffix that matches another allowed IPv4
|
||||||
|
"fde4:8dba:82e1::c0a8:1717": {expectIdent: "sub64-fde4:8dba:82e1::c0a8:1717"},
|
||||||
|
|
||||||
|
"2001:0db8:85a3:0000:0000:8a2e:0370:7334": {expectIdent: "aspecifichost"},
|
||||||
|
"2001:0db8:85a3:0000:0000:8a2e:0370:7334%eth1": {expectIdent: "aspecifichost"},
|
||||||
|
"2001:0db8:85a3:0000:0000:8a2e:0370:7334%eth2": {expectNoMapping: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid user input: non ip or cidr",
|
||||||
|
expectInitErr: true,
|
||||||
|
config: map[string]string{
|
||||||
|
"jimmy": "db",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid user input: v4 ip with an identity containing *",
|
||||||
|
expectInitErr: true,
|
||||||
|
config: map[string]string{
|
||||||
|
"192.168.1.2": "db-*",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid user input: v4 subnet without an identity containing *",
|
||||||
|
expectInitErr: true,
|
||||||
|
config: map[string]string{
|
||||||
|
"192.168.1.0/24": "db-",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid user input: v6 ip with an identity containing *",
|
||||||
|
expectInitErr: true,
|
||||||
|
config: map[string]string{
|
||||||
|
"2001:0db8:85a3:0000:0000:8a2e:0370:7334": "aspecifichost*",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid user input: v6 subnet without identity containing *",
|
||||||
|
expectInitErr: true,
|
||||||
|
config: map[string]string{
|
||||||
|
"fe80::/16%eth1": "db-",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid user input with subnet match: client identity with forbidden zfs dataset name char @",
|
||||||
|
expectInitErr: true,
|
||||||
|
config: map[string]string{
|
||||||
|
"fe80::/16": "db@-*",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid user input with IP match: client identity with forbidden zfs dataset name char @",
|
||||||
|
expectInitErr: true,
|
||||||
|
config: map[string]string{
|
||||||
|
"fe80::1": "db@foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range cases {
|
||||||
|
c := cases[i]
|
||||||
|
t.Run(c.name, func(t *testing.T) {
|
||||||
|
pretty.Fprintf(os.Stderr, "running %#v\n", c)
|
||||||
|
m, err := ipMapFromConfig(c.config)
|
||||||
|
if c.expectInitErr {
|
||||||
|
require.Error(t, err)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, m)
|
||||||
|
}
|
||||||
|
for input, expect := range c.expect {
|
||||||
|
// reuse newIPMapEntry to parse test case input
|
||||||
|
// "test" is not used during testing but must not be empty.
|
||||||
|
ipMapEntry, _ := newIPMapEntry(input, "test")
|
||||||
|
ones, bits := ipMapEntry.subnet.Mask.Size()
|
||||||
|
require.Equal(t, bits, net.IPv6len*8, "and we know ipMapEntry always expands its IPs to 16bytes")
|
||||||
|
require.Equal(t, ones, net.IPv6len*8, "test case addresses must be fully specified")
|
||||||
|
require.NotNil(t, ipMapEntry)
|
||||||
|
ident, err := m.Get(&net.IPAddr{
|
||||||
|
IP: ipMapEntry.subnet.IP,
|
||||||
|
Zone: ipMapEntry.zone,
|
||||||
|
})
|
||||||
|
if expect.expectNoMapping {
|
||||||
|
assert.Empty(t, ident)
|
||||||
|
} else {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, expect.expectIdent, ident)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPackageNetAssumptions(t *testing.T) {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -32,7 +32,7 @@ func TLSListenerFactoryFromConfig(c *config.Global, in *config.TLSServe) (transp
|
|||||||
|
|
||||||
serverCert, err := tls.LoadX509KeyPair(in.Cert, in.Key)
|
serverCert, err := tls.LoadX509KeyPair(in.Cert, in.Key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "cannot parse cer/key pair")
|
return nil, errors.Wrap(err, "cannot parse cert/key pair")
|
||||||
}
|
}
|
||||||
|
|
||||||
clientCNs := make(map[string]struct{}, len(in.ClientCNs))
|
clientCNs := make(map[string]struct{}, len(in.ClientCNs))
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ package transport
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"net"
|
"net"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
"github.com/zrepl/zrepl/logger"
|
"github.com/zrepl/zrepl/logger"
|
||||||
"github.com/zrepl/zrepl/rpc/dataconn/timeoutconn"
|
"github.com/zrepl/zrepl/rpc/dataconn/timeoutconn"
|
||||||
"github.com/zrepl/zrepl/zfs"
|
"github.com/zrepl/zrepl/zfs"
|
||||||
@@ -55,13 +55,10 @@ type Connecter interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// A client identity must be a single component in a ZFS filesystem path
|
// A client identity must be a single component in a ZFS filesystem path
|
||||||
func ValidateClientIdentity(in string) (err error) {
|
func ValidateClientIdentity(in string) error {
|
||||||
path, err := zfs.NewDatasetPath(in)
|
err := zfs.ComponentNamecheck(in)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return errors.Wrap(err, "client identity must be usable as a single dataset path component")
|
||||||
}
|
|
||||||
if path.Length() != 1 {
|
|
||||||
return errors.New("client identity must be a single path comonent (not empty, no '/')")
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user