Compare commits
75 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ffb1d89a72 | |||
| a967986a18 | |||
| c743c7b03f | |||
| a9c61b4b0b | |||
| 206d359dcd | |||
| 2d8c3692ec | |||
| 7769263c2e | |||
| 89f7c76c4e | |||
| c7771f98f5 | |||
| 299f1c906e | |||
| d3f68ae4e8 | |||
| 193abbe6b1 | |||
| 02b215128e | |||
| dc03db7423 | |||
| 1df0f8912a | |||
| e4112d888c | |||
| 53f9bd6d88 | |||
| 43c2a0d9b0 | |||
| e0c7ceedd5 | |||
| 2642c64303 | |||
| 1acafabb5b | |||
| 19b2deb2cf | |||
| ce6701fb33 | |||
| 0121929164 | |||
| bc96f8f212 | |||
| 459508c9d9 | |||
| 4a27cc63a8 | |||
| 0a6840273a | |||
| 76ef84f83b | |||
| 66946df756 | |||
| 556fac3002 | |||
| 1ad7df2df3 | |||
| a3d010c5f0 | |||
| 12503dc55a | |||
| 7d10a71cc0 | |||
| 3d3d1b5679 | |||
| 5240ab4949 | |||
| 19aebd399f | |||
| 04e03f4d06 | |||
| 2e2a8a1d5d | |||
| a2b2e0fe34 | |||
| af2905d245 | |||
| c3f0041efd | |||
| 083f6001eb | |||
| 2d57ec6ee0 | |||
| fb6a9be954 | |||
| c1e2c9826f | |||
| b00b61e967 | |||
| ac147b5a6f | |||
| 1850a332ed | |||
| 20ff9717bc | |||
| c2fbf93365 | |||
| cf5e8e8f26 | |||
| c600cc1f60 | |||
| c6a9ebc71c | |||
| 1f0f2f8569 | |||
| 5104ad3d0b | |||
| a6dbda1ea8 | |||
| 1edb8014bc | |||
| 845195b7ed | |||
| 2c9fcd7c14 | |||
| 4f9b63aa09 | |||
| a8e92971d0 | |||
| b54e477602 | |||
| 959fb08a89 | |||
| 6ac012aa3c | |||
| 3e93b31f75 | |||
| 08df208149 | |||
| 936ed73a45 | |||
| 8fee536260 | |||
| 01b4792974 | |||
| ad80bb3735 | |||
| f5f269bfd5 | |||
| 5b16769057 | |||
| 009bd410af |
+10
-9
@@ -150,7 +150,7 @@ parameters:
|
|||||||
|
|
||||||
release_docker_baseimage_tag:
|
release_docker_baseimage_tag:
|
||||||
type: string
|
type: string
|
||||||
default: "1.16"
|
default: "1.17"
|
||||||
|
|
||||||
workflows:
|
workflows:
|
||||||
version: 2
|
version: 2
|
||||||
@@ -160,15 +160,15 @@ workflows:
|
|||||||
jobs:
|
jobs:
|
||||||
- quickcheck-docs
|
- quickcheck-docs
|
||||||
- quickcheck-go: &quickcheck-go-smoketest
|
- quickcheck-go: &quickcheck-go-smoketest
|
||||||
name: quickcheck-go-amd64-linux-1.16
|
name: quickcheck-go-amd64-linux-1.17
|
||||||
goversion: &latest-go-release "1.16"
|
goversion: &latest-go-release "1.17"
|
||||||
goos: linux
|
goos: linux
|
||||||
goarch: amd64
|
goarch: amd64
|
||||||
- test-go-on-latest-go-release:
|
- test-go-on-latest-go-release:
|
||||||
goversion: *latest-go-release
|
goversion: *latest-go-release
|
||||||
- quickcheck-go:
|
- quickcheck-go:
|
||||||
requires:
|
requires:
|
||||||
- quickcheck-go-amd64-linux-1.16 #quickcheck-go-smoketest.name
|
- quickcheck-go-amd64-linux-1.17 #quickcheck-go-smoketest.name
|
||||||
matrix: &quickcheck-go-matrix
|
matrix: &quickcheck-go-matrix
|
||||||
alias: quickcheck-go-matrix
|
alias: quickcheck-go-matrix
|
||||||
parameters:
|
parameters:
|
||||||
@@ -231,8 +231,6 @@ jobs:
|
|||||||
- install-docdep
|
- install-docdep
|
||||||
- run: make docs
|
- run: make docs
|
||||||
|
|
||||||
- store_artifacts:
|
|
||||||
path: artifacts
|
|
||||||
- download-and-install-minio-client
|
- download-and-install-minio-client
|
||||||
- upload-minio:
|
- upload-minio:
|
||||||
src: artifacts
|
src: artifacts
|
||||||
@@ -296,7 +294,8 @@ jobs:
|
|||||||
# don't save-cache-gomod here, test-go doesn't pull all the dependencies
|
# don't save-cache-gomod here, test-go doesn't pull all the dependencies
|
||||||
|
|
||||||
release-build:
|
release-build:
|
||||||
machine: true
|
machine:
|
||||||
|
image: ubuntu-2004:202201-02
|
||||||
steps:
|
steps:
|
||||||
- checkout
|
- checkout
|
||||||
- run: make release-docker RELEASE_DOCKER_BASEIMAGE_TAG=<<pipeline.parameters.release_docker_baseimage_tag>>
|
- run: make release-docker RELEASE_DOCKER_BASEIMAGE_TAG=<<pipeline.parameters.release_docker_baseimage_tag>>
|
||||||
@@ -304,7 +303,8 @@ jobs:
|
|||||||
root: .
|
root: .
|
||||||
paths: [.]
|
paths: [.]
|
||||||
release-deb:
|
release-deb:
|
||||||
machine: true
|
machine:
|
||||||
|
image: ubuntu-2004:202201-02
|
||||||
steps:
|
steps:
|
||||||
- attach_workspace:
|
- attach_workspace:
|
||||||
at: .
|
at: .
|
||||||
@@ -315,7 +315,8 @@ jobs:
|
|||||||
- "artifacts/*.deb"
|
- "artifacts/*.deb"
|
||||||
|
|
||||||
release-rpm:
|
release-rpm:
|
||||||
machine: true
|
machine:
|
||||||
|
image: ubuntu-2004:202201-02
|
||||||
steps:
|
steps:
|
||||||
- attach_workspace:
|
- attach_workspace:
|
||||||
at: .
|
at: .
|
||||||
|
|||||||
@@ -7,4 +7,10 @@ issues:
|
|||||||
- path: _test\.go
|
- path: _test\.go
|
||||||
linters:
|
linters:
|
||||||
- errcheck
|
- errcheck
|
||||||
|
# Disable staticcheck 'Empty body in an if or else branch' as it's useful
|
||||||
|
# to put a comment into an empty else-clause that explains why whatever
|
||||||
|
# is done in the if-caluse is not necessary if the condition is false.
|
||||||
|
- linters:
|
||||||
|
- staticcheck
|
||||||
|
text: "SA9003:"
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ GO_BUILDFLAGS := $(GO_MOD_READONLY) $(GO_EXTRA_BUILDFLAGS)
|
|||||||
GO_BUILD := $(GO_ENV_VARS) $(GO) build $(GO_BUILDFLAGS) -ldflags $(GO_LDFLAGS)
|
GO_BUILD := $(GO_ENV_VARS) $(GO) build $(GO_BUILDFLAGS) -ldflags $(GO_LDFLAGS)
|
||||||
GOLANGCI_LINT := golangci-lint
|
GOLANGCI_LINT := golangci-lint
|
||||||
GOCOVMERGE := gocovmerge
|
GOCOVMERGE := gocovmerge
|
||||||
RELEASE_DOCKER_BASEIMAGE_TAG ?= 1.16
|
RELEASE_DOCKER_BASEIMAGE_TAG ?= 1.17
|
||||||
RELEASE_DOCKER_BASEIMAGE ?= golang:$(RELEASE_DOCKER_BASEIMAGE_TAG)
|
RELEASE_DOCKER_BASEIMAGE ?= golang:$(RELEASE_DOCKER_BASEIMAGE_TAG)
|
||||||
|
|
||||||
ifneq ($(GOARM),)
|
ifneq ($(GOARM),)
|
||||||
@@ -55,7 +55,7 @@ release: clean
|
|||||||
$(MAKE) wrapup-and-checksum
|
$(MAKE) wrapup-and-checksum
|
||||||
$(MAKE) check-git-clean
|
$(MAKE) check-git-clean
|
||||||
ifeq (SIGN, 1)
|
ifeq (SIGN, 1)
|
||||||
$(make) sign
|
$(MAKE) sign
|
||||||
endif
|
endif
|
||||||
@echo "ZREPL RELEASE ARTIFACTS AVAILABLE IN artifacts/release"
|
@echo "ZREPL RELEASE ARTIFACTS AVAILABLE IN artifacts/release"
|
||||||
|
|
||||||
@@ -173,6 +173,10 @@ check-git-clean:
|
|||||||
fi; \
|
fi; \
|
||||||
fi;
|
fi;
|
||||||
|
|
||||||
|
tag-release:
|
||||||
|
test -n "$(ZREPL_TAG_VERSION)" || exit 1
|
||||||
|
git tag -u E27CA5FC -m "$(ZREPL_TAG_VERSION)" "$(ZREPL_TAG_VERSION)"
|
||||||
|
|
||||||
sign:
|
sign:
|
||||||
gpg -u "89BC 5D89 C845 568B F578 B306 CDBD 8EC8 E27C A5FC" \
|
gpg -u "89BC 5D89 C845 568B F578 B306 CDBD 8EC8 E27C A5FC" \
|
||||||
--armor \
|
--armor \
|
||||||
@@ -189,6 +193,8 @@ GO_SUPPORTS_ILLUMOS := $(shell $(GO) version | gawk -F '.' '/^go version /{split
|
|||||||
bins-all:
|
bins-all:
|
||||||
$(MAKE) $(BINS_ALL_TARGETS) GOOS=freebsd GOARCH=amd64
|
$(MAKE) $(BINS_ALL_TARGETS) GOOS=freebsd GOARCH=amd64
|
||||||
$(MAKE) $(BINS_ALL_TARGETS) GOOS=freebsd GOARCH=386
|
$(MAKE) $(BINS_ALL_TARGETS) GOOS=freebsd GOARCH=386
|
||||||
|
$(MAKE) $(BINS_ALL_TARGETS) GOOS=freebsd GOARCH=arm GOARM=7
|
||||||
|
$(MAKE) $(BINS_ALL_TARGETS) GOOS=freebsd GOARCH=arm64
|
||||||
$(MAKE) $(BINS_ALL_TARGETS) GOOS=linux GOARCH=amd64
|
$(MAKE) $(BINS_ALL_TARGETS) GOOS=linux GOARCH=amd64
|
||||||
$(MAKE) $(BINS_ALL_TARGETS) GOOS=linux GOARCH=arm64
|
$(MAKE) $(BINS_ALL_TARGETS) GOOS=linux GOARCH=arm64
|
||||||
$(MAKE) $(BINS_ALL_TARGETS) GOOS=linux GOARCH=arm GOARM=7
|
$(MAKE) $(BINS_ALL_TARGETS) GOOS=linux GOARCH=arm GOARM=7
|
||||||
@@ -327,12 +333,12 @@ $(ARTIFACTDIR)/go_env.txt:
|
|||||||
|
|
||||||
docs: $(ARTIFACTDIR)/docs
|
docs: $(ARTIFACTDIR)/docs
|
||||||
# https://www.sphinx-doc.org/en/master/man/sphinx-build.html
|
# https://www.sphinx-doc.org/en/master/man/sphinx-build.html
|
||||||
make -C docs \
|
$(MAKE) -C docs \
|
||||||
html \
|
html \
|
||||||
BUILDDIR=../artifacts/docs \
|
BUILDDIR=../artifacts/docs \
|
||||||
SPHINXOPTS="-W --keep-going -n"
|
SPHINXOPTS="-W --keep-going -n"
|
||||||
|
|
||||||
docs-clean:
|
docs-clean:
|
||||||
make -C docs \
|
$(MAKE) -C docs \
|
||||||
clean \
|
clean \
|
||||||
BUILDDIR=../artifacts/docs
|
BUILDDIR=../artifacts/docs
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
[](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://patreon.com/zrepl)
|
[](https://patreon.com/zrepl)
|
||||||
[](https://github.com/sponsors/problame)
|
[](https://github.com/sponsors/problame)
|
||||||
[](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://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)
|
||||||
|
[](https://matrix.to/#/#zrepl:matrix.org)
|
||||||
|
|
||||||
# zrepl
|
# zrepl
|
||||||
zrepl is a one-stop ZFS backup & replication solution.
|
zrepl is a one-stop ZFS backup & replication solution.
|
||||||
@@ -95,7 +96,7 @@ Downstream packagers can read the changelog to determine whether they want to pu
|
|||||||
|
|
||||||
### Additional Notes to Distro Package Maintainers
|
### Additional Notes to Distro Package Maintainers
|
||||||
|
|
||||||
* Use `sudo make test-platform-bin && sudo make test-platform` **on a test system** to validate that zrepl's abstractions on top of ZFS work with the system ZFS.
|
* Run the platform tests (Docs -> Usage -> Platform Tests) **on a test system** to validate that zrepl's abstractions on top of ZFS work with the system ZFS.
|
||||||
* Ship a default config that adheres to your distro's `hier` and logging system.
|
* Ship a default config that adheres to your distro's `hier` and logging system.
|
||||||
* Ship a service manager file and _please_ try to upstream it to this repository.
|
* Ship a service manager file and _please_ try to upstream it to this repository.
|
||||||
* `dist/systemd` contains a Systemd unit template.
|
* `dist/systemd` contains a Systemd unit template.
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
//go:build tools
|
||||||
// +build tools
|
// +build tools
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|||||||
+13
-3
@@ -19,8 +19,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var configcheckArgs struct {
|
var configcheckArgs struct {
|
||||||
format string
|
format string
|
||||||
what string
|
what string
|
||||||
|
skipCertCheck bool
|
||||||
}
|
}
|
||||||
|
|
||||||
var ConfigcheckCmd = &cli.Subcommand{
|
var ConfigcheckCmd = &cli.Subcommand{
|
||||||
@@ -29,6 +30,7 @@ var ConfigcheckCmd = &cli.Subcommand{
|
|||||||
SetupFlags: func(f *pflag.FlagSet) {
|
SetupFlags: func(f *pflag.FlagSet) {
|
||||||
f.StringVar(&configcheckArgs.format, "format", "", "dump parsed config object [pretty|yaml|json]")
|
f.StringVar(&configcheckArgs.format, "format", "", "dump parsed config object [pretty|yaml|json]")
|
||||||
f.StringVar(&configcheckArgs.what, "what", "all", "what to print [all|config|jobs|logging]")
|
f.StringVar(&configcheckArgs.what, "what", "all", "what to print [all|config|jobs|logging]")
|
||||||
|
f.BoolVar(&configcheckArgs.skipCertCheck, "skip-cert-check", false, "skip checking cert files")
|
||||||
},
|
},
|
||||||
Run: func(ctx context.Context, subcommand *cli.Subcommand, args []string) error {
|
Run: func(ctx context.Context, subcommand *cli.Subcommand, args []string) error {
|
||||||
formatMap := map[string]func(interface{}){
|
formatMap := map[string]func(interface{}){
|
||||||
@@ -56,8 +58,16 @@ var ConfigcheckCmd = &cli.Subcommand{
|
|||||||
}
|
}
|
||||||
|
|
||||||
var hadErr bool
|
var hadErr bool
|
||||||
|
|
||||||
|
parseFlags := config.ParseFlagsNone
|
||||||
|
|
||||||
|
if configcheckArgs.skipCertCheck {
|
||||||
|
parseFlags |= config.ParseFlagsNoCertCheck
|
||||||
|
}
|
||||||
|
|
||||||
// further: try to build jobs
|
// further: try to build jobs
|
||||||
confJobs, err := job.JobsFromConfig(subcommand.Config())
|
confJobs, err := job.JobsFromConfig(subcommand.Config(), parseFlags)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err := errors.Wrap(err, "cannot build jobs from config")
|
err := errors.Wrap(err, "cannot build jobs from config")
|
||||||
if configcheckArgs.what == "jobs" {
|
if configcheckArgs.what == "jobs" {
|
||||||
|
|||||||
+1
-1
@@ -129,7 +129,7 @@ func doMigrateReplicationCursor(ctx context.Context, sc *cli.Subcommand, args []
|
|||||||
}
|
}
|
||||||
|
|
||||||
cfg := sc.Config()
|
cfg := sc.Config()
|
||||||
jobs, err := job.JobsFromConfig(cfg)
|
jobs, err := job.JobsFromConfig(cfg, config.ParseFlagsNone)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("cannot parse config:\n%s\n\n", err)
|
fmt.Printf("cannot parse config:\n%s\n\n", err)
|
||||||
fmt.Printf("NOTE: this migration was released together with a change in job name requirements.\n")
|
fmt.Printf("NOTE: this migration was released together with a change in job name requirements.\n")
|
||||||
|
|||||||
@@ -14,11 +14,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Client struct {
|
type Client struct {
|
||||||
h http.Client
|
h *http.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(network, addr string) (*Client, error) {
|
func New(network, addr string) (*Client, error) {
|
||||||
httpc, err := controlHttpClient(func(_ context.Context) (net.Conn, error) { return net.Dial(network, addr) })
|
httpc, err := makeControlHttpClient(func(_ context.Context) (net.Conn, error) { return net.Dial(network, addr) })
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -63,8 +63,8 @@ func (c *Client) SignalReset(job string) error {
|
|||||||
return c.signal(job, "reset")
|
return c.signal(job, "reset")
|
||||||
}
|
}
|
||||||
|
|
||||||
func controlHttpClient(dialfunc func(context.Context) (net.Conn, error)) (client http.Client, err error) {
|
func makeControlHttpClient(dialfunc func(context.Context) (net.Conn, error)) (client *http.Client, err error) {
|
||||||
return http.Client{
|
return &http.Client{
|
||||||
Transport: &http.Transport{
|
Transport: &http.Transport{
|
||||||
DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
|
DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
|
||||||
return dialfunc(ctx)
|
return dialfunc(ctx)
|
||||||
@@ -73,14 +73,24 @@ func controlHttpClient(dialfunc func(context.Context) (net.Conn, error)) (client
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func jsonRequestResponse(c http.Client, endpoint string, req interface{}, res interface{}) error {
|
func jsonRequestResponse(c *http.Client, endpoint string, req interface{}, res interface{}) error {
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
encodeErr := json.NewEncoder(&buf).Encode(req)
|
encodeErr := json.NewEncoder(&buf).Encode(req)
|
||||||
if encodeErr != nil {
|
if encodeErr != nil {
|
||||||
return encodeErr
|
return encodeErr
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := c.Post("http://unix"+endpoint, "application/json", &buf)
|
hreq, err := http.NewRequest("POST", "http://unix"+endpoint, &buf)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
hreq.Header.Set("Content-Type", "application/json")
|
||||||
|
// Prevent EOF errors when client request frequency and server keepalive close are at the same time.
|
||||||
|
// Found this by watching http.Server.ConnState changes, then found
|
||||||
|
// https://stackoverflow.com/questions/17714494/golang-http-request-results-in-eof-errors-when-making-multiple-requests-successi
|
||||||
|
// Note: The issue seems even more prounounced with local TCP sockets than unix domain sockets. So, I used that for debugging.
|
||||||
|
hreq.Close = true
|
||||||
|
resp, err := c.Do(hreq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ func interactive(c Client, flag statusFlags) error {
|
|||||||
FSFilter: func(_ string) bool { return true },
|
FSFilter: func(_ string) bool { return true },
|
||||||
DetailViewWidth: 100,
|
DetailViewWidth: 100,
|
||||||
DetailViewWrap: false,
|
DetailViewWrap: false,
|
||||||
ShortKeybindingOverview: "[::b]Q[::-] quit [::b]<TAB>[::-] switch panes [::b]Shift+M[::-] toggle navbar [::b]Shift+S[::-] signal job [::b]</>[::-] filter filesystems",
|
ShortKeybindingOverview: "[::b]Q[::-] quit [::b]<TAB>[::-] switch panes [::b]W[::-] wrap lines [::b]Shift+M[::-] toggle navbar [::b]Shift+S[::-] signal job [::b]</>[::-] filter filesystems",
|
||||||
}
|
}
|
||||||
paramsMtx := &sync.Mutex{}
|
paramsMtx := &sync.Mutex{}
|
||||||
var redraw func()
|
var redraw func()
|
||||||
|
|||||||
@@ -2,14 +2,22 @@ package viewmodel
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func ByteCountBinaryUint(b uint64) string {
|
||||||
|
if b > math.MaxInt64 {
|
||||||
|
panic(b)
|
||||||
|
}
|
||||||
|
return ByteCountBinary(int64(b))
|
||||||
|
}
|
||||||
|
|
||||||
func ByteCountBinary(b int64) string {
|
func ByteCountBinary(b int64) string {
|
||||||
const unit = 1024
|
const unit = 1024
|
||||||
if b < unit {
|
if b < unit {
|
||||||
return fmt.Sprintf("%d B", b)
|
return fmt.Sprintf("%d B", b)
|
||||||
}
|
}
|
||||||
div, exp := int64(unit), 0
|
div, exp := unit, 0
|
||||||
for n := b / unit; n >= unit; n /= unit {
|
for n := b / unit; n >= unit; n /= unit {
|
||||||
div *= unit
|
div *= unit
|
||||||
exp++
|
exp++
|
||||||
|
|||||||
@@ -4,17 +4,16 @@ import "time"
|
|||||||
|
|
||||||
type byteProgressMeasurement struct {
|
type byteProgressMeasurement struct {
|
||||||
time time.Time
|
time time.Time
|
||||||
val int64
|
val uint64
|
||||||
}
|
}
|
||||||
|
|
||||||
type bytesProgressHistory struct {
|
type bytesProgressHistory struct {
|
||||||
last *byteProgressMeasurement // pointer as poor man's optional
|
last *byteProgressMeasurement // pointer as poor man's optional
|
||||||
changeCount int
|
changeCount int
|
||||||
lastChange time.Time
|
lastChange time.Time
|
||||||
bpsAvg float64
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *bytesProgressHistory) Update(currentVal int64) (bytesPerSecondAvg int64, changeCount int) {
|
func (p *bytesProgressHistory) Update(currentVal uint64) (bytesPerSecondAvg int64, changeCount int) {
|
||||||
|
|
||||||
if p.last == nil {
|
if p.last == nil {
|
||||||
p.last = &byteProgressMeasurement{
|
p.last = &byteProgressMeasurement{
|
||||||
@@ -34,15 +33,17 @@ func (p *bytesProgressHistory) Update(currentVal int64) (bytesPerSecondAvg int64
|
|||||||
return 0, 0
|
return 0, 0
|
||||||
}
|
}
|
||||||
|
|
||||||
deltaV := currentVal - p.last.val
|
var deltaV int64
|
||||||
|
if currentVal >= p.last.val {
|
||||||
|
deltaV = int64(currentVal - p.last.val)
|
||||||
|
} else {
|
||||||
|
deltaV = -int64(p.last.val - currentVal)
|
||||||
|
}
|
||||||
deltaT := time.Since(p.last.time)
|
deltaT := time.Since(p.last.time)
|
||||||
rate := float64(deltaV) / deltaT.Seconds()
|
rate := float64(deltaV) / deltaT.Seconds()
|
||||||
|
|
||||||
factor := 0.3
|
|
||||||
p.bpsAvg = (1-factor)*p.bpsAvg + factor*rate
|
|
||||||
|
|
||||||
p.last.time = time.Now()
|
p.last.time = time.Now()
|
||||||
p.last.val = currentVal
|
p.last.val = currentVal
|
||||||
|
|
||||||
return int64(p.bpsAvg), p.changeCount
|
return int64(rate), p.changeCount
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -216,7 +216,7 @@ func drawJob(t *stringbuilder.B, name string, v *job.Status, history *bytesProgr
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func printFilesystemStatus(t *stringbuilder.B, rep *report.FilesystemReport, active bool, maxFS int) {
|
func printFilesystemStatus(t *stringbuilder.B, rep *report.FilesystemReport, maxFS int) {
|
||||||
|
|
||||||
expected, replicated, containsInvalidSizeEstimates := rep.BytesSum()
|
expected, replicated, containsInvalidSizeEstimates := rep.BytesSum()
|
||||||
sizeEstimationImpreciseNotice := ""
|
sizeEstimationImpreciseNotice := ""
|
||||||
@@ -227,15 +227,28 @@ func printFilesystemStatus(t *stringbuilder.B, rep *report.FilesystemReport, act
|
|||||||
sizeEstimationImpreciseNotice = " (step lacks size estimation)"
|
sizeEstimationImpreciseNotice = " (step lacks size estimation)"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
userVisisbleCurrentStep, userVisibleTotalSteps := rep.CurrentStep, len(rep.Steps)
|
||||||
|
// `.CurrentStep` is == len(rep.Steps) if all steps are done.
|
||||||
|
// Until then, it's an index into .Steps that starts at 0.
|
||||||
|
// For the user, we want it to start at 1.
|
||||||
|
if rep.CurrentStep >= len(rep.Steps) {
|
||||||
|
// rep.CurrentStep is what we want to show.
|
||||||
|
// We check for >= and not == for robustness.
|
||||||
|
} else {
|
||||||
|
// We're not done yet, so, make step count start at 1
|
||||||
|
// (The `.State` is included in the output, indicating we're not done yet)
|
||||||
|
userVisisbleCurrentStep = rep.CurrentStep + 1
|
||||||
|
}
|
||||||
status := fmt.Sprintf("%s (step %d/%d, %s/%s)%s",
|
status := fmt.Sprintf("%s (step %d/%d, %s/%s)%s",
|
||||||
strings.ToUpper(string(rep.State)),
|
strings.ToUpper(string(rep.State)),
|
||||||
rep.CurrentStep, len(rep.Steps),
|
userVisisbleCurrentStep, userVisibleTotalSteps,
|
||||||
ByteCountBinary(replicated), ByteCountBinary(expected),
|
ByteCountBinaryUint(replicated), ByteCountBinaryUint(expected),
|
||||||
sizeEstimationImpreciseNotice,
|
sizeEstimationImpreciseNotice,
|
||||||
)
|
)
|
||||||
|
|
||||||
activeIndicator := " "
|
activeIndicator := " "
|
||||||
if active {
|
if rep.BlockedOn == report.FsBlockedOnNothing &&
|
||||||
|
(rep.State == report.FilesystemPlanning || rep.State == report.FilesystemStepping) {
|
||||||
activeIndicator = "*"
|
activeIndicator = "*"
|
||||||
}
|
}
|
||||||
t.AddIndent(1)
|
t.AddIndent(1)
|
||||||
@@ -260,9 +273,9 @@ func printFilesystemStatus(t *stringbuilder.B, rep *report.FilesystemReport, act
|
|||||||
attribs = append(attribs, "resumed")
|
attribs = append(attribs, "resumed")
|
||||||
}
|
}
|
||||||
|
|
||||||
attribs = append(attribs, fmt.Sprintf("encrypted=%s", nextStep.Info.Encrypted))
|
if len(attribs) > 0 {
|
||||||
|
next += fmt.Sprintf(" (%s)", strings.Join(attribs, ", "))
|
||||||
next += fmt.Sprintf(" (%s)", strings.Join(attribs, ", "))
|
}
|
||||||
} else {
|
} else {
|
||||||
next = "" // individual FSes may still be in planning state
|
next = "" // individual FSes may still be in planning state
|
||||||
}
|
}
|
||||||
@@ -358,12 +371,12 @@ func renderReplicationReport(t *stringbuilder.B, rep *report.Report, history *by
|
|||||||
rate, changeCount := history.Update(replicated)
|
rate, changeCount := history.Update(replicated)
|
||||||
eta := time.Duration(0)
|
eta := time.Duration(0)
|
||||||
if rate > 0 {
|
if rate > 0 {
|
||||||
eta = time.Duration((expected-replicated)/rate) * time.Second
|
eta = time.Duration((float64(expected)-float64(replicated))/float64(rate)) * time.Second
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Write("Progress: ")
|
t.Write("Progress: ")
|
||||||
t.DrawBar(50, replicated, expected, changeCount)
|
t.DrawBar(50, replicated, expected, changeCount)
|
||||||
t.Write(fmt.Sprintf(" %s / %s @ %s/s", ByteCountBinary(replicated), ByteCountBinary(expected), ByteCountBinary(rate)))
|
t.Write(fmt.Sprintf(" %s / %s @ %s/s", ByteCountBinaryUint(replicated), ByteCountBinaryUint(expected), ByteCountBinary(rate)))
|
||||||
if eta != 0 {
|
if eta != 0 {
|
||||||
t.Write(fmt.Sprintf(" (%s remaining)", humanizeDuration(eta)))
|
t.Write(fmt.Sprintf(" (%s remaining)", humanizeDuration(eta)))
|
||||||
}
|
}
|
||||||
@@ -385,7 +398,7 @@ func renderReplicationReport(t *stringbuilder.B, rep *report.Report, history *by
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, fs := range latest.Filesystems {
|
for _, fs := range latest.Filesystems {
|
||||||
printFilesystemStatus(t, fs, false, maxFSLen) // FIXME bring 'active' flag back
|
printFilesystemStatus(t, fs, maxFSLen)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -534,10 +547,20 @@ func renderPrunerReport(t *stringbuilder.B, r *pruner.Report, fsfilter FilterFun
|
|||||||
|
|
||||||
func renderSnapperReport(t *stringbuilder.B, r *snapper.Report, fsfilter FilterFunc) {
|
func renderSnapperReport(t *stringbuilder.B, r *snapper.Report, fsfilter FilterFunc) {
|
||||||
if r == nil {
|
if r == nil {
|
||||||
t.Printf("<snapshot type does not have a report>\n")
|
t.Printf("<no snapshotting report available>\n")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
t.Printf("Type: %s\n", r.Type)
|
||||||
|
if r.Periodic != nil {
|
||||||
|
renderSnapperReportPeriodic(t, r.Periodic, fsfilter)
|
||||||
|
} else if r.Cron != nil {
|
||||||
|
renderSnapperReportCron(t, r.Cron, fsfilter)
|
||||||
|
} else {
|
||||||
|
t.Printf("<no details available>")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderSnapperReportPeriodic(t *stringbuilder.B, r *snapper.PeriodicReport, fsfilter FilterFunc) {
|
||||||
t.Printf("Status: %s", r.State)
|
t.Printf("Status: %s", r.State)
|
||||||
t.Newline()
|
t.Newline()
|
||||||
|
|
||||||
@@ -548,8 +571,25 @@ func renderSnapperReport(t *stringbuilder.B, r *snapper.Report, fsfilter FilterF
|
|||||||
t.Printf("Sleep until: %s\n", r.SleepUntil)
|
t.Printf("Sleep until: %s\n", r.SleepUntil)
|
||||||
}
|
}
|
||||||
|
|
||||||
sort.Slice(r.Progress, func(i, j int) bool {
|
renderSnapperPlanReportFilesystem(t, r.Progress, fsfilter)
|
||||||
return strings.Compare(r.Progress[i].Path, r.Progress[j].Path) == -1
|
}
|
||||||
|
|
||||||
|
func renderSnapperReportCron(t *stringbuilder.B, r *snapper.CronReport, fsfilter FilterFunc) {
|
||||||
|
t.Printf("State: %s\n", r.State)
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
if r.WakeupTime.After(now) {
|
||||||
|
t.Printf("Sleep until: %s (%s remaining)\n", r.WakeupTime, r.WakeupTime.Sub(now).Round(time.Second))
|
||||||
|
} else {
|
||||||
|
t.Printf("Started: %s (lasting %s)\n", r.WakeupTime, now.Sub(r.WakeupTime).Round(time.Second))
|
||||||
|
}
|
||||||
|
|
||||||
|
renderSnapperPlanReportFilesystem(t, r.Progress, fsfilter)
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderSnapperPlanReportFilesystem(t *stringbuilder.B, fss []*snapper.ReportFilesystem, fsfilter FilterFunc) {
|
||||||
|
sort.Slice(fss, func(i, j int) bool {
|
||||||
|
return strings.Compare(fss[i].Path, fss[j].Path) == -1
|
||||||
})
|
})
|
||||||
|
|
||||||
dur := func(d time.Duration) string {
|
dur := func(d time.Duration) string {
|
||||||
@@ -562,8 +602,8 @@ func renderSnapperReport(t *stringbuilder.B, r *snapper.Report, fsfilter FilterF
|
|||||||
var widths struct {
|
var widths struct {
|
||||||
path, state, duration int
|
path, state, duration int
|
||||||
}
|
}
|
||||||
rows := make([]*row, 0, len(r.Progress))
|
rows := make([]*row, 0, len(fss))
|
||||||
for _, fs := range r.Progress {
|
for _, fs := range fss {
|
||||||
if !fsfilter(fs.Path) {
|
if !fsfilter(fs.Path) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -606,9 +646,11 @@ func renderSnapperReport(t *stringbuilder.B, r *snapper.Report, fsfilter FilterF
|
|||||||
t.Printf("%s %s %s", path, state, duration)
|
t.Printf("%s %s %s", path, state, duration)
|
||||||
t.PrintfDrawIndentedAndWrappedIfMultiline(" %s", r.remainder)
|
t.PrintfDrawIndentedAndWrappedIfMultiline(" %s", r.remainder)
|
||||||
if r.hookReport != "" {
|
if r.hookReport != "" {
|
||||||
t.PrintfDrawIndentedAndWrappedIfMultiline("%s", r.hookReport)
|
t.AddIndent(1)
|
||||||
|
t.Newline()
|
||||||
|
t.Printf("%s", r.hookReport)
|
||||||
|
t.AddIndent(-1)
|
||||||
}
|
}
|
||||||
t.Newline()
|
t.Newline()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,11 +99,11 @@ func RightPad(str string, length int, pad string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// changeCount = 0 indicates stall / no progress
|
// changeCount = 0 indicates stall / no progress
|
||||||
func (w *B) DrawBar(length int, bytes, totalBytes int64, changeCount int) {
|
func (w *B) DrawBar(length int, bytes, totalBytes uint64, changeCount int) {
|
||||||
const arrowPositions = `>\|/`
|
const arrowPositions = `>\|/`
|
||||||
var completedLength int
|
var completedLength int
|
||||||
if totalBytes > 0 {
|
if totalBytes > 0 {
|
||||||
completedLength = int(int64(length) * bytes / totalBytes)
|
completedLength = int(uint64(length) * bytes / totalBytes)
|
||||||
if completedLength > length {
|
if completedLength > length {
|
||||||
completedLength = length
|
completedLength = length
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-1
@@ -139,9 +139,11 @@ var testPlaceholder = &cli.Subcommand{
|
|||||||
func runTestPlaceholder(ctx context.Context, subcommand *cli.Subcommand, args []string) error {
|
func runTestPlaceholder(ctx context.Context, subcommand *cli.Subcommand, args []string) error {
|
||||||
|
|
||||||
var checkDPs []*zfs.DatasetPath
|
var checkDPs []*zfs.DatasetPath
|
||||||
|
var datasetWasExplicitArgument bool
|
||||||
|
|
||||||
// all actions first
|
// all actions first
|
||||||
if testPlaceholderArgs.all {
|
if testPlaceholderArgs.all {
|
||||||
|
datasetWasExplicitArgument = false
|
||||||
out, err := zfs.ZFSList(ctx, []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")
|
||||||
@@ -154,6 +156,7 @@ func runTestPlaceholder(ctx context.Context, subcommand *cli.Subcommand, args []
|
|||||||
checkDPs = append(checkDPs, dp)
|
checkDPs = append(checkDPs, dp)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
datasetWasExplicitArgument = true
|
||||||
dp, err := zfs.NewDatasetPath(testPlaceholderArgs.ds)
|
dp, err := zfs.NewDatasetPath(testPlaceholderArgs.ds)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -171,7 +174,12 @@ func runTestPlaceholder(ctx context.Context, subcommand *cli.Subcommand, args []
|
|||||||
return errors.Wrap(err, "cannot get placeholder state")
|
return errors.Wrap(err, "cannot get placeholder state")
|
||||||
}
|
}
|
||||||
if !ph.FSExists {
|
if !ph.FSExists {
|
||||||
panic("placeholder state inconsistent: filesystem " + ph.FS + " must exist in this context")
|
if datasetWasExplicitArgument {
|
||||||
|
return errors.Errorf("filesystem %q does not exist", ph.FS)
|
||||||
|
} else {
|
||||||
|
// got deleted between ZFSList and ZFSGetFilesystemPlaceholderState
|
||||||
|
continue
|
||||||
|
}
|
||||||
}
|
}
|
||||||
is := "yes"
|
is := "yes"
|
||||||
if !ph.IsPlaceholder {
|
if !ph.IsPlaceholder {
|
||||||
|
|||||||
@@ -44,10 +44,11 @@ func doZabsList(ctx context.Context, sc *cli.Subcommand, args []string) error {
|
|||||||
return errors.Wrap(err, "invalid filter specification on command line")
|
return errors.Wrap(err, "invalid filter specification on command line")
|
||||||
}
|
}
|
||||||
|
|
||||||
abstractions, errors, err := endpoint.ListAbstractionsStreamed(ctx, q)
|
abstractions, errors, drainDone, err := endpoint.ListAbstractionsStreamed(ctx, q)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err // context clear by invocation of command
|
return err // context clear by invocation of command
|
||||||
}
|
}
|
||||||
|
defer drainDone()
|
||||||
|
|
||||||
var line chainlock.L
|
var line chainlock.L
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
|
|||||||
+76
-59
@@ -6,16 +6,23 @@ import (
|
|||||||
"log/syslog"
|
"log/syslog"
|
||||||
"os"
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
"regexp"
|
|
||||||
"strconv"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
"github.com/robfig/cron/v3"
|
||||||
"github.com/zrepl/yaml-config"
|
"github.com/zrepl/yaml-config"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/util/datasizeunit"
|
||||||
zfsprop "github.com/zrepl/zrepl/zfs/property"
|
zfsprop "github.com/zrepl/zrepl/zfs/property"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type ParseFlags uint
|
||||||
|
|
||||||
|
const (
|
||||||
|
ParseFlagsNone ParseFlags = 0
|
||||||
|
ParseFlagsNoCertCheck ParseFlags = 1 << iota
|
||||||
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Jobs []JobEnum `yaml:"jobs"`
|
Jobs []JobEnum `yaml:"jobs"`
|
||||||
Global *Global `yaml:"global,optional,fromdefaults"`
|
Global *Global `yaml:"global,optional,fromdefaults"`
|
||||||
@@ -54,12 +61,17 @@ func (j JobEnum) Name() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ActiveJob struct {
|
type ActiveJob struct {
|
||||||
Type string `yaml:"type"`
|
Type string `yaml:"type"`
|
||||||
Name string `yaml:"name"`
|
Name string `yaml:"name"`
|
||||||
Connect ConnectEnum `yaml:"connect"`
|
Connect ConnectEnum `yaml:"connect"`
|
||||||
Pruning PruningSenderReceiver `yaml:"pruning"`
|
Pruning PruningSenderReceiver `yaml:"pruning"`
|
||||||
Debug JobDebugSettings `yaml:"debug,optional"`
|
Debug JobDebugSettings `yaml:"debug,optional"`
|
||||||
Replication *Replication `yaml:"replication,optional,fromdefaults"`
|
Replication *Replication `yaml:"replication,optional,fromdefaults"`
|
||||||
|
ConflictResolution *ConflictResolution `yaml:"conflict_resolution,optional,fromdefaults"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ConflictResolution struct {
|
||||||
|
InitialReplication string `yaml:"initial_replication,optional,default=most_recent"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PassiveJob struct {
|
type PassiveJob struct {
|
||||||
@@ -85,8 +97,10 @@ type SendOptions struct {
|
|||||||
BackupProperties bool `yaml:"backup_properties,optional,default=false"`
|
BackupProperties bool `yaml:"backup_properties,optional,default=false"`
|
||||||
LargeBlocks bool `yaml:"large_blocks,optional,default=false"`
|
LargeBlocks bool `yaml:"large_blocks,optional,default=false"`
|
||||||
Compressed bool `yaml:"compressed,optional,default=false"`
|
Compressed bool `yaml:"compressed,optional,default=false"`
|
||||||
EmbeddedData bool `yaml:"embbeded_data,optional,default=false"`
|
EmbeddedData bool `yaml:"embedded_data,optional,default=false"`
|
||||||
Saved bool `yaml:"saved,optional,default=false"`
|
Saved bool `yaml:"saved,optional,default=false"`
|
||||||
|
|
||||||
|
BandwidthLimit *BandwidthLimit `yaml:"bandwidth_limit,optional,fromdefaults"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type RecvOptions struct {
|
type RecvOptions struct {
|
||||||
@@ -96,6 +110,17 @@ type RecvOptions struct {
|
|||||||
// Reencrypt bool `yaml:"reencrypt"`
|
// Reencrypt bool `yaml:"reencrypt"`
|
||||||
|
|
||||||
Properties *PropertyRecvOptions `yaml:"properties,fromdefaults"`
|
Properties *PropertyRecvOptions `yaml:"properties,fromdefaults"`
|
||||||
|
|
||||||
|
BandwidthLimit *BandwidthLimit `yaml:"bandwidth_limit,optional,fromdefaults"`
|
||||||
|
|
||||||
|
Placeholder *PlaceholderRecvOptions `yaml:"placeholder,fromdefaults"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ yaml.Unmarshaler = &datasizeunit.Bits{}
|
||||||
|
|
||||||
|
type BandwidthLimit struct {
|
||||||
|
Max datasizeunit.Bits `yaml:"max,default=-1 B"`
|
||||||
|
BucketCapacity datasizeunit.Bits `yaml:"bucket_capacity,default=128 KiB"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Replication struct {
|
type Replication struct {
|
||||||
@@ -113,15 +138,15 @@ type ReplicationOptionsConcurrency struct {
|
|||||||
SizeEstimates int `yaml:"size_estimates,optional,default=4"`
|
SizeEstimates int `yaml:"size_estimates,optional,default=4"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *RecvOptions) SetDefault() {
|
|
||||||
*l = RecvOptions{Properties: &PropertyRecvOptions{}}
|
|
||||||
}
|
|
||||||
|
|
||||||
type PropertyRecvOptions struct {
|
type PropertyRecvOptions struct {
|
||||||
Inherit []zfsprop.Property `yaml:"inherit,optional"`
|
Inherit []zfsprop.Property `yaml:"inherit,optional"`
|
||||||
Override map[zfsprop.Property]string `yaml:"override,optional"`
|
Override map[zfsprop.Property]string `yaml:"override,optional"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PlaceholderRecvOptions struct {
|
||||||
|
Encryption string `yaml:"encryption,default=unspecified"`
|
||||||
|
}
|
||||||
|
|
||||||
type PushJob struct {
|
type PushJob struct {
|
||||||
ActiveJob `yaml:",inline"`
|
ActiveJob `yaml:",inline"`
|
||||||
Snapshotting SnapshottingEnum `yaml:"snapshotting"`
|
Snapshotting SnapshottingEnum `yaml:"snapshotting"`
|
||||||
@@ -163,13 +188,10 @@ func (i *PositiveDurationOrManual) UnmarshalYAML(u func(interface{}, bool) error
|
|||||||
return fmt.Errorf("value must not be empty")
|
return fmt.Errorf("value must not be empty")
|
||||||
default:
|
default:
|
||||||
i.Manual = false
|
i.Manual = false
|
||||||
i.Interval, err = time.ParseDuration(s)
|
i.Interval, err = parsePositiveDuration(s)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if i.Interval <= 0 {
|
|
||||||
return fmt.Errorf("value must be a positive duration, got %q", s)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -201,10 +223,42 @@ type SnapshottingEnum struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type SnapshottingPeriodic struct {
|
type SnapshottingPeriodic struct {
|
||||||
Type string `yaml:"type"`
|
Type string `yaml:"type"`
|
||||||
Prefix string `yaml:"prefix"`
|
Prefix string `yaml:"prefix"`
|
||||||
Interval time.Duration `yaml:"interval,positive"`
|
Interval *PositiveDuration `yaml:"interval"`
|
||||||
Hooks HookList `yaml:"hooks,optional"`
|
Hooks HookList `yaml:"hooks,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CronSpec struct {
|
||||||
|
Schedule cron.Schedule
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ yaml.Unmarshaler = &CronSpec{}
|
||||||
|
|
||||||
|
func (s *CronSpec) UnmarshalYAML(unmarshal func(v interface{}, not_strict bool) error) error {
|
||||||
|
var specString string
|
||||||
|
if err := unmarshal(&specString, false); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use standard cron format.
|
||||||
|
// Disable the various "descriptors" (@daily, etc)
|
||||||
|
// They are just aliases to "top of hour", "midnight", etc.
|
||||||
|
parser := cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.SecondOptional)
|
||||||
|
|
||||||
|
sched, err := parser.Parse(specString)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "cron syntax invalid")
|
||||||
|
}
|
||||||
|
s.Schedule = sched
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type SnapshottingCron struct {
|
||||||
|
Type string `yaml:"type"`
|
||||||
|
Prefix string `yaml:"prefix"`
|
||||||
|
Cron CronSpec `yaml:"cron"`
|
||||||
|
Hooks HookList `yaml:"hooks,optional"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SnapshottingManual struct {
|
type SnapshottingManual struct {
|
||||||
@@ -530,6 +584,7 @@ func (t *SnapshottingEnum) UnmarshalYAML(u func(interface{}, bool) error) (err e
|
|||||||
t.Ret, err = enumUnmarshal(u, map[string]interface{}{
|
t.Ret, err = enumUnmarshal(u, map[string]interface{}{
|
||||||
"periodic": &SnapshottingPeriodic{},
|
"periodic": &SnapshottingPeriodic{},
|
||||||
"manual": &SnapshottingManual{},
|
"manual": &SnapshottingManual{},
|
||||||
|
"cron": &SnapshottingCron{},
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -655,41 +710,3 @@ func ParseConfigBytes(bytes []byte) (*Config, error) {
|
|||||||
}
|
}
|
||||||
return c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var durationStringRegex *regexp.Regexp = regexp.MustCompile(`^\s*(\d+)\s*(s|m|h|d|w)\s*$`)
|
|
||||||
|
|
||||||
func parsePositiveDuration(e string) (d time.Duration, err error) {
|
|
||||||
comps := durationStringRegex.FindStringSubmatch(e)
|
|
||||||
if len(comps) != 3 {
|
|
||||||
err = fmt.Errorf("does not match regex: %s %#v", e, comps)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
durationFactor, err := strconv.ParseInt(comps[1], 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
if durationFactor <= 0 {
|
|
||||||
return 0, errors.New("duration must be positive integer")
|
|
||||||
}
|
|
||||||
|
|
||||||
var durationUnit time.Duration
|
|
||||||
switch comps[2] {
|
|
||||||
case "s":
|
|
||||||
durationUnit = time.Second
|
|
||||||
case "m":
|
|
||||||
durationUnit = time.Minute
|
|
||||||
case "h":
|
|
||||||
durationUnit = time.Hour
|
|
||||||
case "d":
|
|
||||||
durationUnit = 24 * time.Hour
|
|
||||||
case "w":
|
|
||||||
durationUnit = 24 * 7 * time.Hour
|
|
||||||
default:
|
|
||||||
err = fmt.Errorf("contains unknown time unit '%s'", comps[2])
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
d = time.Duration(durationFactor) * durationUnit
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/kr/pretty"
|
||||||
|
"github.com/zrepl/yaml-config"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Duration struct{ d time.Duration }
|
||||||
|
|
||||||
|
func (d Duration) Duration() time.Duration { return d.d }
|
||||||
|
|
||||||
|
var _ yaml.Unmarshaler = &Duration{}
|
||||||
|
|
||||||
|
func (d *Duration) UnmarshalYAML(unmarshal func(v interface{}, not_strict bool) error) error {
|
||||||
|
var s string
|
||||||
|
err := unmarshal(&s, false)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
d.d, err = parseDuration(s)
|
||||||
|
if err != nil {
|
||||||
|
d.d = 0
|
||||||
|
return &yaml.TypeError{Errors: []string{fmt.Sprintf("cannot parse value %q: %s", s, err)}}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type PositiveDuration struct{ d Duration }
|
||||||
|
|
||||||
|
var _ yaml.Unmarshaler = &PositiveDuration{}
|
||||||
|
|
||||||
|
func (d PositiveDuration) Duration() time.Duration { return d.d.Duration() }
|
||||||
|
|
||||||
|
func (d *PositiveDuration) UnmarshalYAML(unmarshal func(v interface{}, not_strict bool) error) error {
|
||||||
|
err := d.d.UnmarshalYAML(unmarshal)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if d.d.Duration() <= 0 {
|
||||||
|
return fmt.Errorf("duration must be positive, got %s", d.d.Duration())
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parsePositiveDuration(e string) (time.Duration, error) {
|
||||||
|
d, err := parseDuration(e)
|
||||||
|
if err != nil {
|
||||||
|
return d, err
|
||||||
|
}
|
||||||
|
if d <= 0 {
|
||||||
|
return 0, errors.New("duration must be positive integer")
|
||||||
|
}
|
||||||
|
return d, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var durationStringRegex *regexp.Regexp = regexp.MustCompile(`^\s*([\+-]?\d+)\s*(|s|m|h|d|w)\s*$`)
|
||||||
|
|
||||||
|
func parseDuration(e string) (d time.Duration, err error) {
|
||||||
|
comps := durationStringRegex.FindStringSubmatch(e)
|
||||||
|
if comps == nil {
|
||||||
|
err = fmt.Errorf("must match %s", durationStringRegex)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(comps) != 3 {
|
||||||
|
panic(pretty.Sprint(comps))
|
||||||
|
}
|
||||||
|
|
||||||
|
durationFactor, err := strconv.ParseInt(comps[1], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var durationUnit time.Duration
|
||||||
|
switch comps[2] {
|
||||||
|
case "":
|
||||||
|
if durationFactor != 0 {
|
||||||
|
err = fmt.Errorf("missing time unit")
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
// It's the case where user specified '0'.
|
||||||
|
// We want to allow this, just like time.ParseDuration.
|
||||||
|
}
|
||||||
|
case "s":
|
||||||
|
durationUnit = time.Second
|
||||||
|
case "m":
|
||||||
|
durationUnit = time.Minute
|
||||||
|
case "h":
|
||||||
|
durationUnit = time.Hour
|
||||||
|
case "d":
|
||||||
|
durationUnit = 24 * time.Hour
|
||||||
|
case "w":
|
||||||
|
durationUnit = 24 * 7 * time.Hour
|
||||||
|
default:
|
||||||
|
err = fmt.Errorf("contains unknown time unit '%s'", comps[2])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
d = time.Duration(durationFactor) * durationUnit
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -38,6 +38,13 @@ jobs:
|
|||||||
interval: 10m
|
interval: 10m
|
||||||
`
|
`
|
||||||
|
|
||||||
|
periodicDaily := `
|
||||||
|
snapshotting:
|
||||||
|
type: periodic
|
||||||
|
prefix: zrepl_
|
||||||
|
interval: 1d
|
||||||
|
`
|
||||||
|
|
||||||
hooks := `
|
hooks := `
|
||||||
snapshotting:
|
snapshotting:
|
||||||
type: periodic
|
type: periodic
|
||||||
@@ -74,7 +81,15 @@ jobs:
|
|||||||
c = testValidConfig(t, fillSnapshotting(periodic))
|
c = testValidConfig(t, fillSnapshotting(periodic))
|
||||||
snp := c.Jobs[0].Ret.(*PushJob).Snapshotting.Ret.(*SnapshottingPeriodic)
|
snp := c.Jobs[0].Ret.(*PushJob).Snapshotting.Ret.(*SnapshottingPeriodic)
|
||||||
assert.Equal(t, "periodic", snp.Type)
|
assert.Equal(t, "periodic", snp.Type)
|
||||||
assert.Equal(t, 10*time.Minute, snp.Interval)
|
assert.Equal(t, 10*time.Minute, snp.Interval.Duration())
|
||||||
|
assert.Equal(t, "zrepl_", snp.Prefix)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("periodicDaily", func(t *testing.T) {
|
||||||
|
c = testValidConfig(t, fillSnapshotting(periodicDaily))
|
||||||
|
snp := c.Jobs[0].Ret.(*PushJob).Snapshotting.Ret.(*SnapshottingPeriodic)
|
||||||
|
assert.Equal(t, "periodic", snp.Type)
|
||||||
|
assert.Equal(t, 24*time.Hour, snp.Interval.Duration())
|
||||||
assert.Equal(t, "zrepl_", snp.Prefix)
|
assert.Equal(t, "zrepl_", snp.Prefix)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"github.com/kr/pretty"
|
"github.com/kr/pretty"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/zrepl/yaml-config"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestSampleConfigsAreParsedWithoutErrors(t *testing.T) {
|
func TestSampleConfigsAreParsedWithoutErrors(t *testing.T) {
|
||||||
@@ -86,3 +87,53 @@ func TestTrimSpaceEachLineAndPad(t *testing.T) {
|
|||||||
`
|
`
|
||||||
assert.Equal(t, " \n foo\n bar baz\n \n", trimSpaceEachLineAndPad(foo, " "))
|
assert.Equal(t, " \n foo\n bar baz\n \n", trimSpaceEachLineAndPad(foo, " "))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCronSpec(t *testing.T) {
|
||||||
|
|
||||||
|
expectAccept := []string{
|
||||||
|
`"* * * * *"`,
|
||||||
|
`"0-10 * * * *"`,
|
||||||
|
`"* 0-5,8,12 * * *"`,
|
||||||
|
}
|
||||||
|
|
||||||
|
expectFail := []string{
|
||||||
|
`* * * *`,
|
||||||
|
``,
|
||||||
|
`23`,
|
||||||
|
`"@reboot"`,
|
||||||
|
`"@every 1h30m"`,
|
||||||
|
`"@daily"`,
|
||||||
|
`* * * * * *`,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, input := range expectAccept {
|
||||||
|
t.Run(input, func(t *testing.T) {
|
||||||
|
s := fmt.Sprintf("spec: %s\n", input)
|
||||||
|
var v struct {
|
||||||
|
Spec CronSpec
|
||||||
|
}
|
||||||
|
v.Spec.Schedule = nil
|
||||||
|
t.Logf("input:\n%s", s)
|
||||||
|
err := yaml.UnmarshalStrict([]byte(s), &v)
|
||||||
|
t.Logf("error: %T %s", err, err)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, v.Spec.Schedule)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, input := range expectFail {
|
||||||
|
t.Run(input, func(t *testing.T) {
|
||||||
|
s := fmt.Sprintf("spec: %s\n", input)
|
||||||
|
var v struct {
|
||||||
|
Spec CronSpec
|
||||||
|
}
|
||||||
|
v.Spec.Schedule = nil
|
||||||
|
t.Logf("input: %q", s)
|
||||||
|
err := yaml.UnmarshalStrict([]byte(s), &v)
|
||||||
|
t.Logf("error: %T %s", err, err)
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Nil(t, v.Spec.Schedule)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
|
||||||
|
jobs:
|
||||||
|
- type: sink
|
||||||
|
name: "limited_sink"
|
||||||
|
root_fs: "fs0"
|
||||||
|
recv:
|
||||||
|
bandwidth_limit:
|
||||||
|
max: 12345 B
|
||||||
|
serve:
|
||||||
|
type: local
|
||||||
|
listener_name: localsink
|
||||||
|
|
||||||
|
- type: push
|
||||||
|
name: "limited_push"
|
||||||
|
connect:
|
||||||
|
type: local
|
||||||
|
listener_name: localsink
|
||||||
|
client_identity: local_backup
|
||||||
|
filesystems: {
|
||||||
|
"root<": true,
|
||||||
|
}
|
||||||
|
send:
|
||||||
|
bandwidth_limit:
|
||||||
|
max: 54321 B
|
||||||
|
bucket_capacity: 1024 B
|
||||||
|
snapshotting:
|
||||||
|
type: manual
|
||||||
|
pruning:
|
||||||
|
keep_sender:
|
||||||
|
- type: last_n
|
||||||
|
count: 1
|
||||||
|
keep_receiver:
|
||||||
|
- type: last_n
|
||||||
|
count: 1
|
||||||
|
|
||||||
|
- type: sink
|
||||||
|
name: "nolimit_sink"
|
||||||
|
root_fs: "fs1"
|
||||||
|
serve:
|
||||||
|
type: local
|
||||||
|
listener_name: localsink
|
||||||
@@ -9,8 +9,8 @@ jobs:
|
|||||||
key: /etc/zrepl/prod.key
|
key: /etc/zrepl/prod.key
|
||||||
server_cn: "backups"
|
server_cn: "backups"
|
||||||
filesystems: {
|
filesystems: {
|
||||||
"zroot/var/db": true,
|
"zroot<": true,
|
||||||
"zroot/usr/home<": true,
|
"zroot/var/tmp<": false,
|
||||||
"zroot/usr/home/paranoid": false
|
"zroot/usr/home/paranoid": false
|
||||||
}
|
}
|
||||||
snapshotting:
|
snapshotting:
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
jobs:
|
||||||
|
# Separate job for snapshots and pruning
|
||||||
|
- name: snapshots
|
||||||
|
type: snap
|
||||||
|
filesystems:
|
||||||
|
'tank<': true # all filesystems
|
||||||
|
snapshotting:
|
||||||
|
type: periodic
|
||||||
|
prefix: zrepl_
|
||||||
|
interval: 10m
|
||||||
|
pruning:
|
||||||
|
keep:
|
||||||
|
# Keep non-zrepl snapshots
|
||||||
|
- type: regex
|
||||||
|
negate: true
|
||||||
|
regex: '^zrepl_'
|
||||||
|
# Time-based snapshot retention
|
||||||
|
- type: grid
|
||||||
|
grid: 1x1h(keep=all) | 24x1h | 30x1d | 12x30d
|
||||||
|
regex: '^zrepl_'
|
||||||
|
|
||||||
|
# Source job for target B
|
||||||
|
- name: target_b
|
||||||
|
type: source
|
||||||
|
serve:
|
||||||
|
type: tls
|
||||||
|
listen: :8888
|
||||||
|
ca: /etc/zrepl/b.example.com.crt
|
||||||
|
cert: /etc/zrepl/a.example.com.crt
|
||||||
|
key: /etc/zrepl/a.example.com.key
|
||||||
|
client_cns:
|
||||||
|
- b.example.com
|
||||||
|
filesystems:
|
||||||
|
'tank<': true # all filesystems
|
||||||
|
# Snapshots are handled by the separate snap job
|
||||||
|
snapshotting:
|
||||||
|
type: manual
|
||||||
|
|
||||||
|
# Source job for target C
|
||||||
|
- name: target_c
|
||||||
|
type: source
|
||||||
|
serve:
|
||||||
|
type: tls
|
||||||
|
listen: :8889
|
||||||
|
ca: /etc/zrepl/c.example.com.crt
|
||||||
|
cert: /etc/zrepl/a.example.com.crt
|
||||||
|
key: /etc/zrepl/a.example.com.key
|
||||||
|
client_cns:
|
||||||
|
- c.example.com
|
||||||
|
filesystems:
|
||||||
|
'tank<': true # all filesystems
|
||||||
|
# Snapshots are handled by the separate snap job
|
||||||
|
snapshotting:
|
||||||
|
type: manual
|
||||||
|
|
||||||
|
# Source jobs for remaining targets. Each one should listen on a different port
|
||||||
|
# and reference the correct certificate and client CN.
|
||||||
|
# - name: target_c
|
||||||
|
# ...
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
jobs:
|
||||||
|
# Pull from source server A
|
||||||
|
- name: source_a
|
||||||
|
type: pull
|
||||||
|
connect:
|
||||||
|
type: tls
|
||||||
|
# Use the correct port for this specific client (eg. B is 8888, C is 8889, etc.)
|
||||||
|
address: a.example.com:8888
|
||||||
|
ca: /etc/zrepl/a.example.com.crt
|
||||||
|
# Use the correct key pair for this specific client
|
||||||
|
cert: /etc/zrepl/b.example.com.crt
|
||||||
|
key: /etc/zrepl/b.example.com.key
|
||||||
|
server_cn: a.example.com
|
||||||
|
root_fs: pool0/backup
|
||||||
|
interval: 10m
|
||||||
|
pruning:
|
||||||
|
keep_sender:
|
||||||
|
# Source does the pruning in its snap job
|
||||||
|
- type: regex
|
||||||
|
regex: '.*'
|
||||||
|
# Receiver-side pruning can be configured as desired on each target server
|
||||||
|
keep_receiver:
|
||||||
|
# Keep non-zrepl snapshots
|
||||||
|
- type: regex
|
||||||
|
negate: true
|
||||||
|
regex: '^zrepl_'
|
||||||
|
# Time-based snapshot retention
|
||||||
|
- type: grid
|
||||||
|
grid: 1x1h(keep=all) | 24x1h | 30x1d | 12x30d
|
||||||
|
regex: '^zrepl_'
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
jobs:
|
||||||
|
- name: snapjob
|
||||||
|
type: snap
|
||||||
|
filesystems: {
|
||||||
|
"tank<": true,
|
||||||
|
}
|
||||||
|
snapshotting:
|
||||||
|
type: cron
|
||||||
|
prefix: zrepl_snapjob_
|
||||||
|
cron: "*/5 * * * *"
|
||||||
|
pruning:
|
||||||
|
keep:
|
||||||
|
- type: last_n
|
||||||
|
count: 60
|
||||||
+6
-4
@@ -8,6 +8,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
@@ -124,8 +125,9 @@ func (j *controlJob) Run(ctx context.Context) {
|
|||||||
s := Status{
|
s := Status{
|
||||||
Jobs: jobs,
|
Jobs: jobs,
|
||||||
Global: GlobalStatus{
|
Global: GlobalStatus{
|
||||||
ZFSCmds: globalZFS,
|
ZFSCmds: globalZFS,
|
||||||
Envconst: envconstReport,
|
Envconst: envconstReport,
|
||||||
|
OsEnviron: os.Environ(),
|
||||||
}}
|
}}
|
||||||
return s, nil
|
return s, nil
|
||||||
}})
|
}})
|
||||||
@@ -156,8 +158,8 @@ func (j *controlJob) Run(ctx context.Context) {
|
|||||||
server := http.Server{
|
server := http.Server{
|
||||||
Handler: mux,
|
Handler: mux,
|
||||||
// control socket is local, 1s timeout should be more than sufficient, even on a loaded system
|
// control socket is local, 1s timeout should be more than sufficient, even on a loaded system
|
||||||
WriteTimeout: 1 * time.Second,
|
WriteTimeout: envconst.Duration("ZREPL_DAEMON_CONTROL_SERVER_WRITE_TIMEOUT", 1*time.Second),
|
||||||
ReadTimeout: 1 * time.Second,
|
ReadTimeout: envconst.Duration("ZREPL_DAEMON_CONTROL_SERVER_READ_TIMEOUT", 1*time.Second),
|
||||||
}
|
}
|
||||||
|
|
||||||
outer:
|
outer:
|
||||||
|
|||||||
+4
-3
@@ -51,7 +51,7 @@ func Run(ctx context.Context, conf *config.Config) error {
|
|||||||
}
|
}
|
||||||
outlets.Add(newPrometheusLogOutlet(), logger.Debug)
|
outlets.Add(newPrometheusLogOutlet(), logger.Debug)
|
||||||
|
|
||||||
confJobs, err := job.JobsFromConfig(conf)
|
confJobs, err := job.JobsFromConfig(conf, config.ParseFlagsNone)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "cannot build jobs from config")
|
return errors.Wrap(err, "cannot build jobs from config")
|
||||||
}
|
}
|
||||||
@@ -160,8 +160,9 @@ type Status struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type GlobalStatus struct {
|
type GlobalStatus struct {
|
||||||
ZFSCmds *zfscmd.Report
|
ZFSCmds *zfscmd.Report
|
||||||
Envconst *envconst.Report
|
Envconst *envconst.Report
|
||||||
|
OsEnviron []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *jobs) status() map[string]*job.Status {
|
func (s *jobs) status() map[string]*job.Status {
|
||||||
|
|||||||
@@ -93,7 +93,14 @@ func (r *CommandHookReport) String() string {
|
|||||||
cmdLine.WriteString(fmt.Sprintf("%s'%s'", sep, a))
|
cmdLine.WriteString(fmt.Sprintf("%s'%s'", sep, a))
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Sprintf("command hook invocation: \"%s\"", cmdLine.String()) // no %q to make copy-pastable
|
var msg string
|
||||||
|
if r.Err == nil {
|
||||||
|
msg = "command hook"
|
||||||
|
} else {
|
||||||
|
msg = fmt.Sprintf("command hook failed with %q", r.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%s: \"%s\"", msg, cmdLine.String()) // no %q to make copy-pastable
|
||||||
}
|
}
|
||||||
func (r *CommandHookReport) Error() string {
|
func (r *CommandHookReport) Error() string {
|
||||||
if r.Err == nil {
|
if r.Err == nil {
|
||||||
|
|||||||
@@ -161,7 +161,7 @@ jobs:
|
|||||||
ExpectedEdge: hooks.Pre,
|
ExpectedEdge: hooks.Pre,
|
||||||
ExpectStatus: hooks.StepErr,
|
ExpectStatus: hooks.StepErr,
|
||||||
OutputTest: containsTest(fmt.Sprintf("TEST ERROR pre_testing %s@%s", testFSName, testSnapshotName)),
|
OutputTest: containsTest(fmt.Sprintf("TEST ERROR pre_testing %s@%s", testFSName, testSnapshotName)),
|
||||||
ErrorTest: regexpTest("^command hook invocation.*exit status 1$"),
|
ErrorTest: regexpTest("^command hook failed.*exit status 1$"),
|
||||||
},
|
},
|
||||||
expectStep{ExpectedEdge: hooks.Callback, ExpectStatus: hooks.StepOk},
|
expectStep{ExpectedEdge: hooks.Callback, ExpectStatus: hooks.StepOk},
|
||||||
expectStep{
|
expectStep{
|
||||||
@@ -185,7 +185,7 @@ jobs:
|
|||||||
ExpectedEdge: hooks.Pre,
|
ExpectedEdge: hooks.Pre,
|
||||||
ExpectStatus: hooks.StepErr,
|
ExpectStatus: hooks.StepErr,
|
||||||
OutputTest: containsTest(fmt.Sprintf("TEST ERROR pre_testing %s@%s", testFSName, testSnapshotName)),
|
OutputTest: containsTest(fmt.Sprintf("TEST ERROR pre_testing %s@%s", testFSName, testSnapshotName)),
|
||||||
ErrorTest: regexpTest("^command hook invocation.*exit status 1$"),
|
ErrorTest: regexpTest("^command hook failed.*exit status 1$"),
|
||||||
},
|
},
|
||||||
expectStep{
|
expectStep{
|
||||||
ExpectedEdge: hooks.Pre,
|
ExpectedEdge: hooks.Pre,
|
||||||
@@ -234,7 +234,7 @@ jobs:
|
|||||||
ExpectedEdge: hooks.Post,
|
ExpectedEdge: hooks.Post,
|
||||||
ExpectStatus: hooks.StepErr,
|
ExpectStatus: hooks.StepErr,
|
||||||
OutputTest: containsTest(fmt.Sprintf("TEST ERROR post_testing %s@%s", testFSName, testSnapshotName)),
|
OutputTest: containsTest(fmt.Sprintf("TEST ERROR post_testing %s@%s", testFSName, testSnapshotName)),
|
||||||
ErrorTest: regexpTest("^command hook invocation.*exit status 1$"),
|
ErrorTest: regexpTest("^command hook failed.*exit status 1$"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -267,7 +267,7 @@ jobs:
|
|||||||
ExpectedEdge: hooks.Pre,
|
ExpectedEdge: hooks.Pre,
|
||||||
ExpectStatus: hooks.StepErr,
|
ExpectStatus: hooks.StepErr,
|
||||||
OutputTest: containsTest(fmt.Sprintf("TEST ERROR pre_testing %s@%s", testFSName, testSnapshotName)),
|
OutputTest: containsTest(fmt.Sprintf("TEST ERROR pre_testing %s@%s", testFSName, testSnapshotName)),
|
||||||
ErrorTest: regexpTest("^command hook invocation.*exit status 1$"),
|
ErrorTest: regexpTest("^command hook failed.*exit status 1$"),
|
||||||
},
|
},
|
||||||
expectStep{ExpectedEdge: hooks.Callback, ExpectStatus: hooks.StepOk},
|
expectStep{ExpectedEdge: hooks.Callback, ExpectStatus: hooks.StepOk},
|
||||||
expectStep{
|
expectStep{
|
||||||
@@ -295,7 +295,7 @@ jobs:
|
|||||||
ExpectedEdge: hooks.Pre,
|
ExpectedEdge: hooks.Pre,
|
||||||
ExpectStatus: hooks.StepErr,
|
ExpectStatus: hooks.StepErr,
|
||||||
OutputTest: containsTest(fmt.Sprintf("TEST ERROR pre_testing %s@%s", testFSName, testSnapshotName)),
|
OutputTest: containsTest(fmt.Sprintf("TEST ERROR pre_testing %s@%s", testFSName, testSnapshotName)),
|
||||||
ErrorTest: regexpTest("^command hook invocation.*exit status 1$"),
|
ErrorTest: regexpTest("^command hook failed.*exit status 1$"),
|
||||||
},
|
},
|
||||||
expectStep{ExpectedEdge: hooks.Callback, ExpectStatus: hooks.StepOk},
|
expectStep{ExpectedEdge: hooks.Callback, ExpectStatus: hooks.StepOk},
|
||||||
expectStep{
|
expectStep{
|
||||||
|
|||||||
+31
-8
@@ -42,6 +42,7 @@ type ActiveSide struct {
|
|||||||
promPruneSecs *prometheus.HistogramVec // labels: prune_side
|
promPruneSecs *prometheus.HistogramVec // labels: prune_side
|
||||||
promBytesReplicated *prometheus.CounterVec // labels: filesystem
|
promBytesReplicated *prometheus.CounterVec // labels: filesystem
|
||||||
promReplicationErrors prometheus.Gauge
|
promReplicationErrors prometheus.Gauge
|
||||||
|
promLastSuccessful prometheus.Gauge
|
||||||
|
|
||||||
tasksMtx sync.Mutex
|
tasksMtx sync.Mutex
|
||||||
tasks activeSideTasks
|
tasks activeSideTasks
|
||||||
@@ -100,7 +101,7 @@ type modePush struct {
|
|||||||
receiver *rpc.Client
|
receiver *rpc.Client
|
||||||
senderConfig *endpoint.SenderConfig
|
senderConfig *endpoint.SenderConfig
|
||||||
plannerPolicy *logic.PlannerPolicy
|
plannerPolicy *logic.PlannerPolicy
|
||||||
snapper *snapper.PeriodicOrManual
|
snapper snapper.Snapper
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *modePush) ConnectEndpoints(ctx context.Context, connecter transport.Connecter) {
|
func (m *modePush) ConnectEndpoints(ctx context.Context, connecter transport.Connecter) {
|
||||||
@@ -136,7 +137,8 @@ func (m *modePush) RunPeriodic(ctx context.Context, wakeUpCommon chan<- struct{}
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *modePush) SnapperReport() *snapper.Report {
|
func (m *modePush) SnapperReport() *snapper.Report {
|
||||||
return m.snapper.Report()
|
r := m.snapper.Report()
|
||||||
|
return &r
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *modePush) ResetConnectBackoff() {
|
func (m *modePush) ResetConnectBackoff() {
|
||||||
@@ -161,8 +163,13 @@ func modePushFromConfig(g *config.Global, in *config.PushJob, jobID endpoint.Job
|
|||||||
return nil, errors.Wrap(err, "field `replication`")
|
return nil, errors.Wrap(err, "field `replication`")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
conflictResolution, err := logic.ConflictResolutionFromConfig(in.ConflictResolution)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "field `conflict_resolution`")
|
||||||
|
}
|
||||||
|
|
||||||
m.plannerPolicy = &logic.PlannerPolicy{
|
m.plannerPolicy = &logic.PlannerPolicy{
|
||||||
EncryptedSend: logic.TriFromBool(in.Send.Encrypted),
|
ConflictResolution: conflictResolution,
|
||||||
ReplicationConfig: replicationConfig,
|
ReplicationConfig: replicationConfig,
|
||||||
SizeEstimationConcurrency: in.Replication.Concurrency.SizeEstimates,
|
SizeEstimationConcurrency: in.Replication.Concurrency.SizeEstimates,
|
||||||
}
|
}
|
||||||
@@ -260,8 +267,13 @@ func modePullFromConfig(g *config.Global, in *config.PullJob, jobID endpoint.Job
|
|||||||
return nil, errors.Wrap(err, "field `replication`")
|
return nil, errors.Wrap(err, "field `replication`")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
conflictResolution, err := logic.ConflictResolutionFromConfig(in.ConflictResolution)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "field `conflict_resolution`")
|
||||||
|
}
|
||||||
|
|
||||||
m.plannerPolicy = &logic.PlannerPolicy{
|
m.plannerPolicy = &logic.PlannerPolicy{
|
||||||
EncryptedSend: logic.DontCare,
|
ConflictResolution: conflictResolution,
|
||||||
ReplicationConfig: replicationConfig,
|
ReplicationConfig: replicationConfig,
|
||||||
SizeEstimationConcurrency: in.Replication.Concurrency.SizeEstimates,
|
SizeEstimationConcurrency: in.Replication.Concurrency.SizeEstimates,
|
||||||
}
|
}
|
||||||
@@ -287,7 +299,7 @@ func replicationDriverConfigFromConfig(in *config.Replication) (c driver.Config,
|
|||||||
return c, err
|
return c, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func activeSide(g *config.Global, in *config.ActiveJob, configJob interface{}) (j *ActiveSide, err error) {
|
func activeSide(g *config.Global, in *config.ActiveJob, configJob interface{}, parseFlags config.ParseFlags) (j *ActiveSide, err error) {
|
||||||
|
|
||||||
j = &ActiveSide{}
|
j = &ActiveSide{}
|
||||||
j.name, err = endpoint.MakeJobID(in.Name)
|
j.name, err = endpoint.MakeJobID(in.Name)
|
||||||
@@ -321,7 +333,6 @@ func activeSide(g *config.Global, in *config.ActiveJob, configJob interface{}) (
|
|||||||
Help: "number of bytes replicated from sender to receiver per filesystem",
|
Help: "number of bytes replicated from sender to receiver per filesystem",
|
||||||
ConstLabels: prometheus.Labels{"zrepl_job": j.name.String()},
|
ConstLabels: prometheus.Labels{"zrepl_job": j.name.String()},
|
||||||
}, []string{"filesystem"})
|
}, []string{"filesystem"})
|
||||||
|
|
||||||
j.promReplicationErrors = prometheus.NewGauge(prometheus.GaugeOpts{
|
j.promReplicationErrors = prometheus.NewGauge(prometheus.GaugeOpts{
|
||||||
Namespace: "zrepl",
|
Namespace: "zrepl",
|
||||||
Subsystem: "replication",
|
Subsystem: "replication",
|
||||||
@@ -329,8 +340,15 @@ func activeSide(g *config.Global, in *config.ActiveJob, configJob interface{}) (
|
|||||||
Help: "number of filesystems that failed replication in the latest replication attempt, or -1 if the job failed before enumerating the filesystems",
|
Help: "number of filesystems that failed replication in the latest replication attempt, or -1 if the job failed before enumerating the filesystems",
|
||||||
ConstLabels: prometheus.Labels{"zrepl_job": j.name.String()},
|
ConstLabels: prometheus.Labels{"zrepl_job": j.name.String()},
|
||||||
})
|
})
|
||||||
|
j.promLastSuccessful = prometheus.NewGauge(prometheus.GaugeOpts{
|
||||||
|
Namespace: "zrepl",
|
||||||
|
Subsystem: "replication",
|
||||||
|
Name: "last_successful",
|
||||||
|
Help: "timestamp of last successful replication",
|
||||||
|
ConstLabels: prometheus.Labels{"zrepl_job": j.name.String()},
|
||||||
|
})
|
||||||
|
|
||||||
j.connecter, err = fromconfig.ConnecterFromConfig(g, in.Connect)
|
j.connecter, err = fromconfig.ConnecterFromConfig(g, in.Connect, parseFlags)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "cannot build client")
|
return nil, errors.Wrap(err, "cannot build client")
|
||||||
}
|
}
|
||||||
@@ -360,6 +378,7 @@ func (j *ActiveSide) RegisterMetrics(registerer prometheus.Registerer) {
|
|||||||
registerer.MustRegister(j.promPruneSecs)
|
registerer.MustRegister(j.promPruneSecs)
|
||||||
registerer.MustRegister(j.promBytesReplicated)
|
registerer.MustRegister(j.promBytesReplicated)
|
||||||
registerer.MustRegister(j.promReplicationErrors)
|
registerer.MustRegister(j.promReplicationErrors)
|
||||||
|
registerer.MustRegister(j.promLastSuccessful)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *ActiveSide) Name() string { return j.name.String() }
|
func (j *ActiveSide) Name() string { return j.name.String() }
|
||||||
@@ -494,7 +513,11 @@ func (j *ActiveSide) do(ctx context.Context) {
|
|||||||
repCancel() // always cancel to free up context resources
|
repCancel() // always cancel to free up context resources
|
||||||
|
|
||||||
replicationReport := j.tasks.replicationReport()
|
replicationReport := j.tasks.replicationReport()
|
||||||
j.promReplicationErrors.Set(float64(replicationReport.GetFailedFilesystemsCountInLatestAttempt()))
|
var numErrors = replicationReport.GetFailedFilesystemsCountInLatestAttempt()
|
||||||
|
j.promReplicationErrors.Set(float64(numErrors))
|
||||||
|
if numErrors == 0 {
|
||||||
|
j.promLastSuccessful.SetToCurrentTime()
|
||||||
|
}
|
||||||
|
|
||||||
endSpan()
|
endSpan()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,12 +8,13 @@ import (
|
|||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/config"
|
"github.com/zrepl/zrepl/config"
|
||||||
|
"github.com/zrepl/zrepl/util/bandwidthlimit"
|
||||||
)
|
)
|
||||||
|
|
||||||
func JobsFromConfig(c *config.Config) ([]Job, error) {
|
func JobsFromConfig(c *config.Config, parseFlags config.ParseFlags) ([]Job, error) {
|
||||||
js := make([]Job, len(c.Jobs))
|
js := make([]Job, len(c.Jobs))
|
||||||
for i := range c.Jobs {
|
for i := range c.Jobs {
|
||||||
j, err := buildJob(c.Global, c.Jobs[i])
|
j, err := buildJob(c.Global, c.Jobs[i], parseFlags)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -41,19 +42,19 @@ func JobsFromConfig(c *config.Config) ([]Job, error) {
|
|||||||
return js, nil
|
return js, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildJob(c *config.Global, in config.JobEnum) (j Job, err error) {
|
func buildJob(c *config.Global, in config.JobEnum, parseFlags config.ParseFlags) (j Job, err error) {
|
||||||
cannotBuildJob := func(e error, name string) (Job, error) {
|
cannotBuildJob := func(e error, name string) (Job, error) {
|
||||||
return nil, errors.Wrapf(e, "cannot build job %q", name)
|
return nil, errors.Wrapf(e, "cannot build job %q", name)
|
||||||
}
|
}
|
||||||
// FIXME prettify this
|
// FIXME prettify this
|
||||||
switch v := in.Ret.(type) {
|
switch v := in.Ret.(type) {
|
||||||
case *config.SinkJob:
|
case *config.SinkJob:
|
||||||
j, err = passiveSideFromConfig(c, &v.PassiveJob, v)
|
j, err = passiveSideFromConfig(c, &v.PassiveJob, v, parseFlags)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return cannotBuildJob(err, v.Name)
|
return cannotBuildJob(err, v.Name)
|
||||||
}
|
}
|
||||||
case *config.SourceJob:
|
case *config.SourceJob:
|
||||||
j, err = passiveSideFromConfig(c, &v.PassiveJob, v)
|
j, err = passiveSideFromConfig(c, &v.PassiveJob, v, parseFlags)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return cannotBuildJob(err, v.Name)
|
return cannotBuildJob(err, v.Name)
|
||||||
}
|
}
|
||||||
@@ -63,12 +64,12 @@ func buildJob(c *config.Global, in config.JobEnum) (j Job, err error) {
|
|||||||
return cannotBuildJob(err, v.Name)
|
return cannotBuildJob(err, v.Name)
|
||||||
}
|
}
|
||||||
case *config.PushJob:
|
case *config.PushJob:
|
||||||
j, err = activeSide(c, &v.ActiveJob, v)
|
j, err = activeSide(c, &v.ActiveJob, v, parseFlags)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return cannotBuildJob(err, v.Name)
|
return cannotBuildJob(err, v.Name)
|
||||||
}
|
}
|
||||||
case *config.PullJob:
|
case *config.PullJob:
|
||||||
j, err = activeSide(c, &v.ActiveJob, v)
|
j, err = activeSide(c, &v.ActiveJob, v, parseFlags)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return cannotBuildJob(err, v.Name)
|
return cannotBuildJob(err, v.Name)
|
||||||
}
|
}
|
||||||
@@ -107,3 +108,13 @@ func validateReceivingSidesDoNotOverlap(receivingRootFSs []string) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func buildBandwidthLimitConfig(in *config.BandwidthLimit) (c bandwidthlimit.Config, _ error) {
|
||||||
|
if in.Max.ToBytes() > 0 && int64(in.Max.ToBytes()) == 0 {
|
||||||
|
return c, fmt.Errorf("bandwidth limit `max` is too small, must at least specify one byte")
|
||||||
|
}
|
||||||
|
return bandwidthlimit.Config{
|
||||||
|
Max: int64(in.Max.ToBytes()),
|
||||||
|
BucketCapacity: int64(in.BucketCapacity.ToBytes()),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -22,7 +22,12 @@ func buildSenderConfig(in SendingJobConfig, jobID endpoint.JobID) (*endpoint.Sen
|
|||||||
return nil, errors.Wrap(err, "cannot build filesystem filter")
|
return nil, errors.Wrap(err, "cannot build filesystem filter")
|
||||||
}
|
}
|
||||||
sendOpts := in.GetSendOptions()
|
sendOpts := in.GetSendOptions()
|
||||||
return &endpoint.SenderConfig{
|
bwlim, err := buildBandwidthLimitConfig(sendOpts.BandwidthLimit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "cannot build bandwith limit config")
|
||||||
|
}
|
||||||
|
|
||||||
|
sc := &endpoint.SenderConfig{
|
||||||
FSF: fsf,
|
FSF: fsf,
|
||||||
JobID: jobID,
|
JobID: jobID,
|
||||||
|
|
||||||
@@ -34,7 +39,15 @@ func buildSenderConfig(in SendingJobConfig, jobID endpoint.JobID) (*endpoint.Sen
|
|||||||
SendCompressed: sendOpts.Compressed,
|
SendCompressed: sendOpts.Compressed,
|
||||||
SendEmbeddedData: sendOpts.EmbeddedData,
|
SendEmbeddedData: sendOpts.EmbeddedData,
|
||||||
SendSaved: sendOpts.Saved,
|
SendSaved: sendOpts.Saved,
|
||||||
}, nil
|
|
||||||
|
BandwidthLimit: bwlim,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := sc.Validate(); err != nil {
|
||||||
|
return nil, errors.Wrap(err, "cannot build sender config")
|
||||||
|
}
|
||||||
|
|
||||||
|
return sc, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type ReceivingJobConfig interface {
|
type ReceivingJobConfig interface {
|
||||||
@@ -53,6 +66,22 @@ func buildReceiverConfig(in ReceivingJobConfig, jobID endpoint.JobID) (rc endpoi
|
|||||||
}
|
}
|
||||||
|
|
||||||
recvOpts := in.GetRecvOptions()
|
recvOpts := in.GetRecvOptions()
|
||||||
|
|
||||||
|
bwlim, err := buildBandwidthLimitConfig(recvOpts.BandwidthLimit)
|
||||||
|
if err != nil {
|
||||||
|
return rc, errors.Wrap(err, "cannot build bandwith limit config")
|
||||||
|
}
|
||||||
|
|
||||||
|
placeholderEncryption, err := endpoint.PlaceholderCreationEncryptionPropertyString(recvOpts.Placeholder.Encryption)
|
||||||
|
if err != nil {
|
||||||
|
options := []string{}
|
||||||
|
for _, v := range endpoint.PlaceholderCreationEncryptionPropertyValues() {
|
||||||
|
options = append(options, endpoint.PlaceholderCreationEncryptionProperty(v).String())
|
||||||
|
}
|
||||||
|
return rc, errors.Errorf("placeholder encryption value %q is invalid, must be one of %s",
|
||||||
|
recvOpts.Placeholder.Encryption, options)
|
||||||
|
}
|
||||||
|
|
||||||
rc = endpoint.ReceiverConfig{
|
rc = endpoint.ReceiverConfig{
|
||||||
JobID: jobID,
|
JobID: jobID,
|
||||||
RootWithoutClientComponent: rootFs,
|
RootWithoutClientComponent: rootFs,
|
||||||
@@ -60,6 +89,10 @@ func buildReceiverConfig(in ReceivingJobConfig, jobID endpoint.JobID) (rc endpoi
|
|||||||
|
|
||||||
InheritProperties: recvOpts.Properties.Inherit,
|
InheritProperties: recvOpts.Properties.Inherit,
|
||||||
OverrideProperties: recvOpts.Properties.Override,
|
OverrideProperties: recvOpts.Properties.Override,
|
||||||
|
|
||||||
|
BandwidthLimit: bwlim,
|
||||||
|
|
||||||
|
PlaceholderEncryption: placeholderEncryption,
|
||||||
}
|
}
|
||||||
if err := rc.Validate(); err != nil {
|
if err := rc.Validate(); err != nil {
|
||||||
return rc, errors.Wrap(err, "cannot build receiver config")
|
return rc, errors.Wrap(err, "cannot build receiver config")
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/config"
|
"github.com/zrepl/zrepl/config"
|
||||||
"github.com/zrepl/zrepl/transport/tls"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestValidateReceivingSidesDoNotOverlap(t *testing.T) {
|
func TestValidateReceivingSidesDoNotOverlap(t *testing.T) {
|
||||||
@@ -96,7 +95,7 @@ jobs:
|
|||||||
conf, err := config.ParseConfigBytes([]byte(fill(c.jobName)))
|
conf, err := config.ParseConfigBytes([]byte(fill(c.jobName)))
|
||||||
require.NoError(t, err, "not expecting yaml-config to know about job ids")
|
require.NoError(t, err, "not expecting yaml-config to know about job ids")
|
||||||
require.NotNil(t, conf)
|
require.NotNil(t, conf)
|
||||||
jobs, err := JobsFromConfig(conf)
|
jobs, err := JobsFromConfig(conf, config.ParseFlagsNone)
|
||||||
|
|
||||||
if c.valid {
|
if c.valid {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
@@ -119,6 +118,14 @@ func TestSampleConfigsAreBuiltWithoutErrors(t *testing.T) {
|
|||||||
t.Errorf("glob failed: %+v", err)
|
t.Errorf("glob failed: %+v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type additionalCheck struct {
|
||||||
|
state int
|
||||||
|
test func(t *testing.T, jobs []Job)
|
||||||
|
}
|
||||||
|
additionalChecks := map[string]*additionalCheck{
|
||||||
|
"bandwidth_limit.yml": {test: testSampleConfig_BandwidthLimit},
|
||||||
|
}
|
||||||
|
|
||||||
for _, p := range paths {
|
for _, p := range paths {
|
||||||
|
|
||||||
if path.Ext(p) != ".yml" {
|
if path.Ext(p) != ".yml" {
|
||||||
@@ -126,23 +133,78 @@ func TestSampleConfigsAreBuiltWithoutErrors(t *testing.T) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
filename := path.Base(p)
|
||||||
|
t.Logf("checking for presence additonal checks for file %q", filename)
|
||||||
|
additionalCheck := additionalChecks[filename]
|
||||||
|
if additionalCheck == nil {
|
||||||
|
t.Logf("no additional checks")
|
||||||
|
} else {
|
||||||
|
t.Logf("additional check present")
|
||||||
|
additionalCheck.state = 1
|
||||||
|
}
|
||||||
|
|
||||||
t.Run(p, func(t *testing.T) {
|
t.Run(p, func(t *testing.T) {
|
||||||
c, err := config.ParseConfig(p)
|
c, err := config.ParseConfig(p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("error parsing %s:\n%+v", p, err)
|
t.Fatalf("error parsing %s:\n%+v", p, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Logf("file: %s", p)
|
t.Logf("file: %s", p)
|
||||||
t.Log(pretty.Sprint(c))
|
t.Log(pretty.Sprint(c))
|
||||||
|
|
||||||
tls.FakeCertificateLoading(t)
|
jobs, err := JobsFromConfig(c, config.ParseFlagsNoCertCheck)
|
||||||
jobs, err := JobsFromConfig(c)
|
|
||||||
t.Logf("jobs: %#v", jobs)
|
t.Logf("jobs: %#v", jobs)
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
if additionalCheck != nil {
|
||||||
|
additionalCheck.test(t, jobs)
|
||||||
|
additionalCheck.state = 2
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for basename, c := range additionalChecks {
|
||||||
|
if c.state == 0 {
|
||||||
|
panic("univisited additional check " + basename)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func testSampleConfig_BandwidthLimit(t *testing.T, jobs []Job) {
|
||||||
|
require.Len(t, jobs, 3)
|
||||||
|
|
||||||
|
{
|
||||||
|
limitedSink, ok := jobs[0].(*PassiveSide)
|
||||||
|
require.True(t, ok, "%T", jobs[0])
|
||||||
|
limitedSinkMode, ok := limitedSink.mode.(*modeSink)
|
||||||
|
require.True(t, ok, "%T", limitedSink)
|
||||||
|
|
||||||
|
assert.Equal(t, int64(12345), limitedSinkMode.receiverConfig.BandwidthLimit.Max)
|
||||||
|
assert.Equal(t, int64(1<<17), limitedSinkMode.receiverConfig.BandwidthLimit.BucketCapacity)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
limitedPush, ok := jobs[1].(*ActiveSide)
|
||||||
|
require.True(t, ok, "%T", jobs[1])
|
||||||
|
limitedPushMode, ok := limitedPush.mode.(*modePush)
|
||||||
|
require.True(t, ok, "%T", limitedPush)
|
||||||
|
|
||||||
|
assert.Equal(t, int64(54321), limitedPushMode.senderConfig.BandwidthLimit.Max)
|
||||||
|
assert.Equal(t, int64(1024), limitedPushMode.senderConfig.BandwidthLimit.BucketCapacity)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
unlimitedSink, ok := jobs[2].(*PassiveSide)
|
||||||
|
require.True(t, ok, "%T", jobs[2])
|
||||||
|
unlimitedSinkMode, ok := unlimitedSink.mode.(*modeSink)
|
||||||
|
require.True(t, ok, "%T", unlimitedSink)
|
||||||
|
|
||||||
|
max := unlimitedSinkMode.receiverConfig.BandwidthLimit.Max
|
||||||
|
assert.Less(t, max, int64(0), max, "unlimited mode <=> negative value for .Max, see bandwidthlimit.Config")
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestReplicationOptions(t *testing.T) {
|
func TestReplicationOptions(t *testing.T) {
|
||||||
@@ -235,7 +297,7 @@ jobs:
|
|||||||
t.Logf("testing config:\n%s", cstr)
|
t.Logf("testing config:\n%s", cstr)
|
||||||
c, err := config.ParseConfigBytes([]byte(cstr))
|
c, err := config.ParseConfigBytes([]byte(cstr))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
jobs, err := JobsFromConfig(c)
|
jobs, err := JobsFromConfig(c, config.ParseFlagsNone)
|
||||||
if ts.expectOk != nil {
|
if ts.expectOk != nil {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotNil(t, c)
|
require.NotNil(t, c)
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ func modeSinkFromConfig(g *config.Global, in *config.SinkJob, jobID endpoint.Job
|
|||||||
|
|
||||||
type modeSource struct {
|
type modeSource struct {
|
||||||
senderConfig *endpoint.SenderConfig
|
senderConfig *endpoint.SenderConfig
|
||||||
snapper *snapper.PeriodicOrManual
|
snapper snapper.Snapper
|
||||||
}
|
}
|
||||||
|
|
||||||
func modeSourceFromConfig(g *config.Global, in *config.SourceJob, jobID endpoint.JobID) (m *modeSource, err error) {
|
func modeSourceFromConfig(g *config.Global, in *config.SourceJob, jobID endpoint.JobID) (m *modeSource, err error) {
|
||||||
@@ -88,10 +88,11 @@ func (m *modeSource) RunPeriodic(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *modeSource) SnapperReport() *snapper.Report {
|
func (m *modeSource) SnapperReport() *snapper.Report {
|
||||||
return m.snapper.Report()
|
r := m.snapper.Report()
|
||||||
|
return &r
|
||||||
}
|
}
|
||||||
|
|
||||||
func passiveSideFromConfig(g *config.Global, in *config.PassiveJob, configJob interface{}) (s *PassiveSide, err error) {
|
func passiveSideFromConfig(g *config.Global, in *config.PassiveJob, configJob interface{}, parseFlags config.ParseFlags) (s *PassiveSide, err error) {
|
||||||
|
|
||||||
s = &PassiveSide{}
|
s = &PassiveSide{}
|
||||||
|
|
||||||
@@ -110,7 +111,7 @@ func passiveSideFromConfig(g *config.Global, in *config.PassiveJob, configJob in
|
|||||||
return nil, err // no wrapping necessary
|
return nil, err // no wrapping necessary
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.listen, err = fromconfig.ListenerFactoryFromConfig(g, in.Serve); err != nil {
|
if s.listen, err = fromconfig.ListenerFactoryFromConfig(g, in.Serve, parseFlags); err != nil {
|
||||||
return nil, errors.Wrap(err, "cannot build listener factory")
|
return nil, errors.Wrap(err, "cannot build listener factory")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+10
-5
@@ -10,6 +10,7 @@ import (
|
|||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/daemon/logging/trace"
|
"github.com/zrepl/zrepl/daemon/logging/trace"
|
||||||
|
"github.com/zrepl/zrepl/util/bandwidthlimit"
|
||||||
"github.com/zrepl/zrepl/util/nodefault"
|
"github.com/zrepl/zrepl/util/nodefault"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/config"
|
"github.com/zrepl/zrepl/config"
|
||||||
@@ -25,7 +26,7 @@ import (
|
|||||||
type SnapJob struct {
|
type SnapJob struct {
|
||||||
name endpoint.JobID
|
name endpoint.JobID
|
||||||
fsfilter zfs.DatasetFilter
|
fsfilter zfs.DatasetFilter
|
||||||
snapper *snapper.PeriodicOrManual
|
snapper snapper.Snapper
|
||||||
|
|
||||||
prunerFactory *pruner.LocalPrunerFactory
|
prunerFactory *pruner.LocalPrunerFactory
|
||||||
|
|
||||||
@@ -85,7 +86,8 @@ func (j *SnapJob) Status() *Status {
|
|||||||
s.Pruning = j.pruner.Report()
|
s.Pruning = j.pruner.Report()
|
||||||
}
|
}
|
||||||
j.prunerMtx.Unlock()
|
j.prunerMtx.Unlock()
|
||||||
s.Snapshotting = j.snapper.Report()
|
r := j.snapper.Report()
|
||||||
|
s.Snapshotting = &r
|
||||||
return &Status{Type: t, JobSpecific: s}
|
return &Status{Type: t, JobSpecific: s}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,7 +148,7 @@ type alwaysUpToDateReplicationCursorHistory struct {
|
|||||||
target pruner.Target
|
target pruner.Target
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ pruner.History = (*alwaysUpToDateReplicationCursorHistory)(nil)
|
var _ pruner.Sender = (*alwaysUpToDateReplicationCursorHistory)(nil)
|
||||||
|
|
||||||
func (h alwaysUpToDateReplicationCursorHistory) ReplicationCursor(ctx context.Context, req *pdu.ReplicationCursorReq) (*pdu.ReplicationCursorRes, error) {
|
func (h alwaysUpToDateReplicationCursorHistory) ReplicationCursor(ctx context.Context, req *pdu.ReplicationCursorReq) (*pdu.ReplicationCursorRes, error) {
|
||||||
fsvReq := &pdu.ListFilesystemVersionsReq{
|
fsvReq := &pdu.ListFilesystemVersionsReq{
|
||||||
@@ -179,8 +181,11 @@ func (j *SnapJob) doPrune(ctx context.Context) {
|
|||||||
sender := endpoint.NewSender(endpoint.SenderConfig{
|
sender := endpoint.NewSender(endpoint.SenderConfig{
|
||||||
JobID: j.name,
|
JobID: j.name,
|
||||||
FSF: j.fsfilter,
|
FSF: j.fsfilter,
|
||||||
// FIXME encryption setting is irrelevant for SnapJob because the endpoint is only used as pruner.Target
|
// FIXME the following config fields are irrelevant for SnapJob
|
||||||
Encrypt: &nodefault.Bool{B: true},
|
// because the endpoint is only used as pruner.Target.
|
||||||
|
// However, the implementation requires them to be set.
|
||||||
|
Encrypt: &nodefault.Bool{B: true},
|
||||||
|
BandwidthLimit: bandwidthlimit.NoLimitConfig(),
|
||||||
})
|
})
|
||||||
j.prunerMtx.Lock()
|
j.prunerMtx.Lock()
|
||||||
j.pruner = j.prunerFactory.BuildLocalPruner(ctx, sender, alwaysUpToDateReplicationCursorHistory{sender})
|
j.pruner = j.prunerFactory.BuildLocalPruner(ctx, sender, alwaysUpToDateReplicationCursorHistory{sender})
|
||||||
|
|||||||
@@ -246,7 +246,7 @@ func WithTask(ctx context.Context, taskName string) (context.Context, DoneFunc)
|
|||||||
// the debugString can be quite long and panic won't print it completely
|
// the debugString can be quite long and panic won't print it completely
|
||||||
fmt.Fprintf(os.Stderr, "going to panic due to activeChildTasks:\n%s\n", this.debugString())
|
fmt.Fprintf(os.Stderr, "going to panic due to activeChildTasks:\n%s\n", this.debugString())
|
||||||
}
|
}
|
||||||
panic(errors.WithMessagef(ErrTaskStillHasActiveChildTasks, "end task: %v active child tasks\n", this.activeChildTasks))
|
panic(errors.WithMessagef(ErrTaskStillHasActiveChildTasks, "end task: %v active child tasks (run daemon with env var %s=1 for more details)\n", this.activeChildTasks, debugEnabledEnvVar))
|
||||||
}
|
}
|
||||||
|
|
||||||
// support idempotent task ends
|
// support idempotent task ends
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ import (
|
|||||||
"github.com/zrepl/zrepl/util/envconst"
|
"github.com/zrepl/zrepl/util/envconst"
|
||||||
)
|
)
|
||||||
|
|
||||||
var debugEnabled = envconst.Bool("ZREPL_TRACE_DEBUG_ENABLED", false)
|
const debugEnabledEnvVar = "ZREPL_TRACE_DEBUG_ENABLED"
|
||||||
|
|
||||||
|
var debugEnabled = envconst.Bool(debugEnabledEnvVar, false)
|
||||||
|
|
||||||
func debug(format string, args ...interface{}) {
|
func debug(format string, args ...interface{}) {
|
||||||
if !debugEnabled {
|
if !debugEnabled {
|
||||||
|
|||||||
+20
-12
@@ -19,12 +19,20 @@ import (
|
|||||||
"github.com/zrepl/zrepl/util/envconst"
|
"github.com/zrepl/zrepl/util/envconst"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// The sender in the replication setup.
|
||||||
|
// The pruner uses the Sender to determine which of the Target's filesystems need to be pruned.
|
||||||
|
// Also, it asks the Sender about the replication cursor of each filesystem
|
||||||
|
// to enable the 'not_replicated' pruning rule.
|
||||||
|
//
|
||||||
// Try to keep it compatible with github.com/zrepl/zrepl/endpoint.Endpoint
|
// Try to keep it compatible with github.com/zrepl/zrepl/endpoint.Endpoint
|
||||||
type History interface {
|
type Sender 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The pruning target, i.e., on which snapshots are destroyed.
|
||||||
|
// This can be a replication sender or receiver.
|
||||||
|
//
|
||||||
// Try to keep it compatible with github.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)
|
||||||
@@ -48,7 +56,7 @@ func GetLogger(ctx context.Context) Logger {
|
|||||||
type args struct {
|
type args struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
target Target
|
target Target
|
||||||
receiver History
|
sender Sender
|
||||||
rules []pruning.KeepRule
|
rules []pruning.KeepRule
|
||||||
retryWait time.Duration
|
retryWait time.Duration
|
||||||
considerSnapAtCursorReplicated bool
|
considerSnapAtCursorReplicated bool
|
||||||
@@ -132,12 +140,12 @@ func NewPrunerFactory(in config.PruningSenderReceiver, promPruneSecs *prometheus
|
|||||||
return f, nil
|
return f, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *PrunerFactory) BuildSenderPruner(ctx context.Context, target Target, receiver History) *Pruner {
|
func (f *PrunerFactory) BuildSenderPruner(ctx context.Context, target Target, sender Sender) *Pruner {
|
||||||
p := &Pruner{
|
p := &Pruner{
|
||||||
args: args{
|
args: args{
|
||||||
context.WithValue(ctx, contextKeyPruneSide, "sender"),
|
context.WithValue(ctx, contextKeyPruneSide, "sender"),
|
||||||
target,
|
target,
|
||||||
receiver,
|
sender,
|
||||||
f.senderRules,
|
f.senderRules,
|
||||||
f.retryWait,
|
f.retryWait,
|
||||||
f.considerSnapAtCursorReplicated,
|
f.considerSnapAtCursorReplicated,
|
||||||
@@ -148,12 +156,12 @@ func (f *PrunerFactory) BuildSenderPruner(ctx context.Context, target Target, re
|
|||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *PrunerFactory) BuildReceiverPruner(ctx context.Context, target Target, receiver History) *Pruner {
|
func (f *PrunerFactory) BuildReceiverPruner(ctx context.Context, target Target, sender Sender) *Pruner {
|
||||||
p := &Pruner{
|
p := &Pruner{
|
||||||
args: args{
|
args: args{
|
||||||
context.WithValue(ctx, contextKeyPruneSide, "receiver"),
|
context.WithValue(ctx, contextKeyPruneSide, "receiver"),
|
||||||
target,
|
target,
|
||||||
receiver,
|
sender,
|
||||||
f.receiverRules,
|
f.receiverRules,
|
||||||
f.retryWait,
|
f.retryWait,
|
||||||
false, // senseless here anyways
|
false, // senseless here anyways
|
||||||
@@ -164,12 +172,12 @@ func (f *PrunerFactory) BuildReceiverPruner(ctx context.Context, target Target,
|
|||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *LocalPrunerFactory) BuildLocalPruner(ctx context.Context, target Target, receiver History) *Pruner {
|
func (f *LocalPrunerFactory) BuildLocalPruner(ctx context.Context, target Target, history Sender) *Pruner {
|
||||||
p := &Pruner{
|
p := &Pruner{
|
||||||
args: args{
|
args: args{
|
||||||
context.WithValue(ctx, contextKeyPruneSide, "local"),
|
context.WithValue(ctx, contextKeyPruneSide, "local"),
|
||||||
target,
|
target,
|
||||||
receiver,
|
history,
|
||||||
f.keepRules,
|
f.keepRules,
|
||||||
f.retryWait,
|
f.retryWait,
|
||||||
false, // considerSnapAtCursorReplicated is not relevant for local pruning
|
false, // considerSnapAtCursorReplicated is not relevant for local pruning
|
||||||
@@ -343,9 +351,9 @@ func (s snapshot) Date() time.Time { return s.date }
|
|||||||
|
|
||||||
func doOneAttempt(a *args, u updater) {
|
func doOneAttempt(a *args, u updater) {
|
||||||
|
|
||||||
ctx, target, receiver := a.ctx, a.target, a.receiver
|
ctx, target, sender := a.ctx, a.target, a.sender
|
||||||
|
|
||||||
sfssres, err := receiver.ListFilesystems(ctx, &pdu.ListFilesystemReq{})
|
sfssres, err := sender.ListFilesystems(ctx, &pdu.ListFilesystemReq{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
u(func(p *Pruner) {
|
u(func(p *Pruner) {
|
||||||
p.state = PlanErr
|
p.state = PlanErr
|
||||||
@@ -410,7 +418,7 @@ tfss_loop:
|
|||||||
rcReq := &pdu.ReplicationCursorReq{
|
rcReq := &pdu.ReplicationCursorReq{
|
||||||
Filesystem: tfs.Path,
|
Filesystem: tfs.Path,
|
||||||
}
|
}
|
||||||
rc, err := receiver.ReplicationCursor(ctx, rcReq)
|
rc, err := sender.ReplicationCursor(ctx, rcReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
pfsPlanErrAndLog(err, "cannot get replication cursor bookmark")
|
pfsPlanErrAndLog(err, "cannot get replication cursor bookmark")
|
||||||
continue tfss_loop
|
continue tfss_loop
|
||||||
@@ -456,7 +464,7 @@ tfss_loop:
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
if preCursor {
|
if preCursor {
|
||||||
pfsPlanErrAndLog(fmt.Errorf("replication cursor not found in prune target filesystem versions"), "")
|
pfsPlanErrAndLog(fmt.Errorf("prune target has no snapshot that corresponds to sender replication cursor bookmark"), "")
|
||||||
continue tfss_loop
|
continue tfss_loop
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,172 @@
|
|||||||
|
package snapper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/config"
|
||||||
|
"github.com/zrepl/zrepl/daemon/hooks"
|
||||||
|
"github.com/zrepl/zrepl/zfs"
|
||||||
|
)
|
||||||
|
|
||||||
|
func cronFromConfig(fsf zfs.DatasetFilter, in config.SnapshottingCron) (*Cron, error) {
|
||||||
|
|
||||||
|
hooksList, err := hooks.ListFromConfig(&in.Hooks)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "hook config error")
|
||||||
|
}
|
||||||
|
planArgs := planArgs{
|
||||||
|
prefix: in.Prefix,
|
||||||
|
hooks: hooksList,
|
||||||
|
}
|
||||||
|
return &Cron{config: in, fsf: fsf, planArgs: planArgs}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type Cron struct {
|
||||||
|
config config.SnapshottingCron
|
||||||
|
fsf zfs.DatasetFilter
|
||||||
|
planArgs planArgs
|
||||||
|
|
||||||
|
mtx sync.RWMutex
|
||||||
|
|
||||||
|
running bool
|
||||||
|
wakeupTime time.Time // zero value means uninit
|
||||||
|
lastError error
|
||||||
|
lastPlan *plan
|
||||||
|
wakeupWhileRunningCount int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Cron) Run(ctx context.Context, snapshotsTaken chan<- struct{}) {
|
||||||
|
|
||||||
|
t := time.NewTimer(0)
|
||||||
|
defer func() {
|
||||||
|
if !t.Stop() {
|
||||||
|
select {
|
||||||
|
case <-t.C:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
for {
|
||||||
|
now := time.Now()
|
||||||
|
s.mtx.Lock()
|
||||||
|
s.wakeupTime = s.config.Cron.Schedule.Next(now)
|
||||||
|
s.mtx.Unlock()
|
||||||
|
|
||||||
|
// Re-arm the timer.
|
||||||
|
// Need to Stop before Reset, see docs.
|
||||||
|
if !t.Stop() {
|
||||||
|
// Use non-blocking read from timer channel
|
||||||
|
// because, except for the first loop iteration,
|
||||||
|
// the channel is already drained
|
||||||
|
select {
|
||||||
|
case <-t.C:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.Reset(s.wakeupTime.Sub(now))
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-t.C:
|
||||||
|
getLogger(ctx).Debug("cron timer fired")
|
||||||
|
s.mtx.Lock()
|
||||||
|
if s.running {
|
||||||
|
getLogger(ctx).Warn("snapshotting triggered according to cron rules but previous snapshotting is not done; not taking a snapshot this time")
|
||||||
|
s.wakeupWhileRunningCount++
|
||||||
|
s.mtx.Unlock()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
s.lastError = nil
|
||||||
|
s.lastPlan = nil
|
||||||
|
s.wakeupWhileRunningCount = 0
|
||||||
|
s.running = true
|
||||||
|
s.mtx.Unlock()
|
||||||
|
go func() {
|
||||||
|
err := s.do(ctx)
|
||||||
|
s.mtx.Lock()
|
||||||
|
s.lastError = err
|
||||||
|
s.running = false
|
||||||
|
s.mtx.Unlock()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case snapshotsTaken <- struct{}{}:
|
||||||
|
default:
|
||||||
|
if snapshotsTaken != nil {
|
||||||
|
getLogger(ctx).Warn("callback channel is full, discarding snapshot update event")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Cron) do(ctx context.Context) error {
|
||||||
|
fss, err := zfs.ZFSListMapping(ctx, s.fsf)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "cannot list filesystems")
|
||||||
|
}
|
||||||
|
p := makePlan(s.planArgs, fss)
|
||||||
|
|
||||||
|
s.mtx.Lock()
|
||||||
|
s.lastPlan = p
|
||||||
|
s.lastError = nil
|
||||||
|
s.mtx.Unlock()
|
||||||
|
|
||||||
|
ok := p.execute(ctx, false)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("one or more snapshots could not be created, check logs for details")
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type CronState string
|
||||||
|
|
||||||
|
const (
|
||||||
|
CronStateRunning CronState = "running"
|
||||||
|
CronStateWaiting CronState = "waiting"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CronReport struct {
|
||||||
|
State CronState
|
||||||
|
WakeupTime time.Time
|
||||||
|
Errors []string
|
||||||
|
Progress []*ReportFilesystem
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Cron) Report() Report {
|
||||||
|
|
||||||
|
s.mtx.Lock()
|
||||||
|
defer s.mtx.Unlock()
|
||||||
|
|
||||||
|
r := CronReport{}
|
||||||
|
|
||||||
|
r.WakeupTime = s.wakeupTime
|
||||||
|
|
||||||
|
if s.running {
|
||||||
|
r.State = CronStateRunning
|
||||||
|
} else {
|
||||||
|
r.State = CronStateWaiting
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.lastError != nil {
|
||||||
|
r.Errors = append(r.Errors, s.lastError.Error())
|
||||||
|
}
|
||||||
|
if s.wakeupWhileRunningCount > 0 {
|
||||||
|
r.Errors = append(r.Errors, fmt.Sprintf("cron frequency is too high; snapshots were not taken %d times", s.wakeupWhileRunningCount))
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Progress = nil
|
||||||
|
if s.lastPlan != nil {
|
||||||
|
r.Progress = s.lastPlan.report()
|
||||||
|
}
|
||||||
|
|
||||||
|
return Report{Type: TypeCron, Cron: &r}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
package snapper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/zrepl/yaml-config"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCronLibraryWorks(t *testing.T) {
|
||||||
|
|
||||||
|
type testCase struct {
|
||||||
|
spec string
|
||||||
|
in time.Time
|
||||||
|
expect time.Time
|
||||||
|
}
|
||||||
|
dhm := func(day, hour, minutes int) time.Time {
|
||||||
|
return time.Date(2022, 7, day, hour, minutes, 0, 0, time.UTC)
|
||||||
|
}
|
||||||
|
hm := func(hour, minutes int) time.Time {
|
||||||
|
return dhm(23, hour, minutes)
|
||||||
|
}
|
||||||
|
|
||||||
|
tcs := []testCase{
|
||||||
|
{"0-10 * * * *", dhm(17, 1, 10), dhm(17, 2, 0)},
|
||||||
|
{"0-10 * * * *", dhm(17, 23, 10), dhm(18, 0, 0)},
|
||||||
|
{"0-10 * * * *", hm(1, 9), hm(1, 10)},
|
||||||
|
{"0-10 * * * *", hm(1, 9), hm(1, 10)},
|
||||||
|
|
||||||
|
{"1,3,5 * * * *", hm(1, 1), hm(1, 3)},
|
||||||
|
{"1,3,5 * * * *", hm(1, 2), hm(1, 3)},
|
||||||
|
{"1,3,5 * * * *", hm(1, 3), hm(1, 5)},
|
||||||
|
{"1,3,5 * * * *", hm(1, 5), hm(2, 1)},
|
||||||
|
|
||||||
|
{"* 0-5,8,12 * * *", hm(0, 0), hm(0, 1)},
|
||||||
|
{"* 0-5,8,12 * * *", hm(4, 59), hm(5, 0)},
|
||||||
|
{"* 0-5,8,12 * * *", hm(5, 0), hm(5, 1)},
|
||||||
|
{"* 0-5,8,12 * * *", hm(5, 59), hm(8, 0)},
|
||||||
|
{"* 0-5,8,12 * * *", hm(8, 59), hm(12, 0)},
|
||||||
|
|
||||||
|
// https://github.com/zrepl/zrepl/pull/614#issuecomment-1188358989
|
||||||
|
{"53 17,18,19 * * *", dhm(23, 17, 52), dhm(23, 17, 53)},
|
||||||
|
{"53 17,18,19 * * *", dhm(23, 17, 53), dhm(23, 18, 53)},
|
||||||
|
{"53 17,18,19 * * *", dhm(23, 18, 53), dhm(23, 19, 53)},
|
||||||
|
{"53 17,18,19 * * *", dhm(23, 19, 53), dhm(24 /* ! */, 17, 53)},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, tc := range tcs {
|
||||||
|
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
|
||||||
|
var s struct {
|
||||||
|
Cron config.CronSpec `yaml:"cron"`
|
||||||
|
}
|
||||||
|
inp := fmt.Sprintf("cron: %q", tc.spec)
|
||||||
|
fmt.Println("spec is ", inp)
|
||||||
|
err := yaml.UnmarshalStrict([]byte(inp), &s)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
actual := s.Cron.Schedule.Next(tc.in)
|
||||||
|
assert.Equal(t, tc.expect, actual)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,251 @@
|
|||||||
|
package snapper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/daemon/hooks"
|
||||||
|
"github.com/zrepl/zrepl/daemon/logging"
|
||||||
|
"github.com/zrepl/zrepl/util/chainlock"
|
||||||
|
"github.com/zrepl/zrepl/zfs"
|
||||||
|
)
|
||||||
|
|
||||||
|
type planArgs struct {
|
||||||
|
prefix string
|
||||||
|
hooks *hooks.List
|
||||||
|
}
|
||||||
|
|
||||||
|
type plan struct {
|
||||||
|
mtx chainlock.L
|
||||||
|
args planArgs
|
||||||
|
snaps map[*zfs.DatasetPath]*snapProgress
|
||||||
|
}
|
||||||
|
|
||||||
|
func makePlan(args planArgs, fss []*zfs.DatasetPath) *plan {
|
||||||
|
snaps := make(map[*zfs.DatasetPath]*snapProgress, len(fss))
|
||||||
|
for _, fs := range fss {
|
||||||
|
snaps[fs] = &snapProgress{state: SnapPending}
|
||||||
|
}
|
||||||
|
return &plan{snaps: snaps, args: args}
|
||||||
|
}
|
||||||
|
|
||||||
|
//go:generate stringer -type=SnapState
|
||||||
|
type SnapState uint
|
||||||
|
|
||||||
|
const (
|
||||||
|
SnapPending SnapState = 1 << iota
|
||||||
|
SnapStarted
|
||||||
|
SnapDone
|
||||||
|
SnapError
|
||||||
|
)
|
||||||
|
|
||||||
|
// All fields protected by Snapper.mtx
|
||||||
|
type snapProgress struct {
|
||||||
|
state SnapState
|
||||||
|
|
||||||
|
// SnapStarted, SnapDone, SnapError
|
||||||
|
name string
|
||||||
|
startAt time.Time
|
||||||
|
hookPlan *hooks.Plan
|
||||||
|
|
||||||
|
// SnapDone
|
||||||
|
doneAt time.Time
|
||||||
|
|
||||||
|
// SnapErr TODO disambiguate state
|
||||||
|
runResults hooks.PlanReport
|
||||||
|
}
|
||||||
|
|
||||||
|
func (plan *plan) execute(ctx context.Context, dryRun bool) (ok bool) {
|
||||||
|
|
||||||
|
hookMatchCount := make(map[hooks.Hook]int, len(*plan.args.hooks))
|
||||||
|
for _, h := range *plan.args.hooks {
|
||||||
|
hookMatchCount[h] = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
anyFsHadErr := false
|
||||||
|
// TODO channel programs -> allow a little jitter?
|
||||||
|
for fs, progress := range plan.snaps {
|
||||||
|
suffix := time.Now().In(time.UTC).Format("20060102_150405_000")
|
||||||
|
snapname := fmt.Sprintf("%s%s", plan.args.prefix, suffix)
|
||||||
|
|
||||||
|
ctx := logging.WithInjectedField(ctx, "fs", fs.ToString())
|
||||||
|
ctx = logging.WithInjectedField(ctx, "snap", snapname)
|
||||||
|
|
||||||
|
hookEnvExtra := hooks.Env{
|
||||||
|
hooks.EnvFS: fs.ToString(),
|
||||||
|
hooks.EnvSnapshot: snapname,
|
||||||
|
}
|
||||||
|
|
||||||
|
jobCallback := hooks.NewCallbackHookForFilesystem("snapshot", fs, func(ctx context.Context) (err error) {
|
||||||
|
l := getLogger(ctx)
|
||||||
|
l.Debug("create snapshot")
|
||||||
|
err = zfs.ZFSSnapshot(ctx, fs, snapname, false) // TODO propagate context to ZFSSnapshot
|
||||||
|
if err != nil {
|
||||||
|
l.WithError(err).Error("cannot create snapshot")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
})
|
||||||
|
|
||||||
|
fsHadErr := false
|
||||||
|
var hookPlanReport hooks.PlanReport
|
||||||
|
var hookPlan *hooks.Plan
|
||||||
|
{
|
||||||
|
filteredHooks, err := plan.args.hooks.CopyFilteredForFilesystem(fs)
|
||||||
|
if err != nil {
|
||||||
|
getLogger(ctx).WithError(err).Error("unexpected filter error")
|
||||||
|
fsHadErr = true
|
||||||
|
goto updateFSState
|
||||||
|
}
|
||||||
|
// account for running hooks
|
||||||
|
for _, h := range filteredHooks {
|
||||||
|
hookMatchCount[h] = hookMatchCount[h] + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
var planErr error
|
||||||
|
hookPlan, planErr = hooks.NewPlan(&filteredHooks, hooks.PhaseSnapshot, jobCallback, hookEnvExtra)
|
||||||
|
if planErr != nil {
|
||||||
|
fsHadErr = true
|
||||||
|
getLogger(ctx).WithError(planErr).Error("cannot create job hook plan")
|
||||||
|
goto updateFSState
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plan.mtx.HoldWhile(func() {
|
||||||
|
progress.name = snapname
|
||||||
|
progress.startAt = time.Now()
|
||||||
|
progress.hookPlan = hookPlan
|
||||||
|
progress.state = SnapStarted
|
||||||
|
})
|
||||||
|
|
||||||
|
{
|
||||||
|
getLogger(ctx).WithField("report", hookPlan.Report().String()).Debug("begin run job plan")
|
||||||
|
hookPlan.Run(ctx, dryRun)
|
||||||
|
hookPlanReport = hookPlan.Report()
|
||||||
|
fsHadErr = hookPlanReport.HadError() // not just fatal errors
|
||||||
|
if fsHadErr {
|
||||||
|
getLogger(ctx).WithField("report", hookPlanReport.String()).Error("end run job plan with error")
|
||||||
|
} else {
|
||||||
|
getLogger(ctx).WithField("report", hookPlanReport.String()).Info("end run job plan successful")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFSState:
|
||||||
|
anyFsHadErr = anyFsHadErr || fsHadErr
|
||||||
|
plan.mtx.HoldWhile(func() {
|
||||||
|
progress.doneAt = time.Now()
|
||||||
|
progress.state = SnapDone
|
||||||
|
if fsHadErr {
|
||||||
|
progress.state = SnapError
|
||||||
|
}
|
||||||
|
progress.runResults = hookPlanReport
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for h, mc := range hookMatchCount {
|
||||||
|
if mc == 0 {
|
||||||
|
hookIdx := -1
|
||||||
|
for idx, ah := range *plan.args.hooks {
|
||||||
|
if ah == h {
|
||||||
|
hookIdx = idx
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
getLogger(ctx).WithField("hook", h.String()).WithField("hook_number", hookIdx+1).Warn("hook did not match any snapshotted filesystems")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return !anyFsHadErr
|
||||||
|
}
|
||||||
|
|
||||||
|
type ReportFilesystem struct {
|
||||||
|
Path string
|
||||||
|
State SnapState
|
||||||
|
|
||||||
|
// Valid in SnapStarted and later
|
||||||
|
SnapName string
|
||||||
|
StartAt time.Time
|
||||||
|
Hooks string
|
||||||
|
HooksHadError bool
|
||||||
|
|
||||||
|
// Valid in SnapDone | SnapError
|
||||||
|
DoneAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (plan *plan) report() []*ReportFilesystem {
|
||||||
|
plan.mtx.Lock()
|
||||||
|
defer plan.mtx.Unlock()
|
||||||
|
|
||||||
|
pReps := make([]*ReportFilesystem, 0, len(plan.snaps))
|
||||||
|
for fs, p := range plan.snaps {
|
||||||
|
var hooksStr string
|
||||||
|
var hooksHadError bool
|
||||||
|
if p.hookPlan != nil {
|
||||||
|
hooksStr, hooksHadError = p.report()
|
||||||
|
}
|
||||||
|
pReps = append(pReps, &ReportFilesystem{
|
||||||
|
Path: fs.ToString(),
|
||||||
|
State: p.state,
|
||||||
|
SnapName: p.name,
|
||||||
|
StartAt: p.startAt,
|
||||||
|
DoneAt: p.doneAt,
|
||||||
|
Hooks: hooksStr,
|
||||||
|
HooksHadError: hooksHadError,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(pReps, func(i, j int) bool {
|
||||||
|
return strings.Compare(pReps[i].Path, pReps[j].Path) == -1
|
||||||
|
})
|
||||||
|
|
||||||
|
return pReps
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *snapProgress) report() (hooksStr string, hooksHadError bool) {
|
||||||
|
hr := p.hookPlan.Report()
|
||||||
|
// FIXME: technically this belongs into client
|
||||||
|
// but we can't serialize hooks.Step ATM
|
||||||
|
rightPad := func(str string, length int, pad string) string {
|
||||||
|
if len(str) > length {
|
||||||
|
return str[:length]
|
||||||
|
}
|
||||||
|
return str + strings.Repeat(pad, length-len(str))
|
||||||
|
}
|
||||||
|
hooksHadError = hr.HadError()
|
||||||
|
rows := make([][]string, len(hr))
|
||||||
|
const numCols = 4
|
||||||
|
lens := make([]int, numCols)
|
||||||
|
for i, e := range hr {
|
||||||
|
rows[i] = make([]string, numCols)
|
||||||
|
rows[i][0] = fmt.Sprintf("%d", i+1)
|
||||||
|
rows[i][1] = e.Status.String()
|
||||||
|
runTime := "..."
|
||||||
|
if e.Status != hooks.StepPending {
|
||||||
|
runTime = e.End.Sub(e.Begin).Round(time.Millisecond).String()
|
||||||
|
}
|
||||||
|
rows[i][2] = runTime
|
||||||
|
rows[i][3] = ""
|
||||||
|
if e.Report != nil {
|
||||||
|
rows[i][3] = e.Report.String()
|
||||||
|
}
|
||||||
|
for j, col := range lens {
|
||||||
|
if len(rows[i][j]) > col {
|
||||||
|
lens[j] = len(rows[i][j])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rowsFlat := make([]string, len(hr))
|
||||||
|
for i, r := range rows {
|
||||||
|
colsPadded := make([]string, len(r))
|
||||||
|
for j, c := range r[:len(r)-1] {
|
||||||
|
colsPadded[j] = rightPad(c, lens[j], " ")
|
||||||
|
}
|
||||||
|
colsPadded[len(r)-1] = r[len(r)-1]
|
||||||
|
rowsFlat[i] = strings.Join(colsPadded, " ")
|
||||||
|
}
|
||||||
|
hooksStr = strings.Join(rowsFlat, "\n")
|
||||||
|
|
||||||
|
return hooksStr, hooksHadError
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package snapper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
type manual struct{}
|
||||||
|
|
||||||
|
func (s *manual) Run(ctx context.Context, wakeUpCommon chan<- struct{}) {
|
||||||
|
// nothing to do
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *manual) Report() Report {
|
||||||
|
return Report{Type: TypeManual, Manual: &struct{}{}}
|
||||||
|
}
|
||||||
@@ -0,0 +1,399 @@
|
|||||||
|
package snapper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/daemon/logging/trace"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/config"
|
||||||
|
"github.com/zrepl/zrepl/daemon/hooks"
|
||||||
|
"github.com/zrepl/zrepl/daemon/logging"
|
||||||
|
"github.com/zrepl/zrepl/util/envconst"
|
||||||
|
"github.com/zrepl/zrepl/zfs"
|
||||||
|
)
|
||||||
|
|
||||||
|
func periodicFromConfig(g *config.Global, fsf zfs.DatasetFilter, in *config.SnapshottingPeriodic) (*Periodic, error) {
|
||||||
|
if in.Prefix == "" {
|
||||||
|
return nil, errors.New("prefix must not be empty")
|
||||||
|
}
|
||||||
|
if in.Interval.Duration() <= 0 {
|
||||||
|
return nil, errors.New("interval must be positive")
|
||||||
|
}
|
||||||
|
|
||||||
|
hookList, err := hooks.ListFromConfig(&in.Hooks)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "hook config error")
|
||||||
|
}
|
||||||
|
|
||||||
|
args := periodicArgs{
|
||||||
|
interval: in.Interval.Duration(),
|
||||||
|
fsf: fsf,
|
||||||
|
planArgs: planArgs{
|
||||||
|
prefix: in.Prefix,
|
||||||
|
hooks: hookList,
|
||||||
|
},
|
||||||
|
// ctx and log is set in Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Periodic{state: SyncUp, args: args}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type periodicArgs struct {
|
||||||
|
ctx context.Context
|
||||||
|
interval time.Duration
|
||||||
|
fsf zfs.DatasetFilter
|
||||||
|
planArgs planArgs
|
||||||
|
snapshotsTaken chan<- struct{}
|
||||||
|
dryRun bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type Periodic struct {
|
||||||
|
args periodicArgs
|
||||||
|
|
||||||
|
mtx sync.Mutex
|
||||||
|
state State
|
||||||
|
|
||||||
|
// set in state Plan, used in Waiting
|
||||||
|
lastInvocation time.Time
|
||||||
|
|
||||||
|
// valid for state Snapshotting
|
||||||
|
plan *plan
|
||||||
|
|
||||||
|
// valid for state SyncUp and Waiting
|
||||||
|
sleepUntil time.Time
|
||||||
|
|
||||||
|
// valid for state Err
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
//go:generate stringer -type=State
|
||||||
|
type State uint
|
||||||
|
|
||||||
|
const (
|
||||||
|
SyncUp State = 1 << iota
|
||||||
|
SyncUpErrWait
|
||||||
|
Planning
|
||||||
|
Snapshotting
|
||||||
|
Waiting
|
||||||
|
ErrorWait
|
||||||
|
Stopped
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s State) sf() state {
|
||||||
|
m := map[State]state{
|
||||||
|
SyncUp: periodicStateSyncUp,
|
||||||
|
SyncUpErrWait: periodicStateWait,
|
||||||
|
Planning: periodicStatePlan,
|
||||||
|
Snapshotting: periodicStateSnapshot,
|
||||||
|
Waiting: periodicStateWait,
|
||||||
|
ErrorWait: periodicStateWait,
|
||||||
|
Stopped: nil,
|
||||||
|
}
|
||||||
|
return m[s]
|
||||||
|
}
|
||||||
|
|
||||||
|
type updater func(u func(*Periodic)) State
|
||||||
|
type state func(a periodicArgs, u updater) state
|
||||||
|
|
||||||
|
func (s *Periodic) Run(ctx context.Context, snapshotsTaken chan<- struct{}) {
|
||||||
|
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
|
||||||
|
getLogger(ctx).Debug("start")
|
||||||
|
defer getLogger(ctx).Debug("stop")
|
||||||
|
|
||||||
|
s.args.snapshotsTaken = snapshotsTaken
|
||||||
|
s.args.ctx = ctx
|
||||||
|
s.args.dryRun = false // for future expansion
|
||||||
|
|
||||||
|
u := func(u func(*Periodic)) State {
|
||||||
|
s.mtx.Lock()
|
||||||
|
defer s.mtx.Unlock()
|
||||||
|
if u != nil {
|
||||||
|
u(s)
|
||||||
|
}
|
||||||
|
return s.state
|
||||||
|
}
|
||||||
|
|
||||||
|
var st state = periodicStateSyncUp
|
||||||
|
|
||||||
|
for st != nil {
|
||||||
|
pre := u(nil)
|
||||||
|
st = st(s.args, u)
|
||||||
|
post := u(nil)
|
||||||
|
getLogger(ctx).
|
||||||
|
WithField("transition", fmt.Sprintf("%s=>%s", pre, post)).
|
||||||
|
Debug("state transition")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func onErr(err error, u updater) state {
|
||||||
|
return u(func(s *Periodic) {
|
||||||
|
s.err = err
|
||||||
|
preState := s.state
|
||||||
|
switch s.state {
|
||||||
|
case SyncUp:
|
||||||
|
s.state = SyncUpErrWait
|
||||||
|
case Planning:
|
||||||
|
fallthrough
|
||||||
|
case Snapshotting:
|
||||||
|
s.state = ErrorWait
|
||||||
|
}
|
||||||
|
getLogger(s.args.ctx).WithError(err).WithField("pre_state", preState).WithField("post_state", s.state).Error("snapshotting error")
|
||||||
|
}).sf()
|
||||||
|
}
|
||||||
|
|
||||||
|
func onMainCtxDone(ctx context.Context, u updater) state {
|
||||||
|
return u(func(s *Periodic) {
|
||||||
|
s.err = ctx.Err()
|
||||||
|
s.state = Stopped
|
||||||
|
}).sf()
|
||||||
|
}
|
||||||
|
|
||||||
|
func periodicStateSyncUp(a periodicArgs, u updater) state {
|
||||||
|
u(func(snapper *Periodic) {
|
||||||
|
snapper.lastInvocation = time.Now()
|
||||||
|
})
|
||||||
|
fss, err := listFSes(a.ctx, a.fsf)
|
||||||
|
if err != nil {
|
||||||
|
return onErr(err, u)
|
||||||
|
}
|
||||||
|
syncPoint, err := findSyncPoint(a.ctx, fss, a.planArgs.prefix, a.interval)
|
||||||
|
if err != nil {
|
||||||
|
return onErr(err, u)
|
||||||
|
}
|
||||||
|
u(func(s *Periodic) {
|
||||||
|
s.sleepUntil = syncPoint
|
||||||
|
})
|
||||||
|
t := time.NewTimer(time.Until(syncPoint))
|
||||||
|
defer t.Stop()
|
||||||
|
select {
|
||||||
|
case <-t.C:
|
||||||
|
return u(func(s *Periodic) {
|
||||||
|
s.state = Planning
|
||||||
|
}).sf()
|
||||||
|
case <-a.ctx.Done():
|
||||||
|
return onMainCtxDone(a.ctx, u)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func periodicStatePlan(a periodicArgs, u updater) state {
|
||||||
|
u(func(snapper *Periodic) {
|
||||||
|
snapper.lastInvocation = time.Now()
|
||||||
|
})
|
||||||
|
fss, err := listFSes(a.ctx, a.fsf)
|
||||||
|
if err != nil {
|
||||||
|
return onErr(err, u)
|
||||||
|
}
|
||||||
|
p := makePlan(a.planArgs, fss)
|
||||||
|
return u(func(s *Periodic) {
|
||||||
|
s.state = Snapshotting
|
||||||
|
s.plan = p
|
||||||
|
s.err = nil
|
||||||
|
}).sf()
|
||||||
|
}
|
||||||
|
|
||||||
|
func periodicStateSnapshot(a periodicArgs, u updater) state {
|
||||||
|
|
||||||
|
var plan *plan
|
||||||
|
u(func(snapper *Periodic) {
|
||||||
|
plan = snapper.plan
|
||||||
|
})
|
||||||
|
|
||||||
|
ok := plan.execute(a.ctx, false)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case a.snapshotsTaken <- struct{}{}:
|
||||||
|
default:
|
||||||
|
if a.snapshotsTaken != nil {
|
||||||
|
getLogger(a.ctx).Warn("callback channel is full, discarding snapshot update event")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return u(func(snapper *Periodic) {
|
||||||
|
if !ok {
|
||||||
|
snapper.state = ErrorWait
|
||||||
|
snapper.err = errors.New("one or more snapshots could not be created, check logs for details")
|
||||||
|
} else {
|
||||||
|
snapper.state = Waiting
|
||||||
|
snapper.err = nil
|
||||||
|
}
|
||||||
|
}).sf()
|
||||||
|
}
|
||||||
|
|
||||||
|
func periodicStateWait(a periodicArgs, u updater) state {
|
||||||
|
var sleepUntil time.Time
|
||||||
|
u(func(snapper *Periodic) {
|
||||||
|
lastTick := snapper.lastInvocation
|
||||||
|
snapper.sleepUntil = lastTick.Add(a.interval)
|
||||||
|
sleepUntil = snapper.sleepUntil
|
||||||
|
log := getLogger(a.ctx).WithField("sleep_until", sleepUntil).WithField("duration", a.interval)
|
||||||
|
logFunc := log.Debug
|
||||||
|
if snapper.state == ErrorWait || snapper.state == SyncUpErrWait {
|
||||||
|
logFunc = log.Error
|
||||||
|
}
|
||||||
|
logFunc("enter wait-state after error")
|
||||||
|
})
|
||||||
|
|
||||||
|
t := time.NewTimer(time.Until(sleepUntil))
|
||||||
|
defer t.Stop()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-t.C:
|
||||||
|
return u(func(snapper *Periodic) {
|
||||||
|
snapper.state = Planning
|
||||||
|
}).sf()
|
||||||
|
case <-a.ctx.Done():
|
||||||
|
return onMainCtxDone(a.ctx, u)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func listFSes(ctx context.Context, mf zfs.DatasetFilter) (fss []*zfs.DatasetPath, err error) {
|
||||||
|
return zfs.ZFSListMapping(ctx, mf)
|
||||||
|
}
|
||||||
|
|
||||||
|
var syncUpWarnNoSnapshotUntilSyncupMinDuration = envconst.Duration("ZREPL_SNAPPER_SYNCUP_WARN_MIN_DURATION", 1*time.Second)
|
||||||
|
|
||||||
|
// see docs/snapshotting.rst
|
||||||
|
func findSyncPoint(ctx context.Context, fss []*zfs.DatasetPath, prefix string, interval time.Duration) (syncPoint time.Time, err error) {
|
||||||
|
|
||||||
|
const (
|
||||||
|
prioHasVersions int = iota
|
||||||
|
prioNoVersions
|
||||||
|
)
|
||||||
|
|
||||||
|
type snapTime struct {
|
||||||
|
ds *zfs.DatasetPath
|
||||||
|
prio int // lower is higher
|
||||||
|
time time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(fss) == 0 {
|
||||||
|
return time.Now(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
snaptimes := make([]snapTime, 0, len(fss))
|
||||||
|
hardErrs := 0
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
getLogger(ctx).Debug("examine filesystem state to find sync point")
|
||||||
|
for _, d := range fss {
|
||||||
|
ctx := logging.WithInjectedField(ctx, "fs", d.ToString())
|
||||||
|
syncPoint, err := findSyncPointFSNextOptimalSnapshotTime(ctx, now, interval, prefix, d)
|
||||||
|
if err == findSyncPointFSNoFilesystemVersionsErr {
|
||||||
|
snaptimes = append(snaptimes, snapTime{
|
||||||
|
ds: d,
|
||||||
|
prio: prioNoVersions,
|
||||||
|
time: now,
|
||||||
|
})
|
||||||
|
} else if err != nil {
|
||||||
|
hardErrs++
|
||||||
|
getLogger(ctx).WithError(err).Error("cannot determine optimal sync point for this filesystem")
|
||||||
|
} else {
|
||||||
|
getLogger(ctx).WithField("syncPoint", syncPoint).Debug("found optimal sync point for this filesystem")
|
||||||
|
snaptimes = append(snaptimes, snapTime{
|
||||||
|
ds: d,
|
||||||
|
prio: prioHasVersions,
|
||||||
|
time: syncPoint,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if hardErrs == len(fss) {
|
||||||
|
return time.Time{}, fmt.Errorf("hard errors in determining sync point for every matching filesystem")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(snaptimes) == 0 {
|
||||||
|
panic("implementation error: loop must either inc hardErrs or add result to snaptimes")
|
||||||
|
}
|
||||||
|
|
||||||
|
// sort ascending by (prio,time)
|
||||||
|
// => those filesystems with versions win over those without any
|
||||||
|
sort.Slice(snaptimes, func(i, j int) bool {
|
||||||
|
if snaptimes[i].prio == snaptimes[j].prio {
|
||||||
|
return snaptimes[i].time.Before(snaptimes[j].time)
|
||||||
|
}
|
||||||
|
return snaptimes[i].prio < snaptimes[j].prio
|
||||||
|
})
|
||||||
|
|
||||||
|
winnerSyncPoint := snaptimes[0].time
|
||||||
|
l := getLogger(ctx).WithField("syncPoint", winnerSyncPoint.String())
|
||||||
|
l.Info("determined sync point")
|
||||||
|
if winnerSyncPoint.Sub(now) > syncUpWarnNoSnapshotUntilSyncupMinDuration {
|
||||||
|
for _, st := range snaptimes {
|
||||||
|
if st.prio == prioNoVersions {
|
||||||
|
l.WithField("fs", st.ds.ToString()).Warn("filesystem will not be snapshotted until sync point")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return snaptimes[0].time, nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
var findSyncPointFSNoFilesystemVersionsErr = fmt.Errorf("no filesystem versions")
|
||||||
|
|
||||||
|
func findSyncPointFSNextOptimalSnapshotTime(ctx context.Context, now time.Time, interval time.Duration, prefix string, d *zfs.DatasetPath) (time.Time, error) {
|
||||||
|
|
||||||
|
fsvs, err := zfs.ZFSListFilesystemVersions(ctx, d, zfs.ListFilesystemVersionsOptions{
|
||||||
|
Types: zfs.Snapshots,
|
||||||
|
ShortnamePrefix: prefix,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return time.Time{}, errors.Wrap(err, "list filesystem versions")
|
||||||
|
}
|
||||||
|
if len(fsvs) <= 0 {
|
||||||
|
return time.Time{}, findSyncPointFSNoFilesystemVersionsErr
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort versions by creation
|
||||||
|
sort.SliceStable(fsvs, func(i, j int) bool {
|
||||||
|
return fsvs[i].CreateTXG < fsvs[j].CreateTXG
|
||||||
|
})
|
||||||
|
|
||||||
|
latest := fsvs[len(fsvs)-1]
|
||||||
|
getLogger(ctx).WithField("creation", latest.Creation).Debug("found latest snapshot")
|
||||||
|
|
||||||
|
since := now.Sub(latest.Creation)
|
||||||
|
if since < 0 {
|
||||||
|
return time.Time{}, fmt.Errorf("snapshot %q is from the future: creation=%q now=%q", latest.ToAbsPath(d), latest.Creation, now)
|
||||||
|
}
|
||||||
|
|
||||||
|
return latest.Creation.Add(interval), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type PeriodicReport struct {
|
||||||
|
State State
|
||||||
|
// valid in state SyncUp and Waiting
|
||||||
|
SleepUntil time.Time
|
||||||
|
// valid in state Err
|
||||||
|
Error string
|
||||||
|
// valid in state Snapshotting
|
||||||
|
Progress []*ReportFilesystem
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Periodic) Report() Report {
|
||||||
|
s.mtx.Lock()
|
||||||
|
defer s.mtx.Unlock()
|
||||||
|
|
||||||
|
var progress []*ReportFilesystem = nil
|
||||||
|
if s.plan != nil {
|
||||||
|
progress = s.plan.report()
|
||||||
|
}
|
||||||
|
|
||||||
|
r := &PeriodicReport{
|
||||||
|
State: s.state,
|
||||||
|
SleepUntil: s.sleepUntil,
|
||||||
|
Error: errOrEmptyString(s.err),
|
||||||
|
Progress: progress,
|
||||||
|
}
|
||||||
|
|
||||||
|
return Report{Type: TypePeriodic, Periodic: r}
|
||||||
|
}
|
||||||
+21
-478
@@ -3,497 +3,40 @@ package snapper
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sort"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/daemon/logging/trace"
|
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/config"
|
"github.com/zrepl/zrepl/config"
|
||||||
"github.com/zrepl/zrepl/daemon/hooks"
|
|
||||||
"github.com/zrepl/zrepl/daemon/logging"
|
|
||||||
"github.com/zrepl/zrepl/logger"
|
|
||||||
"github.com/zrepl/zrepl/util/envconst"
|
|
||||||
"github.com/zrepl/zrepl/zfs"
|
"github.com/zrepl/zrepl/zfs"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:generate stringer -type=SnapState
|
type Type string
|
||||||
type SnapState uint
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
SnapPending SnapState = 1 << iota
|
TypePeriodic Type = "periodic"
|
||||||
SnapStarted
|
TypeCron Type = "cron"
|
||||||
SnapDone
|
TypeManual Type = "manual"
|
||||||
SnapError
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// All fields protected by Snapper.mtx
|
type Snapper interface {
|
||||||
type snapProgress struct {
|
Run(ctx context.Context, snapshotsTaken chan<- struct{})
|
||||||
state SnapState
|
Report() Report
|
||||||
|
|
||||||
// SnapStarted, SnapDone, SnapError
|
|
||||||
name string
|
|
||||||
startAt time.Time
|
|
||||||
hookPlan *hooks.Plan
|
|
||||||
|
|
||||||
// SnapDone
|
|
||||||
doneAt time.Time
|
|
||||||
|
|
||||||
// SnapErr TODO disambiguate state
|
|
||||||
runResults hooks.PlanReport
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type args struct {
|
type Report struct {
|
||||||
ctx context.Context
|
Type Type
|
||||||
prefix string
|
Periodic *PeriodicReport
|
||||||
interval time.Duration
|
Cron *CronReport
|
||||||
fsf zfs.DatasetFilter
|
Manual *struct{}
|
||||||
snapshotsTaken chan<- struct{}
|
|
||||||
hooks *hooks.List
|
|
||||||
dryRun bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Snapper struct {
|
func FromConfig(g *config.Global, fsf zfs.DatasetFilter, in config.SnapshottingEnum) (Snapper, error) {
|
||||||
args args
|
switch v := in.Ret.(type) {
|
||||||
|
case *config.SnapshottingPeriodic:
|
||||||
mtx sync.Mutex
|
return periodicFromConfig(g, fsf, v)
|
||||||
state State
|
case *config.SnapshottingCron:
|
||||||
|
return cronFromConfig(fsf, *v)
|
||||||
// set in state Plan, used in Waiting
|
case *config.SnapshottingManual:
|
||||||
lastInvocation time.Time
|
return &manual{}, nil
|
||||||
|
|
||||||
// valid for state Snapshotting
|
|
||||||
plan map[*zfs.DatasetPath]*snapProgress
|
|
||||||
|
|
||||||
// valid for state SyncUp and Waiting
|
|
||||||
sleepUntil time.Time
|
|
||||||
|
|
||||||
// valid for state Err
|
|
||||||
err error
|
|
||||||
}
|
|
||||||
|
|
||||||
//go:generate stringer -type=State
|
|
||||||
type State uint
|
|
||||||
|
|
||||||
const (
|
|
||||||
SyncUp State = 1 << iota
|
|
||||||
SyncUpErrWait
|
|
||||||
Planning
|
|
||||||
Snapshotting
|
|
||||||
Waiting
|
|
||||||
ErrorWait
|
|
||||||
Stopped
|
|
||||||
)
|
|
||||||
|
|
||||||
func (s State) sf() state {
|
|
||||||
m := map[State]state{
|
|
||||||
SyncUp: syncUp,
|
|
||||||
SyncUpErrWait: wait,
|
|
||||||
Planning: plan,
|
|
||||||
Snapshotting: snapshot,
|
|
||||||
Waiting: wait,
|
|
||||||
ErrorWait: wait,
|
|
||||||
Stopped: nil,
|
|
||||||
}
|
|
||||||
return m[s]
|
|
||||||
}
|
|
||||||
|
|
||||||
type updater func(u func(*Snapper)) State
|
|
||||||
type state func(a args, u updater) state
|
|
||||||
|
|
||||||
type Logger = logger.Logger
|
|
||||||
|
|
||||||
func getLogger(ctx context.Context) Logger {
|
|
||||||
return logging.GetLogger(ctx, logging.SubsysSnapshot)
|
|
||||||
}
|
|
||||||
|
|
||||||
func PeriodicFromConfig(g *config.Global, fsf zfs.DatasetFilter, in *config.SnapshottingPeriodic) (*Snapper, error) {
|
|
||||||
if in.Prefix == "" {
|
|
||||||
return nil, errors.New("prefix must not be empty")
|
|
||||||
}
|
|
||||||
if in.Interval <= 0 {
|
|
||||||
return nil, errors.New("interval must be positive")
|
|
||||||
}
|
|
||||||
|
|
||||||
hookList, err := hooks.ListFromConfig(&in.Hooks)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "hook config error")
|
|
||||||
}
|
|
||||||
|
|
||||||
args := args{
|
|
||||||
prefix: in.Prefix,
|
|
||||||
interval: in.Interval,
|
|
||||||
fsf: fsf,
|
|
||||||
hooks: hookList,
|
|
||||||
// ctx and log is set in Run()
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Snapper{state: SyncUp, args: args}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Snapper) Run(ctx context.Context, snapshotsTaken chan<- struct{}) {
|
|
||||||
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
|
|
||||||
getLogger(ctx).Debug("start")
|
|
||||||
defer getLogger(ctx).Debug("stop")
|
|
||||||
|
|
||||||
s.args.snapshotsTaken = snapshotsTaken
|
|
||||||
s.args.ctx = ctx
|
|
||||||
s.args.dryRun = false // for future expansion
|
|
||||||
|
|
||||||
u := func(u func(*Snapper)) State {
|
|
||||||
s.mtx.Lock()
|
|
||||||
defer s.mtx.Unlock()
|
|
||||||
if u != nil {
|
|
||||||
u(s)
|
|
||||||
}
|
|
||||||
return s.state
|
|
||||||
}
|
|
||||||
|
|
||||||
var st state = syncUp
|
|
||||||
|
|
||||||
for st != nil {
|
|
||||||
pre := u(nil)
|
|
||||||
st = st(s.args, u)
|
|
||||||
post := u(nil)
|
|
||||||
getLogger(ctx).
|
|
||||||
WithField("transition", fmt.Sprintf("%s=>%s", pre, post)).
|
|
||||||
Debug("state transition")
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func onErr(err error, u updater) state {
|
|
||||||
return u(func(s *Snapper) {
|
|
||||||
s.err = err
|
|
||||||
preState := s.state
|
|
||||||
switch s.state {
|
|
||||||
case SyncUp:
|
|
||||||
s.state = SyncUpErrWait
|
|
||||||
case Planning:
|
|
||||||
fallthrough
|
|
||||||
case Snapshotting:
|
|
||||||
s.state = ErrorWait
|
|
||||||
}
|
|
||||||
getLogger(s.args.ctx).WithError(err).WithField("pre_state", preState).WithField("post_state", s.state).Error("snapshotting error")
|
|
||||||
}).sf()
|
|
||||||
}
|
|
||||||
|
|
||||||
func onMainCtxDone(ctx context.Context, u updater) state {
|
|
||||||
return u(func(s *Snapper) {
|
|
||||||
s.err = ctx.Err()
|
|
||||||
s.state = Stopped
|
|
||||||
}).sf()
|
|
||||||
}
|
|
||||||
|
|
||||||
func syncUp(a args, u updater) state {
|
|
||||||
u(func(snapper *Snapper) {
|
|
||||||
snapper.lastInvocation = time.Now()
|
|
||||||
})
|
|
||||||
fss, err := listFSes(a.ctx, a.fsf)
|
|
||||||
if err != nil {
|
|
||||||
return onErr(err, u)
|
|
||||||
}
|
|
||||||
syncPoint, err := findSyncPoint(a.ctx, fss, a.prefix, a.interval)
|
|
||||||
if err != nil {
|
|
||||||
return onErr(err, u)
|
|
||||||
}
|
|
||||||
u(func(s *Snapper) {
|
|
||||||
s.sleepUntil = syncPoint
|
|
||||||
})
|
|
||||||
t := time.NewTimer(time.Until(syncPoint))
|
|
||||||
defer t.Stop()
|
|
||||||
select {
|
|
||||||
case <-t.C:
|
|
||||||
return u(func(s *Snapper) {
|
|
||||||
s.state = Planning
|
|
||||||
}).sf()
|
|
||||||
case <-a.ctx.Done():
|
|
||||||
return onMainCtxDone(a.ctx, u)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func plan(a args, u updater) state {
|
|
||||||
u(func(snapper *Snapper) {
|
|
||||||
snapper.lastInvocation = time.Now()
|
|
||||||
})
|
|
||||||
fss, err := listFSes(a.ctx, a.fsf)
|
|
||||||
if err != nil {
|
|
||||||
return onErr(err, u)
|
|
||||||
}
|
|
||||||
|
|
||||||
plan := make(map[*zfs.DatasetPath]*snapProgress, len(fss))
|
|
||||||
for _, fs := range fss {
|
|
||||||
plan[fs] = &snapProgress{state: SnapPending}
|
|
||||||
}
|
|
||||||
return u(func(s *Snapper) {
|
|
||||||
s.state = Snapshotting
|
|
||||||
s.plan = plan
|
|
||||||
s.err = nil
|
|
||||||
}).sf()
|
|
||||||
}
|
|
||||||
|
|
||||||
func snapshot(a args, u updater) state {
|
|
||||||
|
|
||||||
var plan map[*zfs.DatasetPath]*snapProgress
|
|
||||||
u(func(snapper *Snapper) {
|
|
||||||
plan = snapper.plan
|
|
||||||
})
|
|
||||||
|
|
||||||
hookMatchCount := make(map[hooks.Hook]int, len(*a.hooks))
|
|
||||||
for _, h := range *a.hooks {
|
|
||||||
hookMatchCount[h] = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
anyFsHadErr := false
|
|
||||||
// TODO channel programs -> allow a little jitter?
|
|
||||||
for fs, progress := range plan {
|
|
||||||
suffix := time.Now().In(time.UTC).Format("20060102_150405_000")
|
|
||||||
snapname := fmt.Sprintf("%s%s", a.prefix, suffix)
|
|
||||||
|
|
||||||
ctx := logging.WithInjectedField(a.ctx, "fs", fs.ToString())
|
|
||||||
ctx = logging.WithInjectedField(ctx, "snap", snapname)
|
|
||||||
|
|
||||||
hookEnvExtra := hooks.Env{
|
|
||||||
hooks.EnvFS: fs.ToString(),
|
|
||||||
hooks.EnvSnapshot: snapname,
|
|
||||||
}
|
|
||||||
|
|
||||||
jobCallback := hooks.NewCallbackHookForFilesystem("snapshot", fs, func(ctx context.Context) (err error) {
|
|
||||||
l := getLogger(ctx)
|
|
||||||
l.Debug("create snapshot")
|
|
||||||
err = zfs.ZFSSnapshot(ctx, fs, snapname, false) // TODO propagate context to ZFSSnapshot
|
|
||||||
if err != nil {
|
|
||||||
l.WithError(err).Error("cannot create snapshot")
|
|
||||||
}
|
|
||||||
return
|
|
||||||
})
|
|
||||||
|
|
||||||
fsHadErr := false
|
|
||||||
var planReport hooks.PlanReport
|
|
||||||
var plan *hooks.Plan
|
|
||||||
{
|
|
||||||
filteredHooks, err := a.hooks.CopyFilteredForFilesystem(fs)
|
|
||||||
if err != nil {
|
|
||||||
getLogger(ctx).WithError(err).Error("unexpected filter error")
|
|
||||||
fsHadErr = true
|
|
||||||
goto updateFSState
|
|
||||||
}
|
|
||||||
// account for running hooks
|
|
||||||
for _, h := range filteredHooks {
|
|
||||||
hookMatchCount[h] = hookMatchCount[h] + 1
|
|
||||||
}
|
|
||||||
|
|
||||||
var planErr error
|
|
||||||
plan, planErr = hooks.NewPlan(&filteredHooks, hooks.PhaseSnapshot, jobCallback, hookEnvExtra)
|
|
||||||
if planErr != nil {
|
|
||||||
fsHadErr = true
|
|
||||||
getLogger(ctx).WithError(planErr).Error("cannot create job hook plan")
|
|
||||||
goto updateFSState
|
|
||||||
}
|
|
||||||
}
|
|
||||||
u(func(snapper *Snapper) {
|
|
||||||
progress.name = snapname
|
|
||||||
progress.startAt = time.Now()
|
|
||||||
progress.hookPlan = plan
|
|
||||||
progress.state = SnapStarted
|
|
||||||
})
|
|
||||||
{
|
|
||||||
getLogger(ctx).WithField("report", plan.Report().String()).Debug("begin run job plan")
|
|
||||||
plan.Run(ctx, a.dryRun)
|
|
||||||
planReport = plan.Report()
|
|
||||||
fsHadErr = planReport.HadError() // not just fatal errors
|
|
||||||
if fsHadErr {
|
|
||||||
getLogger(ctx).WithField("report", planReport.String()).Error("end run job plan with error")
|
|
||||||
} else {
|
|
||||||
getLogger(ctx).WithField("report", planReport.String()).Info("end run job plan successful")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateFSState:
|
|
||||||
anyFsHadErr = anyFsHadErr || fsHadErr
|
|
||||||
u(func(snapper *Snapper) {
|
|
||||||
progress.doneAt = time.Now()
|
|
||||||
progress.state = SnapDone
|
|
||||||
if fsHadErr {
|
|
||||||
progress.state = SnapError
|
|
||||||
}
|
|
||||||
progress.runResults = planReport
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case a.snapshotsTaken <- struct{}{}:
|
|
||||||
default:
|
default:
|
||||||
if a.snapshotsTaken != nil {
|
return nil, fmt.Errorf("unknown snapshotting type %T", v)
|
||||||
getLogger(a.ctx).Warn("callback channel is full, discarding snapshot update event")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for h, mc := range hookMatchCount {
|
|
||||||
if mc == 0 {
|
|
||||||
hookIdx := -1
|
|
||||||
for idx, ah := range *a.hooks {
|
|
||||||
if ah == h {
|
|
||||||
hookIdx = idx
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
getLogger(a.ctx).WithField("hook", h.String()).WithField("hook_number", hookIdx+1).Warn("hook did not match any snapshotted filesystems")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return u(func(snapper *Snapper) {
|
|
||||||
if anyFsHadErr {
|
|
||||||
snapper.state = ErrorWait
|
|
||||||
snapper.err = errors.New("one or more snapshots could not be created, check logs for details")
|
|
||||||
} else {
|
|
||||||
snapper.state = Waiting
|
|
||||||
snapper.err = nil
|
|
||||||
}
|
|
||||||
}).sf()
|
|
||||||
}
|
|
||||||
|
|
||||||
func wait(a args, u updater) state {
|
|
||||||
var sleepUntil time.Time
|
|
||||||
u(func(snapper *Snapper) {
|
|
||||||
lastTick := snapper.lastInvocation
|
|
||||||
snapper.sleepUntil = lastTick.Add(a.interval)
|
|
||||||
sleepUntil = snapper.sleepUntil
|
|
||||||
log := getLogger(a.ctx).WithField("sleep_until", sleepUntil).WithField("duration", a.interval)
|
|
||||||
logFunc := log.Debug
|
|
||||||
if snapper.state == ErrorWait || snapper.state == SyncUpErrWait {
|
|
||||||
logFunc = log.Error
|
|
||||||
}
|
|
||||||
logFunc("enter wait-state after error")
|
|
||||||
})
|
|
||||||
|
|
||||||
t := time.NewTimer(time.Until(sleepUntil))
|
|
||||||
defer t.Stop()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-t.C:
|
|
||||||
return u(func(snapper *Snapper) {
|
|
||||||
snapper.state = Planning
|
|
||||||
}).sf()
|
|
||||||
case <-a.ctx.Done():
|
|
||||||
return onMainCtxDone(a.ctx, u)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func listFSes(ctx context.Context, mf zfs.DatasetFilter) (fss []*zfs.DatasetPath, err error) {
|
|
||||||
return zfs.ZFSListMapping(ctx, mf)
|
|
||||||
}
|
|
||||||
|
|
||||||
var syncUpWarnNoSnapshotUntilSyncupMinDuration = envconst.Duration("ZREPL_SNAPPER_SYNCUP_WARN_MIN_DURATION", 1*time.Second)
|
|
||||||
|
|
||||||
// see docs/snapshotting.rst
|
|
||||||
func findSyncPoint(ctx context.Context, fss []*zfs.DatasetPath, prefix string, interval time.Duration) (syncPoint time.Time, err error) {
|
|
||||||
|
|
||||||
const (
|
|
||||||
prioHasVersions int = iota
|
|
||||||
prioNoVersions
|
|
||||||
)
|
|
||||||
|
|
||||||
type snapTime struct {
|
|
||||||
ds *zfs.DatasetPath
|
|
||||||
prio int // lower is higher
|
|
||||||
time time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(fss) == 0 {
|
|
||||||
return time.Now(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
snaptimes := make([]snapTime, 0, len(fss))
|
|
||||||
hardErrs := 0
|
|
||||||
|
|
||||||
now := time.Now()
|
|
||||||
|
|
||||||
getLogger(ctx).Debug("examine filesystem state to find sync point")
|
|
||||||
for _, d := range fss {
|
|
||||||
ctx := logging.WithInjectedField(ctx, "fs", d.ToString())
|
|
||||||
syncPoint, err := findSyncPointFSNextOptimalSnapshotTime(ctx, now, interval, prefix, d)
|
|
||||||
if err == findSyncPointFSNoFilesystemVersionsErr {
|
|
||||||
snaptimes = append(snaptimes, snapTime{
|
|
||||||
ds: d,
|
|
||||||
prio: prioNoVersions,
|
|
||||||
time: now,
|
|
||||||
})
|
|
||||||
} else if err != nil {
|
|
||||||
hardErrs++
|
|
||||||
getLogger(ctx).WithError(err).Error("cannot determine optimal sync point for this filesystem")
|
|
||||||
} else {
|
|
||||||
getLogger(ctx).WithField("syncPoint", syncPoint).Debug("found optimal sync point for this filesystem")
|
|
||||||
snaptimes = append(snaptimes, snapTime{
|
|
||||||
ds: d,
|
|
||||||
prio: prioHasVersions,
|
|
||||||
time: syncPoint,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if hardErrs == len(fss) {
|
|
||||||
return time.Time{}, fmt.Errorf("hard errors in determining sync point for every matching filesystem")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(snaptimes) == 0 {
|
|
||||||
panic("implementation error: loop must either inc hardErrs or add result to snaptimes")
|
|
||||||
}
|
|
||||||
|
|
||||||
// sort ascending by (prio,time)
|
|
||||||
// => those filesystems with versions win over those without any
|
|
||||||
sort.Slice(snaptimes, func(i, j int) bool {
|
|
||||||
if snaptimes[i].prio == snaptimes[j].prio {
|
|
||||||
return snaptimes[i].time.Before(snaptimes[j].time)
|
|
||||||
}
|
|
||||||
return snaptimes[i].prio < snaptimes[j].prio
|
|
||||||
})
|
|
||||||
|
|
||||||
winnerSyncPoint := snaptimes[0].time
|
|
||||||
l := getLogger(ctx).WithField("syncPoint", winnerSyncPoint.String())
|
|
||||||
l.Info("determined sync point")
|
|
||||||
if winnerSyncPoint.Sub(now) > syncUpWarnNoSnapshotUntilSyncupMinDuration {
|
|
||||||
for _, st := range snaptimes {
|
|
||||||
if st.prio == prioNoVersions {
|
|
||||||
l.WithField("fs", st.ds.ToString()).Warn("filesystem will not be snapshotted until sync point")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return snaptimes[0].time, nil
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
var findSyncPointFSNoFilesystemVersionsErr = fmt.Errorf("no filesystem versions")
|
|
||||||
|
|
||||||
func findSyncPointFSNextOptimalSnapshotTime(ctx context.Context, now time.Time, interval time.Duration, prefix string, d *zfs.DatasetPath) (time.Time, error) {
|
|
||||||
|
|
||||||
fsvs, err := zfs.ZFSListFilesystemVersions(ctx, d, zfs.ListFilesystemVersionsOptions{
|
|
||||||
Types: zfs.Snapshots,
|
|
||||||
ShortnamePrefix: prefix,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return time.Time{}, errors.Wrap(err, "list filesystem versions")
|
|
||||||
}
|
|
||||||
if len(fsvs) <= 0 {
|
|
||||||
return time.Time{}, findSyncPointFSNoFilesystemVersionsErr
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort versions by creation
|
|
||||||
sort.SliceStable(fsvs, func(i, j int) bool {
|
|
||||||
return fsvs[i].CreateTXG < fsvs[j].CreateTXG
|
|
||||||
})
|
|
||||||
|
|
||||||
latest := fsvs[len(fsvs)-1]
|
|
||||||
getLogger(ctx).WithField("creation", latest.Creation).Debug("found latest snapshot")
|
|
||||||
|
|
||||||
since := now.Sub(latest.Creation)
|
|
||||||
if since < 0 {
|
|
||||||
return time.Time{}, fmt.Errorf("snapshot %q is from the future: creation=%q now=%q", latest.ToAbsPath(d), latest.Creation, now)
|
|
||||||
}
|
|
||||||
|
|
||||||
return latest.Creation.Add(interval), nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
package snapper
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/config"
|
|
||||||
"github.com/zrepl/zrepl/zfs"
|
|
||||||
)
|
|
||||||
|
|
||||||
// FIXME: properly abstract snapshotting:
|
|
||||||
// - split up things that trigger snapshotting from the mechanism
|
|
||||||
// - timer-based trigger (periodic)
|
|
||||||
// - call from control socket (manual)
|
|
||||||
// - mixed modes?
|
|
||||||
// - support a `zrepl snapshot JOBNAME` subcommand for config.SnapshottingManual
|
|
||||||
type PeriodicOrManual struct {
|
|
||||||
s *Snapper
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *PeriodicOrManual) Run(ctx context.Context, wakeUpCommon chan<- struct{}) {
|
|
||||||
if s.s != nil {
|
|
||||||
s.s.Run(ctx, wakeUpCommon)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns nil if manual
|
|
||||||
func (s *PeriodicOrManual) Report() *Report {
|
|
||||||
if s.s != nil {
|
|
||||||
return s.s.Report()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func FromConfig(g *config.Global, fsf zfs.DatasetFilter, in config.SnapshottingEnum) (*PeriodicOrManual, error) {
|
|
||||||
switch v := in.Ret.(type) {
|
|
||||||
case *config.SnapshottingPeriodic:
|
|
||||||
snapper, err := PeriodicFromConfig(g, fsf, v)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &PeriodicOrManual{snapper}, nil
|
|
||||||
case *config.SnapshottingManual:
|
|
||||||
return &PeriodicOrManual{}, nil
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("unknown snapshotting type %T", v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
package snapper
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/daemon/hooks"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Report struct {
|
|
||||||
State State
|
|
||||||
// valid in state SyncUp and Waiting
|
|
||||||
SleepUntil time.Time
|
|
||||||
// valid in state Err
|
|
||||||
Error string
|
|
||||||
// valid in state Snapshotting
|
|
||||||
Progress []*ReportFilesystem
|
|
||||||
}
|
|
||||||
|
|
||||||
type ReportFilesystem struct {
|
|
||||||
Path string
|
|
||||||
State SnapState
|
|
||||||
|
|
||||||
// Valid in SnapStarted and later
|
|
||||||
SnapName string
|
|
||||||
StartAt time.Time
|
|
||||||
Hooks string
|
|
||||||
HooksHadError bool
|
|
||||||
|
|
||||||
// Valid in SnapDone | SnapError
|
|
||||||
DoneAt time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
func errOrEmptyString(e error) string {
|
|
||||||
if e != nil {
|
|
||||||
return e.Error()
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Snapper) Report() *Report {
|
|
||||||
s.mtx.Lock()
|
|
||||||
defer s.mtx.Unlock()
|
|
||||||
|
|
||||||
pReps := make([]*ReportFilesystem, 0, len(s.plan))
|
|
||||||
for fs, p := range s.plan {
|
|
||||||
var hooksStr string
|
|
||||||
var hooksHadError bool
|
|
||||||
if p.hookPlan != nil {
|
|
||||||
hr := p.hookPlan.Report()
|
|
||||||
// FIXME: technically this belongs into client
|
|
||||||
// but we can't serialize hooks.Step ATM
|
|
||||||
rightPad := func(str string, length int, pad string) string {
|
|
||||||
if len(str) > length {
|
|
||||||
return str[:length]
|
|
||||||
}
|
|
||||||
return str + strings.Repeat(pad, length-len(str))
|
|
||||||
}
|
|
||||||
hooksHadError = hr.HadError()
|
|
||||||
rows := make([][]string, len(hr))
|
|
||||||
const numCols = 4
|
|
||||||
lens := make([]int, numCols)
|
|
||||||
for i, e := range hr {
|
|
||||||
rows[i] = make([]string, numCols)
|
|
||||||
rows[i][0] = fmt.Sprintf("%d", i+1)
|
|
||||||
rows[i][1] = e.Status.String()
|
|
||||||
runTime := "..."
|
|
||||||
if e.Status != hooks.StepPending {
|
|
||||||
runTime = e.End.Sub(e.Begin).Round(time.Millisecond).String()
|
|
||||||
}
|
|
||||||
rows[i][2] = runTime
|
|
||||||
rows[i][3] = ""
|
|
||||||
if e.Report != nil {
|
|
||||||
rows[i][3] = e.Report.String()
|
|
||||||
}
|
|
||||||
for j, col := range lens {
|
|
||||||
if len(rows[i][j]) > col {
|
|
||||||
lens[j] = len(rows[i][j])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
rowsFlat := make([]string, len(hr))
|
|
||||||
for i, r := range rows {
|
|
||||||
colsPadded := make([]string, len(r))
|
|
||||||
for j, c := range r[:len(r)-1] {
|
|
||||||
colsPadded[j] = rightPad(c, lens[j], " ")
|
|
||||||
}
|
|
||||||
colsPadded[len(r)-1] = r[len(r)-1]
|
|
||||||
rowsFlat[i] = strings.Join(colsPadded, " ")
|
|
||||||
}
|
|
||||||
hooksStr = strings.Join(rowsFlat, "\n")
|
|
||||||
}
|
|
||||||
pReps = append(pReps, &ReportFilesystem{
|
|
||||||
Path: fs.ToString(),
|
|
||||||
State: p.state,
|
|
||||||
SnapName: p.name,
|
|
||||||
StartAt: p.startAt,
|
|
||||||
DoneAt: p.doneAt,
|
|
||||||
Hooks: hooksStr,
|
|
||||||
HooksHadError: hooksHadError,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Slice(pReps, func(i, j int) bool {
|
|
||||||
return strings.Compare(pReps[i].Path, pReps[j].Path) == -1
|
|
||||||
})
|
|
||||||
|
|
||||||
r := &Report{
|
|
||||||
State: s.state,
|
|
||||||
SleepUntil: s.sleepUntil,
|
|
||||||
Error: errOrEmptyString(s.err),
|
|
||||||
Progress: pReps,
|
|
||||||
}
|
|
||||||
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package snapper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/daemon/logging"
|
||||||
|
"github.com/zrepl/zrepl/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Logger = logger.Logger
|
||||||
|
|
||||||
|
func getLogger(ctx context.Context) Logger {
|
||||||
|
return logging.GetLogger(ctx, logging.SubsysSnapshot)
|
||||||
|
}
|
||||||
|
|
||||||
|
func errOrEmptyString(e error) string {
|
||||||
|
if e != nil {
|
||||||
|
return e.Error()
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
+1
-1
@@ -249,7 +249,7 @@
|
|||||||
"stack": true,
|
"stack": true,
|
||||||
"steppedLine": false,
|
"steppedLine": false,
|
||||||
"targets": [{
|
"targets": [{
|
||||||
"expr": "zrepl_version_daemon{job='$prom_job_name'}",
|
"expr": "sgn(zrepl_start_time{job='$prom_job_name'})",
|
||||||
"format": "time_series",
|
"format": "time_series",
|
||||||
"interval": "",
|
"interval": "",
|
||||||
"intervalFactor": 1,
|
"intervalFactor": 1,
|
||||||
|
|||||||
+66
-19
@@ -16,18 +16,72 @@ Changelog
|
|||||||
The changelog summarizes bugfixes that are deemed relevant for users and package maintainers.
|
The changelog summarizes bugfixes that are deemed relevant for users and package maintainers.
|
||||||
Developers should consult the git commit log or GitHub issue tracker.
|
Developers should consult the git commit log or GitHub issue tracker.
|
||||||
|
|
||||||
We use the following annotations for classifying changes:
|
0.6 (Unreleased)
|
||||||
|
----------------
|
||||||
|
|
||||||
|
* `Feature Wishlist on GitHub <https://github.com/zrepl/zrepl/discussions/547>`_
|
||||||
|
|
||||||
|
* |feature| :ref:`Schedule-based snapshotting<job-snapshotting--cron>` using ``cron`` syntax instead of an interval.
|
||||||
|
* |feature| Add ``ZREPL_DESTROY_MAX_BATCH_SIZE`` env var (default 0=unlimited).
|
||||||
|
* |bugfix| Fix resuming from interrupted replications that use ``send.raw`` on unencrypted datasets.
|
||||||
|
|
||||||
|
* The send options introduced in zrepl 0.4 allow users to specify additional zfs send flags for zrepl to use.
|
||||||
|
Before this fix, when setting ``send.raw=true`` on a job that replicates unencrypted datasets,
|
||||||
|
zrepl would not allow an interrupted replication to resume.
|
||||||
|
The reason were overly cautious checks to support the ``send.encrypted`` option.
|
||||||
|
* This bugfix removes these checks from the replication planner.
|
||||||
|
This makes ``send.encrypted`` a sender-side-only concern, much like all other ``send.*`` flags.
|
||||||
|
* However, this means that the ``zrepl status`` UI no longer indicates whether a replication step uses encrypted sends or not.
|
||||||
|
The setting is still effective though.
|
||||||
|
|
||||||
|
* |break| |feature| convert Prometheus metric ``zrepl_version_daemon`` to ``zrepl_start_time`` metric
|
||||||
|
|
||||||
|
* The metric still reports the zrepl version in a label.
|
||||||
|
But the metric *value* is now the Unix timestamp at the time the daemon was started.
|
||||||
|
The Grafana dashboard in :repomasterlink:`dist/grafana` has been updated.
|
||||||
|
|
||||||
|
* |bugfix| transient zrepl status error: ``Post "http://unix/status": EOF``
|
||||||
|
* |bugfix| don't treat receive-side bookmarks as a replication conflict.
|
||||||
|
This facilitates chaining of replication jobs. See :issue:`490`.
|
||||||
|
|
||||||
|
0.5
|
||||||
|
---
|
||||||
|
|
||||||
|
* |feature| :ref:`Bandwidth limiting <job-send-recv-options--bandwidth-limit>` (Thanks, Prominic.NET, Inc.)
|
||||||
|
* |feature| zrepl status: use a ``*`` to indicate which filesystem is currently replicating
|
||||||
|
* |feature| include daemon environment variables in zrepl status (currently only in ``--raw``)
|
||||||
|
* |bugfix| **fix encrypt-on-receive + placeholders use case** (:issue:`504`)
|
||||||
|
|
||||||
|
* Before this fix, **plain sends** to a receiver with an encrypted ``root_fs`` **could be received unencrypted** if zrepl needed to create placeholders on the receiver.
|
||||||
|
* Existing zrepl users should :ref:`read the docs <job-recv-options--placeholder>` and check ``zfs get -r encryption,zrepl:placeholder PATH_TO_ROOTFS`` on the receiver.
|
||||||
|
* Thanks to `@mologie <https://github.com/mologie>`_ and `@razielgn <https://github.com/razielgn>`_ for reporting and testing!
|
||||||
|
|
||||||
|
* |bugfix| Rename mis-spelled :ref:`send option <job-send-options>` ``embbeded_data`` to ``embedded_data``.
|
||||||
|
* |bugfix| zrepl status: replication step numbers should start at 1
|
||||||
|
* |bugfix| incorrect bandwidth averaging in ``zrepl status``.
|
||||||
|
* |bugfix| FreeBSD with OpenZFS 2.0: zrepl would wait indefinitely for zfs send to exit on timeouts.
|
||||||
|
* |bugfix| fix ``strconv.ParseInt: value out of range`` bug (and use the control RPCs).
|
||||||
|
* |docs| improve description of multiple pruning rules.
|
||||||
|
* |docs| document :ref:`platform tests <usage-platform-tests>`.
|
||||||
|
* |docs| quickstart: make users aware that prune rules apply to all snapshots.
|
||||||
|
* |maint| some platformtests were broken.
|
||||||
|
* |maint| FreeBSD: release armv7 and arm64 binaries.
|
||||||
|
* |maint| apt repo: update instructions due to ``apt-key`` deprecation.
|
||||||
|
|
||||||
|
Note to all users: please read up on the following OpenZFS bugs, as you might be affected:
|
||||||
|
|
||||||
|
* `ZFS send/recv with ashift 9->12 leads to data corruption <https://github.com/openzfs/zfs/issues/12762>`_.
|
||||||
|
* Various bugs with encrypted send/recv (`Leadership meeting notes <https://openzfs.topicbox.com/groups/developer/T24bdaa2886c6cbf5-Mc039a11c3f1507ea0664817b/december-openzfs-leadership-meeting>`_)
|
||||||
|
|
||||||
|
Finally, I'd like to point you to the `GitHub discussion <https://github.com/zrepl/zrepl/discussions/547>`_ about which bugfixes and features should be prioritized in zrepl 0.6 and beyond!
|
||||||
|
|
||||||
|
.. NOTE::
|
||||||
|
| zrepl is a spare-time project primarily developed by `Christian Schwarz <https://cschwarz.com>`_.
|
||||||
|
| You can support maintenance and feature development through one of the following services:
|
||||||
|
| |Donate via Patreon| |Donate via GitHub Sponsors| |Donate via Liberapay| |Donate via PayPal|
|
||||||
|
| Note that PayPal processing fees are relatively high for small donations.
|
||||||
|
| For SEPA wire transfer and **commercial support**, please `contact Christian directly <https://cschwarz.com>`_.
|
||||||
|
|
||||||
* |break_config| Change that breaks the config.
|
|
||||||
As a package maintainer, make sure to warn your users about config breakage somehow.
|
|
||||||
* |break| Change that breaks interoperability or persistent state representation with previous releases.
|
|
||||||
As a package maintainer, make sure to warn your users about config breakage somehow.
|
|
||||||
Note that even updating the package on both sides might not be sufficient, e.g. if persistent state needs to be migrated to a new format.
|
|
||||||
* |mig| Migration that must be run by the user.
|
|
||||||
* |feature| Change that introduces new functionality.
|
|
||||||
* |bugfix| Change that fixes a bug, no regressions or incompatibilities expected.
|
|
||||||
* |docs| Change to the documentation.
|
|
||||||
* |maint| Maintenance changes.
|
|
||||||
|
|
||||||
0.4.0
|
0.4.0
|
||||||
-----
|
-----
|
||||||
@@ -53,13 +107,6 @@ The following bugfix in 0.3.1 :issue:`caused problems for some users <400>`:
|
|||||||
|
|
||||||
* |bugfix| pruning: ``grid``: add all snapshots that do not match the regex to the rule's destroy list.
|
* |bugfix| pruning: ``grid``: add all snapshots that do not match the regex to the rule's destroy list.
|
||||||
|
|
||||||
.. NOTE::
|
|
||||||
| zrepl is a spare-time project primarily developed by `Christian Schwarz <https://cschwarz.com>`_.
|
|
||||||
| You can support maintenance and feature development through one of the following services:
|
|
||||||
| |Donate via Patreon| |Donate via GitHub Sponsors| |Donate via Liberapay| |Donate via PayPal|
|
|
||||||
| Note that PayPal processing fees are relatively high for small donations.
|
|
||||||
| For SEPA wire transfer and **commercial support**, please `contact Christian directly <https://cschwarz.com>`_.
|
|
||||||
|
|
||||||
0.3.1
|
0.3.1
|
||||||
-----
|
-----
|
||||||
|
|
||||||
@@ -243,7 +290,7 @@ Changes
|
|||||||
* |feature| Proper timeout handling for the :ref:`SSH transport <transport-ssh+stdinserver>`
|
* |feature| Proper timeout handling for the :ref:`SSH transport <transport-ssh+stdinserver>`
|
||||||
|
|
||||||
* |break| Requires Go 1.11 or later.
|
* |break| Requires Go 1.11 or later.
|
||||||
|
|
||||||
* |break| |break_config|: mappings are no longer supported
|
* |break| |break_config|: mappings are no longer supported
|
||||||
|
|
||||||
* Receiving sides (``pull`` and ``sink`` job) specify a single ``root_fs``.
|
* Receiving sides (``pull`` and ``sink`` job) specify a single ``root_fs``.
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ Configuration
|
|||||||
configuration/filter_syntax
|
configuration/filter_syntax
|
||||||
configuration/sendrecvoptions
|
configuration/sendrecvoptions
|
||||||
configuration/replication
|
configuration/replication
|
||||||
|
configuration/conflict_resolution
|
||||||
configuration/snapshotting
|
configuration/snapshotting
|
||||||
configuration/prune
|
configuration/prune
|
||||||
configuration/logging
|
configuration/logging
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
.. include:: ../global.rst.inc
|
||||||
|
|
||||||
|
|
||||||
|
Conflict Resolution Options
|
||||||
|
===========================
|
||||||
|
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
- type: push
|
||||||
|
filesystems: ...
|
||||||
|
conflict_resolution:
|
||||||
|
initial_replication: most_recent | all | fail # default: most_recent
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
.. _conflict_resolution-initial_replication-option-send_all_snapshots:
|
||||||
|
|
||||||
|
|
||||||
|
``initial_replication`` option
|
||||||
|
------------------------------
|
||||||
|
|
||||||
|
The ``initial_replication`` option determines how many snapshots zrepl replicates if the filesystem has not been replicated before.
|
||||||
|
If ``most_recent`` (the default), the initial replication will only transfer the most recent snapshot, while ignoring previous snapshots.
|
||||||
|
If all snapshots should be replicated, specify ``all``.
|
||||||
|
Use ``fail`` to make replication of the filesystem fail in case there is no corresponding fileystem on the receiver.
|
||||||
|
|
||||||
|
For example, suppose there are snapshosts ``tank@1``, ``tank@2``, ``tank@3`` on a sender.
|
||||||
|
Then ``most_recent`` will replicate just ``@3``, but ``all`` will replicate ``@1``, ``@2``, and ``@3``.
|
||||||
|
|
||||||
|
If initial replication is interrupted, and there is at least one (maybe partial) snapshot on the receiver, zrepl will always resume in **incremental mode**.
|
||||||
|
And that is regardless of where the initial replication was interrupted.
|
||||||
|
|
||||||
|
For example, if ``initial_replication: all`` and the transfer of ``@1`` is interrupted, zrepl would retry/resume at ``@1``.
|
||||||
|
And even if the user changes the config to ``initial_replication: most_recent`` before resuming, **incremental mode** will still resume at ``@1``.
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
.. _miscellaneous:
|
||||||
|
|
||||||
Miscellaneous
|
Miscellaneous
|
||||||
=============
|
=============
|
||||||
|
|
||||||
|
|||||||
@@ -3,16 +3,17 @@ Overview & Terminology
|
|||||||
======================
|
======================
|
||||||
|
|
||||||
All work zrepl does is performed by the zrepl daemon which is configured in a single YAML configuration file loaded on startup.
|
All work zrepl does is performed by the zrepl daemon which is configured in a single YAML configuration file loaded on startup.
|
||||||
The following paths are considered:
|
The following paths are searched, in this order:
|
||||||
|
|
||||||
* If set, the location specified via the global ``--config`` flag
|
1. The path specified via the global ``--config`` flag
|
||||||
* ``/etc/zrepl/zrepl.yml``
|
2. ``/etc/zrepl/zrepl.yml``
|
||||||
* ``/usr/local/etc/zrepl/zrepl.yml``
|
3. ``/usr/local/etc/zrepl/zrepl.yml``
|
||||||
|
|
||||||
The ``zrepl configcheck`` subcommand can be used to validate the configuration.
|
``zrepl configcheck`` can be used to validate the configuration.
|
||||||
The command will output nothing and exit with zero status code if the configuration is valid.
|
If the configuration is valid, it will output nothing and exit with code ``0``.
|
||||||
The error messages vary in quality and usefulness: please report confusing config errors to the tracking :issue:`155`.
|
The error messages vary in quality and usefulness: please report confusing config errors to the tracking :issue:`155`.
|
||||||
Full example configs such as in the :ref:`quick-start guides <quickstart-toc>` or the :sampleconf:`/` directory might also be helpful.
|
|
||||||
|
Full example configs are available at :ref:`quick-start guides <quickstart-toc>` and :sampleconf:`/`.
|
||||||
However, copy-pasting examples is no substitute for reading documentation!
|
However, copy-pasting examples is no substitute for reading documentation!
|
||||||
|
|
||||||
Config File Structure
|
Config File Structure
|
||||||
@@ -26,9 +27,8 @@ Config File Structure
|
|||||||
type: push
|
type: push
|
||||||
- ...
|
- ...
|
||||||
|
|
||||||
zrepl is configured using a single YAML configuration file with two main sections: ``global`` and ``jobs``.
|
A zrepl configuration file is divided in to two main sections: ``global`` and ``jobs``.
|
||||||
The ``global`` section is filled with sensible defaults and is covered later in this chapter.
|
``global`` has sensible defaults. It is covered in :ref:`logging <logging>`, :ref:`monitoring <monitoring>` \& :ref:`miscellaneous <miscellaneous>`.
|
||||||
The ``jobs`` section is a list of jobs which we are going to explain now.
|
|
||||||
|
|
||||||
.. _job-overview:
|
.. _job-overview:
|
||||||
|
|
||||||
@@ -42,8 +42,7 @@ Jobs are identified by their ``name``, both in log files and the ``zrepl status`
|
|||||||
.. NOTE::
|
.. NOTE::
|
||||||
The job name is persisted in several places on disk and thus :issue:`cannot be changed easily<327>`.
|
The job name is persisted in several places on disk and thus :issue:`cannot be changed easily<327>`.
|
||||||
|
|
||||||
|
Replication always happens between a pair of jobs: one **active side** and one **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 permissions.
|
The passive side responds to requests from the active side after checking its permissions.
|
||||||
|
|
||||||
@@ -72,30 +71,29 @@ How the Active Side Works
|
|||||||
|
|
||||||
The active side (:ref:`push <job-push>` and :ref:`pull <job-pull>` job) executes the replication and pruning logic:
|
The active side (:ref:`push <job-push>` and :ref:`pull <job-pull>` job) executes the replication and pruning logic:
|
||||||
|
|
||||||
* Wakeup because of finished snapshotting (``push`` job) or pull interval ticker (``pull`` job).
|
1. Wakeup after snapshotting (``push`` job) or pull interval ticker (``pull`` job).
|
||||||
* Connect to the corresponding passive side using a :ref:`transport <transport>` and instantiate an RPC client.
|
2. Connect to the passive side and instantiate an RPC client.
|
||||||
* Replicate data from the sending to the receiving side (see below).
|
3. Replicate data from the sender to the receiver.
|
||||||
* Prune on sender & receiver.
|
4. Prune on sender & receiver.
|
||||||
|
|
||||||
.. TIP::
|
.. TIP::
|
||||||
The progress of the active side can be watched live using the ``zrepl status`` subcommand.
|
The progress of the active side can be watched live using ``zrepl status``.
|
||||||
|
|
||||||
.. _overview-passive-side--client-identity:
|
.. _overview-passive-side--client-identity:
|
||||||
|
|
||||||
How the Passive Side Works
|
How the Passive Side Works
|
||||||
--------------------------
|
--------------------------
|
||||||
|
|
||||||
The passive side (:ref:`sink <job-sink>` and :ref:`source <job-source>`) waits for connections from the corresponding active side,
|
The passive side (:ref:`sink <job-sink>` and :ref:`source <job-source>`) waits for connections from the active side,
|
||||||
using the transport listener type specified in the ``serve`` field of the job configuration.
|
on the :ref:`transport <transport>` specified with ``serve`` in the job configuration.
|
||||||
When a client connects, the transport listener performS listener-specific access control (cert validation, IP ACLs, etc)
|
The respective transport then perfoms authentication & authorization, resulting in a stable *client identity*.
|
||||||
and determines the *client identity*.
|
The passive side job uses this *client identity* as follows:
|
||||||
The passive side job then uses this client identity as follows:
|
|
||||||
|
|
||||||
* The ``sink`` job maps requests from different client identities to their respective sub-filesystem tree ``root_fs/${client_identity}``.
|
* In ``sink`` jobs, to map requests from different *client identities* to their respective sub-filesystem tree ``root_fs/${client_identity}``.
|
||||||
* The ``source`` might, in the future, embed the client identity in :ref:`zrepl's ZFS abstraction names <zrepl-zfs-abstractions>` in order to support multi-host replication.
|
* *In the future, ``source`` might embed the client identity in :ref:`zrepl's ZFS abstraction names <zrepl-zfs-abstractions>`, to support multi-host replication.*
|
||||||
|
|
||||||
.. TIP::
|
.. TIP::
|
||||||
The implementation of the ``sink`` job requires that the connecting client identities be a valid ZFS filesystem name components.
|
The use of the client identity in the ``sink`` job implies that it must be usable as a ZFS ZFS filesystem name component.
|
||||||
|
|
||||||
.. _overview-how-replication-works:
|
.. _overview-how-replication-works:
|
||||||
|
|
||||||
@@ -106,7 +104,7 @@ One of the major design goals of the replication module is to avoid any duplicat
|
|||||||
As such, the code works on abstract senders and receiver **endpoints**, where typically one will be implemented by a local program object and the other is an RPC client instance.
|
As such, the code works on abstract senders and receiver **endpoints**, where typically one will be implemented by a local program object and the other is an RPC client instance.
|
||||||
Regardless of push- or pull-style setup, the logic executes on the active side, i.e. in the ``push`` or ``pull`` job.
|
Regardless of push- or pull-style setup, the logic executes on the active side, i.e. in the ``push`` or ``pull`` job.
|
||||||
|
|
||||||
The following high-level steps take place during replication and can be monitored using the ``zrepl status`` subcommand:
|
The following high-level steps take place during replication and can be monitored using ``zrepl status``:
|
||||||
|
|
||||||
* Plan the replication:
|
* Plan the replication:
|
||||||
|
|
||||||
@@ -233,22 +231,11 @@ The ``zrepl zfs-abstraction list`` command provides a listing of all bookmarks a
|
|||||||
|
|
||||||
More details can be found in the design document :repomasterlink:`replication/design.md`.
|
More details can be found in the design document :repomasterlink:`replication/design.md`.
|
||||||
|
|
||||||
Limitations
|
|
||||||
^^^^^^^^^^^
|
|
||||||
|
|
||||||
.. ATTENTION::
|
Caveats With Complex Setups (More Than 2 Jobs or Machines)
|
||||||
|
----------------------------------------------------------
|
||||||
|
|
||||||
Currently, zrepl does not replicate filesystem properties.
|
Most users are served well with a single sender and a single receiver job.
|
||||||
When receiving a filesystem, it is never mounted (`-u` flag) and `mountpoint=none` is set.
|
|
||||||
This is temporary and being worked on :issue:`24`.
|
|
||||||
|
|
||||||
|
|
||||||
.. _jobs-multiple-jobs:
|
|
||||||
|
|
||||||
Multiple Jobs & More than 2 Machines
|
|
||||||
------------------------------------
|
|
||||||
|
|
||||||
The quick-start guides focus on simple setups with a single sender and a single receiver.
|
|
||||||
This section documents considerations for more complex setups.
|
This section documents considerations for more complex setups.
|
||||||
|
|
||||||
.. ATTENTION::
|
.. ATTENTION::
|
||||||
@@ -262,7 +249,7 @@ This section documents considerations for more complex setups.
|
|||||||
|
|
||||||
If you can't find your desired configuration, have questions or would like to see improvements to multi-job setups, please `open an issue on GitHub <https://github.com/zrepl/zrepl/issues/new>`_.
|
If you can't find your desired configuration, have questions or would like to see improvements to multi-job setups, please `open an issue on GitHub <https://github.com/zrepl/zrepl/issues/new>`_.
|
||||||
|
|
||||||
Multiple Jobs on one Machine
|
Multiple Jobs on One Machine
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
As a general rule, multiple jobs configured on one machine **must operate on disjoint sets of filesystems**.
|
As a general rule, multiple jobs configured on one machine **must operate on disjoint sets of filesystems**.
|
||||||
Otherwise, concurrently running jobs might interfere when operating on the same filesystem.
|
Otherwise, concurrently running jobs might interfere when operating on the same filesystem.
|
||||||
@@ -281,18 +268,53 @@ On your setup, ensure that
|
|||||||
This scenario is detailed in one of the :ref:`quick-start guides <quickstart-backup-to-external-disk>`.
|
This scenario is detailed in one of the :ref:`quick-start guides <quickstart-backup-to-external-disk>`.
|
||||||
|
|
||||||
|
|
||||||
More Than 2 Machines
|
Two Or More Machines
|
||||||
^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
This section might be relevant to users who wish to *fan-in* (N machines replicate to 1) or *fan-out* (replicate 1 machine to N machines).
|
This section might be relevant to users who wish to *fan-in* (N machines replicate to 1) or *fan-out* (replicate 1 machine to N machines).
|
||||||
|
|
||||||
**Working setups**:
|
**Working setups**:
|
||||||
|
|
||||||
* N ``push`` identities, 1 ``sink`` (as long as the different push jobs have a different :ref:`client identity <overview-passive-side--client-identity>`)
|
* **Fan-in: N servers replicated to one receiver, disjoint dataset trees.**
|
||||||
|
|
||||||
* ``sink`` constrains each client to a disjoint sub-tree of the sink-side dataset hierarchy ``${root_fs}/${client_identity}``.
|
* This is the common use case of a centralized backup server.
|
||||||
|
|
||||||
|
* Implementation:
|
||||||
|
|
||||||
|
* N ``push`` jobs (one per sender server), 1 ``sink`` (as long as the different push jobs have a different :ref:`client identity <overview-passive-side--client-identity>`)
|
||||||
|
* N ``source`` jobs (one per sender server), N ``pull`` on the receiver server (unique names, disjoint ``root_fs``)
|
||||||
|
|
||||||
|
* The ``sink`` job automatically constrains each client to a disjoint sub-tree of the sink-side dataset hierarchy ``${root_fs}/${client_identity}``.
|
||||||
Therefore, the different clients cannot interfere.
|
Therefore, the different clients cannot interfere.
|
||||||
|
|
||||||
|
* The ``pull`` job only pulls from one host, so it's up to the zrepl user to ensure that the different ``pull`` jobs don't interfere.
|
||||||
|
|
||||||
|
.. _fan-out-replication:
|
||||||
|
|
||||||
|
* **Fan-out: 1 server replicated to N receivers**
|
||||||
|
|
||||||
|
* Can be implemented either in a pull or push fashion.
|
||||||
|
|
||||||
|
* **pull setup**: 1 ``pull`` job on each receiver server, each with a corresponding **unique** ``source`` job on the sender server.
|
||||||
|
* **push setup**: 1 ``sink`` job on each receiver server, each with a corresponding **unique** ``push`` job on the sender server.
|
||||||
|
|
||||||
|
* It is critical that we have one sending-side job (``source``, ``push``) per receiver.
|
||||||
|
The reason is that :ref:`zrepl's ZFS abstractions <zrepl-zfs-abstractions>` (``zrepl zfs-abstraction list``) include the name of the ``source``/``push`` job, but not the receive-side job name or client identity (see :issue:`380`).
|
||||||
|
As a counter-example, suppose we used multiple ``pull`` jobs with only one ``source`` job.
|
||||||
|
All ``pull`` jobs would share the same :ref:`replication cursor bookmark <replication-cursor-and-last-received-hold>` and trip over each other, breaking incremental replication guarantees quickly.
|
||||||
|
The anlogous problem exists for 1 ``push`` to N ``sink`` jobs.
|
||||||
|
|
||||||
|
* The ``filesystems`` matched by the sending side jobs (``source``, ``push``) need not necessarily be disjoint.
|
||||||
|
For this to work, we need to avoid interference between snapshotting and pruning of the different sending jobs.
|
||||||
|
The solution is to centralize sender-side snapshot management in a separate ``snap`` job.
|
||||||
|
Snapshotting in the ``source``/``push`` job should then be disabled (``type: manual``).
|
||||||
|
And sender-side pruning (``keep_sender``) needs to be disabled in the active side (``pull`` / ``push``), since that'll be done by the ``snap job``.
|
||||||
|
|
||||||
|
* **Restore limitations**: when restoring from one of the ``pull`` targets (e.g., using ``zfs send -R``), the replication cursor bookmarks don't exist on the restored system.
|
||||||
|
This can break incremental replication to all other receive-sides after restore.
|
||||||
|
|
||||||
|
* See :ref:`the fan-out replication quick-start guide <quickstart-fan-out-replication>` for an example of this setup.
|
||||||
|
|
||||||
|
|
||||||
**Setups that do not work**:
|
**Setups that do not work**:
|
||||||
|
|
||||||
|
|||||||
@@ -67,8 +67,9 @@ Policy ``not_replicated``
|
|||||||
...
|
...
|
||||||
|
|
||||||
``not_replicated`` keeps all snapshots that have not been replicated to the receiving side.
|
``not_replicated`` keeps all snapshots that have not been replicated to the receiving side.
|
||||||
It only makes sense to specify this rule on a sender (source or push job).
|
It only makes sense to specify this rule for the ``keep_sender``.
|
||||||
The state required to evaluate this rule is stored in the :ref:`replication cursor bookmark <replication-cursor-and-last-received-hold>` on the sending side.
|
The reason is that, by definition, all snapshots on the receiver have already been replicated to there from the sender.
|
||||||
|
To determine whether a sender-side snapshot has already been replicated, zrepl uses the :ref:`replication cursor bookmark <replication-cursor-and-last-received-hold>` which corresponds to the most recent successfully replicated snapshot.
|
||||||
|
|
||||||
.. _prune-keep-retention-grid:
|
.. _prune-keep-retention-grid:
|
||||||
|
|
||||||
@@ -106,7 +107,7 @@ The following procedure happens during pruning:
|
|||||||
#. All subsequent buckets are placed adjacent to their predecessor bucket.
|
#. All subsequent buckets are placed adjacent to their predecessor bucket.
|
||||||
#. Now each snapshot on the axis either falls into one bucket or it is older than our rightmost bucket.
|
#. Now each snapshot on the axis either falls into one bucket or it is older than our rightmost bucket.
|
||||||
Buckets are left-inclusive and right-exclusive which means that a snapshot on the edge of bucket will always 'fall into the right one'.
|
Buckets are left-inclusive and right-exclusive which means that a snapshot on the edge of bucket will always 'fall into the right one'.
|
||||||
#. Snapshots older than the rightmost bucket **not kept** by this gridspec.
|
#. Snapshots older than the rightmost bucket are **not kept** by the grid specification.
|
||||||
#. For each bucket, we only keep the ``keep`` oldest snapshots.
|
#. For each bucket, we only keep the ``keep`` oldest snapshots.
|
||||||
|
|
||||||
The syntax to describe the bucket list is as follows:
|
The syntax to describe the bucket list is as follows:
|
||||||
@@ -124,43 +125,51 @@ The syntax to describe the bucket list is as follows:
|
|||||||
|
|
||||||
::
|
::
|
||||||
|
|
||||||
This grid spec produces the following list of adjacent buckets. For the sake of simplicity,
|
Assume the following grid specification:
|
||||||
we subject all snapshots to the grid pruning policy by settings `regex: .*`.
|
|
||||||
|
|
||||||
`
|
grid: 1x1h(keep=all) | 2x2h | 1x3h
|
||||||
grid: 1x1h(keep=all) | 2x2h | 1x3h
|
|
||||||
regex: .*
|
|
||||||
`
|
|
||||||
|
|
||||||
0h 1h 2h 3h 4h 5h 6h 7h 8h 9h
|
This grid specification produces the following constellation of buckets:
|
||||||
| | | | | | | | | |
|
|
||||||
|-Bucket1-|-----Bucket 2------|------Bucket 3-----|-----------Bucket 4----------|
|
0h 1h 2h 3h 4h 5h 6h 7h 8h 9h
|
||||||
| keep=all| keep=1 | keep=1 | keep=1 |
|
| | | | | | | | | |
|
||||||
|
|-Bucket1-|-----Bucket2-------|------Bucket3------|-----------Bucket4-----------|
|
||||||
|
| keep=all| keep=1 | keep=1 | keep=1 |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Let us consider the following set of snapshots @a-zA-C:
|
Now assume that we have a set of snapshots @a, @b, ..., @D.
|
||||||
|
Snapshot @a is the most recent snapshot.
|
||||||
|
Snapshot @D is the oldest snapshot, it is almost 9 hours older than snapshot @a.
|
||||||
|
We place the snapshots on the same timeline as the buckets:
|
||||||
|
|
||||||
|
|
||||||
| a b c d e f g h i j k l m n o p q r s t u v w x y z A B C D |
|
0h 1h 2h 3h 4h 5h 6h 7h 8h 9h
|
||||||
|
| | | | | | | | | |
|
||||||
|
|-Bucket1-|-----Bucket2-------|------Bucket3------|-----------Bucket4-----------|
|
||||||
|
| keep=all| keep=1 | keep=1 | keep=1 |
|
||||||
|
| | | | |
|
||||||
|
| a b c | d e f g h i j k l m n o p |q r s t u v w x y z |A B C D
|
||||||
|
|
||||||
The `grid` algorithm maps them to their respective buckets:
|
We obtain the following mapping of snapshots to buckets:
|
||||||
|
|
||||||
Bucket 1: a, b, c
|
Bucket1: a,b,c
|
||||||
Bucket 2: d,e,f,g,h,i,j
|
Bucket2: d,e,f,g,h,i
|
||||||
Bucket 3: k,l,m,n,o,p
|
Bucket3: j,k,l,m,n,o,p
|
||||||
Bucket 4: q,r, q,r,s,t,u,v,w,x,y,z
|
Bucket4: q,r,s,t,u,v,w,x,y,z
|
||||||
None: A,B,C,D
|
No bucket: A,B,C,D
|
||||||
|
|
||||||
It then applies the per-bucket pruning logic described above which resulting in the
|
For each bucket, we now prune snapshots until it only contains `keep` snapshots.
|
||||||
following list of remaining snapshots.
|
Newer snapshots are destroyed first.
|
||||||
|
Snapshots that do not fall into a bucket are always destroyed.
|
||||||
|
|
||||||
| a b c j p z |
|
Result after pruning:
|
||||||
|
|
||||||
Note that it only makes sense to grow (not shorten) the interval duration for buckets
|
|
||||||
further in the past since each bucket acts like a low-pass filter for incoming snapshots
|
|
||||||
and adding a less-low-pass-filter after a low-pass one has no effect.
|
|
||||||
|
|
||||||
|
0h 1h 2h 3h 4h 5h 6h 7h 8h 9h
|
||||||
|
| | | | | | | | | |
|
||||||
|
|-Bucket1-|-----Bucket2-------|------Bucket3------|-----------Bucket4-----------|
|
||||||
|
| | | | |
|
||||||
|
| a b c | i | p | z |
|
||||||
|
|
||||||
.. _prune-keep-last-n:
|
.. _prune-keep-last-n:
|
||||||
|
|
||||||
|
|||||||
@@ -36,9 +36,12 @@ See the `upstream man page <https://openzfs.github.io/openzfs-docs/man/8/zfs-sen
|
|||||||
* - ``encrypted``
|
* - ``encrypted``
|
||||||
-
|
-
|
||||||
- Specific to zrepl, :ref:`see below <job-send-options-encrypted>`.
|
- Specific to zrepl, :ref:`see below <job-send-options-encrypted>`.
|
||||||
|
* - ``bandwidth_limit``
|
||||||
|
-
|
||||||
|
- Specific to zrepl, :ref:`see below <job-send-recv-options--bandwidth-limit>`.
|
||||||
* - ``raw``
|
* - ``raw``
|
||||||
- ``-w``
|
- ``-w``
|
||||||
- Use ``encrypted`` to only allow encrypted sends.
|
- Use ``encrypted`` to only allow encrypted sends. Mixed sends are not supported.
|
||||||
* - ``send_properties``
|
* - ``send_properties``
|
||||||
- ``-p``
|
- ``-p``
|
||||||
- **Be careful**, read the :ref:`note on property replication below <job-note-property-replication>`.
|
- **Be careful**, read the :ref:`note on property replication below <job-note-property-replication>`.
|
||||||
@@ -51,7 +54,7 @@ See the `upstream man page <https://openzfs.github.io/openzfs-docs/man/8/zfs-sen
|
|||||||
* - ``compressed``
|
* - ``compressed``
|
||||||
- ``-c``
|
- ``-c``
|
||||||
-
|
-
|
||||||
* - ``embbeded_data``
|
* - ``embedded_data``
|
||||||
- ``-e``
|
- ``-e``
|
||||||
-
|
-
|
||||||
* - ``saved``
|
* - ``saved``
|
||||||
@@ -138,8 +141,16 @@ Recv Options
|
|||||||
override: {
|
override: {
|
||||||
"org.openzfs.systemd:ignore": "on"
|
"org.openzfs.systemd:ignore": "on"
|
||||||
}
|
}
|
||||||
|
bandwidth_limit: ...
|
||||||
|
placeholder:
|
||||||
|
encryption: unspecified | off | inherit
|
||||||
...
|
...
|
||||||
|
|
||||||
|
Jump to
|
||||||
|
:ref:`properties <job-recv-options--inherit-and-override>` ,
|
||||||
|
:ref:`bandwidth_limit <job-send-recv-options--bandwidth-limit>` , and
|
||||||
|
:ref:`placeholder <job-recv-options--placeholder>`.
|
||||||
|
|
||||||
.. _job-recv-options--inherit-and-override:
|
.. _job-recv-options--inherit-and-override:
|
||||||
|
|
||||||
``properties``
|
``properties``
|
||||||
@@ -212,3 +223,60 @@ and property replication is enabled, the receiver must :ref:`inherit the followi
|
|||||||
* ``keylocation``
|
* ``keylocation``
|
||||||
* ``keyformat``
|
* ``keyformat``
|
||||||
* ``encryption``
|
* ``encryption``
|
||||||
|
|
||||||
|
.. _job-recv-options--placeholder:
|
||||||
|
|
||||||
|
Placeholders
|
||||||
|
~~~~~~~~~~~~
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
placeholder:
|
||||||
|
encryption: unspecified | off | inherit
|
||||||
|
|
||||||
|
During replication, zrepl :ref:`creates placeholder datasets <replication-placeholder-property>` on the receiving side if the sending side's ``filesystems`` filter creates gaps in the dataset hierarchy.
|
||||||
|
This is generally fully transparent to the user.
|
||||||
|
However, with OpenZFS Native Encryption, placeholders require zrepl user attention.
|
||||||
|
Specifically, the problem is that, when zrepl attempts to create the placeholder dataset on the receiver, and that placeholder's parent dataset is encrypted, ZFS wants to inherit encryption to the placeholder.
|
||||||
|
This is relevant to two use cases that zrepl supports:
|
||||||
|
|
||||||
|
1. **encrypted-send-to-untrusted-receiver** In this use case, the sender sends an :ref:`encrypted send stream <job-send-options-encrypted>` and the receiver doesn't have the key loaded.
|
||||||
|
2. **send-plain-encrypt-on-receive** The receive-side ``root_fs`` dataset is encrypted, and the senders are unencrypted.
|
||||||
|
The key of ``root_fs`` is loaded, and the goal is that the plain sends (e.g., from production) are encrypted on-the-fly during receive, with ``root_fs``'s key.
|
||||||
|
|
||||||
|
For **encrypted-send-to-untrusted-receiver**, the placeholder datasets need to be created with ``-o encryption=off``.
|
||||||
|
Without it, creation would fail with an error, indicating that the placeholder's parent dataset's key needs to be loaded.
|
||||||
|
But we don't trust the receiver, so we can't expect that to ever happen.
|
||||||
|
|
||||||
|
However, for **send-plain-encrypt-on-receive**, we cannot set ``-o encryption=off``.
|
||||||
|
The reason is that if we did, any of the (non-placeholder) child datasets below the placeholder would inherit ``encryption=off``, thereby silently breaking our encrypt-on-receive use case.
|
||||||
|
So, to cover this use case, we need to create placeholders without specifying ``-o encryption``.
|
||||||
|
This will make ``zfs create`` inherit the encryption mode from the parent dataset, and thereby transitively from ``root_fs``.
|
||||||
|
|
||||||
|
The zrepl config provides the `recv.placeholder.encryption` knob to control this behavior.
|
||||||
|
In ``undefined`` mode (default), placeholder creation bails out and asks the user to configure a behavior.
|
||||||
|
In ``off`` mode, the placeholder is created with ``encryption=off``, i.e., **encrypted-send-to-untrusted-rceiver** use case.
|
||||||
|
In ``inherit`` mode, the placeholder is created without specifying ``-o encryption`` at all, i.e., the **send-plain-encrypt-on-receive** use case.
|
||||||
|
|
||||||
|
|
||||||
|
Common Options
|
||||||
|
~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. _job-send-recv-options--bandwidth-limit:
|
||||||
|
|
||||||
|
Bandwidth Limit (send & recv)
|
||||||
|
-----------------------------
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
bandwidth_limit:
|
||||||
|
max: 23.5 MiB # -1 is the default and disabled rate limiting
|
||||||
|
bucket_capacity: # token bucket capacity in bytes; defaults to 128KiB
|
||||||
|
|
||||||
|
Both ``send`` and ``recv`` can be limited to a maximum bandwidth through ``bandwidth_limit``.
|
||||||
|
For most users, it should be sufficient to just set ``bandwidth_limit.max``.
|
||||||
|
The ``bandwidth_limit.bucket_capacity`` refers to the `token bucket size <https://github.com/juju/ratelimit>`_.
|
||||||
|
|
||||||
|
The bandwidth limit only applies to the payload data, i.e., the ZFS send stream.
|
||||||
|
It does not account for transport protocol overheads.
|
||||||
|
The scope is the job level, i.e., all :ref:`concurrent <replication-option-concurrency>` sends or incoming receives of a job share the bandwidth limit.
|
||||||
|
|||||||
@@ -5,53 +5,117 @@
|
|||||||
Taking Snaphots
|
Taking Snaphots
|
||||||
===============
|
===============
|
||||||
|
|
||||||
The ``push``, ``source`` and ``snap`` jobs can automatically take periodic snapshots of the filesystems matched by the ``filesystems`` filter field.
|
You can configure zrepl to take snapshots of the filesystems in the ``filesystems`` field specified in ``push``, ``source`` and ``snap`` jobs.
|
||||||
The snapshot names are composed of a user-defined prefix followed by a UTC date formatted like ``20060102_150405_000``.
|
|
||||||
We use UTC because it will avoid name conflicts when switching time zones or between summer and winter time.
|
|
||||||
|
|
||||||
When a job is started, the snapshotter attempts to get the snapshotting rhythms of the matched ``filesystems`` in sync because snapshotting all filesystems at the same time results in a more consistent backup.
|
The following snapshotting types are supported:
|
||||||
To find that sync point, the most recent snapshot, made by the snapshotter, in any of the matched ``filesystems`` is used.
|
|
||||||
A filesystem that does not have snapshots by the snapshotter has lower priority than filesystem that do, and thus might not be snapshotted (and replicated) until it is snapshotted at the next sync point.
|
|
||||||
|
|
||||||
For ``push`` jobs, replication is automatically triggered after all filesystems have been snapshotted.
|
|
||||||
|
|
||||||
Note that the ``zrepl signal wakeup JOB`` subcommand does not trigger snapshotting.
|
.. list-table::
|
||||||
|
:widths: 20 70
|
||||||
|
:header-rows: 1
|
||||||
|
|
||||||
|
* - ``snapshotting.type``
|
||||||
|
- Comment
|
||||||
|
* - ``periodic``
|
||||||
|
- Ensure that snapshots are taken at a particular interval.
|
||||||
|
* - ``cron``
|
||||||
|
- Use cron spec to take snapshots at particular points in time.
|
||||||
|
* - ``manual``
|
||||||
|
- zrepl does not take any snapshots by itself.
|
||||||
|
|
||||||
|
The ``periodic`` and ``cron`` snapshotting types share some common options and behavior:
|
||||||
|
|
||||||
|
* **Naming:** The snapshot names are composed of a user-defined ``prefix`` followed by a UTC date formatted like ``20060102_150405_000``.
|
||||||
|
We use UTC because it will avoid name conflicts when switching time zones or between summer and winter time.
|
||||||
|
* **Hooks:** You can configure hooks to run before or after zrepl takes the snapshots. See :ref:`below <job-snapshotting-hooks>` for details.
|
||||||
|
* **Push replication:** After creating all snapshots, the snapshotter will wake up the replication part of the job, if it's a ``push`` job.
|
||||||
|
Note that snapshotting is decoupled from replication, i.e., if it is down or takes too long, snapshots will still be taken.
|
||||||
|
Note further that other jobs are not woken up by snapshotting.
|
||||||
|
|
||||||
|
.. NOTE::
|
||||||
|
|
||||||
|
There is **no concept of ownership** of the snapshots that are created by ``periodic`` or ``cron``.
|
||||||
|
Thus, there is no distinction between zrepl-created snapshots and user-created snapshots during replication or pruning.
|
||||||
|
|
||||||
|
In particular, pruning will take all snapshots into consideration by default.
|
||||||
|
To constrain pruning to just zrepl-created snapshots:
|
||||||
|
|
||||||
|
1. Assign a unique `prefix` to the snapshotter and
|
||||||
|
2. Use the ``regex`` functionality of the various pruning ``keep`` rules to just consider snapshots with that prefix.
|
||||||
|
|
||||||
|
There is currently no way to constrain replication to just zrepl-created snapshots.
|
||||||
|
Follow and comment at :issue:`403` if you need this functionality.
|
||||||
|
|
||||||
|
.. NOTE::
|
||||||
|
|
||||||
|
The ``zrepl signal wakeup JOB`` subcommand does not trigger snapshotting.
|
||||||
|
|
||||||
|
``periodic`` Snapshotting
|
||||||
|
-------------------------
|
||||||
|
|
||||||
::
|
::
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
- type: push
|
- ...
|
||||||
filesystems: {
|
filesystems: { ... }
|
||||||
"<": true,
|
snapshotting:
|
||||||
"tmp": false
|
type: periodic
|
||||||
}
|
prefix: zrepl_
|
||||||
snapshotting:
|
interval: 10m
|
||||||
type: periodic
|
hooks: ...
|
||||||
prefix: zrepl_
|
pruning: ...
|
||||||
interval: 10m
|
|
||||||
hooks: ...
|
|
||||||
...
|
|
||||||
|
|
||||||
There is also a ``manual`` snapshotting type, which covers the following use cases:
|
The ``periodic`` snapshotter ensures that snapshots are taken in the specified ``interval``.
|
||||||
|
If you use zrepl for backup, this translates into your recovery point objective (RPO).
|
||||||
|
To meet your RPO, you still need to monitor that replication, which happens asynchronously to snapshotting, actually works.
|
||||||
|
|
||||||
* Existing infrastructure for automatic snapshots: you only want to use this zrepl job for replication.
|
It is desirable to get all ``filesystems`` snapshotted simultaneously because it results in a more consistent backup.
|
||||||
* Handling snapshotting through a separate ``snap`` job.
|
To accomplish this while still maintaining the ``interval``, the ``periodic`` snapshotter attempts to get the snapshotting rhythms in sync.
|
||||||
|
To find that sync point, the most recent snapshot, created by the snapshotter, in any of the matched ``filesystems`` is used.
|
||||||
|
A filesystem that does not have snapshots by the snapshotter has lower priority than filesystem that do, and thus might not be snapshotted (and replicated) until it is snapshotted at the next sync point.
|
||||||
|
The snapshotter uses the ``prefix`` to identify which snapshots it created.
|
||||||
|
|
||||||
Note that you will have to trigger replication manually using the ``zrepl signal wakeup JOB`` subcommand in that case.
|
.. _job-snapshotting--cron:
|
||||||
|
|
||||||
|
``cron`` Snapshotting
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
- type: snap
|
||||||
|
filesystems: { ... }
|
||||||
|
snapshotting:
|
||||||
|
type: cron
|
||||||
|
prefix: zrepl_
|
||||||
|
# (second, optional) minute hour day-of-month month day-of-week
|
||||||
|
# This example takes snapshots daily at 3:00.
|
||||||
|
cron: "0 3 * * *"
|
||||||
|
pruning: ...
|
||||||
|
|
||||||
|
In ``cron`` mode, the snapshotter takes snaphots at fixed points in time.
|
||||||
|
See https://en.wikipedia.org/wiki/Cron for details on the syntax.
|
||||||
|
zrepl uses the ``the github.com/robfig/cron/v3`` Go package for parsing.
|
||||||
|
An optional field for "seconds" is supported to take snapshots at sub-minute frequencies.
|
||||||
|
|
||||||
|
``manual`` Snapshotting
|
||||||
|
-----------------------
|
||||||
|
|
||||||
::
|
::
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
- type: push
|
- type: push
|
||||||
filesystems: {
|
|
||||||
"<": true,
|
|
||||||
"tmp": false
|
|
||||||
}
|
|
||||||
snapshotting:
|
snapshotting:
|
||||||
type: manual
|
type: manual
|
||||||
...
|
...
|
||||||
|
|
||||||
|
In ``manual`` mode, zrepl does not take snapshots by itself.
|
||||||
|
Manual snapshotting is most useful if you have existing infrastructure for snapshot management.
|
||||||
|
Or, if you want to decouple snapshot management from replication using a zrepl ``snap`` job.
|
||||||
|
See :ref:`this quickstart guide <quickstart-backup-to-external-disk>` for an example.
|
||||||
|
|
||||||
|
To trigger replication after taking snapshots, use the ``zrepl signal wakeup JOB`` command.
|
||||||
|
|
||||||
.. _job-snapshotting-hooks:
|
.. _job-snapshotting-hooks:
|
||||||
|
|
||||||
Pre- and Post-Snapshot Hooks
|
Pre- and Post-Snapshot Hooks
|
||||||
|
|||||||
+4
-3
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
.. |GitHub license| image:: https://img.shields.io/github/license/zrepl/zrepl.svg
|
.. |GitHub license| image:: https://img.shields.io/github/license/zrepl/zrepl.svg
|
||||||
:target: https://github.com/zrepl/zrepl/blob/master/LICENSE
|
:target: https://github.com/zrepl/zrepl/blob/master/LICENSE
|
||||||
.. |Language: Go| image:: https://img.shields.io/badge/language-Go-6ad7e5.svg
|
.. |Language: Go| image:: https://img.shields.io/badge/lang-Go-6ad7e5.svg
|
||||||
:target: https://golang.org/
|
:target: https://golang.org/
|
||||||
.. |User Docs| image:: https://img.shields.io/badge/docs-web-blue.svg
|
.. |User Docs| image:: https://img.shields.io/badge/docs-web-blue.svg
|
||||||
:target: https://zrepl.github.io
|
:target: https://zrepl.github.io
|
||||||
@@ -13,13 +13,14 @@
|
|||||||
:target: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=R5QSXJVYHGX96
|
:target: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=R5QSXJVYHGX96
|
||||||
.. |Donate via Liberapay| image:: https://img.shields.io/liberapay/patrons/zrepl.svg?logo=liberapay
|
.. |Donate via Liberapay| image:: https://img.shields.io/liberapay/patrons/zrepl.svg?logo=liberapay
|
||||||
:target: https://liberapay.com/zrepl/donate
|
:target: https://liberapay.com/zrepl/donate
|
||||||
.. |Donate via Patreon| image:: https://img.shields.io/badge/dynamic/json?color=yellow&label=Patreon&query=data.attributes.patron_count&suffix=%20patrons&url=https%3A%2F%2Fwww.patreon.com%2Fapi%2Fcampaigns%2F3095079
|
.. |Donate via Patreon| image:: https://img.shields.io/badge/dynamic/json?color=yellow&label=Patreon&query=data.attributes.patron_count&url=https%3A%2F%2Fwww.patreon.com%2Fapi%2Fcampaigns%2F3095079
|
||||||
:target: https://www.patreon.com/zrepl
|
:target: https://www.patreon.com/zrepl
|
||||||
.. |Donate via GitHub Sponsors| image:: https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&style=flat&color=yellow
|
.. |Donate via GitHub Sponsors| image:: https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&style=flat&color=yellow
|
||||||
:target: https://github.com/sponsors/problame
|
:target: https://github.com/sponsors/problame
|
||||||
.. |Twitter| image:: https://img.shields.io/twitter/url/https/github.com/zrepl/zrepl.svg?style=social
|
.. |Twitter| image:: https://img.shields.io/twitter/url/https/github.com/zrepl/zrepl.svg?style=social
|
||||||
:target: https://twitter.com/intent/tweet?text=Wow:&url=https%3A%2F%2Fgithub.com%2Fzrepl%2Fzrepl
|
:target: https://twitter.com/intent/tweet?text=Wow:&url=https%3A%2F%2Fgithub.com%2Fzrepl%2Fzrepl
|
||||||
|
.. |Matrix| image:: https://img.shields.io/badge/chat-matrix-blue.svg
|
||||||
|
:target: https://matrix.to/#/#zrepl:matrix.org
|
||||||
|
|
||||||
.. |serve-transport| replace:: :ref:`serve specification<transport>`
|
.. |serve-transport| replace:: :ref:`serve specification<transport>`
|
||||||
.. |connect-transport| replace:: :ref:`connect specification<transport>`
|
.. |connect-transport| replace:: :ref:`connect specification<transport>`
|
||||||
|
|||||||
+5
-3
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
.. include:: global.rst.inc
|
.. include:: global.rst.inc
|
||||||
|
|
||||||
|GitHub license| |Language: Go| |Twitter| |Donate via Patreon| |Donate via GitHub Sponsors| |Donate via Liberapay| |Donate via PayPal|
|
|GitHub license| |Language: Go| |Twitter| |Donate via Patreon| |Donate via GitHub Sponsors| |Donate via Liberapay| |Donate via PayPal| |Matrix|
|
||||||
|
|
||||||
|
|
||||||
zrepl - ZFS replication
|
zrepl - ZFS replication
|
||||||
@@ -25,10 +25,10 @@ zrepl - ZFS replication
|
|||||||
Progress: [=========================\----] 246.7 MiB / 264.7 MiB @ 11.5 MiB/s
|
Progress: [=========================\----] 246.7 MiB / 264.7 MiB @ 11.5 MiB/s
|
||||||
zroot STEPPING (step 1/2, 624 B/1.2 KiB) next: @a => @b
|
zroot STEPPING (step 1/2, 624 B/1.2 KiB) next: @a => @b
|
||||||
zroot/ROOT DONE (step 2/2, 1.2 KiB/1.2 KiB)
|
zroot/ROOT DONE (step 2/2, 1.2 KiB/1.2 KiB)
|
||||||
zroot/ROOT/default STEPPING (step 1/2, 123.4 MiB/129.3 MiB) next: @a => @b
|
* zroot/ROOT/default STEPPING (step 1/2, 123.4 MiB/129.3 MiB) next: @a => @b
|
||||||
zroot/tmp STEPPING (step 1/2, 29.9 KiB/44.2 KiB) next: @a => @b
|
zroot/tmp STEPPING (step 1/2, 29.9 KiB/44.2 KiB) next: @a => @b
|
||||||
zroot/usr STEPPING (step 1/2, 624 B/1.2 KiB) next: @a => @b
|
zroot/usr STEPPING (step 1/2, 624 B/1.2 KiB) next: @a => @b
|
||||||
zroot/usr/home STEPPING (step 1/2, 123.3 MiB/135.3 MiB) next: @a => @b
|
* zroot/usr/home STEPPING (step 1/2, 123.3 MiB/135.3 MiB) next: @a => @b
|
||||||
zroot/var STEPPING (step 1/2, 624 B/1.2 KiB) next: @a => @b
|
zroot/var STEPPING (step 1/2, 624 B/1.2 KiB) next: @a => @b
|
||||||
zroot/var/audit DONE (step 2/2, 1.2 KiB/1.2 KiB)
|
zroot/var/audit DONE (step 2/2, 1.2 KiB/1.2 KiB)
|
||||||
zroot/var/crash DONE (step 2/2, 1.2 KiB/1.2 KiB)
|
zroot/var/crash DONE (step 2/2, 1.2 KiB/1.2 KiB)
|
||||||
@@ -66,6 +66,7 @@ Main Features
|
|||||||
* [x] Large blocks send & receive
|
* [x] Large blocks send & receive
|
||||||
* [x] Embedded data send & receive
|
* [x] Embedded data send & receive
|
||||||
* [x] Resume state send & receive
|
* [x] Resume state send & receive
|
||||||
|
* [x] Bandwidth limiting
|
||||||
|
|
||||||
* **Automatic snapshot management**
|
* **Automatic snapshot management**
|
||||||
|
|
||||||
@@ -136,5 +137,6 @@ Table of Contents
|
|||||||
pr
|
pr
|
||||||
changelog
|
changelog
|
||||||
GitHub Repository & Issue Tracker <https://github.com/zrepl/zrepl>
|
GitHub Repository & Issue Tracker <https://github.com/zrepl/zrepl>
|
||||||
|
Chat: Matrix <https://matrix.to/#/#zrepl:matrix.org>
|
||||||
supporters
|
supporters
|
||||||
|
|
||||||
|
|||||||
@@ -9,18 +9,29 @@ The fingerprint of the signing key is ``E101 418F D3D6 FBCB 9D65 A62D 7086 99FC
|
|||||||
It is available at `<https://zrepl.cschwarz.com/apt/apt-key.asc>`_ .
|
It is available at `<https://zrepl.cschwarz.com/apt/apt-key.asc>`_ .
|
||||||
Please open an issue in on GitHub if you encounter any issues with the repository.
|
Please open an issue in on GitHub if you encounter any issues with the repository.
|
||||||
|
|
||||||
The following snippet configure the repository for your Debian or Ubuntu release:
|
|
||||||
|
|
||||||
::
|
::
|
||||||
|
|
||||||
sudo apt update && sudo apt install curl gnupg lsb-release; \
|
(
|
||||||
ARCH="$(dpkg --print-architecture)"; \
|
set -ex
|
||||||
CODENAME="$(lsb_release -i -s | tr '[:upper:]' '[:lower:]') $(lsb_release -c -s | tr '[:upper:]' '[:lower:]')"; \
|
zrepl_apt_key_url=https://zrepl.cschwarz.com/apt/apt-key.asc
|
||||||
echo "Using Distro and Codename: $CODENAME"; \
|
zrepl_apt_key_dst=/usr/share/keyrings/zrepl.gpg
|
||||||
(curl https://zrepl.cschwarz.com/apt/apt-key.asc | sudo apt-key add -) && \
|
zrepl_apt_repo_file=/etc/apt/sources.list.d/zrepl.list
|
||||||
(echo "deb [arch=$ARCH] https://zrepl.cschwarz.com/apt/$CODENAME main" | sudo tee /etc/apt/sources.list.d/zrepl.list) && \
|
|
||||||
sudo apt update
|
|
||||||
|
|
||||||
|
# Install dependencies for subsequent commands
|
||||||
|
sudo apt update && sudo apt install curl gnupg lsb-release
|
||||||
|
|
||||||
|
# Deploy the zrepl apt key.
|
||||||
|
curl -fsSL "$zrepl_apt_key_url" | tee | gpg --dearmor | sudo tee "$zrepl_apt_key_dst" > /dev/null
|
||||||
|
|
||||||
|
# Add the zrepl apt repo.
|
||||||
|
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"
|
||||||
|
echo "deb [arch=$ARCH signed-by=$zrepl_apt_key_dst] https://zrepl.cschwarz.com/apt/$CODENAME main" | sudo tee /etc/apt/sources.list.d/zrepl.list
|
||||||
|
|
||||||
|
# Update apt repos.
|
||||||
|
sudo apt update
|
||||||
|
)
|
||||||
|
|
||||||
.. NOTE::
|
.. NOTE::
|
||||||
|
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ Enable the zrepl daemon to start automatically at boot:
|
|||||||
|
|
||||||
sysrc zrepl_enable="YES"
|
sysrc zrepl_enable="YES"
|
||||||
|
|
||||||
|
Now jump to :ref:`the summary <installation-freebsd-jail-summary>` below.
|
||||||
|
|
||||||
Plugin
|
Plugin
|
||||||
######
|
######
|
||||||
@@ -134,7 +135,18 @@ Now ``zrepl`` can be started.
|
|||||||
|
|
||||||
service zrepl start
|
service zrepl start
|
||||||
|
|
||||||
|
Now jump to :ref:`the summary <installation-freebsd-jail-summary>` below.
|
||||||
|
|
||||||
|
.. _installation-freebsd-jail-summary:
|
||||||
|
|
||||||
Summary
|
Summary
|
||||||
-------
|
-------
|
||||||
|
|
||||||
Congratulations, you have a working jail!
|
Congratulations, you have a working jail!
|
||||||
|
|
||||||
|
.. NOTE::
|
||||||
|
|
||||||
|
With FreeBSD 13's transition to OpenZFS 2.0, please ensure that your jail's FreeBSD version matches the one in the kernel module.
|
||||||
|
If you are getting cryptic errors such as
|
||||||
|
``cannot receive new filesystem stream: invalid backup stream``
|
||||||
|
the instructions posted `here <https://github.com/zrepl/zrepl/issues/500#issuecomment-966215205>`_ might help.
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ Keep the :ref:`full config documentation <configuration_toc>` handy if a config
|
|||||||
|
|
||||||
quickstart/continuous_server_backup
|
quickstart/continuous_server_backup
|
||||||
quickstart/backup_to_external_disk
|
quickstart/backup_to_external_disk
|
||||||
|
quickstart/fan_out_replication
|
||||||
|
|
||||||
Use ``zrepl configcheck`` to validate your configuration.
|
Use ``zrepl configcheck`` to validate your configuration.
|
||||||
No output indicates that everything is fine.
|
No output indicates that everything is fine.
|
||||||
@@ -51,6 +52,19 @@ We hope that you have found a configuration that fits your use case.
|
|||||||
Use ``zrepl configcheck`` once again to make sure the config is correct (output indicates that everything is fine).
|
Use ``zrepl configcheck`` once again to make sure the config is correct (output indicates that everything is fine).
|
||||||
Then restart the zrepl daemon on all systems involved in the replication, likely using ``service zrepl restart`` or ``systemctl restart zrepl``.
|
Then restart the zrepl daemon on all systems involved in the replication, likely using ``service zrepl restart`` or ``systemctl restart zrepl``.
|
||||||
|
|
||||||
|
.. WARNING::
|
||||||
|
|
||||||
|
Please :ref:`read up carefully <prune>` on the pruning rules before applying the config.
|
||||||
|
In particular, note that most example configs apply to all snapshots, not just zrepl-created snapshots.
|
||||||
|
Use the following keep rule on sender and receiver to prevent this:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
- type: regex
|
||||||
|
negate: true
|
||||||
|
regex: "^zrepl_.*" # <- the 'prefix' specified in snapshotting.prefix
|
||||||
|
|
||||||
|
|
||||||
Watch it Work
|
Watch it Work
|
||||||
=============
|
=============
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ Thus, we only keep one hour worth of high-resolution snapshots, then fade them o
|
|||||||
|
|
||||||
At the end of each work day, we connect our external disk that serves as our workstation's local offline backup.
|
At the end of each work day, we connect our external disk that serves as our workstation's local offline backup.
|
||||||
We want zrepl to inspect the filesystems and snapshots on the external pool, figure out which snapshots were created since the last time we connected the external disk, and use incremental replication to efficiently mirror our workstation to our backup disk.
|
We want zrepl to inspect the filesystems and snapshots on the external pool, figure out which snapshots were created since the last time we connected the external disk, and use incremental replication to efficiently mirror our workstation to our backup disk.
|
||||||
Afterwards, we want to clean up old snapshots on the backup pool: we want to keep all snapshots younger than one hour, 24 for each hor of the first day, then 360 daily backups.
|
Afterwards, we want to clean up old snapshots on the backup pool: we want to keep all snapshots younger than one hour, 24 for each hour of the first day, then 360 daily backups.
|
||||||
|
|
||||||
A few additional requirements:
|
A few additional requirements:
|
||||||
|
|
||||||
|
|||||||
@@ -9,13 +9,13 @@ This config example shows how we can backup our ZFS-based server to another mach
|
|||||||
|
|
||||||
* Production server ``prod`` with filesystems to back up:
|
* Production server ``prod`` with filesystems to back up:
|
||||||
|
|
||||||
* ``zroot/var/db``
|
* The entire pool ``zroot``
|
||||||
* ``zroot/usr/home`` and all its child filesystems
|
* except ``zroot/var/tmp`` and all child datasets of it
|
||||||
* **except** ``zroot/usr/home/paranoid`` belonging to a user doing backups themselves
|
* and except ``zroot/usr/home/paranoid`` which belongs to a user doing backups themselves.
|
||||||
|
|
||||||
* Backup server ``backups`` with
|
* Backup server ``backups`` with a dataset sub-tree for use by zrepl:
|
||||||
|
|
||||||
* Filesystem ``storage/zrepl/sink/prod`` + children dedicated to backups of ``prod``
|
* In our example, that will be ``storage/zrepl/sink/prod``.
|
||||||
|
|
||||||
Our backup solution should fulfill the following requirements:
|
Our backup solution should fulfill the following requirements:
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
.. include:: ../global.rst.inc
|
||||||
|
|
||||||
|
.. _quickstart-fan-out-replication:
|
||||||
|
|
||||||
|
Fan-out replication
|
||||||
|
===================
|
||||||
|
|
||||||
|
This quick-start example demonstrates how to implement a fan-out replication setup where datasets on a server (A) are replicated to multiple targets (B, C, etc.).
|
||||||
|
|
||||||
|
This example uses multiple ``source`` jobs on server A and ``pull`` jobs on the target servers.
|
||||||
|
|
||||||
|
.. WARNING::
|
||||||
|
|
||||||
|
Before implementing this setup, please see the caveats listed in the :ref:`fan-out replication configuration overview <fan-out-replication>`.
|
||||||
|
|
||||||
|
Overview
|
||||||
|
--------
|
||||||
|
|
||||||
|
On the source server (A), there should be:
|
||||||
|
|
||||||
|
* A ``snap`` job
|
||||||
|
|
||||||
|
* Creates the snapshots
|
||||||
|
* Handles the pruning of snapshots
|
||||||
|
|
||||||
|
* A ``source`` job for target B
|
||||||
|
|
||||||
|
* Accepts connections from server B and B only
|
||||||
|
|
||||||
|
* Further ``source`` jobs for each additional target (C, D, etc.)
|
||||||
|
|
||||||
|
* Listens on a unique port
|
||||||
|
* Only accepts connections from the specific target
|
||||||
|
|
||||||
|
On each target server, there should be:
|
||||||
|
|
||||||
|
* A ``pull`` job that connects to the corresponding ``source`` job on A
|
||||||
|
|
||||||
|
* ``prune_sender`` should keep all snapshots since A's ``snap`` job handles the pruning
|
||||||
|
* ``prune_receiver`` can be configured as appropriate on each target server
|
||||||
|
|
||||||
|
Generate TLS Certificates
|
||||||
|
-------------------------
|
||||||
|
|
||||||
|
Mutual TLS via the :ref:`TLS client authentication transport <transport-tcp+tlsclientauth>` can be used to secure the connections between the servers. In this example, a self-signed certificate is created for each server without setting up a CA.
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
source=a.example.com
|
||||||
|
targets=(
|
||||||
|
b.example.com
|
||||||
|
c.example.com
|
||||||
|
# ...
|
||||||
|
)
|
||||||
|
|
||||||
|
for server in "${source}" "${targets[@]}"; do
|
||||||
|
openssl req -x509 -sha256 -nodes \
|
||||||
|
-newkey rsa:4096 \
|
||||||
|
-days 365 \
|
||||||
|
-keyout "${server}.key" \
|
||||||
|
-out "${server}.crt" \
|
||||||
|
-addext "subjectAltName = DNS:${server}" \
|
||||||
|
-subj "/CN=${server}"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Distribute each host's keypair
|
||||||
|
for server in "${source}" "${targets[@]}"; do
|
||||||
|
ssh root@"${server}" mkdir /etc/zrepl
|
||||||
|
scp "${server}".{crt,key} root@"${server}":/etc/zrepl/
|
||||||
|
done
|
||||||
|
|
||||||
|
# Distribute target certificates to the source
|
||||||
|
scp "${targets[@]/%/.crt}" root@"${source}":/etc/zrepl/
|
||||||
|
|
||||||
|
# Distribute source certificate to the targets
|
||||||
|
for server in "${targets[@]}"; do
|
||||||
|
scp "${source}.crt" root@"${server}":/etc/zrepl/
|
||||||
|
done
|
||||||
|
|
||||||
|
Configure source server A
|
||||||
|
-------------------------
|
||||||
|
|
||||||
|
.. literalinclude:: ../../config/samples/quickstart_fan_out_replication_source.yml
|
||||||
|
|
||||||
|
Configure each target server
|
||||||
|
----------------------------
|
||||||
|
|
||||||
|
.. literalinclude:: ../../config/samples/quickstart_fan_out_replication_target.yml
|
||||||
|
|
||||||
|
Go Back To Quickstart Guide
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
:ref:`Click here <quickstart-apply-config>` to go back to the quickstart guide.
|
||||||
@@ -25,6 +25,6 @@ sphinxcontrib-htmlhelp==1.0.2
|
|||||||
sphinxcontrib-jsmath==1.0.1
|
sphinxcontrib-jsmath==1.0.1
|
||||||
sphinxcontrib-qthelp==1.0.2
|
sphinxcontrib-qthelp==1.0.2
|
||||||
sphinxcontrib-serializinghtml==1.1.3
|
sphinxcontrib-serializinghtml==1.1.3
|
||||||
-e git://github.com/rwblair/sphinxcontrib-versioning.git@7e3885a389a809e17ea55261316b7b0e98dbf98f#egg=sphinxcontrib-versioning
|
git+https://github.com/rwblair/sphinxcontrib-versioning.git@7e3885a389a809e17ea55261316b7b0e98dbf98f#egg=sphinxcontrib-versioning
|
||||||
sphinxcontrib-websupport==1.1.2
|
sphinxcontrib-websupport==1.1.2
|
||||||
urllib3==1.25.3
|
urllib3==1.25.3
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ We would like to thank the following people and organizations for supporting zre
|
|||||||
|
|
||||||
<div class="fa fa-code" style="width: 1em;"></div>
|
<div class="fa fa-code" style="width: 1em;"></div>
|
||||||
|
|
||||||
|
* |supporter-gold| Prominic.NET, Inc.
|
||||||
|
* |supporter-std| Torsten Blum
|
||||||
* |supporter-gold| Cyberiada GmbH
|
* |supporter-gold| Cyberiada GmbH
|
||||||
* |supporter-std| `Gordon Schulz <https://github.com/azmodude>`_
|
* |supporter-std| `Gordon Schulz <https://github.com/azmodude>`_
|
||||||
* |supporter-std| `@jwittlincohen <https://github.com/jwittlincohen>`_
|
* |supporter-std| `@jwittlincohen <https://github.com/jwittlincohen>`_
|
||||||
|
|||||||
+72
-2
@@ -26,7 +26,7 @@ CLI Overview
|
|||||||
* - ``zrepl daemon``
|
* - ``zrepl daemon``
|
||||||
- run the daemon, required for all zrepl functionality
|
- run the daemon, required for all zrepl functionality
|
||||||
* - ``zrepl status``
|
* - ``zrepl status``
|
||||||
- show job activity, or with ``--raw`` for JSON output
|
- show job activity, or with ``--mode raw`` for JSON output
|
||||||
* - ``zrepl stdinserver``
|
* - ``zrepl stdinserver``
|
||||||
- see :ref:`transport-ssh+stdinserver`
|
- see :ref:`transport-ssh+stdinserver`
|
||||||
* - ``zrepl signal wakeup JOB``
|
* - ``zrepl signal wakeup JOB``
|
||||||
@@ -77,4 +77,74 @@ Systemd Unit File
|
|||||||
|
|
||||||
A systemd service definition 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`.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
============
|
||||||
|
Ops Runbooks
|
||||||
|
============
|
||||||
|
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
|
||||||
|
usage/runbooks/migrating_sending_side_to_new_zpool.rst
|
||||||
|
|
||||||
|
|
||||||
|
.. _usage-platform-tests:
|
||||||
|
|
||||||
|
==============
|
||||||
|
Platform Tests
|
||||||
|
==============
|
||||||
|
|
||||||
|
Along with the main ``zrepl`` binary, we release the ``platformtest`` binaries.
|
||||||
|
The zrepl platform tests are an integration test suite that is complementary to the pure Go unit tests.
|
||||||
|
Any test that needs to interact with ZFS is a platform test.
|
||||||
|
|
||||||
|
The platform need to run as root.
|
||||||
|
For each test, we create a fresh dummy zpool backed by a file-based vdev.
|
||||||
|
The file path, and a root mountpoint for the dummy zpool, must be specified on the command line:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
mkdir -p /tmp/zreplplatformtest
|
||||||
|
./platformtest \
|
||||||
|
-poolname 'zreplplatformtest' \ # <- name must contain zreplplatformtest
|
||||||
|
-imagepath /tmp/zreplplatformtest.img \ # <- zrepl will create the file
|
||||||
|
-mountpoint /tmp/zreplplatformtest # <- must exist
|
||||||
|
|
||||||
|
|
||||||
|
.. WARNING::
|
||||||
|
|
||||||
|
``platformtest`` will unconditionally overwrite the file at `imagepath`
|
||||||
|
and unconditionally ``zpool destroy $poolname``.
|
||||||
|
So, don't use a production poolname, and consider running the test in a VM.
|
||||||
|
It'll be a lot faster as well because the underlying operations, ``zfs list`` in particular, will be faster.
|
||||||
|
|
||||||
|
|
||||||
|
While the platformtests are running, there will be a log of log output.
|
||||||
|
After all tests have run, it prints a summary with a list of tests, grouped by result type (success, failure, skipped):
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
PASSING TESTS:
|
||||||
|
github.com/zrepl/zrepl/platformtest/tests.BatchDestroy
|
||||||
|
github.com/zrepl/zrepl/platformtest/tests.CreateReplicationCursor
|
||||||
|
github.com/zrepl/zrepl/platformtest/tests.GetNonexistent
|
||||||
|
github.com/zrepl/zrepl/platformtest/tests.HoldsWork
|
||||||
|
...
|
||||||
|
github.com/zrepl/zrepl/platformtest/tests.SendStreamNonEOFReadErrorHandling
|
||||||
|
github.com/zrepl/zrepl/platformtest/tests.UndestroyableSnapshotParsing
|
||||||
|
SKIPPED TESTS:
|
||||||
|
github.com/zrepl/zrepl/platformtest/tests.SendArgsValidationEncryptedSendOfUnencryptedDatasetForbidden__EncryptionSupported_false
|
||||||
|
FAILED TESTS: []
|
||||||
|
|
||||||
|
|
||||||
|
If there is a failure, or a skipped test that you believe should be passing, re-run the test suite, capture stderr & stdout to a text file, and create an issue on GitHub.
|
||||||
|
|
||||||
|
To run a specific test case, or a subset of tests matched by regex, use the ``-run REGEX`` command line flag.
|
||||||
|
|
||||||
|
To stop test execution at the first failing test, and prevent cleanup of the dummy zpool, use the ``-failure.stop-and-keep-pool`` flag.
|
||||||
|
|
||||||
|
To build the platformtests yourself, use ``make test-platform-bin``.
|
||||||
|
There's also the ``make test-platform`` target to run the platform tests with a default command line.
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
|
||||||
|
Migrating Sending Side
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
**Objective**:
|
||||||
|
Move sending-side zpool to new hardware.
|
||||||
|
Make the move fully transparent to the sending-side jobs.
|
||||||
|
After the move is done, all sending-side zrepl jobs should continue to work as if the move had not happened.
|
||||||
|
In particular, incremental replication should be able to pick up where it left before the move.
|
||||||
|
|
||||||
|
Suppose we want to migrate all data from one zpool ``oldpool`` to another zpool ``newpool``.
|
||||||
|
A possible reason might be that we want to change RAID levels, ``ashift``, or just migrate over to next-gen hardware.
|
||||||
|
|
||||||
|
If the pool names are different, zrepl's matching between sender and receiver dataset will break becase the receive-side dataset names contain ``oldpool``.
|
||||||
|
To avoid this, we will need the name of the new pool to match that of the old pool.
|
||||||
|
The following steps will accomplish this:
|
||||||
|
|
||||||
|
1. Stop zrepl.
|
||||||
|
2. Create the new pool: ``zpool create newpool ...``
|
||||||
|
3. Take a snapshot of the old pool so that you have something that you can ``zfs send``.
|
||||||
|
For example, run ``zfs snapshot -r oldpool@migration_oldpool_newpool``.
|
||||||
|
4. Send all of the oldpool's datasets to the new pool:
|
||||||
|
``zfs send -R oldpool@migration_oldpool_newpool | zfs recv -F newpool``
|
||||||
|
5. Export the old pool: ``zpool export oldpool``
|
||||||
|
6. Export the new pool: ``zpool export newpool``
|
||||||
|
7. (Optional) Change the name of the old pool to something that does not conflict with the new pool.
|
||||||
|
We are going to use the name ``oldoldpool`` in this example.
|
||||||
|
Use ``zpool import`` with no arguments to see the pool id.
|
||||||
|
Then ``zpool import <id> oldoldpool && zpool export oldoldpool``.
|
||||||
|
8. Import the new pool, while changing the name to match the old pool: ``zpool import newpool oldpool``
|
||||||
|
9. Start zrepl again and wake up the relevant jobs.
|
||||||
|
10. Use ``zrepl status`` or you monitoring to ensure that replication works.
|
||||||
|
The best test is an end-to-end test where you write some junk data on a sender dataset and wait until a snapshot with that data appears on the receiving side.
|
||||||
|
11. Once you are confident that replication is working, you may dispose of the old pool.
|
||||||
|
|
||||||
|
Note that, depending on pruning rules, it will not be possible to switch back to the old pool seamlessly, i.e., without a full re-replication.
|
||||||
+158
-69
@@ -14,6 +14,7 @@ import (
|
|||||||
"github.com/zrepl/zrepl/daemon/logging/trace"
|
"github.com/zrepl/zrepl/daemon/logging/trace"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/replication/logic/pdu"
|
"github.com/zrepl/zrepl/replication/logic/pdu"
|
||||||
|
"github.com/zrepl/zrepl/util/bandwidthlimit"
|
||||||
"github.com/zrepl/zrepl/util/chainedio"
|
"github.com/zrepl/zrepl/util/chainedio"
|
||||||
"github.com/zrepl/zrepl/util/chainlock"
|
"github.com/zrepl/zrepl/util/chainlock"
|
||||||
"github.com/zrepl/zrepl/util/envconst"
|
"github.com/zrepl/zrepl/util/envconst"
|
||||||
@@ -34,6 +35,8 @@ type SenderConfig struct {
|
|||||||
SendCompressed bool
|
SendCompressed bool
|
||||||
SendEmbeddedData bool
|
SendEmbeddedData bool
|
||||||
SendSaved bool
|
SendSaved bool
|
||||||
|
|
||||||
|
BandwidthLimit bandwidthlimit.Config
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *SenderConfig) Validate() error {
|
func (c *SenderConfig) Validate() error {
|
||||||
@@ -44,6 +47,9 @@ func (c *SenderConfig) Validate() error {
|
|||||||
if _, err := StepHoldTag(c.JobID); err != nil {
|
if _, err := StepHoldTag(c.JobID); err != nil {
|
||||||
return fmt.Errorf("JobID cannot be used for hold tag: %s", err)
|
return fmt.Errorf("JobID cannot be used for hold tag: %s", err)
|
||||||
}
|
}
|
||||||
|
if err := bandwidthlimit.ValidateConfig(c.BandwidthLimit); err != nil {
|
||||||
|
return errors.Wrap(err, "`BandwidthLimit` field invalid")
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,16 +60,21 @@ type Sender struct {
|
|||||||
FSFilter zfs.DatasetFilter
|
FSFilter zfs.DatasetFilter
|
||||||
jobId JobID
|
jobId JobID
|
||||||
config SenderConfig
|
config SenderConfig
|
||||||
|
bwLimit bandwidthlimit.Wrapper
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSender(conf SenderConfig) *Sender {
|
func NewSender(conf SenderConfig) *Sender {
|
||||||
if err := conf.Validate(); err != nil {
|
if err := conf.Validate(); err != nil {
|
||||||
panic("invalid config" + err.Error())
|
panic("invalid config" + err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ratelimiter := bandwidthlimit.WrapperFromConfig(conf.BandwidthLimit)
|
||||||
|
|
||||||
return &Sender{
|
return &Sender{
|
||||||
FSFilter: conf.FSF,
|
FSFilter: conf.FSF,
|
||||||
jobId: conf.JobID,
|
jobId: conf.JobID,
|
||||||
config: conf,
|
config: conf,
|
||||||
|
bwLimit: ratelimiter,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,15 +105,10 @@ func (s *Sender) ListFilesystems(ctx context.Context, r *pdu.ListFilesystemReq)
|
|||||||
}
|
}
|
||||||
rfss := make([]*pdu.Filesystem, len(fss))
|
rfss := make([]*pdu.Filesystem, len(fss))
|
||||||
for i := range fss {
|
for i := range fss {
|
||||||
encEnabled, err := zfs.ZFSGetEncryptionEnabled(ctx, fss[i].ToString())
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "cannot get filesystem encryption status")
|
|
||||||
}
|
|
||||||
rfss[i] = &pdu.Filesystem{
|
rfss[i] = &pdu.Filesystem{
|
||||||
Path: fss[i].ToString(),
|
Path: fss[i].ToString(),
|
||||||
// ResumeToken does not make sense from Sender
|
// ResumeToken does not make sense from Sender
|
||||||
IsPlaceholder: false, // sender FSs are never placeholders
|
IsPlaceholder: false, // sender FSs are never placeholders
|
||||||
IsEncrypted: encEnabled,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
res := &pdu.ListFilesystemRes{Filesystems: rfss}
|
res := &pdu.ListFilesystemRes{Filesystems: rfss}
|
||||||
@@ -148,29 +154,11 @@ func sendArgsFromPDUAndValidateExistsAndGetVersion(ctx context.Context, fs strin
|
|||||||
return version, nil
|
return version, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Sender) Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, io.ReadCloser, error) {
|
func (s *Sender) sendMakeArgs(ctx context.Context, r *pdu.SendReq) (sendArgs zfs.ZFSSendArgsValidated, _ error) {
|
||||||
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
|
|
||||||
|
|
||||||
_, err := s.filterCheckFS(r.Filesystem)
|
_, err := s.filterCheckFS(r.Filesystem)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return sendArgs, err
|
||||||
}
|
|
||||||
switch r.Encrypted {
|
|
||||||
case pdu.Tri_DontCare:
|
|
||||||
// use s.encrypt setting
|
|
||||||
// ok, fallthrough outer
|
|
||||||
case pdu.Tri_False:
|
|
||||||
if s.config.Encrypt.B {
|
|
||||||
return nil, nil, errors.New("only encrypted sends allowed (send -w + encryption!= off), but unencrypted send requested")
|
|
||||||
}
|
|
||||||
// fallthrough outer
|
|
||||||
case pdu.Tri_True:
|
|
||||||
if !s.config.Encrypt.B {
|
|
||||||
return nil, nil, errors.New("only unencrypted sends allowed, but encrypted send requested")
|
|
||||||
}
|
|
||||||
// fallthrough outer
|
|
||||||
default:
|
|
||||||
return nil, nil, fmt.Errorf("unknown pdu.Tri variant %q", r.Encrypted)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sendArgsUnvalidated := zfs.ZFSSendArgsUnvalidated{
|
sendArgsUnvalidated := zfs.ZFSSendArgsUnvalidated{
|
||||||
@@ -190,30 +178,19 @@ func (s *Sender) Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, io.Rea
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
sendArgs, err := sendArgsUnvalidated.Validate(ctx)
|
sendArgs, err = sendArgsUnvalidated.Validate(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, errors.Wrap(err, "validate send arguments")
|
return sendArgs, errors.Wrap(err, "validate send arguments")
|
||||||
}
|
}
|
||||||
|
return sendArgs, nil
|
||||||
|
}
|
||||||
|
|
||||||
si, err := zfs.ZFSSendDry(ctx, sendArgs)
|
func (s *Sender) Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, io.ReadCloser, error) {
|
||||||
|
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
|
||||||
|
|
||||||
|
sendArgs, err := s.sendMakeArgs(ctx, r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, errors.Wrap(err, "zfs send dry failed")
|
return nil, nil, err
|
||||||
}
|
|
||||||
|
|
||||||
// From now on, assume that sendArgs has been validated by ZFSSendDry
|
|
||||||
// (because validation involves shelling out, it's actually a little expensive)
|
|
||||||
|
|
||||||
var expSize int64 = 0 // protocol says 0 means no estimate
|
|
||||||
if si.SizeEstimate != -1 { // but si returns -1 for no size estimate
|
|
||||||
expSize = si.SizeEstimate
|
|
||||||
}
|
|
||||||
res := &pdu.SendRes{
|
|
||||||
ExpectedSize: expSize,
|
|
||||||
UsedResumeToken: r.ResumeToken != "",
|
|
||||||
}
|
|
||||||
|
|
||||||
if r.DryRun {
|
|
||||||
return res, nil, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// create holds or bookmarks of `From` and `To` to guarantee one of the following:
|
// create holds or bookmarks of `From` and `To` to guarantee one of the following:
|
||||||
@@ -301,15 +278,47 @@ func (s *Sender) Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, io.Rea
|
|||||||
abstractionsCacheSingleton.TryBatchDestroy(ctx, s.jobId, sendArgs.FS, destroyTypes, keep, check)
|
abstractionsCacheSingleton.TryBatchDestroy(ctx, s.jobId, sendArgs.FS, destroyTypes, keep, check)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
sendStream, err := zfs.ZFSSend(ctx, sendArgs)
|
var sendStream io.ReadCloser
|
||||||
|
sendStream, err = zfs.ZFSSend(ctx, sendArgs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// it's ok to not destroy the abstractions we just created here, a new send attempt will take care of it
|
// it's ok to not destroy the abstractions we just created here, a new send attempt will take care of it
|
||||||
return nil, nil, errors.Wrap(err, "zfs send failed")
|
return nil, nil, errors.Wrap(err, "zfs send failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// apply rate limit
|
||||||
|
sendStream = s.bwLimit.WrapReadCloser(sendStream)
|
||||||
|
|
||||||
|
res := &pdu.SendRes{
|
||||||
|
ExpectedSize: 0,
|
||||||
|
UsedResumeToken: r.ResumeToken != "",
|
||||||
|
}
|
||||||
|
|
||||||
return res, sendStream, nil
|
return res, sendStream, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Sender) SendDry(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, error) {
|
||||||
|
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
|
||||||
|
|
||||||
|
sendArgs, err := s.sendMakeArgs(ctx, r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
si, err := zfs.ZFSSendDry(ctx, sendArgs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "zfs send dry failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// From now on, assume that sendArgs has been validated by ZFSSendDry
|
||||||
|
// (because validation involves shelling out, it's actually a little expensive)
|
||||||
|
|
||||||
|
res := &pdu.SendRes{
|
||||||
|
ExpectedSize: si.SizeEstimate,
|
||||||
|
UsedResumeToken: r.ResumeToken != "",
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
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) {
|
||||||
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
|
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
|
||||||
|
|
||||||
@@ -439,8 +448,22 @@ type ReceiverConfig struct {
|
|||||||
|
|
||||||
InheritProperties []zfsprop.Property
|
InheritProperties []zfsprop.Property
|
||||||
OverrideProperties map[zfsprop.Property]string
|
OverrideProperties map[zfsprop.Property]string
|
||||||
|
|
||||||
|
BandwidthLimit bandwidthlimit.Config
|
||||||
|
|
||||||
|
PlaceholderEncryption PlaceholderCreationEncryptionProperty
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//go:generate enumer -type=PlaceholderCreationEncryptionProperty -transform=kebab -trimprefix=PlaceholderCreationEncryptionProperty
|
||||||
|
type PlaceholderCreationEncryptionProperty int
|
||||||
|
|
||||||
|
// Note: the constant names, transformed through enumer, are part of the config format!
|
||||||
|
const (
|
||||||
|
PlaceholderCreationEncryptionPropertyUnspecified PlaceholderCreationEncryptionProperty = 1 << iota
|
||||||
|
PlaceholderCreationEncryptionPropertyInherit
|
||||||
|
PlaceholderCreationEncryptionPropertyOff
|
||||||
|
)
|
||||||
|
|
||||||
func (c *ReceiverConfig) copyIn() {
|
func (c *ReceiverConfig) copyIn() {
|
||||||
c.RootWithoutClientComponent = c.RootWithoutClientComponent.Copy()
|
c.RootWithoutClientComponent = c.RootWithoutClientComponent.Copy()
|
||||||
|
|
||||||
@@ -475,6 +498,15 @@ func (c *ReceiverConfig) Validate() error {
|
|||||||
if c.RootWithoutClientComponent.Length() <= 0 {
|
if c.RootWithoutClientComponent.Length() <= 0 {
|
||||||
return errors.New("RootWithoutClientComponent must not be an empty dataset path")
|
return errors.New("RootWithoutClientComponent must not be an empty dataset path")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := bandwidthlimit.ValidateConfig(c.BandwidthLimit); err != nil {
|
||||||
|
return errors.Wrap(err, "`BandwidthLimit` field invalid")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !c.PlaceholderEncryption.IsAPlaceholderCreationEncryptionProperty() {
|
||||||
|
return errors.Errorf("`PlaceholderEncryption` field is invalid")
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -484,7 +516,11 @@ type Receiver struct {
|
|||||||
|
|
||||||
conf ReceiverConfig // validated
|
conf ReceiverConfig // validated
|
||||||
|
|
||||||
|
bwLimit bandwidthlimit.Wrapper
|
||||||
|
|
||||||
recvParentCreationMtx *chainlock.L
|
recvParentCreationMtx *chainlock.L
|
||||||
|
|
||||||
|
Test_OverrideClientIdentityFunc func() string // for use by platformtest
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewReceiver(config ReceiverConfig) *Receiver {
|
func NewReceiver(config ReceiverConfig) *Receiver {
|
||||||
@@ -495,6 +531,7 @@ func NewReceiver(config ReceiverConfig) *Receiver {
|
|||||||
return &Receiver{
|
return &Receiver{
|
||||||
conf: config,
|
conf: config,
|
||||||
recvParentCreationMtx: chainlock.New(),
|
recvParentCreationMtx: chainlock.New(),
|
||||||
|
bwLimit: bandwidthlimit.WrapperFromConfig(config.BandwidthLimit),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -521,9 +558,15 @@ func (s *Receiver) clientRootFromCtx(ctx context.Context) *zfs.DatasetPath {
|
|||||||
return s.conf.RootWithoutClientComponent.Copy()
|
return s.conf.RootWithoutClientComponent.Copy()
|
||||||
}
|
}
|
||||||
|
|
||||||
clientIdentity, ok := ctx.Value(ClientIdentityKey).(string)
|
var clientIdentity string
|
||||||
if !ok {
|
if s.Test_OverrideClientIdentityFunc != nil {
|
||||||
panic("ClientIdentityKey context value must be set")
|
clientIdentity = s.Test_OverrideClientIdentityFunc()
|
||||||
|
} else {
|
||||||
|
var ok bool
|
||||||
|
clientIdentity, ok = ctx.Value(ClientIdentityKey).(string) // no shadow
|
||||||
|
if !ok {
|
||||||
|
panic("ClientIdentityKey context value must be set")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
clientRoot, err := clientRoot(s.conf.RootWithoutClientComponent, clientIdentity)
|
clientRoot, err := clientRoot(s.conf.RootWithoutClientComponent, clientIdentity)
|
||||||
@@ -564,7 +607,8 @@ func (s *Receiver) ListFilesystems(ctx context.Context, req *pdu.ListFilesystemR
|
|||||||
if rphs, err := zfs.ZFSGetFilesystemPlaceholderState(ctx, s.conf.RootWithoutClientComponent); err != nil {
|
if rphs, err := zfs.ZFSGetFilesystemPlaceholderState(ctx, s.conf.RootWithoutClientComponent); err != nil {
|
||||||
return nil, errors.Wrap(err, "cannot determine whether root_fs exists")
|
return nil, errors.Wrap(err, "cannot determine whether root_fs exists")
|
||||||
} else if !rphs.FSExists {
|
} else if !rphs.FSExists {
|
||||||
return nil, errors.New("root_fs does not exist")
|
getLogger(ctx).WithField("root_fs", s.conf.RootWithoutClientComponent).Error("root_fs does not exist")
|
||||||
|
return nil, errors.Errorf("root_fs does not exist")
|
||||||
}
|
}
|
||||||
|
|
||||||
root := s.clientRootFromCtx(ctx)
|
root := s.clientRootFromCtx(ctx)
|
||||||
@@ -592,11 +636,6 @@ func (s *Receiver) ListFilesystems(ctx context.Context, req *pdu.ListFilesystemR
|
|||||||
l.WithError(err).Error("cannot get receive resume token")
|
l.WithError(err).Error("cannot get receive resume token")
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
encEnabled, err := zfs.ZFSGetEncryptionEnabled(ctx, a.ToString())
|
|
||||||
if err != nil {
|
|
||||||
l.WithError(err).Error("cannot get encryption enabled status")
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
l.WithField("receive_resume_token", token).Debug("receive resume token")
|
l.WithField("receive_resume_token", token).Debug("receive resume token")
|
||||||
|
|
||||||
a.TrimPrefix(root)
|
a.TrimPrefix(root)
|
||||||
@@ -605,7 +644,6 @@ func (s *Receiver) ListFilesystems(ctx context.Context, req *pdu.ListFilesystemR
|
|||||||
Path: a.ToString(),
|
Path: a.ToString(),
|
||||||
IsPlaceholder: ph.IsPlaceholder,
|
IsPlaceholder: ph.IsPlaceholder,
|
||||||
ResumeToken: token,
|
ResumeToken: token,
|
||||||
IsEncrypted: encEnabled,
|
|
||||||
}
|
}
|
||||||
fss = append(fss, fs)
|
fss = append(fss, fs)
|
||||||
}
|
}
|
||||||
@@ -668,6 +706,38 @@ func (s *Receiver) Send(ctx context.Context, req *pdu.SendReq) (*pdu.SendRes, io
|
|||||||
return nil, nil, fmt.Errorf("receiver does not implement Send()")
|
return nil, nil, fmt.Errorf("receiver does not implement Send()")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Receiver) SendDry(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, error) {
|
||||||
|
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
|
||||||
|
return nil, fmt.Errorf("receiver does not implement SendDry()")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Receiver) receive_GetPlaceholderCreationEncryptionValue(client_root, path *zfs.DatasetPath) (zfs.FilesystemPlaceholderCreateEncryptionValue, error) {
|
||||||
|
if !s.conf.PlaceholderEncryption.IsAPlaceholderCreationEncryptionProperty() {
|
||||||
|
panic(s.conf.PlaceholderEncryption)
|
||||||
|
}
|
||||||
|
|
||||||
|
if client_root.Equal(path) && s.conf.PlaceholderEncryption == PlaceholderCreationEncryptionPropertyUnspecified {
|
||||||
|
// If our Receiver is configured to append a client component to s.conf.RootWithoutClientComponent
|
||||||
|
// then that dataset is always going to be a placeholder.
|
||||||
|
// We don't want to burden users with the concept of placeholders if their `filesystems` filter on the sender
|
||||||
|
// doesn't introduce any gaps.
|
||||||
|
// Since the dataset hierarchy up to and including that client component dataset is still fully controlled by us,
|
||||||
|
// using `inherit` is going to make it work in all expected use cases.
|
||||||
|
return zfs.FilesystemPlaceholderCreateEncryptionInherit, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch s.conf.PlaceholderEncryption {
|
||||||
|
case PlaceholderCreationEncryptionPropertyUnspecified:
|
||||||
|
return 0, fmt.Errorf("placeholder filesystem encryption handling is unspecified in receiver config")
|
||||||
|
case PlaceholderCreationEncryptionPropertyInherit:
|
||||||
|
return zfs.FilesystemPlaceholderCreateEncryptionInherit, nil
|
||||||
|
case PlaceholderCreationEncryptionPropertyOff:
|
||||||
|
return zfs.FilesystemPlaceholderCreateEncryptionOff, nil
|
||||||
|
default:
|
||||||
|
panic(s.conf.PlaceholderEncryption)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Receiver) Receive(ctx context.Context, req *pdu.ReceiveReq, receive io.ReadCloser) (*pdu.ReceiveRes, error) {
|
func (s *Receiver) Receive(ctx context.Context, req *pdu.ReceiveReq, receive io.ReadCloser) (*pdu.ReceiveRes, error) {
|
||||||
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
|
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
|
||||||
|
|
||||||
@@ -708,15 +778,18 @@ func (s *Receiver) Receive(ctx context.Context, req *pdu.ReceiveReq, receive io.
|
|||||||
if v.Path.Equal(lp) {
|
if v.Path.Equal(lp) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
l := getLogger(ctx).
|
||||||
|
WithField("placeholder_fs", v.Path.ToString()).
|
||||||
|
WithField("receive_fs", lp.ToString())
|
||||||
|
|
||||||
ph, err := zfs.ZFSGetFilesystemPlaceholderState(ctx, v.Path)
|
ph, err := zfs.ZFSGetFilesystemPlaceholderState(ctx, v.Path)
|
||||||
getLogger(ctx).
|
l.WithField("placeholder_state", fmt.Sprintf("%#v", ph)).
|
||||||
WithField("fs", v.Path.ToString()).
|
|
||||||
WithField("placeholder_state", fmt.Sprintf("%#v", ph)).
|
|
||||||
WithField("err", fmt.Sprintf("%s", err)).
|
WithField("err", fmt.Sprintf("%s", err)).
|
||||||
WithField("errType", fmt.Sprintf("%T", err)).
|
WithField("errType", fmt.Sprintf("%T", err)).
|
||||||
Debug("placeholder state for filesystem")
|
Debug("get placeholder state for filesystem")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
visitErr = err
|
visitErr = errors.Wrapf(err, "cannot get placeholder state of %s", v.Path.ToString())
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -727,21 +800,34 @@ func (s *Receiver) Receive(ctx context.Context, req *pdu.ReceiveReq, receive io.
|
|||||||
} else {
|
} else {
|
||||||
visitErr = fmt.Errorf("root_fs %q does not exist", s.conf.RootWithoutClientComponent.ToString())
|
visitErr = fmt.Errorf("root_fs %q does not exist", s.conf.RootWithoutClientComponent.ToString())
|
||||||
}
|
}
|
||||||
getLogger(ctx).WithError(visitErr).Error("placeholders are only created automatically below root_fs")
|
l.WithError(visitErr).Error("placeholders are only created automatically below root_fs")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
l := getLogger(ctx).WithField("placeholder_fs", v.Path)
|
|
||||||
l.Debug("create placeholder filesystem")
|
// compute the value lazily so that users who don't rely on
|
||||||
err := zfs.ZFSCreatePlaceholderFilesystem(ctx, v.Path, v.Parent.Path)
|
// placeholders can use the default value PlaceholderCreationEncryptionPropertyUnspecified
|
||||||
|
placeholderEncryption, err := s.receive_GetPlaceholderCreationEncryptionValue(root, v.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.WithError(err).Error("cannot create placeholder filesystem")
|
l.WithError(err).Error("cannot create placeholder filesystem") // logger already contains path
|
||||||
visitErr = err
|
visitErr = errors.Wrapf(err, "cannot create placeholder filesystem %s", v.Path.ToString())
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
l := l.WithField("encryption", placeholderEncryption)
|
||||||
|
|
||||||
|
l.Debug("creating placeholder filesystem")
|
||||||
|
err = zfs.ZFSCreatePlaceholderFilesystem(ctx, v.Path, v.Parent.Path, placeholderEncryption)
|
||||||
|
if err != nil {
|
||||||
|
l.WithError(err).Error("cannot create placeholder filesystem") // logger already contains path
|
||||||
|
visitErr = errors.Wrapf(err, "cannot create placeholder filesystem %s", v.Path.ToString())
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
l.Info("created placeholder filesystem")
|
||||||
return true
|
return true
|
||||||
|
} else {
|
||||||
|
l.Debug("filesystem exists")
|
||||||
|
return true // leave this fs as is
|
||||||
}
|
}
|
||||||
getLogger(ctx).WithField("filesystem", v.Path.ToString()).Debug("exists")
|
|
||||||
return true // leave this fs as is
|
|
||||||
})
|
})
|
||||||
}()
|
}()
|
||||||
getLogger(ctx).WithField("visitErr", visitErr).Debug("complete tree-walk")
|
getLogger(ctx).WithField("visitErr", visitErr).Debug("complete tree-walk")
|
||||||
@@ -787,6 +873,9 @@ func (s *Receiver) Receive(ctx context.Context, req *pdu.ReceiveReq, receive io.
|
|||||||
return nil, errors.Wrap(err, "cannot determine whether we can use resumable send & recv")
|
return nil, errors.Wrap(err, "cannot determine whether we can use resumable send & recv")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// apply rate limit
|
||||||
|
receive = s.bwLimit.WrapReadCloser(receive)
|
||||||
|
|
||||||
var peek bytes.Buffer
|
var peek bytes.Buffer
|
||||||
var MaxPeek = envconst.Int64("ZREPL_ENDPOINT_RECV_PEEK_SIZE", 1<<20)
|
var MaxPeek = envconst.Int64("ZREPL_ENDPOINT_RECV_PEEK_SIZE", 1<<20)
|
||||||
log.WithField("max_peek_bytes", MaxPeek).Info("peeking incoming stream")
|
log.WithField("max_peek_bytes", MaxPeek).Info("peeking incoming stream")
|
||||||
|
|||||||
@@ -516,10 +516,12 @@ func (e ListAbstractionsErrors) Error() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ListAbstractions(ctx context.Context, query ListZFSHoldsAndBookmarksQuery) (out []Abstraction, outErrs []ListAbstractionsError, err error) {
|
func ListAbstractions(ctx context.Context, query ListZFSHoldsAndBookmarksQuery) (out []Abstraction, outErrs []ListAbstractionsError, err error) {
|
||||||
outChan, outErrsChan, err := ListAbstractionsStreamed(ctx, query)
|
outChan, outErrsChan, drainDone, err := ListAbstractionsStreamed(ctx, query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
defer drainDone()
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
@@ -540,19 +542,20 @@ func ListAbstractions(ctx context.Context, query ListZFSHoldsAndBookmarksQuery)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// if err != nil, the returned channels are both 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
|
// 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) {
|
// After draining is done, the caller must call the returned drainDone func.
|
||||||
|
func ListAbstractionsStreamed(ctx context.Context, query ListZFSHoldsAndBookmarksQuery) (_ <-chan Abstraction, _ <-chan ListAbstractionsError, drainDone func(), _ error) {
|
||||||
|
|
||||||
// impl note: structure the query processing in such a way that
|
// impl note: structure the query processing in such a way that
|
||||||
// a minimum amount of zfs shell-outs needs to be done
|
// a minimum amount of zfs shell-outs needs to be done
|
||||||
|
|
||||||
if err := query.Validate(); err != nil {
|
if err := query.Validate(); err != nil {
|
||||||
return nil, nil, errors.Wrap(err, "validate query")
|
return nil, nil, nil, errors.Wrap(err, "validate query")
|
||||||
}
|
}
|
||||||
|
|
||||||
fss, err := query.FS.Filesystems(ctx)
|
fss, err := query.FS.Filesystems(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, errors.Wrap(err, "list filesystems")
|
return nil, nil, nil, errors.Wrap(err, "list filesystems")
|
||||||
}
|
}
|
||||||
|
|
||||||
outErrs := make(chan ListAbstractionsError)
|
outErrs := make(chan ListAbstractionsError)
|
||||||
@@ -574,7 +577,6 @@ func ListAbstractionsStreamed(ctx context.Context, query ListZFSHoldsAndBookmark
|
|||||||
sem := semaphore.New(int64(query.Concurrency))
|
sem := semaphore.New(int64(query.Concurrency))
|
||||||
ctx, endTask := trace.WithTask(ctx, "list-abstractions-streamed-producer")
|
ctx, endTask := trace.WithTask(ctx, "list-abstractions-streamed-producer")
|
||||||
go func() {
|
go func() {
|
||||||
defer endTask()
|
|
||||||
defer close(out)
|
defer close(out)
|
||||||
defer close(outErrs)
|
defer close(outErrs)
|
||||||
|
|
||||||
@@ -596,7 +598,11 @@ func ListAbstractionsStreamed(ctx context.Context, query ListZFSHoldsAndBookmark
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
return out, outErrs, nil
|
drainDone = func() {
|
||||||
|
endTask()
|
||||||
|
}
|
||||||
|
|
||||||
|
return out, outErrs, drainDone, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func listAbstractionsImplFS(ctx context.Context, fs string, query *ListZFSHoldsAndBookmarksQuery, emitCandidate putListAbstraction, errCb putListAbstractionErr) {
|
func listAbstractionsImplFS(ctx context.Context, fs string, query *ListZFSHoldsAndBookmarksQuery, emitCandidate putListAbstraction, errCb putListAbstractionErr) {
|
||||||
@@ -813,7 +819,7 @@ func listStaleFiltering(abs []Abstraction, sinceBound *CreateTXGRangeBound) *Sta
|
|||||||
l := by[k]
|
l := by[k]
|
||||||
|
|
||||||
if k.Type == AbstractionStepHold {
|
if k.Type == AbstractionStepHold {
|
||||||
// all older than the most recent cursor are stale, others are always live
|
// all step holds older than the most recent cursor are stale, others are always live
|
||||||
|
|
||||||
// if we don't have a replication cursor yet, use untilBound = nil
|
// if we don't have a replication cursor yet, use untilBound = nil
|
||||||
// to consider all steps stale (...at first)
|
// to consider all steps stale (...at first)
|
||||||
@@ -821,11 +827,10 @@ func listStaleFiltering(abs []Abstraction, sinceBound *CreateTXGRangeBound) *Sta
|
|||||||
{
|
{
|
||||||
sfnsc := stepFirstNotStaleCandidates[k.fsAndJobId]
|
sfnsc := stepFirstNotStaleCandidates[k.fsAndJobId]
|
||||||
|
|
||||||
// if there's a replication cursor, use it as a cutoff between live and stale
|
// 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
|
// for both cursors and holds.
|
||||||
// the most recent step hold live, since that's what our initial replication strategy
|
// If there's no cursor, we are in initial replication and only need to keep
|
||||||
// uses (both initially and on resume)
|
// the most recent step hold live, which hold the .To of the initial send step.
|
||||||
// (FIXME hardcoded replication strategy)
|
|
||||||
if sfnsc.cursor != nil {
|
if sfnsc.cursor != nil {
|
||||||
untilBound = &CreateTXGRangeBound{
|
untilBound = &CreateTXGRangeBound{
|
||||||
CreateTXG: (*sfnsc.cursor).GetCreateTXG(),
|
CreateTXG: (*sfnsc.cursor).GetCreateTXG(),
|
||||||
@@ -858,7 +863,7 @@ func listStaleFiltering(abs []Abstraction, sinceBound *CreateTXGRangeBound) *Sta
|
|||||||
}
|
}
|
||||||
|
|
||||||
} else if k.Type == AbstractionReplicationCursorBookmarkV2 || k.Type == AbstractionLastReceivedHold {
|
} else if k.Type == AbstractionReplicationCursorBookmarkV2 || k.Type == AbstractionLastReceivedHold {
|
||||||
// all but the most recent are stale by definition (we always _move_ them)
|
// all cursors but the most recent cursor are stale by definition (we always _move_ them)
|
||||||
// NOTE: must not use firstNotStale in this branch, not computed for these types
|
// NOTE: must not use firstNotStale in this branch, not computed for these types
|
||||||
|
|
||||||
// sort descending (highest createtxg first), then cut off
|
// sort descending (highest createtxg first), then cut off
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
// Code generated by "enumer -type=PlaceholderCreationEncryptionProperty -transform=kebab -trimprefix=PlaceholderCreationEncryptionProperty"; DO NOT EDIT.
|
||||||
|
|
||||||
|
//
|
||||||
|
package endpoint
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
_PlaceholderCreationEncryptionPropertyName_0 = "unspecifiedinherit"
|
||||||
|
_PlaceholderCreationEncryptionPropertyName_1 = "off"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
_PlaceholderCreationEncryptionPropertyIndex_0 = [...]uint8{0, 11, 18}
|
||||||
|
_PlaceholderCreationEncryptionPropertyIndex_1 = [...]uint8{0, 3}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (i PlaceholderCreationEncryptionProperty) String() string {
|
||||||
|
switch {
|
||||||
|
case 1 <= i && i <= 2:
|
||||||
|
i -= 1
|
||||||
|
return _PlaceholderCreationEncryptionPropertyName_0[_PlaceholderCreationEncryptionPropertyIndex_0[i]:_PlaceholderCreationEncryptionPropertyIndex_0[i+1]]
|
||||||
|
case i == 4:
|
||||||
|
return _PlaceholderCreationEncryptionPropertyName_1
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("PlaceholderCreationEncryptionProperty(%d)", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _PlaceholderCreationEncryptionPropertyValues = []PlaceholderCreationEncryptionProperty{1, 2, 4}
|
||||||
|
|
||||||
|
var _PlaceholderCreationEncryptionPropertyNameToValueMap = map[string]PlaceholderCreationEncryptionProperty{
|
||||||
|
_PlaceholderCreationEncryptionPropertyName_0[0:11]: 1,
|
||||||
|
_PlaceholderCreationEncryptionPropertyName_0[11:18]: 2,
|
||||||
|
_PlaceholderCreationEncryptionPropertyName_1[0:3]: 4,
|
||||||
|
}
|
||||||
|
|
||||||
|
// PlaceholderCreationEncryptionPropertyString retrieves an enum value from the enum constants string name.
|
||||||
|
// Throws an error if the param is not part of the enum.
|
||||||
|
func PlaceholderCreationEncryptionPropertyString(s string) (PlaceholderCreationEncryptionProperty, error) {
|
||||||
|
if val, ok := _PlaceholderCreationEncryptionPropertyNameToValueMap[s]; ok {
|
||||||
|
return val, nil
|
||||||
|
}
|
||||||
|
return 0, fmt.Errorf("%s does not belong to PlaceholderCreationEncryptionProperty values", s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PlaceholderCreationEncryptionPropertyValues returns all values of the enum
|
||||||
|
func PlaceholderCreationEncryptionPropertyValues() []PlaceholderCreationEncryptionProperty {
|
||||||
|
return _PlaceholderCreationEncryptionPropertyValues
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsAPlaceholderCreationEncryptionProperty returns "true" if the value is listed in the enum definition. "false" otherwise
|
||||||
|
func (i PlaceholderCreationEncryptionProperty) IsAPlaceholderCreationEncryptionProperty() bool {
|
||||||
|
for _, v := range _PlaceholderCreationEncryptionPropertyValues {
|
||||||
|
if i == v {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
@@ -8,21 +8,19 @@ require (
|
|||||||
github.com/gdamore/tcell/v2 v2.2.0
|
github.com/gdamore/tcell/v2 v2.2.0
|
||||||
github.com/gitchander/permutation v0.0.0-20181107151852-9e56b92e9909
|
github.com/gitchander/permutation v0.0.0-20181107151852-9e56b92e9909
|
||||||
github.com/go-logfmt/logfmt v0.4.0
|
github.com/go-logfmt/logfmt v0.4.0
|
||||||
github.com/go-playground/universal-translator v0.17.0 // indirect
|
|
||||||
github.com/go-playground/validator v9.31.0+incompatible
|
github.com/go-playground/validator v9.31.0+incompatible
|
||||||
github.com/go-playground/validator/v10 v10.4.1
|
github.com/go-playground/validator/v10 v10.4.1
|
||||||
github.com/go-sql-driver/mysql v1.4.1-0.20190907122137-b2c03bcae3d4
|
github.com/go-sql-driver/mysql v1.4.1-0.20190907122137-b2c03bcae3d4
|
||||||
github.com/golang/protobuf v1.4.3
|
github.com/golang/protobuf v1.4.3
|
||||||
github.com/google/uuid v1.1.2
|
github.com/google/uuid v1.1.2
|
||||||
github.com/jinzhu/copier v0.0.0-20170922082739-db4671f3a9b8
|
github.com/jinzhu/copier v0.0.0-20170922082739-db4671f3a9b8
|
||||||
github.com/kisielk/gotool v1.0.0 // indirect
|
github.com/juju/ratelimit v1.0.1
|
||||||
|
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88 // indirect
|
||||||
github.com/kr/pretty v0.1.0
|
github.com/kr/pretty v0.1.0
|
||||||
github.com/leodido/go-urn v1.2.1 // indirect
|
github.com/leodido/go-urn v1.2.1 // indirect
|
||||||
github.com/lib/pq v1.2.0
|
github.com/lib/pq v1.2.0
|
||||||
github.com/mattn/go-colorable v0.1.4 // indirect
|
github.com/mattn/go-colorable v0.1.4 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.8
|
github.com/mattn/go-isatty v0.0.8
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // // go1.12 mod tidy adds this dependency as 'indirect', but go1.13 mod tidy removes it if the trailing comment is 'indirect' => add this comment to make the build work without changing go.mod on both go1.12 and go1.13
|
|
||||||
github.com/modern-go/reflect2 v1.0.1 // go1.12 mod tidy adds this dependency as 'indirect', but go1.13 mod tidy removes it if the trailing comment is 'indirect' => add this comment to make the build work without changing go.mod on both go1.12 and go1.13
|
|
||||||
github.com/montanaflynn/stats v0.5.0
|
github.com/montanaflynn/stats v0.5.0
|
||||||
github.com/onsi/ginkgo v1.10.2 // indirect
|
github.com/onsi/ginkgo v1.10.2 // indirect
|
||||||
github.com/onsi/gomega v1.7.0 // indirect
|
github.com/onsi/gomega v1.7.0 // indirect
|
||||||
@@ -31,22 +29,23 @@ require (
|
|||||||
github.com/problame/go-netssh v0.0.0-20200601114649-26439f9f0dc5
|
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/prometheus/common v0.7.0
|
github.com/prometheus/common v0.7.0
|
||||||
github.com/sergi/go-diff v1.0.1-0.20180205163309-da645544ed44 // go1.12 thinks it needs this
|
github.com/robfig/cron/v3 v3.0.1
|
||||||
|
github.com/sergi/go-diff v1.0.1-0.20180205163309-da645544ed44 // indirect; go1.12 thinks it needs this
|
||||||
github.com/spf13/cobra v0.0.2
|
github.com/spf13/cobra v0.0.2
|
||||||
github.com/spf13/pflag v1.0.5
|
github.com/spf13/pflag v1.0.5
|
||||||
github.com/stretchr/testify v1.6.1
|
github.com/stretchr/testify v1.6.1
|
||||||
github.com/willf/bitset v1.1.10
|
github.com/willf/bitset v1.1.10
|
||||||
github.com/yudai/gojsondiff v0.0.0-20170107030110-7b1b7adf999d
|
github.com/yudai/gojsondiff v0.0.0-20170107030110-7b1b7adf999d
|
||||||
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // go1.12 thinks it needs this
|
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // indirect; go1.12 thinks it needs this
|
||||||
|
github.com/yudai/pp v2.0.1+incompatible // indirect
|
||||||
github.com/zrepl/yaml-config v0.0.0-20191220194647-cbb6b0cf4bdd
|
github.com/zrepl/yaml-config v0.0.0-20191220194647-cbb6b0cf4bdd
|
||||||
gitlab.com/tslocum/cview v1.5.3
|
gitlab.com/tslocum/cview v1.5.3
|
||||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777
|
golang.org/x/net v0.0.0-20210119194325-5f4716e94777
|
||||||
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-20210124154548-22da62e12c0c
|
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c
|
||||||
golang.org/x/text v0.3.5 // indirect
|
|
||||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135
|
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135
|
||||||
gonum.org/v1/gonum v0.7.0 // indirect
|
|
||||||
google.golang.org/genproto v0.0.0-20210122163508-8081c04a3579 // indirect
|
google.golang.org/genproto v0.0.0-20210122163508-8081c04a3579 // indirect
|
||||||
google.golang.org/grpc v1.35.0
|
google.golang.org/grpc v1.35.0
|
||||||
google.golang.org/protobuf v1.25.0
|
google.golang.org/protobuf v1.25.0
|
||||||
|
gopkg.in/go-playground/assert.v1 v1.2.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,20 +1,12 @@
|
|||||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
github.com/DATA-DOG/go-sqlmock v1.3.3 h1:CWUqKXe0s8A2z6qCgkP4Kru7wC11YoAnoupUKFDnH08=
|
|
||||||
github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
|
github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
|
||||||
github.com/OpenPeeDeeP/depguard v0.0.0-20180806142446-a69c782687b2/go.mod h1:7/4sitnI9YlQgTLLk734QlzXT8DuHVnAyztLplQjk+o=
|
|
||||||
github.com/OpenPeeDeeP/depguard v0.0.0-20181229194401-1f388ab2d810/go.mod h1:7/4sitnI9YlQgTLLk734QlzXT8DuHVnAyztLplQjk+o=
|
|
||||||
github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
|
|
||||||
github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
|
|
||||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
|
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
|
||||||
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=
|
||||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4 h1:Hs82Z41s6SdL1CELW+XaDYmOH4hkBN4/N9og/AsOv7E=
|
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4 h1:Hs82Z41s6SdL1CELW+XaDYmOH4hkBN4/N9og/AsOv7E=
|
||||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||||
github.com/alvaroloes/enumer v1.1.1/go.mod h1:FxrjvuXoDAx9isTJrv4c+T410zFi0DtXIT0m65DJ+Wo=
|
|
||||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
|
||||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0=
|
|
||||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
@@ -24,9 +16,6 @@ github.com/cespare/xxhash/v2 v2.1.0 h1:yTUvW7Vhb89inJ+8irsUqiWjh8iT6sQPZiQzI6ReG
|
|||||||
github.com/cespare/xxhash/v2 v2.1.0/go.mod h1:dgIUBU3pDso/gPgZ1osOZ0iQf77oPR28Tjxl5dIMyVM=
|
github.com/cespare/xxhash/v2 v2.1.0/go.mod h1:dgIUBU3pDso/gPgZ1osOZ0iQf77oPR28Tjxl5dIMyVM=
|
||||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||||
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||||
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
|
||||||
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
|
|
||||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
@@ -34,10 +23,8 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF
|
|||||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||||
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=
|
||||||
@@ -46,24 +33,18 @@ github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdk
|
|||||||
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
|
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
|
||||||
github.com/gdamore/tcell v1.2.0 h1:ikixzsxc8K8o3V2/CEmyoEW8mJZaNYQQ3NP3VIQdUe4=
|
github.com/gdamore/tcell v1.2.0 h1:ikixzsxc8K8o3V2/CEmyoEW8mJZaNYQQ3NP3VIQdUe4=
|
||||||
github.com/gdamore/tcell v1.2.0/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebKS4zMM=
|
github.com/gdamore/tcell v1.2.0/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebKS4zMM=
|
||||||
github.com/gdamore/tcell v1.4.0 h1:vUnHwJRvcPQa3tzi+0QI4U9JINXYJlOz9yiaiPQ2wMU=
|
|
||||||
github.com/gdamore/tcell/v2 v2.0.0-dev/go.mod h1:vSVL/GV5mCSlPC6thFP5kfOFdM9MGZcalipmpTxTgQA=
|
github.com/gdamore/tcell/v2 v2.0.0-dev/go.mod h1:vSVL/GV5mCSlPC6thFP5kfOFdM9MGZcalipmpTxTgQA=
|
||||||
github.com/gdamore/tcell/v2 v2.1.1-0.20201225194624-29bb185874fd/go.mod h1:vSVL/GV5mCSlPC6thFP5kfOFdM9MGZcalipmpTxTgQA=
|
github.com/gdamore/tcell/v2 v2.1.1-0.20201225194624-29bb185874fd/go.mod h1:vSVL/GV5mCSlPC6thFP5kfOFdM9MGZcalipmpTxTgQA=
|
||||||
github.com/gdamore/tcell/v2 v2.2.0 h1:vSyEgKwraXPSOkvCk7IwOSyX+Pv3V2cV9CikJMXg4U4=
|
github.com/gdamore/tcell/v2 v2.2.0 h1:vSyEgKwraXPSOkvCk7IwOSyX+Pv3V2cV9CikJMXg4U4=
|
||||||
github.com/gdamore/tcell/v2 v2.2.0/go.mod h1:cTTuF84Dlj/RqmaCIV5p4w8uG1zWdk0SF6oBpwHp4fU=
|
github.com/gdamore/tcell/v2 v2.2.0/go.mod h1:cTTuF84Dlj/RqmaCIV5p4w8uG1zWdk0SF6oBpwHp4fU=
|
||||||
github.com/gitchander/permutation v0.0.0-20181107151852-9e56b92e9909 h1:9NC8seTx6/zRmMTAdsHj/uOMi0EGHGQtjyLafBjk77Q=
|
github.com/gitchander/permutation v0.0.0-20181107151852-9e56b92e9909 h1:9NC8seTx6/zRmMTAdsHj/uOMi0EGHGQtjyLafBjk77Q=
|
||||||
github.com/gitchander/permutation v0.0.0-20181107151852-9e56b92e9909/go.mod h1:lP+DW8LR6Rw3ru9Vo2/y/3iiLaLWmofYql/va+7zJOk=
|
github.com/gitchander/permutation v0.0.0-20181107151852-9e56b92e9909/go.mod h1:lP+DW8LR6Rw3ru9Vo2/y/3iiLaLWmofYql/va+7zJOk=
|
||||||
github.com/go-critic/go-critic v0.3.4/go.mod h1:AHR42Lk/E/aOznsrYdMYeIQS5RH10HZHSqP+rD6AJrc=
|
|
||||||
github.com/go-critic/go-critic v0.3.5-0.20190526074819-1df300866540/go.mod h1:+sE8vrLDS2M0pZkBk0wy6+nLdKexVDrl/jBqQOTDThA=
|
|
||||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||||
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||||
github.com/go-lintpack/lintpack v0.5.1/go.mod h1:NwZuYi2nUHho8XEIZ6SIxihrnPoqBTDqfpXvXAN0sXM=
|
|
||||||
github.com/go-lintpack/lintpack v0.5.2/go.mod h1:NwZuYi2nUHho8XEIZ6SIxihrnPoqBTDqfpXvXAN0sXM=
|
|
||||||
github.com/go-logfmt/logfmt v0.3.0 h1:8HUsc87TaSWLKwrnumgC8/YconD2fJQsRJAsWaPg2ic=
|
|
||||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||||
github.com/go-logfmt/logfmt v0.4.0 h1:MP4Eh7ZCb31lleYCFuwm0oe4/YGak+5l1vA2NOE80nA=
|
github.com/go-logfmt/logfmt v0.4.0 h1:MP4Eh7ZCb31lleYCFuwm0oe4/YGak+5l1vA2NOE80nA=
|
||||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||||
github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8=
|
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
|
||||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
|
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
|
||||||
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||||
@@ -76,86 +57,30 @@ github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn
|
|||||||
github.com/go-sql-driver/mysql v1.4.1-0.20190907122137-b2c03bcae3d4 h1:0suja/iKSDbEIYLbrS/8C7iArJiWpgCNcR+zwAHu7Ig=
|
github.com/go-sql-driver/mysql v1.4.1-0.20190907122137-b2c03bcae3d4 h1:0suja/iKSDbEIYLbrS/8C7iArJiWpgCNcR+zwAHu7Ig=
|
||||||
github.com/go-sql-driver/mysql v1.4.1-0.20190907122137-b2c03bcae3d4/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
github.com/go-sql-driver/mysql v1.4.1-0.20190907122137-b2c03bcae3d4/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||||
github.com/go-toolsmith/astcast v0.0.0-20181028201508-b7a89ed70af1/go.mod h1:TEo3Ghaj7PsZawQHxT/oBvo4HK/sl1RcuUHDKTTju+o=
|
|
||||||
github.com/go-toolsmith/astcast v1.0.0/go.mod h1:mt2OdQTeAQcY4DQgPSArJjHCcOwlX+Wl/kwN+LbLGQ4=
|
|
||||||
github.com/go-toolsmith/astcopy v0.0.0-20180903214859-79b422d080c4/go.mod h1:c9CPdq2AzM8oPomdlPniEfPAC6g1s7NqZzODt8y6ib8=
|
|
||||||
github.com/go-toolsmith/astcopy v1.0.0/go.mod h1:vrgyG+5Bxrnz4MZWPF+pI4R8h3qKRjjyvV/DSez4WVQ=
|
|
||||||
github.com/go-toolsmith/astequal v0.0.0-20180903214952-dcb477bfacd6/go.mod h1:H+xSiq0+LtiDC11+h1G32h7Of5O3CYFJ99GVbS5lDKY=
|
|
||||||
github.com/go-toolsmith/astequal v1.0.0/go.mod h1:H+xSiq0+LtiDC11+h1G32h7Of5O3CYFJ99GVbS5lDKY=
|
|
||||||
github.com/go-toolsmith/astfmt v0.0.0-20180903215011-8f8ee99c3086/go.mod h1:mP93XdblcopXwlyN4X4uodxXQhldPGZbcEJIimQHrkg=
|
|
||||||
github.com/go-toolsmith/astfmt v1.0.0/go.mod h1:cnWmsOAuq4jJY6Ct5YWlVLmcmLMn1JUPuQIHCY7CJDw=
|
|
||||||
github.com/go-toolsmith/astinfo v0.0.0-20180906194353-9809ff7efb21/go.mod h1:dDStQCHtmZpYOmjRP/8gHHnCCch3Zz3oEgCdZVdtweU=
|
|
||||||
github.com/go-toolsmith/astp v0.0.0-20180903215135-0af7e3c24f30/go.mod h1:SV2ur98SGypH1UjcPpCatrV5hPazG6+IfNHbkDXBRrk=
|
|
||||||
github.com/go-toolsmith/astp v1.0.0/go.mod h1:RSyrtpVlfTFGDYRbrjyWP1pYu//tSFcvdYrA8meBmLI=
|
|
||||||
github.com/go-toolsmith/pkgload v0.0.0-20181119091011-e9e65178eee8/go.mod h1:WoMrjiy4zvdS+Bg6z9jZH82QXwkcgCBX6nOfnmdaHks=
|
|
||||||
github.com/go-toolsmith/pkgload v1.0.0/go.mod h1:5eFArkbO80v7Z0kdngIxsRXRMTaX4Ilcwuh3clNrQJc=
|
|
||||||
github.com/go-toolsmith/strparse v0.0.0-20180903215201-830b6daa1241/go.mod h1:YI2nUKP9YGZnL/L1/DLFBfixrcjslWct4wyljWhSRy8=
|
|
||||||
github.com/go-toolsmith/strparse v1.0.0/go.mod h1:YI2nUKP9YGZnL/L1/DLFBfixrcjslWct4wyljWhSRy8=
|
|
||||||
github.com/go-toolsmith/typep v0.0.0-20181030061450-d63dc7650676/go.mod h1:JSQCQMUPdRlMZFswiq3TGpNp1GMktqkR2Ns5AIQkATU=
|
|
||||||
github.com/go-toolsmith/typep v1.0.0/go.mod h1:JSQCQMUPdRlMZFswiq3TGpNp1GMktqkR2Ns5AIQkATU=
|
|
||||||
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/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
|
||||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
|
|
||||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
github.com/golang/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/mock v1.0.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
|
||||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
|
||||||
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
|
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
|
|
||||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||||
github.com/golang/protobuf v1.4.1 h1:ZFgWrT+bLgsYPirOnRfKLYJLvssAegOj/hgyMFdJZe0=
|
|
||||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||||
github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
|
|
||||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||||
github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM=
|
github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM=
|
||||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||||
github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2/go.mod h1:k9Qvh+8juN+UKMCS/3jFtGICgW8O96FVaZsaxdzDkR4=
|
|
||||||
github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a/go.mod h1:ryS0uhF+x9jgbj/N71xsEqODy9BN81/GonCZiOzirOk=
|
|
||||||
github.com/golangci/errcheck v0.0.0-20181223084120-ef45e06d44b6/go.mod h1:DbHgvLiFKX1Sh2T1w8Q/h4NAI8MHIpzCdnBUDTXU3I0=
|
|
||||||
github.com/golangci/go-misc v0.0.0-20180628070357-927a3d87b613/go.mod h1:SyvUF2NxV+sN8upjjeVYr5W7tyxaT1JVtvhKhOn2ii8=
|
|
||||||
github.com/golangci/go-tools v0.0.0-20190124090046-35a9f45a5db0/go.mod h1:unzUULGw35sjyOYjUt0jMTXqHlZPpPc6e+xfO4cd6mM=
|
|
||||||
github.com/golangci/go-tools v0.0.0-20190318060251-af6baa5dc196/go.mod h1:unzUULGw35sjyOYjUt0jMTXqHlZPpPc6e+xfO4cd6mM=
|
|
||||||
github.com/golangci/goconst v0.0.0-20180610141641-041c5f2b40f3/go.mod h1:JXrF4TWy4tXYn62/9x8Wm/K/dm06p8tCKwFRDPZG/1o=
|
|
||||||
github.com/golangci/gocyclo v0.0.0-20180528134321-2becd97e67ee/go.mod h1:ozx7R9SIwqmqf5pRP90DhR2Oay2UIjGuKheCBCNwAYU=
|
|
||||||
github.com/golangci/gocyclo v0.0.0-20180528144436-0a533e8fa43d/go.mod h1:ozx7R9SIwqmqf5pRP90DhR2Oay2UIjGuKheCBCNwAYU=
|
|
||||||
github.com/golangci/gofmt v0.0.0-20181222123516-0b8337e80d98/go.mod h1:9qCChq59u/eW8im404Q2WWTrnBUQKjpNYKMbU4M7EFU=
|
|
||||||
github.com/golangci/golangci-lint v1.17.1/go.mod h1:+5sJSl2h3aly+fpmL2meSP8CaSKua2E4Twi9LPy7b1g=
|
|
||||||
github.com/golangci/gosec v0.0.0-20180901114220-8afd9cbb6cfb/go.mod h1:ON/c2UR0VAAv6ZEAFKhjCLplESSmRFfZcDLASbI1GWo=
|
|
||||||
github.com/golangci/gosec v0.0.0-20190211064107-66fb7fc33547/go.mod h1:0qUabqiIQgfmlAmulqxyiGkkyF6/tOGSnY2cnPVwrzU=
|
|
||||||
github.com/golangci/ineffassign v0.0.0-20190609212857-42439a7714cc/go.mod h1:e5tpTHCfVze+7EpLEozzMB3eafxo2KT5veNg1k6byQU=
|
|
||||||
github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219/go.mod h1:/X8TswGSh1pIozq4ZwCfxS0WA5JGXguxk94ar/4c87Y=
|
|
||||||
github.com/golangci/lint-1 v0.0.0-20190420132249-ee948d087217/go.mod h1:66R6K6P6VWk9I95jvqGxkqJxVWGFy9XlDwLwVz1RCFg=
|
|
||||||
github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca/go.mod h1:tvlJhZqDe4LMs4ZHD0oMUlt9G2LWuDGoisJTBzLMV9o=
|
|
||||||
github.com/golangci/misspell v0.0.0-20180809174111-950f5d19e770/go.mod h1:dEbvlSfYbMQDtrpRMQU675gSDLDNa8sCPPChZ7PhiVA=
|
|
||||||
github.com/golangci/misspell v0.3.4/go.mod h1:dEbvlSfYbMQDtrpRMQU675gSDLDNa8sCPPChZ7PhiVA=
|
|
||||||
github.com/golangci/prealloc v0.0.0-20180630174525-215b22d4de21/go.mod h1:tf5+bzsHdTM0bsB7+8mt0GUMvjCgwLpTapNZHU8AajI=
|
|
||||||
github.com/golangci/revgrep v0.0.0-20180526074752-d9c87f5ffaf0/go.mod h1:qOQCunEYvmd/TLamH+7LlVccLvUH5kZNhbCgTHoBbp4=
|
|
||||||
github.com/golangci/revgrep v0.0.0-20180812185044-276a5c0a1039/go.mod h1:qOQCunEYvmd/TLamH+7LlVccLvUH5kZNhbCgTHoBbp4=
|
|
||||||
github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4/go.mod h1:Izgrg8RkN3rCIMLGE9CyYmU9pY2Jer6DgANEnZ/L/cQ=
|
|
||||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||||
github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
|
|
||||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w=
|
||||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
|
|
||||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
|
||||||
github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
|
github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
|
||||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/gostaticanalysis/analysisutil v0.0.0-20190318220348-4088753ea4d3/go.mod h1:eEOZF4jCKGi+aprrirO9e7WKB3beBRtWgqGunKl6pKE=
|
|
||||||
github.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w=
|
|
||||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
|
||||||
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
||||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||||
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
||||||
@@ -164,20 +89,13 @@ github.com/jinzhu/copier v0.0.0-20170922082739-db4671f3a9b8 h1:+dKzeuiDYbD/Cfi/s
|
|||||||
github.com/jinzhu/copier v0.0.0-20170922082739-db4671f3a9b8/go.mod h1:yL958EeXv8Ylng6IfnvG4oflryUi3vgA3xPs9hmII1s=
|
github.com/jinzhu/copier v0.0.0-20170922082739-db4671f3a9b8/go.mod h1:yL958EeXv8Ylng6IfnvG4oflryUi3vgA3xPs9hmII1s=
|
||||||
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/juju/ratelimit v1.0.1 h1:+7AIFJVQ0EQgq/K9+0Krm7m530Du7tIz0METWzN0RgY=
|
||||||
|
github.com/juju/ratelimit v1.0.1/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk=
|
||||||
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/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
|
||||||
github.com/kisielk/gotool v0.0.0-20161130080628-0de1eaf82fa3/go.mod h1:jxZFDH7ILpTPQTk+E2s+z4CUas9lVNjIuKR4c5/zKgM=
|
|
||||||
github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg=
|
|
||||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
|
||||||
github.com/klauspost/compress v1.4.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
|
||||||
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
|
||||||
github.com/klauspost/cpuid v0.0.0-20180405133222-e7e905edc00e/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
|
||||||
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
|
||||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY=
|
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY=
|
||||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||||
@@ -190,60 +108,32 @@ github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
|
|||||||
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
||||||
github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0=
|
github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0=
|
||||||
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||||
github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
|
|
||||||
github.com/lucasb-eyer/go-colorful v1.0.2 h1:mCMFu6PgSozg9tDNMMK3g18oJBX7oYGrC09mS6CXfO4=
|
|
||||||
github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s=
|
github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s=
|
||||||
github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac=
|
github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac=
|
||||||
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
github.com/magiconair/properties v1.7.6/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
|
||||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
|
||||||
github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4=
|
|
||||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
|
||||||
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
|
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
|
||||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||||
github.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI=
|
|
||||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
|
||||||
github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE=
|
github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE=
|
||||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||||
github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y=
|
|
||||||
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||||
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||||
github.com/mattn/go-runewidth v0.0.10 h1:CoZ3S2P7pvtP45xOtBw+/mDL2z0RKI576gSkzRRpdGg=
|
github.com/mattn/go-runewidth v0.0.10 h1:CoZ3S2P7pvtP45xOtBw+/mDL2z0RKI576gSkzRRpdGg=
|
||||||
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||||
github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw=
|
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.0/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
|
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||||
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
|
||||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
|
||||||
github.com/mitchellh/go-ps v0.0.0-20170309133038-4fdf99ab2936/go.mod h1:r1VsdOzOPt1ZSrGZWFoNhsAedKnEd6r9Np1+5blZCWk=
|
|
||||||
github.com/mitchellh/mapstructure v0.0.0-20180220230111-00c29f56e238/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
|
||||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||||
github.com/montanaflynn/stats v0.5.0 h1:2EkzeTSqBB4V4bJwWrt5gIIrZmpJBcoIRGS2kWLgzmk=
|
github.com/montanaflynn/stats v0.5.0 h1:2EkzeTSqBB4V4bJwWrt5gIIrZmpJBcoIRGS2kWLgzmk=
|
||||||
github.com/montanaflynn/stats v0.5.0/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
|
github.com/montanaflynn/stats v0.5.0/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
|
||||||
github.com/mozilla/tls-observatory v0.0.0-20180409132520-8791a200eb40/go.mod h1:SrKMQvPiws7F7iqYp8/TX+IhxCYhzr6N/1yb8cwHsGk=
|
|
||||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||||
github.com/nbutton23/zxcvbn-go v0.0.0-20160627004424-a22cb81b2ecd/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU=
|
|
||||||
github.com/nbutton23/zxcvbn-go v0.0.0-20171102151520-eafdab6b0663/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU=
|
|
||||||
github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
|
||||||
github.com/onsi/ginkgo v1.6.0 h1:Ix8l273rp3QzYgXSR+c8d1fTG7UPgYkOSELPhiY/YGw=
|
|
||||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
github.com/onsi/ginkgo v1.10.2 h1:uqH7bpe+ERSiDa34FDOF7RikN6RzXgduUF8yarlZp94=
|
github.com/onsi/ginkgo v1.10.2 h1:uqH7bpe+ERSiDa34FDOF7RikN6RzXgduUF8yarlZp94=
|
||||||
github.com/onsi/ginkgo v1.10.2/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/onsi/ginkgo v1.10.2/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
|
|
||||||
github.com/onsi/gomega v1.4.2 h1:3mYCb7aPxS/RU7TI1y4rkEn1oKmPRjNJLNEXgw7MH2I=
|
|
||||||
github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
|
||||||
github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME=
|
github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME=
|
||||||
github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||||
github.com/pascaldekloe/name v0.0.0-20180628100202-0fd16699aae1/go.mod h1:eD5JxqMiuNYyFNmyY9rkJ/slN8y59oEu4Ei7F8OoKWQ=
|
|
||||||
github.com/pelletier/go-toml v1.1.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
|
||||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
|
||||||
github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
|
|
||||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
@@ -251,138 +141,76 @@ github.com/pkg/profile v1.2.1 h1:F++O52m40owAmADcojzM+9gyjmMOY/T4oYJkgFDH8RE=
|
|||||||
github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=
|
github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/problame/go-netssh v0.0.0-20190110232351-09d6bc45d284 h1:GX8YFBfSL6GlKymQZOsQllPbxVamfEYpis7oaJLXYZA=
|
|
||||||
github.com/problame/go-netssh v0.0.0-20190110232351-09d6bc45d284/go.mod h1:XMR9fjP5/wMyFK0Ifk9ZHDFem4Rukm+ChvoYr2nxrgw=
|
|
||||||
github.com/problame/go-netssh v0.0.0-20191026123024-f34099f4f6b1 h1:HH8yzlaZq/A8xdJaSj/eu32yFFySWAJMTmOQPAGwfYg=
|
|
||||||
github.com/problame/go-netssh v0.0.0-20191026123024-f34099f4f6b1/go.mod h1:JRs3nyBjvOv/9YHC+Vlw9z1Jfzl5s5t0BNnTzmclj1c=
|
|
||||||
github.com/problame/go-netssh v0.0.0-20191209123953-18d8aa6923c7 h1:Oik8tLrLhoMq4fduDRuNNOAQ+q0M0dXkjAYLUf4+mAc=
|
|
||||||
github.com/problame/go-netssh v0.0.0-20191209123953-18d8aa6923c7/go.mod h1:Ba6RaFpK1+IHWs1wLZ6sCBuXkt4iAVLeOG5GinXHusM=
|
|
||||||
github.com/problame/go-netssh v0.0.0-20200601114649-26439f9f0dc5 h1:1eSrO2WAeSXjMol8WRKW9LekL1252VIMaKQwWkmEdvs=
|
github.com/problame/go-netssh v0.0.0-20200601114649-26439f9f0dc5 h1:1eSrO2WAeSXjMol8WRKW9LekL1252VIMaKQwWkmEdvs=
|
||||||
github.com/problame/go-netssh v0.0.0-20200601114649-26439f9f0dc5/go.mod h1:Ba6RaFpK1+IHWs1wLZ6sCBuXkt4iAVLeOG5GinXHusM=
|
github.com/problame/go-netssh v0.0.0-20200601114649-26439f9f0dc5/go.mod h1:Ba6RaFpK1+IHWs1wLZ6sCBuXkt4iAVLeOG5GinXHusM=
|
||||||
github.com/prometheus/client_golang v0.0.0-20180410130117-e11c6ff8170b/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
|
||||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
github.com/prometheus/client_golang 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=
|
||||||
github.com/prometheus/client_golang v1.2.1 h1:JnMpQc6ppsNgw9QPAGF6Dod479itz7lvlsMzzNayLOI=
|
github.com/prometheus/client_golang v1.2.1 h1:JnMpQc6ppsNgw9QPAGF6Dod479itz7lvlsMzzNayLOI=
|
||||||
github.com/prometheus/client_golang v1.2.1/go.mod h1:XMU6Z2MjaRKVu/dC1qupJI9SiNkDYzz3xecMgSW/F+U=
|
github.com/prometheus/client_golang v1.2.1/go.mod h1:XMU6Z2MjaRKVu/dC1qupJI9SiNkDYzz3xecMgSW/F+U=
|
||||||
github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
|
||||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM=
|
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM=
|
||||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||||
github.com/prometheus/common v0.0.0-20180413074202-d0f7cd64bda4/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
|
||||||
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||||
github.com/prometheus/common v0.7.0 h1:L+1lyG48J1zAQXA3RBX/nG/B3gjlHq0zTt2tlbJLyCY=
|
github.com/prometheus/common v0.7.0 h1:L+1lyG48J1zAQXA3RBX/nG/B3gjlHq0zTt2tlbJLyCY=
|
||||||
github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA=
|
github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA=
|
||||||
github.com/prometheus/procfs v0.0.0-20180408092902-8b1c2da0d56d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
|
||||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||||
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||||
github.com/prometheus/procfs v0.0.5 h1:3+auTFlqw+ZaQYJARz6ArODtkaIwtvBTx3N2NehQlL8=
|
github.com/prometheus/procfs v0.0.5 h1:3+auTFlqw+ZaQYJARz6ArODtkaIwtvBTx3N2NehQlL8=
|
||||||
github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
|
github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
|
||||||
github.com/quasilyte/go-consistent v0.0.0-20190521200055-c6f3937de18c/go.mod h1:5STLWrekHfjyYwxBRVRXNOSewLJ3PWfDJd1VyTS21fI=
|
|
||||||
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||||
github.com/rogpeppe/go-internal v1.2.1/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||||
github.com/ryanuber/go-glob v0.0.0-20170128012129-256dc444b735/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
|
|
||||||
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
|
|
||||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
|
||||||
github.com/sergi/go-diff v1.0.1-0.20180205163309-da645544ed44 h1:tB9NOR21++IjLyVx3/PCPhWMwqGNCMQEH96A6dMZ/gc=
|
github.com/sergi/go-diff v1.0.1-0.20180205163309-da645544ed44 h1:tB9NOR21++IjLyVx3/PCPhWMwqGNCMQEH96A6dMZ/gc=
|
||||||
github.com/sergi/go-diff v1.0.1-0.20180205163309-da645544ed44/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
github.com/sergi/go-diff v1.0.1-0.20180205163309-da645544ed44/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||||
github.com/shirou/gopsutil v0.0.0-20180427012116-c95755e4bcd7/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
|
|
||||||
github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc=
|
|
||||||
github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
|
|
||||||
github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=
|
|
||||||
github.com/sirupsen/logrus v1.0.5/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
|
|
||||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||||
github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
|
||||||
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
|
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
|
||||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||||
github.com/sourcegraph/go-diff v0.5.1/go.mod h1:j2dHj3m8aZgQO8lMTcTnBcXkRRRqi34cd2MNlA9u1mE=
|
|
||||||
github.com/spf13/afero v1.1.0/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
|
||||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
|
||||||
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
|
|
||||||
github.com/spf13/cast v1.2.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg=
|
|
||||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
|
||||||
github.com/spf13/cobra v0.0.2 h1:NfkwRbgViGoyjBKsLI0QMDcuMnhM+SBg3T0cGfpvKDE=
|
github.com/spf13/cobra v0.0.2 h1:NfkwRbgViGoyjBKsLI0QMDcuMnhM+SBg3T0cGfpvKDE=
|
||||||
github.com/spf13/cobra v0.0.2/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
github.com/spf13/cobra v0.0.2/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
||||||
github.com/spf13/jwalterweatherman v0.0.0-20180109140146-7c0cea34c8ec/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
|
||||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
|
||||||
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
|
|
||||||
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
|
||||||
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
|
|
||||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
|
||||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
github.com/spf13/viper v1.0.2/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM=
|
|
||||||
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/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=
|
||||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
|
|
||||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||||
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/theckman/goconstraint v1.11.0 h1:oBUwN5wpE4dwyPhRGraEgJsFTr+JtLWiDnaJZJeeXI0=
|
github.com/theckman/goconstraint v1.11.0 h1:oBUwN5wpE4dwyPhRGraEgJsFTr+JtLWiDnaJZJeeXI0=
|
||||||
github.com/theckman/goconstraint v1.11.0/go.mod h1:zkCR/f2kOULTk/h1ujgyB9BlCNLaqlQ6GN2Zl4mg81g=
|
github.com/theckman/goconstraint v1.11.0/go.mod h1:zkCR/f2kOULTk/h1ujgyB9BlCNLaqlQ6GN2Zl4mg81g=
|
||||||
github.com/timakin/bodyclose v0.0.0-20190407043127-4a873e97b2bb/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk=
|
|
||||||
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
|
||||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
|
||||||
github.com/valyala/fasthttp v1.2.0/go.mod h1:4vX61m6KN+xDduDNwXrhIAVZaZaZiQ1luJk8LWSxF3s=
|
|
||||||
github.com/valyala/quicktemplate v1.1.1/go.mod h1:EH+4AkTd43SvgIbQHYu59/cJyxDoOVRUAfrukLPuGJ4=
|
|
||||||
github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
|
|
||||||
github.com/willf/bitset v1.1.10 h1:NotGKqX0KwQ72NUzqrjZq5ipPNDQex9lo3WpaS8L2sc=
|
github.com/willf/bitset v1.1.10 h1:NotGKqX0KwQ72NUzqrjZq5ipPNDQex9lo3WpaS8L2sc=
|
||||||
github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
|
github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
|
||||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
|
||||||
github.com/yudai/gojsondiff v0.0.0-20170107030110-7b1b7adf999d h1:yJIizrfO599ot2kQ6Af1enICnwBD3XoxgX3MrMwot2M=
|
github.com/yudai/gojsondiff v0.0.0-20170107030110-7b1b7adf999d h1:yJIizrfO599ot2kQ6Af1enICnwBD3XoxgX3MrMwot2M=
|
||||||
github.com/yudai/gojsondiff v0.0.0-20170107030110-7b1b7adf999d/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg=
|
github.com/yudai/gojsondiff v0.0.0-20170107030110-7b1b7adf999d/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg=
|
||||||
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M=
|
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M=
|
||||||
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM=
|
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM=
|
||||||
github.com/yudai/pp v2.0.1+incompatible h1:Q4//iY4pNF6yPLZIigmvcl7k/bPgrcTPIFIcmawg5bI=
|
github.com/yudai/pp v2.0.1+incompatible h1:Q4//iY4pNF6yPLZIigmvcl7k/bPgrcTPIFIcmawg5bI=
|
||||||
github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc=
|
github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc=
|
||||||
github.com/zrepl/yaml-config v0.0.0-20190928121844-af7ca3f8448f h1:3MuiGfgMHCSwKUcsuI7ODbi50j+evTB7SsoOBMNC5Fk=
|
|
||||||
github.com/zrepl/yaml-config v0.0.0-20190928121844-af7ca3f8448f/go.mod h1:JmNwisZzOvW4GfpfLvhZ+gtyKLsIiA+WC+wNKJGJaFg=
|
|
||||||
github.com/zrepl/yaml-config v0.0.0-20191220194647-cbb6b0cf4bdd h1:SSo67WLS+99QESvbW8Meibz7zCrxshP71U9dH5KOCXM=
|
github.com/zrepl/yaml-config v0.0.0-20191220194647-cbb6b0cf4bdd h1:SSo67WLS+99QESvbW8Meibz7zCrxshP71U9dH5KOCXM=
|
||||||
github.com/zrepl/yaml-config v0.0.0-20191220194647-cbb6b0cf4bdd/go.mod h1:JmNwisZzOvW4GfpfLvhZ+gtyKLsIiA+WC+wNKJGJaFg=
|
github.com/zrepl/yaml-config v0.0.0-20191220194647-cbb6b0cf4bdd/go.mod h1:JmNwisZzOvW4GfpfLvhZ+gtyKLsIiA+WC+wNKJGJaFg=
|
||||||
github.com/zrepl/zrepl v0.2.0/go.mod h1:M3Zv2IGSO8iYpUjsZD6ayZ2LHy7zyMfzet9XatKOrZ8=
|
|
||||||
gitlab.com/tslocum/cbind v0.1.4 h1:cbZXPPcieXspk8cShoT6efz7HAT8yMNQcofYWNizis4=
|
gitlab.com/tslocum/cbind v0.1.4 h1:cbZXPPcieXspk8cShoT6efz7HAT8yMNQcofYWNizis4=
|
||||||
gitlab.com/tslocum/cbind v0.1.4/go.mod h1:RvwYE3auSjBNlCmWeGspzn+jdLUVQ8C2QGC+0nP9ChI=
|
gitlab.com/tslocum/cbind v0.1.4/go.mod h1:RvwYE3auSjBNlCmWeGspzn+jdLUVQ8C2QGC+0nP9ChI=
|
||||||
gitlab.com/tslocum/cview v1.5.3 h1:6OTCtIUp1EkfGeLqQFRHtW8ynMJ66BhoBwuW8oZ84AQ=
|
gitlab.com/tslocum/cview v1.5.3 h1:6OTCtIUp1EkfGeLqQFRHtW8ynMJ66BhoBwuW8oZ84AQ=
|
||||||
gitlab.com/tslocum/cview v1.5.3/go.mod h1:k/eLWRIF3B26VLDgtRRPkjLUXmcCsy+YCSPEAtNQgIY=
|
gitlab.com/tslocum/cview v1.5.3/go.mod h1:k/eLWRIF3B26VLDgtRRPkjLUXmcCsy+YCSPEAtNQgIY=
|
||||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
|
||||||
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
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-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2 h1:y102fOLFqhV41b+4GPiJoa0k/x+pJcEi2/HB1Y5T6fU=
|
|
||||||
golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
|
||||||
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
|
|
||||||
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
|
||||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
golang.org/x/net v0.0.0-20170915142106-8351a756f30f/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180911220305-26e67e76b6c3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980 h1:dfGZHvZk057jK2MCeWus/TowKpJ8y4AmooUzdBSR9GU=
|
|
||||||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777 h1:003p0dJM77cxMSyCPFphvZf/Y5/NXf5fzg6ufd1/Oew=
|
golang.org/x/net v0.0.0-20210119194325-5f4716e94777 h1:003p0dJM77cxMSyCPFphvZf/Y5/NXf5fzg6ufd1/Oew=
|
||||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
@@ -392,23 +220,16 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ
|
|||||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sys v0.0.0-20171026204733-164713f0dfce/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756 h1:9nuHUbU8dRnRRfj9KjWUVrJeoexdbeMjttk6Oh1rD10=
|
|
||||||
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20191010194322-b09406accb47 h1:/XfQ9z7ib8eEJX2hdgFTZJ/ntt0swNk5oYBziWeTCvY=
|
|
||||||
golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
|
|
||||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20201013132646-2da7054afaeb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201013132646-2da7054afaeb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210113181707-4bcb84eeeb78/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210113181707-4bcb84eeeb78/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
@@ -417,59 +238,30 @@ golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf h1:MZ2shdL+ZM/XzY3ZGOnh4Nlpnxz5GSOhOmtHo3iPU6M=
|
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf h1:MZ2shdL+ZM/XzY3ZGOnh4Nlpnxz5GSOhOmtHo3iPU6M=
|
||||||
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/text v0.0.0-20170915090833-1cbadb444a80/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
|
||||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ=
|
golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ=
|
||||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/tools v0.0.0-20170915040203-e531a2a1c15f/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
|
||||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
|
||||||
golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
|
||||||
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20181117154741-2ddaf7f79a09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
|
||||||
golang.org/x/tools v0.0.0-20181205014116-22934f0fdb62/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
|
||||||
golang.org/x/tools v0.0.0-20190110163146-51295c7ec13a/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
|
||||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20190121143147-24cd39ecf745/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
|
||||||
golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e h1:Io7mpb+aUAGF0MKxbyQ7HQl1VgB+cL6ZJZUFaFNqVV4=
|
|
||||||
golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
|
||||||
golang.org/x/tools v0.0.0-20190213192042-740235f6c0d8/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
|
||||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||||
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-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-20190524140312-2c0ae7006135 h1:5Beo0mZN8dRzgrMMkDp0jc8YXQKx9DiJ2k1dkvGsn5A=
|
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135 h1:5Beo0mZN8dRzgrMMkDp0jc8YXQKx9DiJ2k1dkvGsn5A=
|
||||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||||
golang.org/x/tools v0.0.0-20190524210228-3d17549cdc6b/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo=
|
|
||||||
gonum.org/v1/gonum v0.7.0 h1:Hdks0L0hgznZLG9nzXb8vZ0rRvqNvAcgAp84y7Mwkgw=
|
|
||||||
gonum.org/v1/gonum v0.7.0/go.mod h1:L02bwd0sqlsvRv41G7wGWFCsVNZFv/k1xzGIxeANHGM=
|
|
||||||
gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
|
|
||||||
gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc=
|
|
||||||
google.golang.org/appengine v1.1.0 h1:igQkv0AAhEIvTEpD5LIpAfav2eeVO9HBTjvKHVJPRSs=
|
|
||||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||||
|
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
|
||||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc=
|
|
||||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||||
google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898 h1:yvw+zsSmSM02Z5H3ZdEV7B7Ql7eFrjQTnmByJvK+3J8=
|
|
||||||
google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg=
|
|
||||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY=
|
|
||||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||||
google.golang.org/genproto v0.0.0-20210122163508-8081c04a3579 h1:Iwh0ba2kTgq2Q6mJiXhzrrjD7h11nEVnbMHFmp0/HsQ=
|
google.golang.org/genproto v0.0.0-20210122163508-8081c04a3579 h1:Iwh0ba2kTgq2Q6mJiXhzrrjD7h11nEVnbMHFmp0/HsQ=
|
||||||
google.golang.org/genproto v0.0.0-20210122163508-8081c04a3579/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
google.golang.org/genproto v0.0.0-20210122163508-8081c04a3579/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||||
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
|
|
||||||
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.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||||
google.golang.org/grpc v1.27.0 h1:rRYRFMVgRv6E0D70Skyfsr28tDXIuuPZyWGMPdMcnXg=
|
|
||||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||||
google.golang.org/grpc v1.35.0 h1:TwIQcH3es+MojMVojxxfQ3l3OF2KzlRxML2xZq0kRo8=
|
google.golang.org/grpc v1.35.0 h1:TwIQcH3es+MojMVojxxfQ3l3OF2KzlRxML2xZq0kRo8=
|
||||||
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||||
@@ -484,32 +276,20 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD
|
|||||||
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
|
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
|
||||||
google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
|
google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
|
||||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||||
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
|
|
||||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc=
|
gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc=
|
||||||
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 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/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
|
||||||
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
||||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||||
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
|
gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM=
|
||||||
|
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||||
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
|
||||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
|
||||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed/go.mod h1:Xkxe497xwlCKkIaQYRfC7CSLworTXY9RMqwhhCm+8Nc=
|
|
||||||
mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b/go.mod h1:2odslEg/xrtNQqCYg2/jCoyKnw3vv5biOc3JnIcYfL4=
|
|
||||||
mvdan.cc/unparam v0.0.0-20190209190245-fbb59629db34/go.mod h1:H6SUd1XjIs+qQCyskXg5OFSrilMRUkD8ePJpHKDPaeY=
|
|
||||||
mvdan.cc/unparam v0.0.0-20190310220240-1b9ccfa71afe/go.mod h1:BnhuWBAqxH3+J5bDybdxgw5ZfS+DsVd4iylsKQePN8o=
|
|
||||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
|
||||||
sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0=
|
|
||||||
sourcegraph.com/sqs/pbtypes v1.0.0/go.mod h1:3AciMUv4qUuRHRHhOG4TZOB+72GdPVz5k+c648qsFS4=
|
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
FROM debian:latest
|
FROM debian:latest
|
||||||
|
|
||||||
|
# binutils are for cross-compilation to work in bullseye
|
||||||
RUN apt-get update && apt-get install -y \
|
RUN apt-get update && apt-get install -y \
|
||||||
build-essential \
|
build-essential \
|
||||||
devscripts \
|
devscripts \
|
||||||
dh-exec
|
dh-exec \
|
||||||
|
binutils-aarch64-linux-gnu \
|
||||||
|
binutils-arm-linux-gnueabihf \
|
||||||
|
binutils-i686-linux-gnu
|
||||||
|
|
||||||
RUN mkdir -p /build/src && chmod -R 0777 /build
|
RUN mkdir -p /build/src && chmod -R 0777 /build
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"container/list"
|
||||||
"context"
|
"context"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -37,6 +38,7 @@ func main() {
|
|||||||
flag.StringVar(&args.CreateArgs.Mountpoint, "mountpoint", "", "")
|
flag.StringVar(&args.CreateArgs.Mountpoint, "mountpoint", "", "")
|
||||||
flag.BoolVar(&args.StopAndKeepPoolOnFail, "failure.stop-and-keep-pool", false, "if a test case fails, stop test execution and keep pool as it was when the test failed")
|
flag.BoolVar(&args.StopAndKeepPoolOnFail, "failure.stop-and-keep-pool", false, "if a test case fails, stop test execution and keep pool as it was when the test failed")
|
||||||
flag.StringVar(&args.Run, "run", "", "")
|
flag.StringVar(&args.Run, "run", "", "")
|
||||||
|
flag.DurationVar(&platformtest.ZpoolExportTimeout, "zfs.zpool-export-timeout", platformtest.ZpoolExportTimeout, "")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
if err := HarnessRun(args); err != nil {
|
if err := HarnessRun(args); err != nil {
|
||||||
@@ -52,6 +54,40 @@ type HarnessArgs struct {
|
|||||||
Run string
|
Run string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type invocation struct {
|
||||||
|
runFunc tests.Case
|
||||||
|
idstring string
|
||||||
|
result *testCaseResult
|
||||||
|
children map[string]*invocation
|
||||||
|
}
|
||||||
|
|
||||||
|
func newInvocation(runFunc tests.Case, id string) *invocation {
|
||||||
|
return &invocation{
|
||||||
|
runFunc: runFunc,
|
||||||
|
idstring: id,
|
||||||
|
children: make(map[string]*invocation),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *invocation) String() string {
|
||||||
|
idsuffix := ""
|
||||||
|
if i.idstring != "" {
|
||||||
|
idsuffix = fmt.Sprintf(": %s", i.idstring)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s%s", i.runFunc.String(), idsuffix)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *invocation) RegisterChild(c *invocation) error {
|
||||||
|
if c.idstring == "" {
|
||||||
|
return fmt.Errorf("child must have id string")
|
||||||
|
}
|
||||||
|
if oc := i.children[c.idstring]; oc != nil {
|
||||||
|
return fmt.Errorf("idstring %q is already taken by %s", c.idstring, oc)
|
||||||
|
}
|
||||||
|
i.children[c.idstring] = c
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func HarnessRun(args HarnessArgs) error {
|
func HarnessRun(args HarnessArgs) error {
|
||||||
|
|
||||||
runRE := regexp.MustCompile(args.Run)
|
runRE := regexp.MustCompile(args.Run)
|
||||||
@@ -78,21 +114,19 @@ func HarnessRun(args HarnessArgs) error {
|
|||||||
ctx = logging.WithLoggers(ctx, logging.SubsystemLoggersWithUniversalLogger(logger))
|
ctx = logging.WithLoggers(ctx, logging.SubsystemLoggersWithUniversalLogger(logger))
|
||||||
ex := platformtest.NewEx(logger)
|
ex := platformtest.NewEx(logger)
|
||||||
|
|
||||||
type invocation struct {
|
testQueue := list.New()
|
||||||
runFunc tests.Case
|
|
||||||
result *testCaseResult
|
|
||||||
}
|
|
||||||
|
|
||||||
invocations := make([]*invocation, 0, len(tests.Cases))
|
|
||||||
for _, c := range tests.Cases {
|
for _, c := range tests.Cases {
|
||||||
if runRE.MatchString(c.String()) {
|
if runRE.MatchString(c.String()) {
|
||||||
invocations = append(invocations, &invocation{runFunc: c})
|
testQueue.PushBack(newInvocation(c, ""))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, inv := range invocations {
|
completedTests := list.New()
|
||||||
|
|
||||||
bold.Printf("BEGIN TEST CASE %s\n", inv.runFunc.String())
|
for testQueue.Len() > 0 {
|
||||||
|
inv := testQueue.Remove(testQueue.Front()).(*invocation)
|
||||||
|
|
||||||
|
bold.Printf("BEGIN TEST CASE %s\n", inv)
|
||||||
|
|
||||||
pool, err := platformtest.CreateOrReplaceZpool(ctx, ex, args.CreateArgs)
|
pool, err := platformtest.CreateOrReplaceZpool(ctx, ex, args.CreateArgs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -102,6 +136,15 @@ func HarnessRun(args HarnessArgs) error {
|
|||||||
ctx := &platformtest.Context{
|
ctx := &platformtest.Context{
|
||||||
Context: ctx,
|
Context: ctx,
|
||||||
RootDataset: filepath.Join(pool.Name(), "rootds"),
|
RootDataset: filepath.Join(pool.Name(), "rootds"),
|
||||||
|
QueueSubtest: func(id string, stf func(*platformtest.Context)) {
|
||||||
|
stinv := newInvocation(stf, id)
|
||||||
|
err := inv.RegisterChild(stinv)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
bold.Printf(" QUEUING SUBTEST %q\n", id)
|
||||||
|
testQueue.PushFront(stinv)
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
res := runTestCase(ctx, ex, inv.runFunc)
|
res := runTestCase(ctx, ex, inv.runFunc)
|
||||||
@@ -119,6 +162,8 @@ func HarnessRun(args HarnessArgs) error {
|
|||||||
panic(fmt.Sprintf("error destroying test pool: %s", err))
|
panic(fmt.Sprintf("error destroying test pool: %s", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
completedTests.PushBack(inv)
|
||||||
|
|
||||||
if res.failed {
|
if res.failed {
|
||||||
boldRed.Printf("TEST FAILED\n")
|
boldRed.Printf("TEST FAILED\n")
|
||||||
} else if res.skipped {
|
} else if res.skipped {
|
||||||
@@ -135,7 +180,8 @@ func HarnessRun(args HarnessArgs) error {
|
|||||||
var summary struct {
|
var summary struct {
|
||||||
succ, fail, skip []*invocation
|
succ, fail, skip []*invocation
|
||||||
}
|
}
|
||||||
for _, inv := range invocations {
|
for completedTests.Len() > 0 {
|
||||||
|
inv := completedTests.Remove(completedTests.Front()).(*invocation)
|
||||||
var bucket *[]*invocation
|
var bucket *[]*invocation
|
||||||
if inv.result.failed {
|
if inv.result.failed {
|
||||||
bucket = &summary.fail
|
bucket = &summary.fail
|
||||||
@@ -156,7 +202,7 @@ func HarnessRun(args HarnessArgs) error {
|
|||||||
}
|
}
|
||||||
fmt.Printf("\n")
|
fmt.Printf("\n")
|
||||||
for _, inv := range bucket {
|
for _, inv := range bucket {
|
||||||
fmt.Printf(" %s\n", inv.runFunc.String())
|
fmt.Printf(" %s\n", inv)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
printBucket("PASSING TESTS", boldGreen, summary.succ)
|
printBucket("PASSING TESTS", boldGreen, summary.succ)
|
||||||
|
|||||||
@@ -11,6 +11,13 @@ import (
|
|||||||
type Context struct {
|
type Context struct {
|
||||||
context.Context
|
context.Context
|
||||||
RootDataset string
|
RootDataset string
|
||||||
|
// Use this callback from a top-level test case to queue the
|
||||||
|
// execution of sub-tests after this test case is complete.
|
||||||
|
//
|
||||||
|
// Note that the testing harness executes the subtest
|
||||||
|
// _after_ the current top-level test. Hence, the subtest
|
||||||
|
// cannot use any ZFS state of the top-level test.
|
||||||
|
QueueSubtest func(id string, stf func(*Context))
|
||||||
}
|
}
|
||||||
|
|
||||||
var FailNowSentinel = fmt.Errorf("platformtest: FailNow called on context")
|
var FailNowSentinel = fmt.Errorf("platformtest: FailNow called on context")
|
||||||
|
|||||||
@@ -5,12 +5,17 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/zfs"
|
"github.com/zrepl/zrepl/zfs"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var ZpoolExportTimeout time.Duration = 500 * time.Millisecond
|
||||||
|
|
||||||
type Zpool struct {
|
type Zpool struct {
|
||||||
args ZpoolCreateArgs
|
args ZpoolCreateArgs
|
||||||
}
|
}
|
||||||
@@ -93,8 +98,23 @@ func (p *Zpool) Name() string { return p.args.PoolName }
|
|||||||
|
|
||||||
func (p *Zpool) Destroy(ctx context.Context, e Execer) error {
|
func (p *Zpool) Destroy(ctx context.Context, e Execer) error {
|
||||||
|
|
||||||
if err := e.RunExpectSuccessNoOutput(ctx, "zpool", "export", p.args.PoolName); err != nil {
|
exportDeadline := time.Now().Add(ZpoolExportTimeout)
|
||||||
return errors.Wrapf(err, "export pool %q", p.args.PoolName)
|
|
||||||
|
for {
|
||||||
|
if time.Now().After(exportDeadline) {
|
||||||
|
return errors.Errorf("could not zpool export (got 'pool is busy'): %s", p.args.PoolName)
|
||||||
|
}
|
||||||
|
err := e.RunExpectSuccessNoOutput(ctx, "zpool", "export", p.args.PoolName)
|
||||||
|
if err == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if strings.Contains(err.Error(), "pool is busy") {
|
||||||
|
runtime.Gosched()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "export pool %q", p.args.PoolName)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.Remove(p.args.ImagePath); err != nil {
|
if err := os.Remove(p.args.ImagePath); err != nil {
|
||||||
|
|||||||
@@ -21,18 +21,30 @@ var Cases = []Case{BatchDestroy,
|
|||||||
ReplicationIncrementalCleansUpStaleAbstractionsWithoutCacheOnSecondReplication,
|
ReplicationIncrementalCleansUpStaleAbstractionsWithoutCacheOnSecondReplication,
|
||||||
ReplicationIncrementalDestroysStepHoldsIffIncrementalStepHoldsAreDisabledButStepHoldsExist,
|
ReplicationIncrementalDestroysStepHoldsIffIncrementalStepHoldsAreDisabledButStepHoldsExist,
|
||||||
ReplicationIncrementalIsPossibleIfCommonSnapshotIsDestroyed,
|
ReplicationIncrementalIsPossibleIfCommonSnapshotIsDestroyed,
|
||||||
|
ReplicationInitialAll,
|
||||||
|
ReplicationInitialFail,
|
||||||
|
ReplicationInitialMostRecent,
|
||||||
ReplicationIsResumableFullSend__both_GuaranteeResumability,
|
ReplicationIsResumableFullSend__both_GuaranteeResumability,
|
||||||
ReplicationIsResumableFullSend__initial_GuaranteeIncrementalReplication_incremental_GuaranteeIncrementalReplication,
|
ReplicationIsResumableFullSend__initial_GuaranteeIncrementalReplication_incremental_GuaranteeIncrementalReplication,
|
||||||
ReplicationIsResumableFullSend__initial_GuaranteeResumability_incremental_GuaranteeIncrementalReplication,
|
ReplicationIsResumableFullSend__initial_GuaranteeResumability_incremental_GuaranteeIncrementalReplication,
|
||||||
|
ReplicationPlaceholderEncryption__EncryptOnReceiverUseCase__WorksIfConfiguredWithInherit,
|
||||||
|
ReplicationPlaceholderEncryption__UnspecifiedIsOkForClientIdentityPlaceholder,
|
||||||
|
ReplicationPlaceholderEncryption__UnspecifiedLeadsToFailureAtRuntimeWhenCreatingPlaceholders,
|
||||||
ReplicationPropertyReplicationWorks,
|
ReplicationPropertyReplicationWorks,
|
||||||
ReplicationReceiverErrorWhileStillSending,
|
ReplicationReceiverErrorWhileStillSending,
|
||||||
ReplicationStepCompletedLostBehavior__GuaranteeIncrementalReplication,
|
ReplicationStepCompletedLostBehavior__GuaranteeIncrementalReplication,
|
||||||
ReplicationStepCompletedLostBehavior__GuaranteeResumability,
|
ReplicationStepCompletedLostBehavior__GuaranteeResumability,
|
||||||
ResumableRecvAndTokenHandling,
|
ResumableRecvAndTokenHandling,
|
||||||
ResumeTokenParsing,
|
ResumeTokenParsing,
|
||||||
|
SendArgsValidationEE_EncryptionAndRaw,
|
||||||
SendArgsValidationEncryptedSendOfUnencryptedDatasetForbidden__EncryptionSupported_false,
|
SendArgsValidationEncryptedSendOfUnencryptedDatasetForbidden__EncryptionSupported_false,
|
||||||
SendArgsValidationEncryptedSendOfUnencryptedDatasetForbidden__EncryptionSupported_true,
|
SendArgsValidationEncryptedSendOfUnencryptedDatasetForbidden__EncryptionSupported_true,
|
||||||
SendArgsValidationResumeTokenDifferentFilesystemForbidden,
|
SendArgsValidationResumeTokenDifferentFilesystemForbidden,
|
||||||
SendArgsValidationResumeTokenEncryptionMismatchForbidden,
|
SendArgsValidationResumeTokenEncryptionMismatchForbidden,
|
||||||
|
SendStreamCloseAfterBlockedOnPipeWrite,
|
||||||
|
SendStreamCloseAfterEOFRead,
|
||||||
|
SendStreamMultipleCloseAfterEOF,
|
||||||
|
SendStreamMultipleCloseBeforeEOF,
|
||||||
|
SendStreamNonEOFReadErrorHandling,
|
||||||
UndestroyableSnapshotParsing,
|
UndestroyableSnapshotParsing,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/daemon/filters"
|
||||||
"github.com/zrepl/zrepl/platformtest"
|
"github.com/zrepl/zrepl/platformtest"
|
||||||
"github.com/zrepl/zrepl/util/limitio"
|
"github.com/zrepl/zrepl/util/limitio"
|
||||||
"github.com/zrepl/zrepl/zfs"
|
"github.com/zrepl/zrepl/zfs"
|
||||||
@@ -174,3 +175,8 @@ func datasetToStringSortedTrimPrefix(prefix *zfs.DatasetPath, paths []*zfs.Datas
|
|||||||
sort.Strings(pstrs)
|
sort.Strings(pstrs)
|
||||||
return pstrs
|
return pstrs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func mustAddToSFilter(ctx *platformtest.Context, f *filters.DatasetMapFilter, fs string) {
|
||||||
|
err := f.Add(fs, "ok")
|
||||||
|
require.NoError(ctx, err)
|
||||||
|
}
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ func ReceiveForceRollbackWorksUnencrypted(ctx *platformtest.Context) {
|
|||||||
|
|
||||||
sendStream, err := zfs.ZFSSend(ctx, sendArgs)
|
sendStream, err := zfs.ZFSSend(ctx, sendArgs)
|
||||||
require.NoError(ctx, err)
|
require.NoError(ctx, err)
|
||||||
|
defer sendStream.Close()
|
||||||
|
|
||||||
recvOpts := zfs.RecvOptions{
|
recvOpts := zfs.RecvOptions{
|
||||||
RollbackAndForceRecv: true,
|
RollbackAndForceRecv: true,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/kr/pretty"
|
"github.com/kr/pretty"
|
||||||
@@ -20,6 +21,7 @@ import (
|
|||||||
"github.com/zrepl/zrepl/replication/logic"
|
"github.com/zrepl/zrepl/replication/logic"
|
||||||
"github.com/zrepl/zrepl/replication/logic/pdu"
|
"github.com/zrepl/zrepl/replication/logic/pdu"
|
||||||
"github.com/zrepl/zrepl/replication/report"
|
"github.com/zrepl/zrepl/replication/report"
|
||||||
|
"github.com/zrepl/zrepl/util/bandwidthlimit"
|
||||||
"github.com/zrepl/zrepl/util/limitio"
|
"github.com/zrepl/zrepl/util/limitio"
|
||||||
"github.com/zrepl/zrepl/util/nodefault"
|
"github.com/zrepl/zrepl/util/nodefault"
|
||||||
"github.com/zrepl/zrepl/zfs"
|
"github.com/zrepl/zrepl/zfs"
|
||||||
@@ -33,15 +35,17 @@ import (
|
|||||||
// of a new sender and receiver instance and one blocking invocation
|
// of a new sender and receiver instance and one blocking invocation
|
||||||
// of the replication engine without encryption
|
// of the replication engine without encryption
|
||||||
type replicationInvocation struct {
|
type replicationInvocation struct {
|
||||||
sjid, rjid endpoint.JobID
|
sjid, rjid endpoint.JobID
|
||||||
sfs string
|
sfs string
|
||||||
sfilter *filters.DatasetMapFilter
|
sfilter *filters.DatasetMapFilter
|
||||||
rfsRoot string
|
rfsRoot string
|
||||||
interceptSender func(e *endpoint.Sender) logic.Sender
|
interceptSender func(e *endpoint.Sender) logic.Sender
|
||||||
interceptReceiver func(e *endpoint.Receiver) logic.Receiver
|
interceptReceiver func(e *endpoint.Receiver) logic.Receiver
|
||||||
guarantee *pdu.ReplicationConfigProtection
|
guarantee *pdu.ReplicationConfigProtection
|
||||||
senderConfigHook func(*endpoint.SenderConfig)
|
senderConfigHook func(*endpoint.SenderConfig)
|
||||||
receiverConfigHook func(*endpoint.ReceiverConfig)
|
receiverConfigHook func(*endpoint.ReceiverConfig)
|
||||||
|
plannerPolicyHook func(*logic.PlannerPolicy)
|
||||||
|
skipSendArgsValidation bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i replicationInvocation) Do(ctx *platformtest.Context) *report.Report {
|
func (i replicationInvocation) Do(ctx *platformtest.Context) *report.Report {
|
||||||
@@ -63,9 +67,10 @@ func (i replicationInvocation) Do(ctx *platformtest.Context) *report.Report {
|
|||||||
}
|
}
|
||||||
|
|
||||||
senderConfig := endpoint.SenderConfig{
|
senderConfig := endpoint.SenderConfig{
|
||||||
FSF: i.sfilter.AsFilter(),
|
FSF: i.sfilter.AsFilter(),
|
||||||
Encrypt: &nodefault.Bool{B: false},
|
Encrypt: &nodefault.Bool{B: false},
|
||||||
JobID: i.sjid,
|
JobID: i.sjid,
|
||||||
|
BandwidthLimit: bandwidthlimit.NoLimitConfig(),
|
||||||
}
|
}
|
||||||
if i.senderConfigHook != nil {
|
if i.senderConfigHook != nil {
|
||||||
i.senderConfigHook(&senderConfig)
|
i.senderConfigHook(&senderConfig)
|
||||||
@@ -75,6 +80,8 @@ func (i replicationInvocation) Do(ctx *platformtest.Context) *report.Report {
|
|||||||
JobID: i.rjid,
|
JobID: i.rjid,
|
||||||
AppendClientIdentity: false,
|
AppendClientIdentity: false,
|
||||||
RootWithoutClientComponent: mustDatasetPath(i.rfsRoot),
|
RootWithoutClientComponent: mustDatasetPath(i.rfsRoot),
|
||||||
|
BandwidthLimit: bandwidthlimit.NoLimitConfig(),
|
||||||
|
PlaceholderEncryption: endpoint.PlaceholderCreationEncryptionPropertyUnspecified,
|
||||||
}
|
}
|
||||||
if i.receiverConfigHook != nil {
|
if i.receiverConfigHook != nil {
|
||||||
i.receiverConfigHook(&receiverConfig)
|
i.receiverConfigHook(&receiverConfig)
|
||||||
@@ -86,14 +93,25 @@ func (i replicationInvocation) Do(ctx *platformtest.Context) *report.Report {
|
|||||||
sender := i.interceptSender(endpoint.NewSender(senderConfig))
|
sender := i.interceptSender(endpoint.NewSender(senderConfig))
|
||||||
receiver := i.interceptReceiver(endpoint.NewReceiver(receiverConfig))
|
receiver := i.interceptReceiver(endpoint.NewReceiver(receiverConfig))
|
||||||
plannerPolicy := logic.PlannerPolicy{
|
plannerPolicy := logic.PlannerPolicy{
|
||||||
EncryptedSend: logic.TriFromBool(false),
|
|
||||||
ReplicationConfig: &pdu.ReplicationConfig{
|
ReplicationConfig: &pdu.ReplicationConfig{
|
||||||
Protection: i.guarantee,
|
Protection: i.guarantee,
|
||||||
},
|
},
|
||||||
|
ConflictResolution: &logic.ConflictResolution{
|
||||||
|
InitialReplication: logic.InitialReplicationAutoResolutionMostRecent,
|
||||||
|
},
|
||||||
|
SizeEstimationConcurrency: 1,
|
||||||
|
}
|
||||||
|
if i.plannerPolicyHook != nil {
|
||||||
|
i.plannerPolicyHook(&plannerPolicy)
|
||||||
|
}
|
||||||
|
|
||||||
|
var doCtx context.Context = ctx
|
||||||
|
if i.skipSendArgsValidation {
|
||||||
|
doCtx = zfs.ZFSSendArgsSkipValidation(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
report, wait := replication.Do(
|
report, wait := replication.Do(
|
||||||
ctx,
|
doCtx,
|
||||||
driver.Config{
|
driver.Config{
|
||||||
MaxAttempts: 1,
|
MaxAttempts: 1,
|
||||||
StepQueueConcurrency: 1,
|
StepQueueConcurrency: 1,
|
||||||
@@ -804,13 +822,22 @@ type NeverEndingSender struct {
|
|||||||
*endpoint.Sender
|
*endpoint.Sender
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *NeverEndingSender) SendDry(ctx context.Context, req *pdu.SendReq) (r *pdu.SendRes, err error) {
|
||||||
|
r, _, err = s.sendImpl(ctx, req, true)
|
||||||
|
return r, err
|
||||||
|
}
|
||||||
|
|
||||||
func (s *NeverEndingSender) Send(ctx context.Context, req *pdu.SendReq) (r *pdu.SendRes, stream io.ReadCloser, _ error) {
|
func (s *NeverEndingSender) Send(ctx context.Context, req *pdu.SendReq) (r *pdu.SendRes, stream io.ReadCloser, _ error) {
|
||||||
|
return s.sendImpl(ctx, req, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *NeverEndingSender) sendImpl(ctx context.Context, req *pdu.SendReq, dry bool) (r *pdu.SendRes, stream io.ReadCloser, _ error) {
|
||||||
stream = nil
|
stream = nil
|
||||||
r = &pdu.SendRes{
|
r = &pdu.SendRes{
|
||||||
UsedResumeToken: false,
|
UsedResumeToken: false,
|
||||||
ExpectedSize: 1 << 30,
|
ExpectedSize: 1 << 30,
|
||||||
}
|
}
|
||||||
if req.DryRun {
|
if dry {
|
||||||
return r, stream, nil
|
return r, stream, nil
|
||||||
}
|
}
|
||||||
dz, err := os.Open("/dev/zero")
|
dz, err := os.Open("/dev/zero")
|
||||||
@@ -889,13 +916,9 @@ func ReplicationFailingInitialParentProhibitsChildReplication(ctx *platformtest.
|
|||||||
fsAA := ctx.RootDataset + "/sender/aa"
|
fsAA := ctx.RootDataset + "/sender/aa"
|
||||||
|
|
||||||
sfilter := filters.NewDatasetMapFilter(3, true)
|
sfilter := filters.NewDatasetMapFilter(3, true)
|
||||||
mustAddToSFilter := func(fs string) {
|
mustAddToSFilter(ctx, sfilter, fsA)
|
||||||
err := sfilter.Add(fs, "ok")
|
mustAddToSFilter(ctx, sfilter, fsAChild)
|
||||||
require.NoError(ctx, err)
|
mustAddToSFilter(ctx, sfilter, fsAA)
|
||||||
}
|
|
||||||
mustAddToSFilter(fsA)
|
|
||||||
mustAddToSFilter(fsAChild)
|
|
||||||
mustAddToSFilter(fsAA)
|
|
||||||
rfsRoot := ctx.RootDataset + "/receiver"
|
rfsRoot := ctx.RootDataset + "/receiver"
|
||||||
|
|
||||||
mockRecvErr := fmt.Errorf("yifae4ohPhaquaes0hohghiep9oufie4roo7quoWooluaj2ee8")
|
mockRecvErr := fmt.Errorf("yifae4ohPhaquaes0hohghiep9oufie4roo7quoWooluaj2ee8")
|
||||||
@@ -951,6 +974,7 @@ func ReplicationPropertyReplicationWorks(ctx *platformtest.Context) {
|
|||||||
+ "sender/a/child"
|
+ "sender/a/child"
|
||||||
+ "sender/a/child@1"
|
+ "sender/a/child@1"
|
||||||
+ "receiver"
|
+ "receiver"
|
||||||
|
R zfs create -p "${ROOTDS}/receiver/${ROOTDS}/sender"
|
||||||
`)
|
`)
|
||||||
|
|
||||||
sjid := endpoint.MustMakeJobID("sender-job")
|
sjid := endpoint.MustMakeJobID("sender-job")
|
||||||
@@ -960,12 +984,8 @@ func ReplicationPropertyReplicationWorks(ctx *platformtest.Context) {
|
|||||||
fsAChild := ctx.RootDataset + "/sender/a/child"
|
fsAChild := ctx.RootDataset + "/sender/a/child"
|
||||||
|
|
||||||
sfilter := filters.NewDatasetMapFilter(2, true)
|
sfilter := filters.NewDatasetMapFilter(2, true)
|
||||||
mustAddToSFilter := func(fs string) {
|
mustAddToSFilter(ctx, sfilter, fsA)
|
||||||
err := sfilter.Add(fs, "ok")
|
mustAddToSFilter(ctx, sfilter, fsAChild)
|
||||||
require.NoError(ctx, err)
|
|
||||||
}
|
|
||||||
mustAddToSFilter(fsA)
|
|
||||||
mustAddToSFilter(fsAChild)
|
|
||||||
rfsRoot := ctx.RootDataset + "/receiver"
|
rfsRoot := ctx.RootDataset + "/receiver"
|
||||||
|
|
||||||
type testPropExpectation struct {
|
type testPropExpectation struct {
|
||||||
@@ -1054,6 +1074,9 @@ func ReplicationPropertyReplicationWorks(ctx *platformtest.Context) {
|
|||||||
rep := fsByName[fs]
|
rep := fsByName[fs]
|
||||||
require.Len(ctx, rep.Steps, 1)
|
require.Len(ctx, rep.Steps, 1)
|
||||||
require.Nil(ctx, rep.PlanError)
|
require.Nil(ctx, rep.PlanError)
|
||||||
|
if rep.StepError != nil && strings.Contains(rep.StepError.Error(), "invalid option 'x'") {
|
||||||
|
ctx.SkipNow() // XXX feature detection
|
||||||
|
}
|
||||||
require.Nil(ctx, rep.StepError)
|
require.Nil(ctx, rep.StepError)
|
||||||
require.Len(ctx, rep.Steps, 1)
|
require.Len(ctx, rep.Steps, 1)
|
||||||
require.Equal(ctx, 1, rep.CurrentStep)
|
require.Equal(ctx, 1, rep.CurrentStep)
|
||||||
@@ -1090,3 +1113,284 @@ func ReplicationPropertyReplicationWorks(ctx *platformtest.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ReplicationPlaceholderEncryption__UnspecifiedLeadsToFailureAtRuntimeWhenCreatingPlaceholders(ctx *platformtest.Context) {
|
||||||
|
|
||||||
|
platformtest.Run(ctx, platformtest.PanicErr, ctx.RootDataset, `
|
||||||
|
CREATEROOT
|
||||||
|
+ "sender"
|
||||||
|
+ "sender/a"
|
||||||
|
+ "sender/a/child"
|
||||||
|
+ "receiver"
|
||||||
|
R zfs snapshot -r ${ROOTDS}/sender@initial
|
||||||
|
`)
|
||||||
|
|
||||||
|
sjid := endpoint.MustMakeJobID("sender-job")
|
||||||
|
rjid := endpoint.MustMakeJobID("receiver-job")
|
||||||
|
|
||||||
|
childfs := ctx.RootDataset + "/sender/a/child"
|
||||||
|
|
||||||
|
sfilter := filters.NewDatasetMapFilter(3, true)
|
||||||
|
|
||||||
|
mustAddToSFilter(ctx, sfilter, childfs)
|
||||||
|
rfsRoot := ctx.RootDataset + "/receiver"
|
||||||
|
|
||||||
|
rep := replicationInvocation{
|
||||||
|
sjid: sjid,
|
||||||
|
rjid: rjid,
|
||||||
|
sfilter: sfilter,
|
||||||
|
rfsRoot: rfsRoot,
|
||||||
|
guarantee: pdu.ReplicationConfigProtectionWithKind(pdu.ReplicationGuaranteeKind_GuaranteeResumability),
|
||||||
|
receiverConfigHook: func(rc *endpoint.ReceiverConfig) {
|
||||||
|
rc.PlaceholderEncryption = endpoint.PlaceholderCreationEncryptionPropertyUnspecified
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
r := rep.Do(ctx)
|
||||||
|
ctx.Logf("\n%s", pretty.Sprint(r))
|
||||||
|
|
||||||
|
require.Len(ctx, r.Attempts, 1)
|
||||||
|
attempt := r.Attempts[0]
|
||||||
|
require.Nil(ctx, attempt.PlanError)
|
||||||
|
require.Len(ctx, attempt.Filesystems, 1)
|
||||||
|
|
||||||
|
afs := attempt.Filesystems[0]
|
||||||
|
require.Equal(ctx, childfs, afs.Info.Name)
|
||||||
|
|
||||||
|
require.Equal(ctx, 1, len(afs.Steps))
|
||||||
|
require.Equal(ctx, 0, afs.CurrentStep)
|
||||||
|
|
||||||
|
require.Equal(ctx, report.FilesystemSteppingErrored, afs.State)
|
||||||
|
|
||||||
|
childfsFirstComponent := strings.Split(childfs, "/")[0]
|
||||||
|
require.Contains(ctx, afs.StepError.Err, "cannot create placeholder filesystem "+rfsRoot+"/"+childfsFirstComponent+": placeholder filesystem encryption handling is unspecified in receiver config")
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClientIdentityReceiver struct {
|
||||||
|
clientIdentity string
|
||||||
|
*endpoint.Receiver
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ClientIdentityReceiver) Receive(ctx context.Context, req *pdu.ReceiveReq, stream io.ReadCloser) (*pdu.ReceiveRes, error) {
|
||||||
|
ctx = context.WithValue(ctx, endpoint.ClientIdentityKey, r.clientIdentity)
|
||||||
|
return r.Receiver.Receive(ctx, req, stream)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReplicationPlaceholderEncryption__UnspecifiedIsOkForClientIdentityPlaceholder(ctx *platformtest.Context) {
|
||||||
|
platformtest.Run(ctx, platformtest.PanicErr, ctx.RootDataset, `
|
||||||
|
CREATEROOT
|
||||||
|
+ "receiver"
|
||||||
|
`)
|
||||||
|
|
||||||
|
sjid := endpoint.MustMakeJobID("sender-job")
|
||||||
|
rjid := endpoint.MustMakeJobID("receiver-job")
|
||||||
|
|
||||||
|
sfilter := filters.NewDatasetMapFilter(1, true)
|
||||||
|
|
||||||
|
// hacky...
|
||||||
|
comps := strings.Split(ctx.RootDataset, "/")
|
||||||
|
require.GreaterOrEqual(ctx, len(comps), 2)
|
||||||
|
pool := comps[0]
|
||||||
|
require.Contains(ctx, pool, "zreplplatformtest", "don't want to cause accidents")
|
||||||
|
poolchild := pool + "/" + comps[1]
|
||||||
|
|
||||||
|
err := zfs.ZFSSnapshot(ctx, mustDatasetPath(pool), "testsnap", false)
|
||||||
|
require.NoError(ctx, err)
|
||||||
|
|
||||||
|
err = zfs.ZFSSnapshot(ctx, mustDatasetPath(poolchild), "testsnap", false)
|
||||||
|
require.NoError(ctx, err)
|
||||||
|
|
||||||
|
mustAddToSFilter(ctx, sfilter, pool)
|
||||||
|
mustAddToSFilter(ctx, sfilter, poolchild)
|
||||||
|
|
||||||
|
clientIdentity := "testclientid"
|
||||||
|
|
||||||
|
rfsRoot := ctx.RootDataset + "/receiver"
|
||||||
|
|
||||||
|
rep := replicationInvocation{
|
||||||
|
sjid: sjid,
|
||||||
|
rjid: rjid,
|
||||||
|
sfilter: sfilter,
|
||||||
|
rfsRoot: rfsRoot,
|
||||||
|
guarantee: pdu.ReplicationConfigProtectionWithKind(pdu.ReplicationGuaranteeKind_GuaranteeResumability),
|
||||||
|
receiverConfigHook: func(rc *endpoint.ReceiverConfig) {
|
||||||
|
rc.PlaceholderEncryption = endpoint.PlaceholderCreationEncryptionPropertyUnspecified
|
||||||
|
rc.AppendClientIdentity = true
|
||||||
|
},
|
||||||
|
interceptReceiver: func(r *endpoint.Receiver) logic.Receiver {
|
||||||
|
r.Test_OverrideClientIdentityFunc = func() string { return clientIdentity }
|
||||||
|
return r
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
r := rep.Do(ctx)
|
||||||
|
ctx.Logf("\n%s", pretty.Sprint(r))
|
||||||
|
|
||||||
|
require.Len(ctx, r.Attempts, 1)
|
||||||
|
attempt := r.Attempts[0]
|
||||||
|
require.Nil(ctx, attempt.PlanError)
|
||||||
|
require.Len(ctx, attempt.Filesystems, 2)
|
||||||
|
|
||||||
|
filesystemsByName := make(map[string]*report.FilesystemReport)
|
||||||
|
for _, fs := range attempt.Filesystems {
|
||||||
|
filesystemsByName[fs.Info.Name] = fs
|
||||||
|
}
|
||||||
|
require.Len(ctx, filesystemsByName, len(attempt.Filesystems))
|
||||||
|
|
||||||
|
afs, ok := filesystemsByName[pool]
|
||||||
|
require.True(ctx, ok)
|
||||||
|
require.Nil(ctx, afs.PlanError)
|
||||||
|
require.Nil(ctx, afs.StepError)
|
||||||
|
require.Equal(ctx, report.FilesystemDone, afs.State)
|
||||||
|
|
||||||
|
afs, ok = filesystemsByName[poolchild]
|
||||||
|
require.True(ctx, ok)
|
||||||
|
require.Nil(ctx, afs.PlanError)
|
||||||
|
require.Nil(ctx, afs.StepError)
|
||||||
|
require.Equal(ctx, report.FilesystemDone, afs.State)
|
||||||
|
|
||||||
|
mustGetFilesystemVersion(ctx, rfsRoot+"/"+clientIdentity+"/"+pool+"@testsnap")
|
||||||
|
mustGetFilesystemVersion(ctx, rfsRoot+"/"+clientIdentity+"/"+poolchild+"@testsnap")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func replicationPlaceholderEncryption__EncryptOnReceiverUseCase__impl(ctx *platformtest.Context, placeholderEncryption endpoint.PlaceholderCreationEncryptionProperty) {
|
||||||
|
|
||||||
|
platformtest.Run(ctx, platformtest.PanicErr, ctx.RootDataset, `
|
||||||
|
CREATEROOT
|
||||||
|
+ "sender"
|
||||||
|
+ "sender/a"
|
||||||
|
+ "sender/a/child"
|
||||||
|
+ "receiver" encrypted
|
||||||
|
R zfs snapshot -r ${ROOTDS}/sender@initial
|
||||||
|
`)
|
||||||
|
|
||||||
|
sjid := endpoint.MustMakeJobID("sender-job")
|
||||||
|
rjid := endpoint.MustMakeJobID("receiver-job")
|
||||||
|
|
||||||
|
childfs := ctx.RootDataset + "/sender/a/child"
|
||||||
|
|
||||||
|
sfilter := filters.NewDatasetMapFilter(3, true)
|
||||||
|
|
||||||
|
mustAddToSFilter(ctx, sfilter, childfs)
|
||||||
|
rfsRoot := ctx.RootDataset + "/receiver"
|
||||||
|
|
||||||
|
rep := replicationInvocation{
|
||||||
|
sjid: sjid,
|
||||||
|
rjid: rjid,
|
||||||
|
sfilter: sfilter,
|
||||||
|
rfsRoot: rfsRoot,
|
||||||
|
guarantee: pdu.ReplicationConfigProtectionWithKind(pdu.ReplicationGuaranteeKind_GuaranteeResumability),
|
||||||
|
receiverConfigHook: func(rc *endpoint.ReceiverConfig) {
|
||||||
|
rc.PlaceholderEncryption = placeholderEncryption
|
||||||
|
rc.AppendClientIdentity = false
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
r := rep.Do(ctx)
|
||||||
|
ctx.Logf("\n%s", pretty.Sprint(r))
|
||||||
|
|
||||||
|
require.Len(ctx, r.Attempts, 1)
|
||||||
|
attempt := r.Attempts[0]
|
||||||
|
require.Equal(ctx, report.AttemptDone, attempt.State)
|
||||||
|
require.Len(ctx, attempt.Filesystems, 1)
|
||||||
|
afs := attempt.Filesystems[0]
|
||||||
|
require.Equal(ctx, childfs, afs.Info.Name)
|
||||||
|
|
||||||
|
require.Equal(ctx, 1, len(afs.Steps))
|
||||||
|
|
||||||
|
rfs := mustDatasetPath(rfsRoot + "/" + childfs)
|
||||||
|
mustGetFilesystemVersion(ctx, rfs.ToString()+"@initial")
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReplicationPlaceholderEncryption__EncryptOnReceiverUseCase__WorksIfConfiguredWithInherit(ctx *platformtest.Context) {
|
||||||
|
placeholderEncryption := endpoint.PlaceholderCreationEncryptionPropertyInherit
|
||||||
|
|
||||||
|
replicationPlaceholderEncryption__EncryptOnReceiverUseCase__impl(ctx, placeholderEncryption)
|
||||||
|
childfs := ctx.RootDataset + "/sender/a/child"
|
||||||
|
rfsRoot := ctx.RootDataset + "/receiver"
|
||||||
|
rfs := mustDatasetPath(rfsRoot + "/" + childfs)
|
||||||
|
|
||||||
|
// The leaf child dataset should be inhering from rfsRoot.
|
||||||
|
// If we had replicated with PlaceholderCreationEncryptionPropertyOff
|
||||||
|
// then it would be unencrypted and inherit from the placeholder.
|
||||||
|
props, err := zfs.ZFSGet(ctx, rfs, []string{"encryptionroot"})
|
||||||
|
require.NoError(ctx, err)
|
||||||
|
require.Equal(ctx, rfsRoot, props.Get("encryptionroot"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func replicationInitialImpl(ctx *platformtest.Context, iras logic.InitialReplicationAutoResolution, expectExactRfsSnaps []string) *report.Report {
|
||||||
|
// reverse order for snap names to expose sorting assumptions
|
||||||
|
platformtest.Run(ctx, platformtest.PanicErr, ctx.RootDataset, `
|
||||||
|
CREATEROOT
|
||||||
|
+ "sender"
|
||||||
|
+ "sender@3"
|
||||||
|
+ "sender@2"
|
||||||
|
+ "sender@1"
|
||||||
|
+ "receiver"
|
||||||
|
R zfs create -p "${ROOTDS}/receiver/${ROOTDS}"
|
||||||
|
`)
|
||||||
|
|
||||||
|
sjid := endpoint.MustMakeJobID("sender-job")
|
||||||
|
rjid := endpoint.MustMakeJobID("receiver-job")
|
||||||
|
|
||||||
|
sfs := ctx.RootDataset + "/sender"
|
||||||
|
rfsRoot := ctx.RootDataset + "/receiver"
|
||||||
|
|
||||||
|
rep := replicationInvocation{
|
||||||
|
sjid: sjid,
|
||||||
|
rjid: rjid,
|
||||||
|
sfs: sfs,
|
||||||
|
rfsRoot: rfsRoot,
|
||||||
|
guarantee: pdu.ReplicationConfigProtectionWithKind(pdu.ReplicationGuaranteeKind_GuaranteeResumability),
|
||||||
|
plannerPolicyHook: func(pp *logic.PlannerPolicy) {
|
||||||
|
pp.ConflictResolution.InitialReplication = iras
|
||||||
|
},
|
||||||
|
}
|
||||||
|
rfs := rep.ReceiveSideFilesystem()
|
||||||
|
|
||||||
|
// first replication
|
||||||
|
report := rep.Do(ctx)
|
||||||
|
ctx.Logf("\n%s", pretty.Sprint(report))
|
||||||
|
|
||||||
|
versions, err := zfs.ZFSListFilesystemVersions(ctx, mustDatasetPath(rfs), zfs.ListFilesystemVersionsOptions{Types: zfs.Snapshots})
|
||||||
|
if _, ok := err.(*zfs.DatasetDoesNotExist); ok {
|
||||||
|
versions = nil
|
||||||
|
} else {
|
||||||
|
require.NoError(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
bySnapName := make(map[string]int)
|
||||||
|
for _, v := range versions {
|
||||||
|
bySnapName[v.GetName()] += 1
|
||||||
|
}
|
||||||
|
for _, v := range expectExactRfsSnaps {
|
||||||
|
bySnapName[v] -= 1
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range bySnapName {
|
||||||
|
if v != 0 {
|
||||||
|
ctx.Logf("unexpected snaps:\n%#v", bySnapName)
|
||||||
|
ctx.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return report
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReplicationInitialAll(ctx *platformtest.Context) {
|
||||||
|
replicationInitialImpl(ctx, logic.InitialReplicationAutoResolutionAll, []string{"3", "2", "1"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReplicationInitialMostRecent(ctx *platformtest.Context) {
|
||||||
|
replicationInitialImpl(ctx, logic.InitialReplicationAutoResolutionMostRecent, []string{"1"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReplicationInitialFail(ctx *platformtest.Context) {
|
||||||
|
report := replicationInitialImpl(ctx, logic.InitialReplicationAutoResolutionFail, []string{})
|
||||||
|
require.Len(ctx, report.Attempts, 1)
|
||||||
|
require.Nil(ctx, report.Attempts[0].PlanError)
|
||||||
|
require.Len(ctx, report.Attempts[0].Filesystems, 1)
|
||||||
|
require.NotNil(ctx, report.Attempts[0].Filesystems[0].PlanError)
|
||||||
|
require.Contains(ctx, report.Attempts[0].Filesystems[0].PlanError.Err, "automatic conflict resolution for initial replication is disabled in config")
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,10 +2,16 @@ package tests
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"path"
|
||||||
|
|
||||||
|
"github.com/kr/pretty"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/endpoint"
|
||||||
"github.com/zrepl/zrepl/platformtest"
|
"github.com/zrepl/zrepl/platformtest"
|
||||||
|
"github.com/zrepl/zrepl/replication/logic"
|
||||||
|
"github.com/zrepl/zrepl/replication/logic/pdu"
|
||||||
|
"github.com/zrepl/zrepl/replication/report"
|
||||||
"github.com/zrepl/zrepl/util/nodefault"
|
"github.com/zrepl/zrepl/util/nodefault"
|
||||||
"github.com/zrepl/zrepl/zfs"
|
"github.com/zrepl/zrepl/zfs"
|
||||||
)
|
)
|
||||||
@@ -208,3 +214,225 @@ func SendArgsValidationResumeTokenDifferentFilesystemForbidden(ctx *platformtest
|
|||||||
require.True(ctx, ok)
|
require.True(ctx, ok)
|
||||||
require.Equal(ctx, mismatchError.What, zfs.ZFSSendArgsResumeTokenMismatchFilesystem)
|
require.Equal(ctx, mismatchError.What, zfs.ZFSSendArgsResumeTokenMismatchFilesystem)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type sendArgsValidationEndToEndTestOutcome string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ValidationAccepts sendArgsValidationEndToEndTestOutcome = "accept"
|
||||||
|
ValidationRejects sendArgsValidationEndToEndTestOutcome = "rejects"
|
||||||
|
)
|
||||||
|
|
||||||
|
type sendArgsValidationEndToEndTest struct {
|
||||||
|
encryptedSenderFilesystem bool
|
||||||
|
senderConfigHook func(config *endpoint.SenderConfig)
|
||||||
|
expectedOutcome sendArgsValidationEndToEndTestOutcome
|
||||||
|
outcomeRejectsInspectError func(require.TestingT, *report.FilesystemReport, bool)
|
||||||
|
inspectReceiverFSAfterSuccessfulCycle func(rfs string)
|
||||||
|
}
|
||||||
|
|
||||||
|
func implSendArgsValidationEndToEndTest(ctx *platformtest.Context, setup sendArgsValidationEndToEndTest) {
|
||||||
|
|
||||||
|
senderEncrypted := ""
|
||||||
|
if setup.encryptedSenderFilesystem {
|
||||||
|
senderEncrypted = "encrypted"
|
||||||
|
}
|
||||||
|
|
||||||
|
platformtest.Run(ctx, platformtest.PanicErr, ctx.RootDataset, fmt.Sprintf(`
|
||||||
|
CREATEROOT
|
||||||
|
+ "sender" %s
|
||||||
|
+ "receiver"
|
||||||
|
R zfs create -p "${ROOTDS}/receiver/${ROOTDS}"
|
||||||
|
`, senderEncrypted))
|
||||||
|
|
||||||
|
sjid := endpoint.MustMakeJobID("sender-job")
|
||||||
|
rjid := endpoint.MustMakeJobID("receiver-job")
|
||||||
|
|
||||||
|
sfs := ctx.RootDataset + "/sender"
|
||||||
|
rfsRoot := ctx.RootDataset + "/receiver"
|
||||||
|
|
||||||
|
sfsmp, err := zfs.ZFSGetMountpoint(ctx, sfs)
|
||||||
|
require.NoError(ctx, err)
|
||||||
|
require.True(ctx, sfsmp.Mounted)
|
||||||
|
|
||||||
|
// Two cycles. one initial replication, one incremental replication.
|
||||||
|
// Within each cycle: interrupt replication at least once.
|
||||||
|
// This exercises both the no-resume-token-present and the resume-token-present validation code paths.
|
||||||
|
initial_then_incremental:
|
||||||
|
for i := 0; i < 2; i++ {
|
||||||
|
writeDummyData(path.Join(sfsmp.Mountpoint, "dummy.data"), 2*(1<<20))
|
||||||
|
mustSnapshot(ctx, fmt.Sprintf("%s@%d", sfs, i))
|
||||||
|
|
||||||
|
rep := replicationInvocation{
|
||||||
|
sjid: sjid,
|
||||||
|
rjid: rjid,
|
||||||
|
sfs: sfs,
|
||||||
|
rfsRoot: rfsRoot,
|
||||||
|
senderConfigHook: setup.senderConfigHook,
|
||||||
|
interceptSender: func(e *endpoint.Sender) logic.Sender {
|
||||||
|
return &PartialSender{Sender: e, failAfterByteCount: 1 << 20}
|
||||||
|
},
|
||||||
|
guarantee: pdu.ReplicationConfigProtectionWithKind(pdu.ReplicationGuaranteeKind_GuaranteeResumability),
|
||||||
|
skipSendArgsValidation: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
rfs := rep.ReceiveSideFilesystem()
|
||||||
|
|
||||||
|
// PartialSender interrupts after 1MiB, and we wrote 2 MiB of data
|
||||||
|
// => Give it 3 attempts to replicate. after that, we should have a stable outcome
|
||||||
|
var lastReport *report.Report
|
||||||
|
lastResumeToken := ""
|
||||||
|
interrupt_current_step:
|
||||||
|
for j := 0; j < 3; j++ {
|
||||||
|
|
||||||
|
lastReport = rep.Do(ctx)
|
||||||
|
ctx.Logf("\nreport=%s", pretty.Sprint(lastReport))
|
||||||
|
require.Len(ctx, lastReport.Attempts, 1)
|
||||||
|
require.Len(ctx, lastReport.Attempts[0].Filesystems, 1)
|
||||||
|
lastReportFS := lastReport.Attempts[0].Filesystems[0]
|
||||||
|
|
||||||
|
var rfsExists bool
|
||||||
|
rfsResumeToken, err := zfs.ZFSGetReceiveResumeTokenOrEmptyStringIfNotSupported(ctx, mustDatasetPath(rfs))
|
||||||
|
if err != nil {
|
||||||
|
_, ok := err.(*zfs.DatasetDoesNotExist) // no shadow
|
||||||
|
require.True(ctx, ok, "no other errors expected")
|
||||||
|
rfsExists = false
|
||||||
|
rfsResumeToken = ""
|
||||||
|
} else {
|
||||||
|
rfsExists = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if setup.expectedOutcome == ValidationRejects {
|
||||||
|
|
||||||
|
// When expecting rejection, it should manifest immediately, before sending anything.
|
||||||
|
// This is tested in the j=0 iteration (for both initial and incremental repl (i=0, i=1)).
|
||||||
|
// But we also want to assert correct behavior in case zrepl observes resume tokens.
|
||||||
|
// Specifically, cases where the send parameters encoded in the token conflict with the
|
||||||
|
// configured encryption policy.
|
||||||
|
// Hence, for scenarios that are expected to reject, after we validated that they reject
|
||||||
|
// for the non-resuming case (j==0), fabricate a resuming scenario by temporarily disabling
|
||||||
|
// send args validation. After fabricating the scenario, proceed into j==1 to exercise
|
||||||
|
// the resume token validation.
|
||||||
|
if j == 0 {
|
||||||
|
if i == 0 {
|
||||||
|
require.False(ctx, rfsExists, "the sender should not have sent anything")
|
||||||
|
} else {
|
||||||
|
// we fabricate a scenario where rfsExists below, hence can't assert non-existence anymore
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Logf("skipping send args validation to test resuming case")
|
||||||
|
|
||||||
|
rep.skipSendArgsValidation = true
|
||||||
|
setupResumeReport := rep.Do(ctx)
|
||||||
|
ctx.Logf("setupResumeReport=%s", pretty.Sprint(setupResumeReport))
|
||||||
|
rep.skipSendArgsValidation = false
|
||||||
|
rt, err := zfs.ZFSGetReceiveResumeTokenOrEmptyStringIfNotSupported(ctx, mustDatasetPath(rfs))
|
||||||
|
require.NoError(ctx, err)
|
||||||
|
require.NotEmpty(ctx, rt, "we disabled send args validation, so the .Do above should have resulted in a resume token on rfs")
|
||||||
|
lastResumeToken = rt
|
||||||
|
continue interrupt_current_step // next iteration will test resume case with send args validation enabled
|
||||||
|
} else { // j > 0
|
||||||
|
require.Equal(ctx, lastResumeToken, rfsResumeToken, "we expect policy to refuse replication, no progress must happen")
|
||||||
|
_, err := zfs.ZFSGetFilesystemVersion(ctx, fmt.Sprintf("%s@%d", rfs, i))
|
||||||
|
_, ok := err.(*zfs.DatasetDoesNotExist)
|
||||||
|
require.True(ctx, ok, "another check that no progress is happening")
|
||||||
|
}
|
||||||
|
|
||||||
|
setup.outcomeRejectsInspectError(ctx, lastReportFS, j == 0)
|
||||||
|
|
||||||
|
// XXX: check rejection cases for incremental replication as well
|
||||||
|
break initial_then_incremental
|
||||||
|
|
||||||
|
} else {
|
||||||
|
require.Equal(ctx, ValidationAccepts, setup.expectedOutcome)
|
||||||
|
_, err := zfs.ZFSGetFilesystemVersion(ctx, fmt.Sprintf("%s@%d", rfs, i))
|
||||||
|
_, notExist := err.(*zfs.DatasetDoesNotExist)
|
||||||
|
if notExist {
|
||||||
|
require.NotEmpty(ctx, rfsResumeToken)
|
||||||
|
continue interrupt_current_step // next iteration will resume
|
||||||
|
} else {
|
||||||
|
require.NoError(ctx, err)
|
||||||
|
// version exists
|
||||||
|
|
||||||
|
// make sure all the filesystem versions we created so far were replicated by the replication loop
|
||||||
|
for j := 0; j <= i; j++ {
|
||||||
|
_ = fsversion(ctx, rfs, fmt.Sprintf("@%d", j))
|
||||||
|
}
|
||||||
|
|
||||||
|
setup.inspectReceiverFSAfterSuccessfulCycle(rfs)
|
||||||
|
continue initial_then_incremental
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func SendArgsValidationEE_EncryptionAndRaw(ctx *platformtest.Context) {
|
||||||
|
type TC struct {
|
||||||
|
// create sender filesystem with encryption enabled yes/no
|
||||||
|
SFSEnc bool
|
||||||
|
SndEnc bool // send flag
|
||||||
|
SndRaw bool // send flag
|
||||||
|
RFSEnc bool
|
||||||
|
Outcome sendArgsValidationEndToEndTestOutcome
|
||||||
|
RejectErrorNoResume string
|
||||||
|
RejectErrorResume string
|
||||||
|
}
|
||||||
|
tcs := []TC{
|
||||||
|
// Sender FS is unencrypted
|
||||||
|
{SFSEnc: false, SndEnc: false, SndRaw: false, RFSEnc: false, Outcome: ValidationAccepts},
|
||||||
|
{SFSEnc: false, SndEnc: false, SndRaw: true, RFSEnc: false, Outcome: ValidationAccepts}, // allow unencrypted raw sends (#503)
|
||||||
|
{SFSEnc: false, SndEnc: true, SndRaw: false, RFSEnc: false, Outcome: ValidationRejects,
|
||||||
|
RejectErrorNoResume: `encrypted send mandated by policy, but filesystem .* is not encrypted`,
|
||||||
|
RejectErrorResume: `encrypted send mandated by policy, but filesystem .* is not encrypted`,
|
||||||
|
},
|
||||||
|
{SFSEnc: false, SndEnc: true, SndRaw: true, RFSEnc: false, Outcome: ValidationRejects,
|
||||||
|
RejectErrorNoResume: `encrypted send mandated by policy, but filesystem .* is not encrypted`,
|
||||||
|
RejectErrorResume: `encrypted send mandated by policy, but filesystem .* is not encrypted`,
|
||||||
|
},
|
||||||
|
// Sender FS is encrypted
|
||||||
|
{SFSEnc: true, SndEnc: false, SndRaw: false, RFSEnc: false, Outcome: ValidationAccepts}, // passes because keys are loaded, thus can send plain.
|
||||||
|
{SFSEnc: true, SndEnc: false, SndRaw: true, RFSEnc: false, Outcome: ValidationRejects,
|
||||||
|
RejectErrorNoResume: `policy mandates raw\+unencrypted sends, but filesystem .* is encrypted`,
|
||||||
|
RejectErrorResume: `resume token has rawok=true which would result in encrypted send, but policy mandates unencrypted sends only`,
|
||||||
|
},
|
||||||
|
{SFSEnc: true, SndEnc: true, SndRaw: false, RFSEnc: true, Outcome: ValidationAccepts},
|
||||||
|
{SFSEnc: true, SndEnc: true, SndRaw: true, RFSEnc: true, Outcome: ValidationAccepts},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tcs {
|
||||||
|
tc := tc // closure would copy by ref otherwise
|
||||||
|
ctx.QueueSubtest(fmt.Sprintf("%#v", tc), func(ctx *platformtest.Context) {
|
||||||
|
implSendArgsValidationEndToEndTest(ctx, sendArgsValidationEndToEndTest{
|
||||||
|
encryptedSenderFilesystem: tc.SFSEnc,
|
||||||
|
senderConfigHook: func(c *endpoint.SenderConfig) {
|
||||||
|
c.Encrypt = &nodefault.Bool{B: tc.SndEnc}
|
||||||
|
c.SendRaw = tc.SndRaw
|
||||||
|
},
|
||||||
|
expectedOutcome: tc.Outcome,
|
||||||
|
outcomeRejectsInspectError: func(ctx require.TestingT, fr *report.FilesystemReport, isResume bool) {
|
||||||
|
// this callback is only called for ValidationRejects
|
||||||
|
|
||||||
|
// validation should be failing during dry send => planning stage
|
||||||
|
// XXX mock out ZFS to ensure we never call a zfs send that would send data
|
||||||
|
// if we're expecting validation to fail
|
||||||
|
require.Equal(ctx, report.FilesystemPlanningErrored, fr.State)
|
||||||
|
|
||||||
|
if isResume {
|
||||||
|
require.NotEmpty(ctx, tc.RejectErrorResume)
|
||||||
|
require.Regexp(ctx, tc.RejectErrorResume, fr.PlanError)
|
||||||
|
} else {
|
||||||
|
require.NotEmpty(ctx, tc.RejectErrorNoResume)
|
||||||
|
require.Regexp(ctx, tc.RejectErrorNoResume, fr.PlanError)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
inspectReceiverFSAfterSuccessfulCycle: func(rfs string) {
|
||||||
|
enabled, err := zfs.ZFSGetEncryptionEnabled(ctx, rfs)
|
||||||
|
require.NoError(ctx, err)
|
||||||
|
require.Equal(ctx, tc.RFSEnc, enabled, "receiver filesystem encryption settings unexpected")
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,186 @@
|
|||||||
|
package tests
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/platformtest"
|
||||||
|
"github.com/zrepl/zrepl/util/nodefault"
|
||||||
|
"github.com/zrepl/zrepl/zfs"
|
||||||
|
)
|
||||||
|
|
||||||
|
func sendStreamTest(ctx *platformtest.Context) *zfs.SendStream {
|
||||||
|
|
||||||
|
platformtest.Run(ctx, platformtest.PanicErr, ctx.RootDataset, `
|
||||||
|
DESTROYROOT
|
||||||
|
CREATEROOT
|
||||||
|
+ "sender"
|
||||||
|
`)
|
||||||
|
|
||||||
|
fs := fmt.Sprintf("%s/sender", ctx.RootDataset)
|
||||||
|
|
||||||
|
fsmpo, err := zfs.ZFSGetMountpoint(ctx, fs)
|
||||||
|
require.NoError(ctx, err)
|
||||||
|
|
||||||
|
writeDummyData(path.Join(fsmpo.Mountpoint, "dummy.data"), 1<<26)
|
||||||
|
mustSnapshot(ctx, fs+"@1")
|
||||||
|
snap := fsversion(ctx, fs, "@1")
|
||||||
|
snapSendArg := snap.ToSendArgVersion()
|
||||||
|
|
||||||
|
sendArgs, err := zfs.ZFSSendArgsUnvalidated{
|
||||||
|
FS: fs,
|
||||||
|
From: nil,
|
||||||
|
To: &snapSendArg,
|
||||||
|
ZFSSendFlags: zfs.ZFSSendFlags{
|
||||||
|
Encrypted: &nodefault.Bool{B: false},
|
||||||
|
},
|
||||||
|
}.Validate(ctx)
|
||||||
|
require.NoError(ctx, err)
|
||||||
|
|
||||||
|
sendStream, err := zfs.ZFSSend(ctx, sendArgs)
|
||||||
|
require.NoError(ctx, err)
|
||||||
|
|
||||||
|
return sendStream
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func SendStreamCloseAfterBlockedOnPipeWrite(ctx *platformtest.Context) {
|
||||||
|
|
||||||
|
sendStream := sendStreamTest(ctx)
|
||||||
|
|
||||||
|
// let the pipe buffer fill and the zfs process block uninterruptibly
|
||||||
|
|
||||||
|
ctx.Logf("waiting for pipe write to block")
|
||||||
|
time.Sleep(5 * time.Second) // XXX need a platform-neutral way to detect that the pipe is full and the writer is blocked
|
||||||
|
|
||||||
|
ctx.Logf("closing send stream")
|
||||||
|
err := sendStream.Close() // this is what this test case is about
|
||||||
|
ctx.Logf("close error: %T %s", err, err)
|
||||||
|
require.NoError(ctx, err)
|
||||||
|
|
||||||
|
exitErrZfsError := sendStream.TestOnly_ExitErr()
|
||||||
|
require.Contains(ctx, exitErrZfsError.Error(), "signal")
|
||||||
|
require.Error(ctx, exitErrZfsError)
|
||||||
|
exitErr, ok := exitErrZfsError.WaitErr.(*exec.ExitError)
|
||||||
|
require.True(ctx, ok)
|
||||||
|
if exitErr.Exited() {
|
||||||
|
// some ZFS impls (FreeBSD 12) behaves that way
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProcessState is only available after exit
|
||||||
|
// => use as proxy that the process was wait()ed upon and is gone
|
||||||
|
ctx.Logf("%#v", exitErr.ProcessState)
|
||||||
|
require.NotNil(ctx, exitErr.ProcessState)
|
||||||
|
// and let's verify that the process got killed, so that we know it was the call to .Close() above
|
||||||
|
waitStatus := exitErr.ProcessState.Sys().(syscall.WaitStatus)
|
||||||
|
ctx.Logf("wait status: %#v", waitStatus)
|
||||||
|
ctx.Logf("exit status: %v", waitStatus.ExitStatus())
|
||||||
|
require.True(ctx, waitStatus.Signaled())
|
||||||
|
switch waitStatus.Signal() {
|
||||||
|
case unix.SIGKILL:
|
||||||
|
fallthrough
|
||||||
|
case unix.SIGPIPE:
|
||||||
|
// ok
|
||||||
|
|
||||||
|
default:
|
||||||
|
ctx.Errorf("%T %s\n%v", waitStatus.Signal(), waitStatus.Signal(), waitStatus.Signal())
|
||||||
|
ctx.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func SendStreamCloseAfterEOFRead(ctx *platformtest.Context) {
|
||||||
|
|
||||||
|
sendStream := sendStreamTest(ctx)
|
||||||
|
|
||||||
|
_, err := io.Copy(ioutil.Discard, sendStream)
|
||||||
|
require.NoError(ctx, err)
|
||||||
|
|
||||||
|
var buf [128]byte
|
||||||
|
n, err := sendStream.Read(buf[:])
|
||||||
|
require.Zero(ctx, n)
|
||||||
|
require.Equal(ctx, io.EOF, err)
|
||||||
|
|
||||||
|
err = sendStream.Close()
|
||||||
|
require.NoError(ctx, err)
|
||||||
|
|
||||||
|
n, err = sendStream.Read(buf[:])
|
||||||
|
require.Zero(ctx, n)
|
||||||
|
require.Equal(ctx, os.ErrClosed, err, "same read error should be returned")
|
||||||
|
}
|
||||||
|
|
||||||
|
func SendStreamMultipleCloseAfterEOF(ctx *platformtest.Context) {
|
||||||
|
|
||||||
|
sendStream := sendStreamTest(ctx)
|
||||||
|
|
||||||
|
_, err := io.Copy(ioutil.Discard, sendStream)
|
||||||
|
require.NoError(ctx, err)
|
||||||
|
|
||||||
|
var buf [128]byte
|
||||||
|
n, err := sendStream.Read(buf[:])
|
||||||
|
require.Zero(ctx, n)
|
||||||
|
require.Equal(ctx, io.EOF, err)
|
||||||
|
|
||||||
|
err = sendStream.Close()
|
||||||
|
require.NoError(ctx, err)
|
||||||
|
|
||||||
|
err = sendStream.Close()
|
||||||
|
require.Equal(ctx, os.ErrClosed, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func SendStreamMultipleCloseBeforeEOF(ctx *platformtest.Context) {
|
||||||
|
|
||||||
|
sendStream := sendStreamTest(ctx)
|
||||||
|
|
||||||
|
err := sendStream.Close()
|
||||||
|
require.NoError(ctx, err)
|
||||||
|
|
||||||
|
err = sendStream.Close()
|
||||||
|
require.Equal(ctx, os.ErrClosed, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
type failingReadCloser struct {
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ io.ReadCloser = &failingReadCloser{}
|
||||||
|
|
||||||
|
func (c *failingReadCloser) Read(p []byte) (int, error) { return 0, c.err }
|
||||||
|
func (c *failingReadCloser) Close() error { return c.err }
|
||||||
|
|
||||||
|
func SendStreamNonEOFReadErrorHandling(ctx *platformtest.Context) {
|
||||||
|
|
||||||
|
sendStream := sendStreamTest(ctx)
|
||||||
|
|
||||||
|
var buf [128]byte
|
||||||
|
n, err := sendStream.Read(buf[:])
|
||||||
|
require.Equal(ctx, len(buf), n)
|
||||||
|
require.NoError(ctx, err)
|
||||||
|
|
||||||
|
var mockError = fmt.Errorf("taeghaefow4piesahwahjocu7ul5tiachaiLipheijae8ooZ8Pies8shohGee9feeTeirai5aiFeiyaecai4kiaLoh4azeih0tea")
|
||||||
|
mock := &failingReadCloser{err: mockError}
|
||||||
|
orig := sendStream.TestOnly_ReplaceStdoutReader(mock)
|
||||||
|
|
||||||
|
n, err = sendStream.Read(buf[:])
|
||||||
|
require.Equal(ctx, 0, n)
|
||||||
|
require.Equal(ctx, mockError, err)
|
||||||
|
|
||||||
|
if sendStream.TestOnly_ReplaceStdoutReader(orig) != mock {
|
||||||
|
panic("incorrect test impl")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = sendStream.Close()
|
||||||
|
require.NoError(ctx, err) // if we can't kill the child then this will be a flaky test, but let's assume we can kill the child
|
||||||
|
|
||||||
|
err = sendStream.Close()
|
||||||
|
require.Equal(ctx, os.ErrClosed, err)
|
||||||
|
}
|
||||||
@@ -151,6 +151,8 @@ type fs struct {
|
|||||||
|
|
||||||
l *chainlock.L
|
l *chainlock.L
|
||||||
|
|
||||||
|
blockedOn report.FsBlockedOn
|
||||||
|
|
||||||
// ordering relationship that must be maintained for initial replication
|
// ordering relationship that must be maintained for initial replication
|
||||||
initialRepOrd struct {
|
initialRepOrd struct {
|
||||||
parents, children []*fs
|
parents, children []*fs
|
||||||
@@ -158,8 +160,9 @@ type fs struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
planning struct {
|
planning struct {
|
||||||
done bool
|
waitingForStepQueue bool
|
||||||
err *timedError
|
done bool
|
||||||
|
err *timedError
|
||||||
}
|
}
|
||||||
|
|
||||||
// valid iff planning.done && planning.err == nil
|
// valid iff planning.done && planning.err == nil
|
||||||
@@ -337,8 +340,9 @@ func (a *attempt) doGlobalPlanning(ctx context.Context, prev *attempt) map[*fs]*
|
|||||||
|
|
||||||
for _, pfs := range pfss {
|
for _, pfs := range pfss {
|
||||||
fs := &fs{
|
fs := &fs{
|
||||||
fs: pfs,
|
fs: pfs,
|
||||||
l: a.l,
|
l: a.l,
|
||||||
|
blockedOn: report.FsBlockedOnNothing,
|
||||||
}
|
}
|
||||||
fs.initialRepOrd.parentDidUpdate = make(chan struct{}, 1)
|
fs.initialRepOrd.parentDidUpdate = make(chan struct{}, 1)
|
||||||
a.fss = append(a.fss, fs)
|
a.fss = append(a.fss, fs)
|
||||||
@@ -448,6 +452,10 @@ func (a *attempt) doFilesystems(ctx context.Context, prevs map[*fs]*fs) {
|
|||||||
ctx, endTask := trace.WithTaskAndSpan(ctx, "repl-fs", f.report().Info.Name)
|
ctx, endTask := trace.WithTaskAndSpan(ctx, "repl-fs", f.report().Info.Name)
|
||||||
defer endTask()
|
defer endTask()
|
||||||
f.do(ctx, stepQueue, prevs[f])
|
f.do(ctx, stepQueue, prevs[f])
|
||||||
|
f.l.HoldWhile(func() {
|
||||||
|
// every return from f means it's unblocked...
|
||||||
|
f.blockedOn = report.FsBlockedOnNothing
|
||||||
|
})
|
||||||
}(f)
|
}(f)
|
||||||
}
|
}
|
||||||
a.l.DropWhile(func() {
|
a.l.DropWhile(func() {
|
||||||
@@ -486,11 +494,16 @@ func (f *fs) do(ctx context.Context, pq *stepQueue, prev *fs) {
|
|||||||
var psteps []Step
|
var psteps []Step
|
||||||
var errTime time.Time
|
var errTime time.Time
|
||||||
var err error
|
var err error
|
||||||
|
f.blockedOn = report.FsBlockedOnPlanningStepQueue
|
||||||
f.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(ctx, f, targetDate)()
|
defer pq.WaitReady(ctx, f, targetDate)()
|
||||||
|
f.l.HoldWhile(func() {
|
||||||
|
// transition before we call PlanFS
|
||||||
|
f.blockedOn = report.FsBlockedOnNothing
|
||||||
|
})
|
||||||
psteps, err = f.fs.PlanFS(ctx) // no shadow
|
psteps, err = f.fs.PlanFS(ctx) // no shadow
|
||||||
errTime = time.Now() // no shadow
|
errTime = time.Now() // no shadow
|
||||||
})
|
})
|
||||||
@@ -545,6 +558,7 @@ func (f *fs) do(ctx context.Context, pq *stepQueue, prev *fs) {
|
|||||||
f.planning.done = true
|
f.planning.done = true
|
||||||
|
|
||||||
// wait for parents' initial replication
|
// wait for parents' initial replication
|
||||||
|
f.blockedOn = report.FsBlockedOnParentInitialRepl
|
||||||
var parents []string
|
var parents []string
|
||||||
for _, p := range f.initialRepOrd.parents {
|
for _, p := range f.initialRepOrd.parents {
|
||||||
parents = append(parents, p.fs.ReportInfo().Name)
|
parents = append(parents, p.fs.ReportInfo().Name)
|
||||||
@@ -613,7 +627,6 @@ func (f *fs) do(ctx context.Context, pq *stepQueue, prev *fs) {
|
|||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
f.planned.stepErr = newTimedError(ctx.Err(), time.Now())
|
f.planned.stepErr = newTimedError(ctx.Err(), time.Now())
|
||||||
return
|
|
||||||
case <-f.initialRepOrd.parentDidUpdate:
|
case <-f.initialRepOrd.parentDidUpdate:
|
||||||
// loop
|
// loop
|
||||||
}
|
}
|
||||||
@@ -631,7 +644,9 @@ func (f *fs) do(ctx context.Context, pq *stepQueue, prev *fs) {
|
|||||||
f.l.DropWhile(func() {
|
f.l.DropWhile(func() {
|
||||||
// wait for parallel replication
|
// wait for parallel replication
|
||||||
targetDate := s.step.TargetDate()
|
targetDate := s.step.TargetDate()
|
||||||
|
f.l.HoldWhile(func() { f.blockedOn = report.FsBlockedOnReplStepQueue })
|
||||||
defer pq.WaitReady(ctx, f, targetDate)()
|
defer pq.WaitReady(ctx, f, targetDate)()
|
||||||
|
f.l.HoldWhile(func() { f.blockedOn = report.FsBlockedOnNothing })
|
||||||
// do the step
|
// do the step
|
||||||
ctx, endSpan := trace.WithSpan(ctx, fmt.Sprintf("%#v", s.step.ReportInfo()))
|
ctx, endSpan := trace.WithSpan(ctx, fmt.Sprintf("%#v", s.step.ReportInfo()))
|
||||||
defer endSpan()
|
defer endSpan()
|
||||||
@@ -725,6 +740,7 @@ func (f *fs) report() *report.FilesystemReport {
|
|||||||
r := &report.FilesystemReport{
|
r := &report.FilesystemReport{
|
||||||
Info: f.fs.ReportInfo(),
|
Info: f.fs.ReportInfo(),
|
||||||
State: state,
|
State: state,
|
||||||
|
BlockedOn: f.blockedOn,
|
||||||
PlanError: f.planning.err.IntoReportError(),
|
PlanError: f.planning.err.IntoReportError(),
|
||||||
StepError: f.planned.stepErr.IntoReportError(),
|
StepError: f.planned.stepErr.IntoReportError(),
|
||||||
Steps: make([]*report.StepReport, len(f.planned.steps)),
|
Steps: make([]*report.StepReport, len(f.planned.steps)),
|
||||||
|
|||||||
@@ -14,11 +14,12 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/daemon/logging/trace"
|
"github.com/zrepl/zrepl/daemon/logging/trace"
|
||||||
|
"github.com/zrepl/zrepl/util/zreplcircleci"
|
||||||
)
|
)
|
||||||
|
|
||||||
// FIXME: this test relies on timing and is thus rather flaky
|
|
||||||
// (relies on scheduler responsiveness of < 500ms)
|
|
||||||
func TestPqNotconcurrent(t *testing.T) {
|
func TestPqNotconcurrent(t *testing.T) {
|
||||||
|
zreplcircleci.SkipOnCircleCI(t, "because it relies on scheduler responsiveness < 500ms")
|
||||||
|
|
||||||
ctx, end := trace.WithTaskFromStack(context.Background())
|
ctx, end := trace.WithTaskFromStack(context.Background())
|
||||||
defer end()
|
defer end()
|
||||||
var ctr uint32
|
var ctr uint32
|
||||||
@@ -90,6 +91,8 @@ func (r record) String() string {
|
|||||||
// Hence, perform some statistics on the wakeup times and assert that the mean wakeup
|
// Hence, perform some statistics on the wakeup times and assert that the mean wakeup
|
||||||
// times for each step are close together.
|
// times for each step are close together.
|
||||||
func TestPqConcurrent(t *testing.T) {
|
func TestPqConcurrent(t *testing.T) {
|
||||||
|
zreplcircleci.SkipOnCircleCI(t, "because it relies on scheduler responsiveness < 500ms")
|
||||||
|
|
||||||
ctx, end := trace.WithTaskFromStack(context.Background())
|
ctx, end := trace.WithTaskFromStack(context.Background())
|
||||||
defer end()
|
defer end()
|
||||||
|
|
||||||
|
|||||||
@@ -49,6 +49,23 @@ func (c *ConflictDiverged) Error() string {
|
|||||||
return buf.String()
|
return buf.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ConflictNoSenderSnapshots struct{}
|
||||||
|
|
||||||
|
func (c *ConflictNoSenderSnapshots) Error() string {
|
||||||
|
return "no snapshots available on sender side"
|
||||||
|
}
|
||||||
|
|
||||||
|
type ConflictMostRecentSnapshotAlreadyPresent struct {
|
||||||
|
SortedSenderVersions, SortedReceiverVersions []*FilesystemVersion
|
||||||
|
CommonAncestor *FilesystemVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConflictMostRecentSnapshotAlreadyPresent) Error() string {
|
||||||
|
var buf strings.Builder
|
||||||
|
fmt.Fprintf(&buf, "the most recent sender snapshot is already present on the receiver (guid=%v, name=%q)", c.CommonAncestor.GetGuid(), c.CommonAncestor.RelName())
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
|
|
||||||
func SortVersionListByCreateTXGThenBookmarkLTSnapshot(fsvslice []*FilesystemVersion) []*FilesystemVersion {
|
func SortVersionListByCreateTXGThenBookmarkLTSnapshot(fsvslice []*FilesystemVersion) []*FilesystemVersion {
|
||||||
lesser := func(s []*FilesystemVersion) func(i, j int) bool {
|
lesser := func(s []*FilesystemVersion) func(i, j int) bool {
|
||||||
return func(i, j int) bool {
|
return func(i, j int) bool {
|
||||||
@@ -71,9 +88,27 @@ func SortVersionListByCreateTXGThenBookmarkLTSnapshot(fsvslice []*FilesystemVers
|
|||||||
return sorted
|
return sorted
|
||||||
}
|
}
|
||||||
|
|
||||||
// conflict may be a *ConflictDiverged or a *ConflictNoCommonAncestor
|
func StripBookmarksFromVersionList(fsvslice []*FilesystemVersion) []*FilesystemVersion {
|
||||||
|
fslice := make([]*FilesystemVersion, 0, len(fsvslice))
|
||||||
|
for _, fv := range fsvslice {
|
||||||
|
if fv.Type != FilesystemVersion_Bookmark {
|
||||||
|
fslice = append(fslice, fv)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fslice
|
||||||
|
}
|
||||||
|
|
||||||
func IncrementalPath(receiver, sender []*FilesystemVersion) (incPath []*FilesystemVersion, conflict error) {
|
func IncrementalPath(receiver, sender []*FilesystemVersion) (incPath []*FilesystemVersion, conflict error) {
|
||||||
|
|
||||||
|
// Receive-side bookmarks can't be used as incremental-from,
|
||||||
|
// and don't cause recv to fail if there is a newer bookmark than incremetal-form on the receiver.
|
||||||
|
// So, simply mask them out.
|
||||||
|
// This will also hide them in the report, but it keeps the code in this function simple,
|
||||||
|
// and a user who complains about them missing in a conflict message will likely require
|
||||||
|
// more education about bookmarks than a slightly more accurate error message. They'll get
|
||||||
|
// that when they open an issue.
|
||||||
|
receiver = StripBookmarksFromVersionList(receiver)
|
||||||
|
|
||||||
receiver = SortVersionListByCreateTXGThenBookmarkLTSnapshot(receiver)
|
receiver = SortVersionListByCreateTXGThenBookmarkLTSnapshot(receiver)
|
||||||
sender = SortVersionListByCreateTXGThenBookmarkLTSnapshot(sender)
|
sender = SortVersionListByCreateTXGThenBookmarkLTSnapshot(sender)
|
||||||
|
|
||||||
@@ -98,9 +133,13 @@ findCandidate:
|
|||||||
|
|
||||||
// handle failure cases
|
// handle failure cases
|
||||||
if !mrcaCandidate.found {
|
if !mrcaCandidate.found {
|
||||||
return nil, &ConflictNoCommonAncestor{
|
if len(sender) == 0 {
|
||||||
SortedSenderVersions: sender,
|
return nil, &ConflictNoSenderSnapshots{}
|
||||||
SortedReceiverVersions: receiver,
|
} else {
|
||||||
|
return nil, &ConflictNoCommonAncestor{
|
||||||
|
SortedSenderVersions: sender,
|
||||||
|
SortedReceiverVersions: receiver,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if mrcaCandidate.r != len(receiver)-1 {
|
} else if mrcaCandidate.r != len(receiver)-1 {
|
||||||
return nil, &ConflictDiverged{
|
return nil, &ConflictDiverged{
|
||||||
@@ -125,7 +164,11 @@ findCandidate:
|
|||||||
}
|
}
|
||||||
if len(incPath) == 1 {
|
if len(incPath) == 1 {
|
||||||
// nothing to do
|
// nothing to do
|
||||||
incPath = incPath[1:]
|
return nil, &ConflictMostRecentSnapshotAlreadyPresent{
|
||||||
|
SortedSenderVersions: sender,
|
||||||
|
SortedReceiverVersions: receiver,
|
||||||
|
CommonAncestor: sender[mrcaCandidate.s],
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return incPath, nil
|
return incPath, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,12 +96,37 @@ func TestIncrementalPath_SnapshotsOnly(t *testing.T) {
|
|||||||
assert.Equal(t, l("@c,3", "@d,4"), path)
|
assert.Equal(t, l("@c,3", "@d,4"), path)
|
||||||
})
|
})
|
||||||
|
|
||||||
// sender with earlier but also current version as sender is not a conflict
|
// nothing to do if fully shared history
|
||||||
|
doTest(l("@a,1", "@b,2"), l("@a,1", "@b,2"), func(incpath []*FilesystemVersion, conflict error) {
|
||||||
|
assert.Nil(t, incpath)
|
||||||
|
assert.NotNil(t, conflict)
|
||||||
|
_, ok := conflict.(*ConflictMostRecentSnapshotAlreadyPresent)
|
||||||
|
assert.True(t, ok)
|
||||||
|
})
|
||||||
|
|
||||||
|
// ...but it's sufficient if the most recent snapshot is present
|
||||||
doTest(l("@c,3"), l("@a,1", "@b,2", "@c,3"), func(path []*FilesystemVersion, conflict error) {
|
doTest(l("@c,3"), l("@a,1", "@b,2", "@c,3"), func(path []*FilesystemVersion, conflict error) {
|
||||||
t.Logf("path: %#v", path)
|
assert.Nil(t, path)
|
||||||
t.Logf("conflict: %#v", conflict)
|
_, ok := conflict.(*ConflictMostRecentSnapshotAlreadyPresent)
|
||||||
assert.Empty(t, path)
|
assert.True(t, ok)
|
||||||
assert.Nil(t, conflict)
|
})
|
||||||
|
|
||||||
|
// no sender snapshots errors: empty receiver
|
||||||
|
doTest(l(), l(), func(incpath []*FilesystemVersion, conflict error) {
|
||||||
|
assert.Nil(t, incpath)
|
||||||
|
assert.NotNil(t, conflict)
|
||||||
|
t.Logf("%T", conflict)
|
||||||
|
_, ok := conflict.(*ConflictNoSenderSnapshots)
|
||||||
|
assert.True(t, ok)
|
||||||
|
})
|
||||||
|
|
||||||
|
// no sender snapshots errors: snapshots on receiver
|
||||||
|
doTest(l("@a,1"), l(), func(incpath []*FilesystemVersion, conflict error) {
|
||||||
|
assert.Nil(t, incpath)
|
||||||
|
assert.NotNil(t, conflict)
|
||||||
|
t.Logf("%T", conflict)
|
||||||
|
_, ok := conflict.(*ConflictNoSenderSnapshots)
|
||||||
|
assert.True(t, ok)
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -127,4 +152,18 @@ func TestIncrementalPath_BookmarkSupport(t *testing.T) {
|
|||||||
assert.Equal(t, l("@a,1", "@b,2"), path)
|
assert.Equal(t, l("@a,1", "@b,2"), path)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// test that receive-side bookmarks younger than the most recent common ancestor do not cause a conflict
|
||||||
|
doTest(l("@a,1", "#b,2"), l("@a,1", "@c,3"), func(path []*FilesystemVersion, conflict error) {
|
||||||
|
assert.NoError(t, conflict)
|
||||||
|
require.Len(t, path, 2)
|
||||||
|
assert.Equal(t, l("@a,1")[0], path[0])
|
||||||
|
assert.Equal(t, l("@c,3")[0], path[1])
|
||||||
|
})
|
||||||
|
doTest(l("#a,1"), l("@a,1", "@b,2"), func(path []*FilesystemVersion, conflict error) {
|
||||||
|
assert.Nil(t, path)
|
||||||
|
ca, ok := conflict.(*ConflictNoCommonAncestor)
|
||||||
|
require.True(t, ok)
|
||||||
|
assert.Equal(t, l(), ca.SortedReceiverVersions, "See comment in IncrementalPath() on why we don't include the boomkmark here")
|
||||||
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
// Code generated by "enumer -type=InitialReplicationAutoResolution -trimprefix=InitialReplicationAutoResolution"; DO NOT EDIT.
|
||||||
|
|
||||||
|
//
|
||||||
|
package logic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
_InitialReplicationAutoResolutionName_0 = "MostRecentAll"
|
||||||
|
_InitialReplicationAutoResolutionName_1 = "Fail"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
_InitialReplicationAutoResolutionIndex_0 = [...]uint8{0, 10, 13}
|
||||||
|
_InitialReplicationAutoResolutionIndex_1 = [...]uint8{0, 4}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (i InitialReplicationAutoResolution) String() string {
|
||||||
|
switch {
|
||||||
|
case 1 <= i && i <= 2:
|
||||||
|
i -= 1
|
||||||
|
return _InitialReplicationAutoResolutionName_0[_InitialReplicationAutoResolutionIndex_0[i]:_InitialReplicationAutoResolutionIndex_0[i+1]]
|
||||||
|
case i == 4:
|
||||||
|
return _InitialReplicationAutoResolutionName_1
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("InitialReplicationAutoResolution(%d)", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _InitialReplicationAutoResolutionValues = []InitialReplicationAutoResolution{1, 2, 4}
|
||||||
|
|
||||||
|
var _InitialReplicationAutoResolutionNameToValueMap = map[string]InitialReplicationAutoResolution{
|
||||||
|
_InitialReplicationAutoResolutionName_0[0:10]: 1,
|
||||||
|
_InitialReplicationAutoResolutionName_0[10:13]: 2,
|
||||||
|
_InitialReplicationAutoResolutionName_1[0:4]: 4,
|
||||||
|
}
|
||||||
|
|
||||||
|
// InitialReplicationAutoResolutionString retrieves an enum value from the enum constants string name.
|
||||||
|
// Throws an error if the param is not part of the enum.
|
||||||
|
func InitialReplicationAutoResolutionString(s string) (InitialReplicationAutoResolution, error) {
|
||||||
|
if val, ok := _InitialReplicationAutoResolutionNameToValueMap[s]; ok {
|
||||||
|
return val, nil
|
||||||
|
}
|
||||||
|
return 0, fmt.Errorf("%s does not belong to InitialReplicationAutoResolution values", s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// InitialReplicationAutoResolutionValues returns all values of the enum
|
||||||
|
func InitialReplicationAutoResolutionValues() []InitialReplicationAutoResolution {
|
||||||
|
return _InitialReplicationAutoResolutionValues
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsAInitialReplicationAutoResolution returns "true" if the value is listed in the enum definition. "false" otherwise
|
||||||
|
func (i InitialReplicationAutoResolution) IsAInitialReplicationAutoResolution() bool {
|
||||||
|
for _, v := range _InitialReplicationAutoResolutionValues {
|
||||||
|
if i == v {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
+215
-307
@@ -1,7 +1,7 @@
|
|||||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// protoc-gen-go v1.25.0
|
// protoc-gen-go v1.25.0
|
||||||
// protoc v3.12.4
|
// protoc v3.14.0
|
||||||
// source: pdu.proto
|
// source: pdu.proto
|
||||||
|
|
||||||
package pdu
|
package pdu
|
||||||
@@ -25,55 +25,6 @@ const (
|
|||||||
// of the legacy proto package is being used.
|
// of the legacy proto package is being used.
|
||||||
const _ = proto.ProtoPackageIsVersion4
|
const _ = proto.ProtoPackageIsVersion4
|
||||||
|
|
||||||
type Tri int32
|
|
||||||
|
|
||||||
const (
|
|
||||||
Tri_DontCare Tri = 0
|
|
||||||
Tri_False Tri = 1
|
|
||||||
Tri_True Tri = 2
|
|
||||||
)
|
|
||||||
|
|
||||||
// Enum value maps for Tri.
|
|
||||||
var (
|
|
||||||
Tri_name = map[int32]string{
|
|
||||||
0: "DontCare",
|
|
||||||
1: "False",
|
|
||||||
2: "True",
|
|
||||||
}
|
|
||||||
Tri_value = map[string]int32{
|
|
||||||
"DontCare": 0,
|
|
||||||
"False": 1,
|
|
||||||
"True": 2,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
func (x Tri) Enum() *Tri {
|
|
||||||
p := new(Tri)
|
|
||||||
*p = x
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x Tri) String() string {
|
|
||||||
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (Tri) Descriptor() protoreflect.EnumDescriptor {
|
|
||||||
return file_pdu_proto_enumTypes[0].Descriptor()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (Tri) Type() protoreflect.EnumType {
|
|
||||||
return &file_pdu_proto_enumTypes[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x Tri) Number() protoreflect.EnumNumber {
|
|
||||||
return protoreflect.EnumNumber(x)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deprecated: Use Tri.Descriptor instead.
|
|
||||||
func (Tri) EnumDescriptor() ([]byte, []int) {
|
|
||||||
return file_pdu_proto_rawDescGZIP(), []int{0}
|
|
||||||
}
|
|
||||||
|
|
||||||
type ReplicationGuaranteeKind int32
|
type ReplicationGuaranteeKind int32
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -110,11 +61,11 @@ func (x ReplicationGuaranteeKind) String() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ReplicationGuaranteeKind) Descriptor() protoreflect.EnumDescriptor {
|
func (ReplicationGuaranteeKind) Descriptor() protoreflect.EnumDescriptor {
|
||||||
return file_pdu_proto_enumTypes[1].Descriptor()
|
return file_pdu_proto_enumTypes[0].Descriptor()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ReplicationGuaranteeKind) Type() protoreflect.EnumType {
|
func (ReplicationGuaranteeKind) Type() protoreflect.EnumType {
|
||||||
return &file_pdu_proto_enumTypes[1]
|
return &file_pdu_proto_enumTypes[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x ReplicationGuaranteeKind) Number() protoreflect.EnumNumber {
|
func (x ReplicationGuaranteeKind) Number() protoreflect.EnumNumber {
|
||||||
@@ -123,7 +74,7 @@ func (x ReplicationGuaranteeKind) Number() protoreflect.EnumNumber {
|
|||||||
|
|
||||||
// Deprecated: Use ReplicationGuaranteeKind.Descriptor instead.
|
// Deprecated: Use ReplicationGuaranteeKind.Descriptor instead.
|
||||||
func (ReplicationGuaranteeKind) EnumDescriptor() ([]byte, []int) {
|
func (ReplicationGuaranteeKind) EnumDescriptor() ([]byte, []int) {
|
||||||
return file_pdu_proto_rawDescGZIP(), []int{1}
|
return file_pdu_proto_rawDescGZIP(), []int{0}
|
||||||
}
|
}
|
||||||
|
|
||||||
type FilesystemVersion_VersionType int32
|
type FilesystemVersion_VersionType int32
|
||||||
@@ -156,11 +107,11 @@ func (x FilesystemVersion_VersionType) String() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (FilesystemVersion_VersionType) Descriptor() protoreflect.EnumDescriptor {
|
func (FilesystemVersion_VersionType) Descriptor() protoreflect.EnumDescriptor {
|
||||||
return file_pdu_proto_enumTypes[2].Descriptor()
|
return file_pdu_proto_enumTypes[1].Descriptor()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (FilesystemVersion_VersionType) Type() protoreflect.EnumType {
|
func (FilesystemVersion_VersionType) Type() protoreflect.EnumType {
|
||||||
return &file_pdu_proto_enumTypes[2]
|
return &file_pdu_proto_enumTypes[1]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x FilesystemVersion_VersionType) Number() protoreflect.EnumNumber {
|
func (x FilesystemVersion_VersionType) Number() protoreflect.EnumNumber {
|
||||||
@@ -265,7 +216,6 @@ type Filesystem struct {
|
|||||||
Path string `protobuf:"bytes,1,opt,name=Path,proto3" json:"Path,omitempty"`
|
Path string `protobuf:"bytes,1,opt,name=Path,proto3" json:"Path,omitempty"`
|
||||||
ResumeToken string `protobuf:"bytes,2,opt,name=ResumeToken,proto3" json:"ResumeToken,omitempty"`
|
ResumeToken string `protobuf:"bytes,2,opt,name=ResumeToken,proto3" json:"ResumeToken,omitempty"`
|
||||||
IsPlaceholder bool `protobuf:"varint,3,opt,name=IsPlaceholder,proto3" json:"IsPlaceholder,omitempty"`
|
IsPlaceholder bool `protobuf:"varint,3,opt,name=IsPlaceholder,proto3" json:"IsPlaceholder,omitempty"`
|
||||||
IsEncrypted bool `protobuf:"varint,4,opt,name=IsEncrypted,proto3" json:"IsEncrypted,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *Filesystem) Reset() {
|
func (x *Filesystem) Reset() {
|
||||||
@@ -321,13 +271,6 @@ func (x *Filesystem) GetIsPlaceholder() bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *Filesystem) GetIsEncrypted() bool {
|
|
||||||
if x != nil {
|
|
||||||
return x.IsEncrypted
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
type ListFilesystemVersionsReq struct {
|
type ListFilesystemVersionsReq struct {
|
||||||
state protoimpl.MessageState
|
state protoimpl.MessageState
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
@@ -517,9 +460,7 @@ type SendReq struct {
|
|||||||
// ResumeToken is not empty, the GUIDs of From and To MUST correspond to those
|
// ResumeToken is not empty, the GUIDs of From and To MUST correspond to those
|
||||||
// encoded in the ResumeToken. Otherwise, the Sender MUST return an error.
|
// encoded in the ResumeToken. Otherwise, the Sender MUST return an error.
|
||||||
ResumeToken string `protobuf:"bytes,4,opt,name=ResumeToken,proto3" json:"ResumeToken,omitempty"`
|
ResumeToken string `protobuf:"bytes,4,opt,name=ResumeToken,proto3" json:"ResumeToken,omitempty"`
|
||||||
Encrypted Tri `protobuf:"varint,5,opt,name=Encrypted,proto3,enum=Tri" json:"Encrypted,omitempty"`
|
ReplicationConfig *ReplicationConfig `protobuf:"bytes,6,opt,name=ReplicationConfig,proto3" json:"ReplicationConfig,omitempty"`
|
||||||
DryRun bool `protobuf:"varint,6,opt,name=DryRun,proto3" json:"DryRun,omitempty"`
|
|
||||||
ReplicationConfig *ReplicationConfig `protobuf:"bytes,7,opt,name=ReplicationConfig,proto3" json:"ReplicationConfig,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *SendReq) Reset() {
|
func (x *SendReq) Reset() {
|
||||||
@@ -582,20 +523,6 @@ func (x *SendReq) GetResumeToken() string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *SendReq) GetEncrypted() Tri {
|
|
||||||
if x != nil {
|
|
||||||
return x.Encrypted
|
|
||||||
}
|
|
||||||
return Tri_DontCare
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *SendReq) GetDryRun() bool {
|
|
||||||
if x != nil {
|
|
||||||
return x.DryRun
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *SendReq) GetReplicationConfig() *ReplicationConfig {
|
func (x *SendReq) GetReplicationConfig() *ReplicationConfig {
|
||||||
if x != nil {
|
if x != nil {
|
||||||
return x.ReplicationConfig
|
return x.ReplicationConfig
|
||||||
@@ -766,12 +693,11 @@ type SendRes struct {
|
|||||||
unknownFields protoimpl.UnknownFields
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
// Whether the resume token provided in the request has been used or not.
|
// Whether the resume token provided in the request has been used or not.
|
||||||
// If the SendReq.ResumeToken == "", this field has no meaning.
|
// If the SendReq.ResumeToken == "", this field MUST be false.
|
||||||
UsedResumeToken bool `protobuf:"varint,2,opt,name=UsedResumeToken,proto3" json:"UsedResumeToken,omitempty"`
|
UsedResumeToken bool `protobuf:"varint,1,opt,name=UsedResumeToken,proto3" json:"UsedResumeToken,omitempty"`
|
||||||
// Expected stream size determined by dry run, not exact.
|
// Expected stream size determined by dry run, not exact.
|
||||||
// 0 indicates that for the given SendReq, no size estimate could be made.
|
// 0 indicates that for the given SendReq, no size estimate could be made.
|
||||||
ExpectedSize int64 `protobuf:"varint,3,opt,name=ExpectedSize,proto3" json:"ExpectedSize,omitempty"`
|
ExpectedSize uint64 `protobuf:"varint,2,opt,name=ExpectedSize,proto3" json:"ExpectedSize,omitempty"`
|
||||||
Properties []*Property `protobuf:"bytes,4,rep,name=Properties,proto3" json:"Properties,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *SendRes) Reset() {
|
func (x *SendRes) Reset() {
|
||||||
@@ -813,20 +739,13 @@ func (x *SendRes) GetUsedResumeToken() bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *SendRes) GetExpectedSize() int64 {
|
func (x *SendRes) GetExpectedSize() uint64 {
|
||||||
if x != nil {
|
if x != nil {
|
||||||
return x.ExpectedSize
|
return x.ExpectedSize
|
||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *SendRes) GetProperties() []*Property {
|
|
||||||
if x != nil {
|
|
||||||
return x.Properties
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type SendCompletedReq struct {
|
type SendCompletedReq struct {
|
||||||
state protoimpl.MessageState
|
state protoimpl.MessageState
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
@@ -1412,165 +1331,155 @@ var file_pdu_proto_rawDesc = []byte{
|
|||||||
0x65, 0x6d, 0x52, 0x65, 0x73, 0x12, 0x2d, 0x0a, 0x0b, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73,
|
0x65, 0x6d, 0x52, 0x65, 0x73, 0x12, 0x2d, 0x0a, 0x0b, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73,
|
||||||
0x74, 0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x46, 0x69, 0x6c,
|
0x74, 0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x46, 0x69, 0x6c,
|
||||||
0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x52, 0x0b, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73,
|
0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x52, 0x0b, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73,
|
||||||
0x74, 0x65, 0x6d, 0x73, 0x22, 0x8a, 0x01, 0x0a, 0x0a, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73,
|
0x74, 0x65, 0x6d, 0x73, 0x22, 0x68, 0x0a, 0x0a, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74,
|
||||||
0x74, 0x65, 0x6d, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28,
|
0x65, 0x6d, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
|
||||||
0x09, 0x52, 0x04, 0x50, 0x61, 0x74, 0x68, 0x12, 0x20, 0x0a, 0x0b, 0x52, 0x65, 0x73, 0x75, 0x6d,
|
0x52, 0x04, 0x50, 0x61, 0x74, 0x68, 0x12, 0x20, 0x0a, 0x0b, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x65,
|
||||||
0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x52, 0x65,
|
0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x52, 0x65, 0x73,
|
||||||
0x73, 0x75, 0x6d, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x24, 0x0a, 0x0d, 0x49, 0x73, 0x50,
|
0x75, 0x6d, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x24, 0x0a, 0x0d, 0x49, 0x73, 0x50, 0x6c,
|
||||||
0x6c, 0x61, 0x63, 0x65, 0x68, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08,
|
0x61, 0x63, 0x65, 0x68, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52,
|
||||||
0x52, 0x0d, 0x49, 0x73, 0x50, 0x6c, 0x61, 0x63, 0x65, 0x68, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x12,
|
0x0d, 0x49, 0x73, 0x50, 0x6c, 0x61, 0x63, 0x65, 0x68, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x22, 0x3b,
|
||||||
0x20, 0x0a, 0x0b, 0x49, 0x73, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x18, 0x04,
|
|
||||||
0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x49, 0x73, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65,
|
|
||||||
0x64, 0x22, 0x3b, 0x0a, 0x19, 0x4c, 0x69, 0x73, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73,
|
|
||||||
0x74, 0x65, 0x6d, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x12, 0x1e,
|
|
||||||
0x0a, 0x0a, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01,
|
|
||||||
0x28, 0x09, 0x52, 0x0a, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x22, 0x4b,
|
|
||||||
0x0a, 0x19, 0x4c, 0x69, 0x73, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d,
|
0x0a, 0x19, 0x4c, 0x69, 0x73, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d,
|
||||||
0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x12, 0x2e, 0x0a, 0x08, 0x56,
|
0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x12, 0x1e, 0x0a, 0x0a, 0x46,
|
||||||
0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e,
|
0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
|
||||||
0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f,
|
0x0a, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x22, 0x4b, 0x0a, 0x19, 0x4c,
|
||||||
0x6e, 0x52, 0x08, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0xd4, 0x01, 0x0a, 0x11,
|
0x69, 0x73, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x56, 0x65, 0x72,
|
||||||
0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f,
|
0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x12, 0x2e, 0x0a, 0x08, 0x56, 0x65, 0x72, 0x73,
|
||||||
0x6e, 0x12, 0x32, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32,
|
0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x46, 0x69, 0x6c,
|
||||||
0x1e, 0x2e, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x56, 0x65, 0x72, 0x73,
|
0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x08,
|
||||||
0x69, 0x6f, 0x6e, 0x2e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x52,
|
0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0xd4, 0x01, 0x0a, 0x11, 0x46, 0x69, 0x6c,
|
||||||
0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20,
|
0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x32,
|
||||||
0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x47, 0x75, 0x69,
|
0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1e, 0x2e, 0x46,
|
||||||
0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x04, 0x47, 0x75, 0x69, 0x64, 0x12, 0x1c, 0x0a,
|
|
||||||
0x09, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x58, 0x47, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04,
|
|
||||||
0x52, 0x09, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x58, 0x47, 0x12, 0x1a, 0x0a, 0x08, 0x43,
|
|
||||||
0x72, 0x65, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x43,
|
|
||||||
0x72, 0x65, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x29, 0x0a, 0x0b, 0x56, 0x65, 0x72, 0x73, 0x69,
|
|
||||||
0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0c, 0x0a, 0x08, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68,
|
|
||||||
0x6f, 0x74, 0x10, 0x00, 0x12, 0x0c, 0x0a, 0x08, 0x42, 0x6f, 0x6f, 0x6b, 0x6d, 0x61, 0x72, 0x6b,
|
|
||||||
0x10, 0x01, 0x22, 0x95, 0x02, 0x0a, 0x07, 0x53, 0x65, 0x6e, 0x64, 0x52, 0x65, 0x71, 0x12, 0x1e,
|
|
||||||
0x0a, 0x0a, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01,
|
|
||||||
0x28, 0x09, 0x52, 0x0a, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x12, 0x26,
|
|
||||||
0x0a, 0x04, 0x46, 0x72, 0x6f, 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x46,
|
|
||||||
0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e,
|
0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e,
|
||||||
0x52, 0x04, 0x46, 0x72, 0x6f, 0x6d, 0x12, 0x22, 0x0a, 0x02, 0x54, 0x6f, 0x18, 0x03, 0x20, 0x01,
|
0x2e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x54, 0x79,
|
||||||
0x28, 0x0b, 0x32, 0x12, 0x2e, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x56,
|
0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09,
|
||||||
0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x02, 0x54, 0x6f, 0x12, 0x20, 0x0a, 0x0b, 0x52, 0x65,
|
0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x47, 0x75, 0x69, 0x64, 0x18, 0x03,
|
||||||
0x73, 0x75, 0x6d, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52,
|
0x20, 0x01, 0x28, 0x04, 0x52, 0x04, 0x47, 0x75, 0x69, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x43, 0x72,
|
||||||
0x0b, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x22, 0x0a, 0x09,
|
0x65, 0x61, 0x74, 0x65, 0x54, 0x58, 0x47, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x43,
|
||||||
0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0e, 0x32,
|
0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x58, 0x47, 0x12, 0x1a, 0x0a, 0x08, 0x43, 0x72, 0x65, 0x61,
|
||||||
0x04, 0x2e, 0x54, 0x72, 0x69, 0x52, 0x09, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64,
|
0x74, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x43, 0x72, 0x65, 0x61,
|
||||||
0x12, 0x16, 0x0a, 0x06, 0x44, 0x72, 0x79, 0x52, 0x75, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08,
|
0x74, 0x69, 0x6f, 0x6e, 0x22, 0x29, 0x0a, 0x0b, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x54,
|
||||||
0x52, 0x06, 0x44, 0x72, 0x79, 0x52, 0x75, 0x6e, 0x12, 0x40, 0x0a, 0x11, 0x52, 0x65, 0x70, 0x6c,
|
0x79, 0x70, 0x65, 0x12, 0x0c, 0x0a, 0x08, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x10,
|
||||||
0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x07, 0x20,
|
0x00, 0x12, 0x0c, 0x0a, 0x08, 0x42, 0x6f, 0x6f, 0x6b, 0x6d, 0x61, 0x72, 0x6b, 0x10, 0x01, 0x22,
|
||||||
0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f,
|
0xd9, 0x01, 0x0a, 0x07, 0x53, 0x65, 0x6e, 0x64, 0x52, 0x65, 0x71, 0x12, 0x1e, 0x0a, 0x0a, 0x46,
|
||||||
0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x11, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61,
|
0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
|
||||||
0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x51, 0x0a, 0x11, 0x52, 0x65,
|
0x0a, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x12, 0x26, 0x0a, 0x04, 0x46,
|
||||||
0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12,
|
0x72, 0x6f, 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x46, 0x69, 0x6c, 0x65,
|
||||||
0x3c, 0x0a, 0x0a, 0x70, 0x72, 0x6f, 0x74, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20,
|
0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x04, 0x46,
|
||||||
0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f,
|
0x72, 0x6f, 0x6d, 0x12, 0x22, 0x0a, 0x02, 0x54, 0x6f, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32,
|
||||||
0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x50, 0x72, 0x6f, 0x74, 0x65, 0x63, 0x74, 0x69, 0x6f,
|
0x12, 0x2e, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x56, 0x65, 0x72, 0x73,
|
||||||
0x6e, 0x52, 0x0a, 0x70, 0x72, 0x6f, 0x74, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x8f, 0x01,
|
0x69, 0x6f, 0x6e, 0x52, 0x02, 0x54, 0x6f, 0x12, 0x20, 0x0a, 0x0b, 0x52, 0x65, 0x73, 0x75, 0x6d,
|
||||||
0x0a, 0x1b, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e,
|
0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x52, 0x65,
|
||||||
0x66, 0x69, 0x67, 0x50, 0x72, 0x6f, 0x74, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x33, 0x0a,
|
0x73, 0x75, 0x6d, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x40, 0x0a, 0x11, 0x52, 0x65, 0x70,
|
||||||
0x07, 0x49, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x19,
|
0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x06,
|
||||||
0x2e, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x47, 0x75, 0x61, 0x72,
|
0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69,
|
||||||
0x61, 0x6e, 0x74, 0x65, 0x65, 0x4b, 0x69, 0x6e, 0x64, 0x52, 0x07, 0x49, 0x6e, 0x69, 0x74, 0x69,
|
0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x11, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63,
|
||||||
0x61, 0x6c, 0x12, 0x3b, 0x0a, 0x0b, 0x49, 0x6e, 0x63, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61,
|
0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x51, 0x0a, 0x11, 0x52,
|
||||||
0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x19, 0x2e, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63,
|
0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67,
|
||||||
0x61, 0x74, 0x69, 0x6f, 0x6e, 0x47, 0x75, 0x61, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x65, 0x4b, 0x69,
|
0x12, 0x3c, 0x0a, 0x0a, 0x70, 0x72, 0x6f, 0x74, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01,
|
||||||
0x6e, 0x64, 0x52, 0x0b, 0x49, 0x6e, 0x63, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x6c, 0x22,
|
0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69,
|
||||||
0x34, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x4e,
|
0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x50, 0x72, 0x6f, 0x74, 0x65, 0x63, 0x74, 0x69,
|
||||||
0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12,
|
0x6f, 0x6e, 0x52, 0x0a, 0x70, 0x72, 0x6f, 0x74, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x8f,
|
||||||
0x14, 0x0a, 0x05, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05,
|
0x01, 0x0a, 0x1b, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f,
|
||||||
0x56, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x82, 0x01, 0x0a, 0x07, 0x53, 0x65, 0x6e, 0x64, 0x52, 0x65,
|
0x6e, 0x66, 0x69, 0x67, 0x50, 0x72, 0x6f, 0x74, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x33,
|
||||||
|
0x0a, 0x07, 0x49, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32,
|
||||||
|
0x19, 0x2e, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x47, 0x75, 0x61,
|
||||||
|
0x72, 0x61, 0x6e, 0x74, 0x65, 0x65, 0x4b, 0x69, 0x6e, 0x64, 0x52, 0x07, 0x49, 0x6e, 0x69, 0x74,
|
||||||
|
0x69, 0x61, 0x6c, 0x12, 0x3b, 0x0a, 0x0b, 0x49, 0x6e, 0x63, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74,
|
||||||
|
0x61, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x19, 0x2e, 0x52, 0x65, 0x70, 0x6c, 0x69,
|
||||||
|
0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x47, 0x75, 0x61, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x65, 0x4b,
|
||||||
|
0x69, 0x6e, 0x64, 0x52, 0x0b, 0x49, 0x6e, 0x63, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x6c,
|
||||||
|
0x22, 0x34, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x79, 0x12, 0x12, 0x0a, 0x04,
|
||||||
|
0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65,
|
||||||
|
0x12, 0x14, 0x0a, 0x05, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52,
|
||||||
|
0x05, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x57, 0x0a, 0x07, 0x53, 0x65, 0x6e, 0x64, 0x52, 0x65,
|
||||||
0x73, 0x12, 0x28, 0x0a, 0x0f, 0x55, 0x73, 0x65, 0x64, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x65, 0x54,
|
0x73, 0x12, 0x28, 0x0a, 0x0f, 0x55, 0x73, 0x65, 0x64, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x65, 0x54,
|
||||||
0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x55, 0x73, 0x65, 0x64,
|
0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x55, 0x73, 0x65, 0x64,
|
||||||
0x52, 0x65, 0x73, 0x75, 0x6d, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x22, 0x0a, 0x0c, 0x45,
|
0x52, 0x65, 0x73, 0x75, 0x6d, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x22, 0x0a, 0x0c, 0x45,
|
||||||
0x78, 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, 0x53, 0x69, 0x7a, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28,
|
0x78, 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, 0x53, 0x69, 0x7a, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28,
|
||||||
0x03, 0x52, 0x0c, 0x45, 0x78, 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, 0x53, 0x69, 0x7a, 0x65, 0x12,
|
0x04, 0x52, 0x0c, 0x45, 0x78, 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, 0x53, 0x69, 0x7a, 0x65, 0x22,
|
||||||
0x29, 0x0a, 0x0a, 0x50, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x69, 0x65, 0x73, 0x18, 0x04, 0x20,
|
0x3e, 0x0a, 0x10, 0x53, 0x65, 0x6e, 0x64, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64,
|
||||||
0x03, 0x28, 0x0b, 0x32, 0x09, 0x2e, 0x50, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x79, 0x52, 0x0a,
|
0x52, 0x65, 0x71, 0x12, 0x2a, 0x0a, 0x0b, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x61, 0x6c, 0x52,
|
||||||
0x50, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x69, 0x65, 0x73, 0x22, 0x3e, 0x0a, 0x10, 0x53, 0x65,
|
0x65, 0x71, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x08, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x52,
|
||||||
0x6e, 0x64, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x52, 0x65, 0x71, 0x12, 0x2a,
|
0x65, 0x71, 0x52, 0x0b, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x61, 0x6c, 0x52, 0x65, 0x71, 0x22,
|
||||||
0x0a, 0x0b, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x61, 0x6c, 0x52, 0x65, 0x71, 0x18, 0x02, 0x20,
|
0x12, 0x0a, 0x10, 0x53, 0x65, 0x6e, 0x64, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64,
|
||||||
0x01, 0x28, 0x0b, 0x32, 0x08, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x52, 0x65, 0x71, 0x52, 0x0b, 0x4f,
|
0x52, 0x65, 0x73, 0x22, 0xbe, 0x01, 0x0a, 0x0a, 0x52, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x52,
|
||||||
0x72, 0x69, 0x67, 0x69, 0x6e, 0x61, 0x6c, 0x52, 0x65, 0x71, 0x22, 0x12, 0x0a, 0x10, 0x53, 0x65,
|
0x65, 0x71, 0x12, 0x1e, 0x0a, 0x0a, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d,
|
||||||
0x6e, 0x64, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x52, 0x65, 0x73, 0x22, 0xbe,
|
0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74,
|
||||||
0x01, 0x0a, 0x0a, 0x52, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x52, 0x65, 0x71, 0x12, 0x1e, 0x0a,
|
0x65, 0x6d, 0x12, 0x22, 0x0a, 0x02, 0x54, 0x6f, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12,
|
||||||
0x0a, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28,
|
|
||||||
0x09, 0x52, 0x0a, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x12, 0x22, 0x0a,
|
|
||||||
0x02, 0x54, 0x6f, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x46, 0x69, 0x6c, 0x65,
|
|
||||||
0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x02, 0x54,
|
|
||||||
0x6f, 0x12, 0x2a, 0x0a, 0x10, 0x43, 0x6c, 0x65, 0x61, 0x72, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x65,
|
|
||||||
0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x43, 0x6c, 0x65,
|
|
||||||
0x61, 0x72, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x40, 0x0a,
|
|
||||||
0x11, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66,
|
|
||||||
0x69, 0x67, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x52, 0x65, 0x70, 0x6c, 0x69,
|
|
||||||
0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x11, 0x52, 0x65,
|
|
||||||
0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22,
|
|
||||||
0x0c, 0x0a, 0x0a, 0x52, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x52, 0x65, 0x73, 0x22, 0x67, 0x0a,
|
|
||||||
0x13, 0x44, 0x65, 0x73, 0x74, 0x72, 0x6f, 0x79, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74,
|
|
||||||
0x73, 0x52, 0x65, 0x71, 0x12, 0x1e, 0x0a, 0x0a, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74,
|
|
||||||
0x65, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79,
|
|
||||||
0x73, 0x74, 0x65, 0x6d, 0x12, 0x30, 0x0a, 0x09, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74,
|
|
||||||
0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79,
|
|
||||||
0x73, 0x74, 0x65, 0x6d, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x53, 0x6e, 0x61,
|
|
||||||
0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x22, 0x5a, 0x0a, 0x12, 0x44, 0x65, 0x73, 0x74, 0x72, 0x6f,
|
|
||||||
0x79, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x52, 0x65, 0x73, 0x12, 0x2e, 0x0a, 0x08,
|
|
||||||
0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12,
|
|
||||||
0x2e, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x56, 0x65, 0x72, 0x73, 0x69,
|
0x2e, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x56, 0x65, 0x72, 0x73, 0x69,
|
||||||
0x6f, 0x6e, 0x52, 0x08, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x12, 0x14, 0x0a, 0x05,
|
0x6f, 0x6e, 0x52, 0x02, 0x54, 0x6f, 0x12, 0x2a, 0x0a, 0x10, 0x43, 0x6c, 0x65, 0x61, 0x72, 0x52,
|
||||||
0x45, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x45, 0x72, 0x72,
|
0x65, 0x73, 0x75, 0x6d, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08,
|
||||||
0x6f, 0x72, 0x22, 0x44, 0x0a, 0x13, 0x44, 0x65, 0x73, 0x74, 0x72, 0x6f, 0x79, 0x53, 0x6e, 0x61,
|
0x52, 0x10, 0x43, 0x6c, 0x65, 0x61, 0x72, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x65, 0x54, 0x6f, 0x6b,
|
||||||
0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x52, 0x65, 0x73, 0x12, 0x2d, 0x0a, 0x07, 0x52, 0x65, 0x73,
|
0x65, 0x6e, 0x12, 0x40, 0x0a, 0x11, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f,
|
||||||
0x75, 0x6c, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x44, 0x65, 0x73,
|
0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e,
|
||||||
0x74, 0x72, 0x6f, 0x79, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x52, 0x65, 0x73, 0x52,
|
0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69,
|
||||||
0x07, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x22, 0x36, 0x0a, 0x14, 0x52, 0x65, 0x70, 0x6c,
|
0x67, 0x52, 0x11, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f,
|
||||||
0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x75, 0x72, 0x73, 0x6f, 0x72, 0x52, 0x65, 0x71,
|
0x6e, 0x66, 0x69, 0x67, 0x22, 0x0c, 0x0a, 0x0a, 0x52, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x52,
|
||||||
0x12, 0x1e, 0x0a, 0x0a, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x18, 0x01,
|
0x65, 0x73, 0x22, 0x67, 0x0a, 0x13, 0x44, 0x65, 0x73, 0x74, 0x72, 0x6f, 0x79, 0x53, 0x6e, 0x61,
|
||||||
0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d,
|
0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x52, 0x65, 0x71, 0x12, 0x1e, 0x0a, 0x0a, 0x46, 0x69, 0x6c,
|
||||||
0x22, 0x54, 0x0a, 0x14, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43,
|
0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x46,
|
||||||
0x75, 0x72, 0x73, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x12, 0x14, 0x0a, 0x04, 0x47, 0x75, 0x69, 0x64,
|
0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x12, 0x30, 0x0a, 0x09, 0x53, 0x6e, 0x61,
|
||||||
0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x48, 0x00, 0x52, 0x04, 0x47, 0x75, 0x69, 0x64, 0x12, 0x1c,
|
0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x46,
|
||||||
0x0a, 0x08, 0x4e, 0x6f, 0x74, 0x65, 0x78, 0x69, 0x73, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08,
|
0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e,
|
||||||
0x48, 0x00, 0x52, 0x08, 0x4e, 0x6f, 0x74, 0x65, 0x78, 0x69, 0x73, 0x74, 0x42, 0x08, 0x0a, 0x06,
|
0x52, 0x09, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x22, 0x5a, 0x0a, 0x12, 0x44,
|
||||||
0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x22, 0x23, 0x0a, 0x07, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65,
|
0x65, 0x73, 0x74, 0x72, 0x6f, 0x79, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x52, 0x65,
|
||||||
0x71, 0x12, 0x18, 0x0a, 0x07, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01,
|
0x73, 0x12, 0x2e, 0x0a, 0x08, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x18, 0x01, 0x20,
|
||||||
0x28, 0x09, 0x52, 0x07, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x1d, 0x0a, 0x07, 0x50,
|
0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d,
|
||||||
0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x45, 0x63, 0x68, 0x6f, 0x18, 0x01,
|
0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x08, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f,
|
||||||
0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x45, 0x63, 0x68, 0x6f, 0x2a, 0x28, 0x0a, 0x03, 0x54, 0x72,
|
0x74, 0x12, 0x14, 0x0a, 0x05, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09,
|
||||||
0x69, 0x12, 0x0c, 0x0a, 0x08, 0x44, 0x6f, 0x6e, 0x74, 0x43, 0x61, 0x72, 0x65, 0x10, 0x00, 0x12,
|
0x52, 0x05, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x44, 0x0a, 0x13, 0x44, 0x65, 0x73, 0x74, 0x72,
|
||||||
0x09, 0x0a, 0x05, 0x46, 0x61, 0x6c, 0x73, 0x65, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x54, 0x72,
|
0x6f, 0x79, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x52, 0x65, 0x73, 0x12, 0x2d,
|
||||||
0x75, 0x65, 0x10, 0x02, 0x2a, 0x86, 0x01, 0x0a, 0x18, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61,
|
0x0a, 0x07, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32,
|
||||||
0x74, 0x69, 0x6f, 0x6e, 0x47, 0x75, 0x61, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x65, 0x4b, 0x69, 0x6e,
|
0x13, 0x2e, 0x44, 0x65, 0x73, 0x74, 0x72, 0x6f, 0x79, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f,
|
||||||
0x64, 0x12, 0x14, 0x0a, 0x10, 0x47, 0x75, 0x61, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x65, 0x49, 0x6e,
|
0x74, 0x52, 0x65, 0x73, 0x52, 0x07, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x22, 0x36, 0x0a,
|
||||||
0x76, 0x61, 0x6c, 0x69, 0x64, 0x10, 0x00, 0x12, 0x19, 0x0a, 0x15, 0x47, 0x75, 0x61, 0x72, 0x61,
|
0x14, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x75, 0x72, 0x73,
|
||||||
0x6e, 0x74, 0x65, 0x65, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79,
|
0x6f, 0x72, 0x52, 0x65, 0x71, 0x12, 0x1e, 0x0a, 0x0a, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73,
|
||||||
0x10, 0x01, 0x12, 0x23, 0x0a, 0x1f, 0x47, 0x75, 0x61, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x65, 0x49,
|
0x74, 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x46, 0x69, 0x6c, 0x65, 0x73,
|
||||||
0x6e, 0x63, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x6c, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63,
|
0x79, 0x73, 0x74, 0x65, 0x6d, 0x22, 0x54, 0x0a, 0x14, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61,
|
||||||
0x61, 0x74, 0x69, 0x6f, 0x6e, 0x10, 0x02, 0x12, 0x14, 0x0a, 0x10, 0x47, 0x75, 0x61, 0x72, 0x61,
|
0x74, 0x69, 0x6f, 0x6e, 0x43, 0x75, 0x72, 0x73, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x12, 0x14, 0x0a,
|
||||||
0x6e, 0x74, 0x65, 0x65, 0x4e, 0x6f, 0x74, 0x68, 0x69, 0x6e, 0x67, 0x10, 0x03, 0x32, 0xf0, 0x02,
|
0x04, 0x47, 0x75, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x48, 0x00, 0x52, 0x04, 0x47,
|
||||||
0x0a, 0x0b, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1a, 0x0a,
|
0x75, 0x69, 0x64, 0x12, 0x1c, 0x0a, 0x08, 0x4e, 0x6f, 0x74, 0x65, 0x78, 0x69, 0x73, 0x74, 0x18,
|
||||||
0x04, 0x50, 0x69, 0x6e, 0x67, 0x12, 0x08, 0x2e, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x1a,
|
0x02, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x08, 0x4e, 0x6f, 0x74, 0x65, 0x78, 0x69, 0x73,
|
||||||
0x08, 0x2e, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x12, 0x39, 0x0a, 0x0f, 0x4c, 0x69, 0x73,
|
0x74, 0x42, 0x08, 0x0a, 0x06, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x22, 0x23, 0x0a, 0x07, 0x50,
|
||||||
0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x73, 0x12, 0x12, 0x2e, 0x4c,
|
0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x12, 0x18, 0x0a, 0x07, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67,
|
||||||
0x69, 0x73, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x52, 0x65, 0x71,
|
0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65,
|
||||||
0x1a, 0x12, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65,
|
0x22, 0x1d, 0x0a, 0x07, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x45,
|
||||||
0x6d, 0x52, 0x65, 0x73, 0x12, 0x50, 0x0a, 0x16, 0x4c, 0x69, 0x73, 0x74, 0x46, 0x69, 0x6c, 0x65,
|
0x63, 0x68, 0x6f, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x45, 0x63, 0x68, 0x6f, 0x2a,
|
||||||
0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x1a,
|
0x86, 0x01, 0x0a, 0x18, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x47,
|
||||||
0x2e, 0x4c, 0x69, 0x73, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x56,
|
0x75, 0x61, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x65, 0x4b, 0x69, 0x6e, 0x64, 0x12, 0x14, 0x0a, 0x10,
|
||||||
0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x1a, 0x1a, 0x2e, 0x4c, 0x69, 0x73,
|
0x47, 0x75, 0x61, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x65, 0x49, 0x6e, 0x76, 0x61, 0x6c, 0x69, 0x64,
|
||||||
0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x56, 0x65, 0x72, 0x73, 0x69,
|
0x10, 0x00, 0x12, 0x19, 0x0a, 0x15, 0x47, 0x75, 0x61, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x65, 0x52,
|
||||||
0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x12, 0x3e, 0x0a, 0x10, 0x44, 0x65, 0x73, 0x74, 0x72, 0x6f,
|
0x65, 0x73, 0x75, 0x6d, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x10, 0x01, 0x12, 0x23, 0x0a,
|
||||||
0x79, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x12, 0x14, 0x2e, 0x44, 0x65, 0x73,
|
0x1f, 0x47, 0x75, 0x61, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x65, 0x49, 0x6e, 0x63, 0x72, 0x65, 0x6d,
|
||||||
0x74, 0x72, 0x6f, 0x79, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x52, 0x65, 0x71,
|
0x65, 0x6e, 0x74, 0x61, 0x6c, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e,
|
||||||
0x1a, 0x14, 0x2e, 0x44, 0x65, 0x73, 0x74, 0x72, 0x6f, 0x79, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68,
|
0x10, 0x02, 0x12, 0x14, 0x0a, 0x10, 0x47, 0x75, 0x61, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x65, 0x4e,
|
||||||
0x6f, 0x74, 0x73, 0x52, 0x65, 0x73, 0x12, 0x41, 0x0a, 0x11, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63,
|
0x6f, 0x74, 0x68, 0x69, 0x6e, 0x67, 0x10, 0x03, 0x32, 0x8f, 0x03, 0x0a, 0x0b, 0x52, 0x65, 0x70,
|
||||||
0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x75, 0x72, 0x73, 0x6f, 0x72, 0x12, 0x15, 0x2e, 0x52, 0x65,
|
0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1a, 0x0a, 0x04, 0x50, 0x69, 0x6e, 0x67,
|
||||||
0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x75, 0x72, 0x73, 0x6f, 0x72, 0x52,
|
0x12, 0x08, 0x2e, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x1a, 0x08, 0x2e, 0x50, 0x69, 0x6e,
|
||||||
0x65, 0x71, 0x1a, 0x15, 0x2e, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e,
|
0x67, 0x52, 0x65, 0x73, 0x12, 0x39, 0x0a, 0x0f, 0x4c, 0x69, 0x73, 0x74, 0x46, 0x69, 0x6c, 0x65,
|
||||||
0x43, 0x75, 0x72, 0x73, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x12, 0x35, 0x0a, 0x0d, 0x53, 0x65, 0x6e,
|
0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x73, 0x12, 0x12, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x46, 0x69,
|
||||||
0x64, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x12, 0x11, 0x2e, 0x53, 0x65, 0x6e,
|
0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x52, 0x65, 0x71, 0x1a, 0x12, 0x2e, 0x4c, 0x69,
|
||||||
0x64, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x52, 0x65, 0x71, 0x1a, 0x11, 0x2e,
|
0x73, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x52, 0x65, 0x73, 0x12,
|
||||||
0x53, 0x65, 0x6e, 0x64, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x52, 0x65, 0x73,
|
0x50, 0x0a, 0x16, 0x4c, 0x69, 0x73, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65,
|
||||||
0x42, 0x07, 0x5a, 0x05, 0x2e, 0x3b, 0x70, 0x64, 0x75, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f,
|
0x6d, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x1a, 0x2e, 0x4c, 0x69, 0x73, 0x74,
|
||||||
0x33,
|
0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f,
|
||||||
|
0x6e, 0x73, 0x52, 0x65, 0x71, 0x1a, 0x1a, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x46, 0x69, 0x6c, 0x65,
|
||||||
|
0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65,
|
||||||
|
0x73, 0x12, 0x3e, 0x0a, 0x10, 0x44, 0x65, 0x73, 0x74, 0x72, 0x6f, 0x79, 0x53, 0x6e, 0x61, 0x70,
|
||||||
|
0x73, 0x68, 0x6f, 0x74, 0x73, 0x12, 0x14, 0x2e, 0x44, 0x65, 0x73, 0x74, 0x72, 0x6f, 0x79, 0x53,
|
||||||
|
0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x52, 0x65, 0x71, 0x1a, 0x14, 0x2e, 0x44, 0x65,
|
||||||
|
0x73, 0x74, 0x72, 0x6f, 0x79, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x52, 0x65,
|
||||||
|
0x73, 0x12, 0x41, 0x0a, 0x11, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e,
|
||||||
|
0x43, 0x75, 0x72, 0x73, 0x6f, 0x72, 0x12, 0x15, 0x2e, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61,
|
||||||
|
0x74, 0x69, 0x6f, 0x6e, 0x43, 0x75, 0x72, 0x73, 0x6f, 0x72, 0x52, 0x65, 0x71, 0x1a, 0x15, 0x2e,
|
||||||
|
0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x75, 0x72, 0x73, 0x6f,
|
||||||
|
0x72, 0x52, 0x65, 0x73, 0x12, 0x1d, 0x0a, 0x07, 0x53, 0x65, 0x6e, 0x64, 0x44, 0x72, 0x79, 0x12,
|
||||||
|
0x08, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x52, 0x65, 0x71, 0x1a, 0x08, 0x2e, 0x53, 0x65, 0x6e, 0x64,
|
||||||
|
0x52, 0x65, 0x73, 0x12, 0x35, 0x0a, 0x0d, 0x53, 0x65, 0x6e, 0x64, 0x43, 0x6f, 0x6d, 0x70, 0x6c,
|
||||||
|
0x65, 0x74, 0x65, 0x64, 0x12, 0x11, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x43, 0x6f, 0x6d, 0x70, 0x6c,
|
||||||
|
0x65, 0x74, 0x65, 0x64, 0x52, 0x65, 0x71, 0x1a, 0x11, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x43, 0x6f,
|
||||||
|
0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x52, 0x65, 0x73, 0x42, 0x07, 0x5a, 0x05, 0x2e, 0x3b,
|
||||||
|
0x70, 0x64, 0x75, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -1585,70 +1494,69 @@ func file_pdu_proto_rawDescGZIP() []byte {
|
|||||||
return file_pdu_proto_rawDescData
|
return file_pdu_proto_rawDescData
|
||||||
}
|
}
|
||||||
|
|
||||||
var file_pdu_proto_enumTypes = make([]protoimpl.EnumInfo, 3)
|
var file_pdu_proto_enumTypes = make([]protoimpl.EnumInfo, 2)
|
||||||
var file_pdu_proto_msgTypes = make([]protoimpl.MessageInfo, 22)
|
var file_pdu_proto_msgTypes = make([]protoimpl.MessageInfo, 22)
|
||||||
var file_pdu_proto_goTypes = []interface{}{
|
var file_pdu_proto_goTypes = []interface{}{
|
||||||
(Tri)(0), // 0: Tri
|
(ReplicationGuaranteeKind)(0), // 0: ReplicationGuaranteeKind
|
||||||
(ReplicationGuaranteeKind)(0), // 1: ReplicationGuaranteeKind
|
(FilesystemVersion_VersionType)(0), // 1: FilesystemVersion.VersionType
|
||||||
(FilesystemVersion_VersionType)(0), // 2: FilesystemVersion.VersionType
|
(*ListFilesystemReq)(nil), // 2: ListFilesystemReq
|
||||||
(*ListFilesystemReq)(nil), // 3: ListFilesystemReq
|
(*ListFilesystemRes)(nil), // 3: ListFilesystemRes
|
||||||
(*ListFilesystemRes)(nil), // 4: ListFilesystemRes
|
(*Filesystem)(nil), // 4: Filesystem
|
||||||
(*Filesystem)(nil), // 5: Filesystem
|
(*ListFilesystemVersionsReq)(nil), // 5: ListFilesystemVersionsReq
|
||||||
(*ListFilesystemVersionsReq)(nil), // 6: ListFilesystemVersionsReq
|
(*ListFilesystemVersionsRes)(nil), // 6: ListFilesystemVersionsRes
|
||||||
(*ListFilesystemVersionsRes)(nil), // 7: ListFilesystemVersionsRes
|
(*FilesystemVersion)(nil), // 7: FilesystemVersion
|
||||||
(*FilesystemVersion)(nil), // 8: FilesystemVersion
|
(*SendReq)(nil), // 8: SendReq
|
||||||
(*SendReq)(nil), // 9: SendReq
|
(*ReplicationConfig)(nil), // 9: ReplicationConfig
|
||||||
(*ReplicationConfig)(nil), // 10: ReplicationConfig
|
(*ReplicationConfigProtection)(nil), // 10: ReplicationConfigProtection
|
||||||
(*ReplicationConfigProtection)(nil), // 11: ReplicationConfigProtection
|
(*Property)(nil), // 11: Property
|
||||||
(*Property)(nil), // 12: Property
|
(*SendRes)(nil), // 12: SendRes
|
||||||
(*SendRes)(nil), // 13: SendRes
|
(*SendCompletedReq)(nil), // 13: SendCompletedReq
|
||||||
(*SendCompletedReq)(nil), // 14: SendCompletedReq
|
(*SendCompletedRes)(nil), // 14: SendCompletedRes
|
||||||
(*SendCompletedRes)(nil), // 15: SendCompletedRes
|
(*ReceiveReq)(nil), // 15: ReceiveReq
|
||||||
(*ReceiveReq)(nil), // 16: ReceiveReq
|
(*ReceiveRes)(nil), // 16: ReceiveRes
|
||||||
(*ReceiveRes)(nil), // 17: ReceiveRes
|
(*DestroySnapshotsReq)(nil), // 17: DestroySnapshotsReq
|
||||||
(*DestroySnapshotsReq)(nil), // 18: DestroySnapshotsReq
|
(*DestroySnapshotRes)(nil), // 18: DestroySnapshotRes
|
||||||
(*DestroySnapshotRes)(nil), // 19: DestroySnapshotRes
|
(*DestroySnapshotsRes)(nil), // 19: DestroySnapshotsRes
|
||||||
(*DestroySnapshotsRes)(nil), // 20: DestroySnapshotsRes
|
(*ReplicationCursorReq)(nil), // 20: ReplicationCursorReq
|
||||||
(*ReplicationCursorReq)(nil), // 21: ReplicationCursorReq
|
(*ReplicationCursorRes)(nil), // 21: ReplicationCursorRes
|
||||||
(*ReplicationCursorRes)(nil), // 22: ReplicationCursorRes
|
(*PingReq)(nil), // 22: PingReq
|
||||||
(*PingReq)(nil), // 23: PingReq
|
(*PingRes)(nil), // 23: PingRes
|
||||||
(*PingRes)(nil), // 24: PingRes
|
|
||||||
}
|
}
|
||||||
var file_pdu_proto_depIdxs = []int32{
|
var file_pdu_proto_depIdxs = []int32{
|
||||||
5, // 0: ListFilesystemRes.Filesystems:type_name -> Filesystem
|
4, // 0: ListFilesystemRes.Filesystems:type_name -> Filesystem
|
||||||
8, // 1: ListFilesystemVersionsRes.Versions:type_name -> FilesystemVersion
|
7, // 1: ListFilesystemVersionsRes.Versions:type_name -> FilesystemVersion
|
||||||
2, // 2: FilesystemVersion.Type:type_name -> FilesystemVersion.VersionType
|
1, // 2: FilesystemVersion.Type:type_name -> FilesystemVersion.VersionType
|
||||||
8, // 3: SendReq.From:type_name -> FilesystemVersion
|
7, // 3: SendReq.From:type_name -> FilesystemVersion
|
||||||
8, // 4: SendReq.To:type_name -> FilesystemVersion
|
7, // 4: SendReq.To:type_name -> FilesystemVersion
|
||||||
0, // 5: SendReq.Encrypted:type_name -> Tri
|
9, // 5: SendReq.ReplicationConfig:type_name -> ReplicationConfig
|
||||||
10, // 6: SendReq.ReplicationConfig:type_name -> ReplicationConfig
|
10, // 6: ReplicationConfig.protection:type_name -> ReplicationConfigProtection
|
||||||
11, // 7: ReplicationConfig.protection:type_name -> ReplicationConfigProtection
|
0, // 7: ReplicationConfigProtection.Initial:type_name -> ReplicationGuaranteeKind
|
||||||
1, // 8: ReplicationConfigProtection.Initial:type_name -> ReplicationGuaranteeKind
|
0, // 8: ReplicationConfigProtection.Incremental:type_name -> ReplicationGuaranteeKind
|
||||||
1, // 9: ReplicationConfigProtection.Incremental:type_name -> ReplicationGuaranteeKind
|
8, // 9: SendCompletedReq.OriginalReq:type_name -> SendReq
|
||||||
12, // 10: SendRes.Properties:type_name -> Property
|
7, // 10: ReceiveReq.To:type_name -> FilesystemVersion
|
||||||
9, // 11: SendCompletedReq.OriginalReq:type_name -> SendReq
|
9, // 11: ReceiveReq.ReplicationConfig:type_name -> ReplicationConfig
|
||||||
8, // 12: ReceiveReq.To:type_name -> FilesystemVersion
|
7, // 12: DestroySnapshotsReq.Snapshots:type_name -> FilesystemVersion
|
||||||
10, // 13: ReceiveReq.ReplicationConfig:type_name -> ReplicationConfig
|
7, // 13: DestroySnapshotRes.Snapshot:type_name -> FilesystemVersion
|
||||||
8, // 14: DestroySnapshotsReq.Snapshots:type_name -> FilesystemVersion
|
18, // 14: DestroySnapshotsRes.Results:type_name -> DestroySnapshotRes
|
||||||
8, // 15: DestroySnapshotRes.Snapshot:type_name -> FilesystemVersion
|
22, // 15: Replication.Ping:input_type -> PingReq
|
||||||
19, // 16: DestroySnapshotsRes.Results:type_name -> DestroySnapshotRes
|
2, // 16: Replication.ListFilesystems:input_type -> ListFilesystemReq
|
||||||
23, // 17: Replication.Ping:input_type -> PingReq
|
5, // 17: Replication.ListFilesystemVersions:input_type -> ListFilesystemVersionsReq
|
||||||
3, // 18: Replication.ListFilesystems:input_type -> ListFilesystemReq
|
17, // 18: Replication.DestroySnapshots:input_type -> DestroySnapshotsReq
|
||||||
6, // 19: Replication.ListFilesystemVersions:input_type -> ListFilesystemVersionsReq
|
20, // 19: Replication.ReplicationCursor:input_type -> ReplicationCursorReq
|
||||||
18, // 20: Replication.DestroySnapshots:input_type -> DestroySnapshotsReq
|
8, // 20: Replication.SendDry:input_type -> SendReq
|
||||||
21, // 21: Replication.ReplicationCursor:input_type -> ReplicationCursorReq
|
13, // 21: Replication.SendCompleted:input_type -> SendCompletedReq
|
||||||
14, // 22: Replication.SendCompleted:input_type -> SendCompletedReq
|
23, // 22: Replication.Ping:output_type -> PingRes
|
||||||
24, // 23: Replication.Ping:output_type -> PingRes
|
3, // 23: Replication.ListFilesystems:output_type -> ListFilesystemRes
|
||||||
4, // 24: Replication.ListFilesystems:output_type -> ListFilesystemRes
|
6, // 24: Replication.ListFilesystemVersions:output_type -> ListFilesystemVersionsRes
|
||||||
7, // 25: Replication.ListFilesystemVersions:output_type -> ListFilesystemVersionsRes
|
19, // 25: Replication.DestroySnapshots:output_type -> DestroySnapshotsRes
|
||||||
20, // 26: Replication.DestroySnapshots:output_type -> DestroySnapshotsRes
|
21, // 26: Replication.ReplicationCursor:output_type -> ReplicationCursorRes
|
||||||
22, // 27: Replication.ReplicationCursor:output_type -> ReplicationCursorRes
|
12, // 27: Replication.SendDry:output_type -> SendRes
|
||||||
15, // 28: Replication.SendCompleted:output_type -> SendCompletedRes
|
14, // 28: Replication.SendCompleted:output_type -> SendCompletedRes
|
||||||
23, // [23:29] is the sub-list for method output_type
|
22, // [22:29] is the sub-list for method output_type
|
||||||
17, // [17:23] is the sub-list for method input_type
|
15, // [15:22] is the sub-list for method input_type
|
||||||
17, // [17:17] is the sub-list for extension type_name
|
15, // [15:15] is the sub-list for extension type_name
|
||||||
17, // [17:17] is the sub-list for extension extendee
|
15, // [15:15] is the sub-list for extension extendee
|
||||||
0, // [0:17] is the sub-list for field type_name
|
0, // [0:15] is the sub-list for field type_name
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() { file_pdu_proto_init() }
|
func init() { file_pdu_proto_init() }
|
||||||
@@ -1931,7 +1839,7 @@ func file_pdu_proto_init() {
|
|||||||
File: protoimpl.DescBuilder{
|
File: protoimpl.DescBuilder{
|
||||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||||
RawDescriptor: file_pdu_proto_rawDesc,
|
RawDescriptor: file_pdu_proto_rawDesc,
|
||||||
NumEnums: 3,
|
NumEnums: 2,
|
||||||
NumMessages: 22,
|
NumMessages: 22,
|
||||||
NumExtensions: 0,
|
NumExtensions: 0,
|
||||||
NumServices: 1,
|
NumServices: 1,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ service Replication {
|
|||||||
returns (ListFilesystemVersionsRes);
|
returns (ListFilesystemVersionsRes);
|
||||||
rpc DestroySnapshots(DestroySnapshotsReq) returns (DestroySnapshotsRes);
|
rpc DestroySnapshots(DestroySnapshotsReq) returns (DestroySnapshotsRes);
|
||||||
rpc ReplicationCursor(ReplicationCursorReq) returns (ReplicationCursorRes);
|
rpc ReplicationCursor(ReplicationCursorReq) returns (ReplicationCursorRes);
|
||||||
|
rpc SendDry(SendReq) returns (SendRes);
|
||||||
rpc SendCompleted(SendCompletedReq) returns (SendCompletedRes);
|
rpc SendCompleted(SendCompletedReq) returns (SendCompletedRes);
|
||||||
// for Send and Recv, see package rpc
|
// for Send and Recv, see package rpc
|
||||||
}
|
}
|
||||||
@@ -20,7 +21,6 @@ message Filesystem {
|
|||||||
string Path = 1;
|
string Path = 1;
|
||||||
string ResumeToken = 2;
|
string ResumeToken = 2;
|
||||||
bool IsPlaceholder = 3;
|
bool IsPlaceholder = 3;
|
||||||
bool IsEncrypted = 4;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message ListFilesystemVersionsReq { string Filesystem = 1; }
|
message ListFilesystemVersionsReq { string Filesystem = 1; }
|
||||||
@@ -39,12 +39,6 @@ message FilesystemVersion {
|
|||||||
string Creation = 5; // RFC 3339
|
string Creation = 5; // RFC 3339
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Tri {
|
|
||||||
DontCare = 0;
|
|
||||||
False = 1;
|
|
||||||
True = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
message SendReq {
|
message SendReq {
|
||||||
string Filesystem = 1;
|
string Filesystem = 1;
|
||||||
// May be empty / null to request a full transfer of To
|
// May be empty / null to request a full transfer of To
|
||||||
@@ -58,11 +52,8 @@ message SendReq {
|
|||||||
// ResumeToken is not empty, the GUIDs of From and To MUST correspond to those
|
// ResumeToken is not empty, the GUIDs of From and To MUST correspond to those
|
||||||
// encoded in the ResumeToken. Otherwise, the Sender MUST return an error.
|
// encoded in the ResumeToken. Otherwise, the Sender MUST return an error.
|
||||||
string ResumeToken = 4;
|
string ResumeToken = 4;
|
||||||
Tri Encrypted = 5;
|
|
||||||
|
|
||||||
bool DryRun = 6;
|
ReplicationConfig ReplicationConfig = 6;
|
||||||
|
|
||||||
ReplicationConfig ReplicationConfig = 7;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message ReplicationConfig {
|
message ReplicationConfig {
|
||||||
@@ -89,14 +80,12 @@ message Property {
|
|||||||
|
|
||||||
message SendRes {
|
message SendRes {
|
||||||
// Whether the resume token provided in the request has been used or not.
|
// Whether the resume token provided in the request has been used or not.
|
||||||
// If the SendReq.ResumeToken == "", this field has no meaning.
|
// If the SendReq.ResumeToken == "", this field MUST be false.
|
||||||
bool UsedResumeToken = 2;
|
bool UsedResumeToken = 1;
|
||||||
|
|
||||||
// Expected stream size determined by dry run, not exact.
|
// Expected stream size determined by dry run, not exact.
|
||||||
// 0 indicates that for the given SendReq, no size estimate could be made.
|
// 0 indicates that for the given SendReq, no size estimate could be made.
|
||||||
int64 ExpectedSize = 3;
|
uint64 ExpectedSize = 2;
|
||||||
|
|
||||||
repeated Property Properties = 4;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message SendCompletedReq {
|
message SendCompletedReq {
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ type ReplicationClient interface {
|
|||||||
ListFilesystemVersions(ctx context.Context, in *ListFilesystemVersionsReq, opts ...grpc.CallOption) (*ListFilesystemVersionsRes, error)
|
ListFilesystemVersions(ctx context.Context, in *ListFilesystemVersionsReq, opts ...grpc.CallOption) (*ListFilesystemVersionsRes, error)
|
||||||
DestroySnapshots(ctx context.Context, in *DestroySnapshotsReq, opts ...grpc.CallOption) (*DestroySnapshotsRes, error)
|
DestroySnapshots(ctx context.Context, in *DestroySnapshotsReq, opts ...grpc.CallOption) (*DestroySnapshotsRes, error)
|
||||||
ReplicationCursor(ctx context.Context, in *ReplicationCursorReq, opts ...grpc.CallOption) (*ReplicationCursorRes, error)
|
ReplicationCursor(ctx context.Context, in *ReplicationCursorReq, opts ...grpc.CallOption) (*ReplicationCursorRes, error)
|
||||||
|
SendDry(ctx context.Context, in *SendReq, opts ...grpc.CallOption) (*SendRes, error)
|
||||||
SendCompleted(ctx context.Context, in *SendCompletedReq, opts ...grpc.CallOption) (*SendCompletedRes, error)
|
SendCompleted(ctx context.Context, in *SendCompletedReq, opts ...grpc.CallOption) (*SendCompletedRes, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,6 +80,15 @@ func (c *replicationClient) ReplicationCursor(ctx context.Context, in *Replicati
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *replicationClient) SendDry(ctx context.Context, in *SendReq, opts ...grpc.CallOption) (*SendRes, error) {
|
||||||
|
out := new(SendRes)
|
||||||
|
err := c.cc.Invoke(ctx, "/Replication/SendDry", in, out, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *replicationClient) SendCompleted(ctx context.Context, in *SendCompletedReq, opts ...grpc.CallOption) (*SendCompletedRes, error) {
|
func (c *replicationClient) SendCompleted(ctx context.Context, in *SendCompletedReq, opts ...grpc.CallOption) (*SendCompletedRes, error) {
|
||||||
out := new(SendCompletedRes)
|
out := new(SendCompletedRes)
|
||||||
err := c.cc.Invoke(ctx, "/Replication/SendCompleted", in, out, opts...)
|
err := c.cc.Invoke(ctx, "/Replication/SendCompleted", in, out, opts...)
|
||||||
@@ -97,6 +107,7 @@ type ReplicationServer interface {
|
|||||||
ListFilesystemVersions(context.Context, *ListFilesystemVersionsReq) (*ListFilesystemVersionsRes, error)
|
ListFilesystemVersions(context.Context, *ListFilesystemVersionsReq) (*ListFilesystemVersionsRes, error)
|
||||||
DestroySnapshots(context.Context, *DestroySnapshotsReq) (*DestroySnapshotsRes, error)
|
DestroySnapshots(context.Context, *DestroySnapshotsReq) (*DestroySnapshotsRes, error)
|
||||||
ReplicationCursor(context.Context, *ReplicationCursorReq) (*ReplicationCursorRes, error)
|
ReplicationCursor(context.Context, *ReplicationCursorReq) (*ReplicationCursorRes, error)
|
||||||
|
SendDry(context.Context, *SendReq) (*SendRes, error)
|
||||||
SendCompleted(context.Context, *SendCompletedReq) (*SendCompletedRes, error)
|
SendCompleted(context.Context, *SendCompletedReq) (*SendCompletedRes, error)
|
||||||
mustEmbedUnimplementedReplicationServer()
|
mustEmbedUnimplementedReplicationServer()
|
||||||
}
|
}
|
||||||
@@ -120,6 +131,9 @@ func (UnimplementedReplicationServer) DestroySnapshots(context.Context, *Destroy
|
|||||||
func (UnimplementedReplicationServer) ReplicationCursor(context.Context, *ReplicationCursorReq) (*ReplicationCursorRes, error) {
|
func (UnimplementedReplicationServer) ReplicationCursor(context.Context, *ReplicationCursorReq) (*ReplicationCursorRes, error) {
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method ReplicationCursor not implemented")
|
return nil, status.Errorf(codes.Unimplemented, "method ReplicationCursor not implemented")
|
||||||
}
|
}
|
||||||
|
func (UnimplementedReplicationServer) SendDry(context.Context, *SendReq) (*SendRes, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method SendDry not implemented")
|
||||||
|
}
|
||||||
func (UnimplementedReplicationServer) SendCompleted(context.Context, *SendCompletedReq) (*SendCompletedRes, error) {
|
func (UnimplementedReplicationServer) SendCompleted(context.Context, *SendCompletedReq) (*SendCompletedRes, error) {
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method SendCompleted not implemented")
|
return nil, status.Errorf(codes.Unimplemented, "method SendCompleted not implemented")
|
||||||
}
|
}
|
||||||
@@ -226,6 +240,24 @@ func _Replication_ReplicationCursor_Handler(srv interface{}, ctx context.Context
|
|||||||
return interceptor(ctx, in, info, handler)
|
return interceptor(ctx, in, info, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func _Replication_SendDry_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(SendReq)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(ReplicationServer).SendDry(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: "/Replication/SendDry",
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(ReplicationServer).SendDry(ctx, req.(*SendReq))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
func _Replication_SendCompleted_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
func _Replication_SendCompleted_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
in := new(SendCompletedReq)
|
in := new(SendCompletedReq)
|
||||||
if err := dec(in); err != nil {
|
if err := dec(in); err != nil {
|
||||||
@@ -271,6 +303,10 @@ var Replication_ServiceDesc = grpc.ServiceDesc{
|
|||||||
MethodName: "ReplicationCursor",
|
MethodName: "ReplicationCursor",
|
||||||
Handler: _Replication_ReplicationCursor_Handler,
|
Handler: _Replication_ReplicationCursor_Handler,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
MethodName: "SendDry",
|
||||||
|
Handler: _Replication_SendDry_Handler,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
MethodName: "SendCompleted",
|
MethodName: "SendCompleted",
|
||||||
Handler: _Replication_SendCompleted_Handler,
|
Handler: _Replication_SendCompleted_Handler,
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ type Sender interface {
|
|||||||
// any next call to the parent github.com/zrepl/zrepl/replication.Endpoint.
|
// any next call to the parent github.com/zrepl/zrepl/replication.Endpoint.
|
||||||
// If the send request is for dry run the io.ReadCloser will be nil
|
// If the send request is for dry run the io.ReadCloser will be nil
|
||||||
Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, io.ReadCloser, error)
|
Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, io.ReadCloser, error)
|
||||||
|
SendDry(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, error)
|
||||||
SendCompleted(ctx context.Context, r *pdu.SendCompletedReq) (*pdu.SendCompletedRes, error)
|
SendCompleted(ctx context.Context, r *pdu.SendCompletedReq) (*pdu.SendCompletedRes, error)
|
||||||
ReplicationCursor(ctx context.Context, req *pdu.ReplicationCursorReq) (*pdu.ReplicationCursorRes, error)
|
ReplicationCursor(ctx context.Context, req *pdu.ReplicationCursorReq) (*pdu.ReplicationCursorRes, error)
|
||||||
}
|
}
|
||||||
@@ -155,10 +156,9 @@ type Step struct {
|
|||||||
|
|
||||||
parent *Filesystem
|
parent *Filesystem
|
||||||
from, to *pdu.FilesystemVersion // from may be nil, indicating full send
|
from, to *pdu.FilesystemVersion // from may be nil, indicating full send
|
||||||
encrypt tri
|
resumeToken string // empty means no resume token shall be used
|
||||||
resumeToken string // empty means no resume token shall be used
|
|
||||||
|
|
||||||
expectedSize int64 // 0 means no size estimate present / possible
|
expectedSize uint64 // 0 means no size estimate present / possible
|
||||||
|
|
||||||
// byteCounter is nil initially, and set later in Step.doReplication
|
// byteCounter is nil initially, and set later in Step.doReplication
|
||||||
// => concurrent read of that pointer from Step.ReportInfo must be protected
|
// => concurrent read of that pointer from Step.ReportInfo must be protected
|
||||||
@@ -189,7 +189,7 @@ func (s *Step) Step(ctx context.Context) error {
|
|||||||
func (s *Step) ReportInfo() *report.StepInfo {
|
func (s *Step) ReportInfo() *report.StepInfo {
|
||||||
|
|
||||||
// get current byteCounter value
|
// get current byteCounter value
|
||||||
var byteCounter int64
|
var byteCounter uint64
|
||||||
s.byteCounterMtx.Lock()
|
s.byteCounterMtx.Lock()
|
||||||
if s.byteCounter != nil {
|
if s.byteCounter != nil {
|
||||||
byteCounter = s.byteCounter.Count()
|
byteCounter = s.byteCounter.Count()
|
||||||
@@ -200,22 +200,10 @@ func (s *Step) ReportInfo() *report.StepInfo {
|
|||||||
if s.from != nil {
|
if s.from != nil {
|
||||||
from = s.from.RelName()
|
from = s.from.RelName()
|
||||||
}
|
}
|
||||||
var encrypted report.EncryptedEnum
|
|
||||||
switch s.encrypt {
|
|
||||||
case DontCare:
|
|
||||||
encrypted = report.EncryptedSenderDependent
|
|
||||||
case True:
|
|
||||||
encrypted = report.EncryptedTrue
|
|
||||||
case False:
|
|
||||||
encrypted = report.EncryptedFalse
|
|
||||||
default:
|
|
||||||
panic(fmt.Sprintf("unknown variant %s", s.encrypt))
|
|
||||||
}
|
|
||||||
return &report.StepInfo{
|
return &report.StepInfo{
|
||||||
From: from,
|
From: from,
|
||||||
To: s.to.RelName(),
|
To: s.to.RelName(),
|
||||||
Resumed: s.resumeToken != "",
|
Resumed: s.resumeToken != "",
|
||||||
Encrypted: encrypted,
|
|
||||||
BytesExpected: s.expectedSize,
|
BytesExpected: s.expectedSize,
|
||||||
BytesReplicated: byteCounter,
|
BytesReplicated: byteCounter,
|
||||||
}
|
}
|
||||||
@@ -234,25 +222,55 @@ func NewPlanner(secsPerState *prometheus.HistogramVec, bytesReplicated *promethe
|
|||||||
promBytesReplicated: bytesReplicated,
|
promBytesReplicated: bytesReplicated,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
func resolveConflict(conflict error) (path []*pdu.FilesystemVersion, msg string) {
|
|
||||||
|
func tryAutoresolveConflict(conflict error, policy ConflictResolution) (path []*pdu.FilesystemVersion, reason error) {
|
||||||
|
|
||||||
|
if _, ok := conflict.(*ConflictMostRecentSnapshotAlreadyPresent); ok {
|
||||||
|
// replicatoin is a no-op
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
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
|
|
||||||
// NOTE: Keep in sync with listStaleFiltering, it depends on this hard-coded assumption
|
if len(noCommonAncestor.SortedSenderVersions) == 0 {
|
||||||
var mostRecentSnap *pdu.FilesystemVersion
|
return nil, fmt.Errorf("no snapshots available on sender side")
|
||||||
for n := len(noCommonAncestor.SortedSenderVersions) - 1; n >= 0; n-- {
|
}
|
||||||
if noCommonAncestor.SortedSenderVersions[n].Type == pdu.FilesystemVersion_Snapshot {
|
|
||||||
mostRecentSnap = noCommonAncestor.SortedSenderVersions[n]
|
switch policy.InitialReplication {
|
||||||
break
|
|
||||||
|
case InitialReplicationAutoResolutionMostRecent:
|
||||||
|
|
||||||
|
var mostRecentSnap *pdu.FilesystemVersion
|
||||||
|
for n := len(noCommonAncestor.SortedSenderVersions) - 1; n >= 0; n-- {
|
||||||
|
if noCommonAncestor.SortedSenderVersions[n].Type == pdu.FilesystemVersion_Snapshot {
|
||||||
|
mostRecentSnap = noCommonAncestor.SortedSenderVersions[n]
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return []*pdu.FilesystemVersion{nil, mostRecentSnap}, nil
|
||||||
|
|
||||||
|
case InitialReplicationAutoResolutionAll:
|
||||||
|
|
||||||
|
path = append(path, nil)
|
||||||
|
|
||||||
|
for n := 0; n < len(noCommonAncestor.SortedSenderVersions); n++ {
|
||||||
|
if noCommonAncestor.SortedSenderVersions[n].Type == pdu.FilesystemVersion_Snapshot {
|
||||||
|
path = append(path, noCommonAncestor.SortedSenderVersions[n])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return path, nil
|
||||||
|
|
||||||
|
case InitialReplicationAutoResolutionFail:
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("automatic conflict resolution for initial replication is disabled in config")
|
||||||
|
|
||||||
|
default:
|
||||||
|
panic(fmt.Sprintf("unimplemented: %#v", policy.InitialReplication))
|
||||||
}
|
}
|
||||||
if mostRecentSnap == nil {
|
|
||||||
return nil, "no snapshots available on sender side"
|
|
||||||
}
|
|
||||||
return []*pdu.FilesystemVersion{mostRecentSnap}, fmt.Sprintf("start replication at most recent snapshot %s", mostRecentSnap.RelName())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil, "no automated way to handle conflict type"
|
return nil, conflict
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Planner) doPlanning(ctx context.Context) ([]*Filesystem, error) {
|
func (p *Planner) doPlanning(ctx context.Context) ([]*Filesystem, error) {
|
||||||
@@ -315,10 +333,6 @@ func (fs *Filesystem) doPlanning(ctx context.Context) ([]*Step, error) {
|
|||||||
|
|
||||||
log(ctx).Debug("assessing filesystem")
|
log(ctx).Debug("assessing filesystem")
|
||||||
|
|
||||||
if fs.policy.EncryptedSend == True && !fs.senderFS.GetIsEncrypted() {
|
|
||||||
return nil, fmt.Errorf("sender filesystem is not encrypted but policy mandates encrypted send")
|
|
||||||
}
|
|
||||||
|
|
||||||
sfsvsres, err := fs.sender.ListFilesystemVersions(ctx, &pdu.ListFilesystemVersionsReq{Filesystem: fs.Path})
|
sfsvsres, err := fs.sender.ListFilesystemVersions(ctx, &pdu.ListFilesystemVersionsReq{Filesystem: fs.Path})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log(ctx).WithError(err).Error("cannot get remote filesystem versions")
|
log(ctx).WithError(err).Error("cannot get remote filesystem versions")
|
||||||
@@ -392,24 +406,7 @@ func (fs *Filesystem) doPlanning(ctx context.Context) ([]*Step, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
encryptionMatches := false
|
if toVersion == nil {
|
||||||
switch fs.policy.EncryptedSend {
|
|
||||||
case True:
|
|
||||||
encryptionMatches = resumeToken.RawOK && resumeToken.CompressOK
|
|
||||||
case False:
|
|
||||||
encryptionMatches = !resumeToken.RawOK && !resumeToken.CompressOK
|
|
||||||
case DontCare:
|
|
||||||
encryptionMatches = true
|
|
||||||
}
|
|
||||||
|
|
||||||
log(ctx).WithField("fromVersion", fromVersion).
|
|
||||||
WithField("toVersion", toVersion).
|
|
||||||
WithField("encryptionMatches", encryptionMatches).
|
|
||||||
Debug("result of resume-token-matching to sender's versions")
|
|
||||||
|
|
||||||
if !encryptionMatches {
|
|
||||||
return nil, fmt.Errorf("resume token `rawok`=%v and `compressok`=%v are incompatible with encryption policy=%v", resumeToken.RawOK, resumeToken.CompressOK, fs.policy.EncryptedSend)
|
|
||||||
} else if toVersion == nil {
|
|
||||||
return nil, fmt.Errorf("resume token `toguid` = %v not found on sender (`toname` = %q)", resumeToken.ToGUID, resumeToken.ToName)
|
return nil, fmt.Errorf("resume token `toguid` = %v not found on sender (`toname` = %q)", resumeToken.ToGUID, resumeToken.ToName)
|
||||||
} else if fromVersion == toVersion {
|
} else if fromVersion == toVersion {
|
||||||
return nil, fmt.Errorf("resume token `fromguid` and `toguid` match same version on sener")
|
return nil, fmt.Errorf("resume token `fromguid` and `toguid` match same version on sener")
|
||||||
@@ -421,9 +418,8 @@ func (fs *Filesystem) doPlanning(ctx context.Context) ([]*Step, error) {
|
|||||||
sender: fs.sender,
|
sender: fs.sender,
|
||||||
receiver: fs.receiver,
|
receiver: fs.receiver,
|
||||||
|
|
||||||
from: fromVersion,
|
from: fromVersion,
|
||||||
to: toVersion,
|
to: toVersion,
|
||||||
encrypt: fs.policy.EncryptedSend,
|
|
||||||
|
|
||||||
resumeToken: resumeTokenRaw,
|
resumeToken: resumeTokenRaw,
|
||||||
}
|
}
|
||||||
@@ -449,47 +445,37 @@ func (fs *Filesystem) doPlanning(ctx context.Context) ([]*Step, error) {
|
|||||||
receiver: fs.receiver,
|
receiver: fs.receiver,
|
||||||
from: remainingSFSVs[i],
|
from: remainingSFSVs[i],
|
||||||
to: remainingSFSVs[i+1],
|
to: remainingSFSVs[i+1],
|
||||||
encrypt: fs.policy.EncryptedSend,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} else { // resumeToken == nil
|
} else { // resumeToken == nil
|
||||||
path, conflict := IncrementalPath(rfsvs, sfsvs)
|
path, conflict := IncrementalPath(rfsvs, sfsvs)
|
||||||
if conflict != nil {
|
if conflict != nil {
|
||||||
var msg string
|
updPath, updConflict := tryAutoresolveConflict(conflict, *fs.policy.ConflictResolution)
|
||||||
path, msg = resolveConflict(conflict) // no shadowing allowed!
|
if updConflict == nil {
|
||||||
if path != nil {
|
log(ctx).WithField("conflict", conflict).Info("conflict automatically resolved")
|
||||||
log(ctx).WithField("conflict", conflict).Info("conflict")
|
|
||||||
log(ctx).WithField("resolution", msg).Info("automatically resolved")
|
|
||||||
} else {
|
} else {
|
||||||
log(ctx).WithField("conflict", conflict).Error("conflict")
|
log(ctx).WithField("conflict", conflict).Error("cannot resolve conflict")
|
||||||
log(ctx).WithField("problem", msg).Error("cannot resolve conflict")
|
|
||||||
}
|
}
|
||||||
|
path, conflict = updPath, updConflict
|
||||||
}
|
}
|
||||||
if len(path) == 0 {
|
if conflict != nil {
|
||||||
return nil, conflict
|
return nil, conflict
|
||||||
}
|
}
|
||||||
|
if len(path) == 0 {
|
||||||
steps = make([]*Step, 0, len(path)) // shadow
|
steps = nil
|
||||||
if len(path) == 1 {
|
} else if len(path) == 1 {
|
||||||
steps = append(steps, &Step{
|
panic(fmt.Sprintf("len(path) must be two for incremental repl, and initial repl must start with nil, got path[0]=%#v", path[0]))
|
||||||
parent: fs,
|
|
||||||
sender: fs.sender,
|
|
||||||
receiver: fs.receiver,
|
|
||||||
|
|
||||||
from: nil,
|
|
||||||
to: path[0],
|
|
||||||
encrypt: fs.policy.EncryptedSend,
|
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
|
steps = make([]*Step, 0, len(path)) // shadow
|
||||||
for i := 0; i < len(path)-1; i++ {
|
for i := 0; i < len(path)-1; i++ {
|
||||||
steps = append(steps, &Step{
|
steps = append(steps, &Step{
|
||||||
parent: fs,
|
parent: fs,
|
||||||
sender: fs.sender,
|
sender: fs.sender,
|
||||||
receiver: fs.receiver,
|
receiver: fs.receiver,
|
||||||
|
|
||||||
from: path[i],
|
from: path[i], // nil in case of initial repl
|
||||||
to: path[i+1],
|
to: path[i+1],
|
||||||
encrypt: fs.policy.EncryptedSend,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -546,10 +532,10 @@ func (s *Step) updateSizeEstimate(ctx context.Context) error {
|
|||||||
|
|
||||||
log := getLogger(ctx)
|
log := getLogger(ctx)
|
||||||
|
|
||||||
sr := s.buildSendRequest(true)
|
sr := s.buildSendRequest()
|
||||||
|
|
||||||
log.Debug("initiate dry run send request")
|
log.Debug("initiate dry run send request")
|
||||||
sres, _, err := s.sender.Send(ctx, sr)
|
sres, err := s.sender.SendDry(ctx, sr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithError(err).Error("dry run send request failed")
|
log.WithError(err).Error("dry run send request failed")
|
||||||
return err
|
return err
|
||||||
@@ -563,15 +549,13 @@ func (s *Step) updateSizeEstimate(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Step) buildSendRequest(dryRun bool) (sr *pdu.SendReq) {
|
func (s *Step) buildSendRequest() (sr *pdu.SendReq) {
|
||||||
fs := s.parent.Path
|
fs := s.parent.Path
|
||||||
sr = &pdu.SendReq{
|
sr = &pdu.SendReq{
|
||||||
Filesystem: fs,
|
Filesystem: fs,
|
||||||
From: s.from, // may be nil
|
From: s.from, // may be nil
|
||||||
To: s.to,
|
To: s.to,
|
||||||
Encrypted: s.encrypt.ToPDU(),
|
|
||||||
ResumeToken: s.resumeToken,
|
ResumeToken: s.resumeToken,
|
||||||
DryRun: dryRun,
|
|
||||||
ReplicationConfig: s.parent.policy.ReplicationConfig,
|
ReplicationConfig: s.parent.policy.ReplicationConfig,
|
||||||
}
|
}
|
||||||
return sr
|
return sr
|
||||||
@@ -582,7 +566,7 @@ func (s *Step) doReplication(ctx context.Context) error {
|
|||||||
fs := s.parent.Path
|
fs := s.parent.Path
|
||||||
|
|
||||||
log := getLogger(ctx).WithField("filesystem", fs)
|
log := getLogger(ctx).WithField("filesystem", fs)
|
||||||
sr := s.buildSendRequest(false)
|
sr := s.buildSendRequest()
|
||||||
|
|
||||||
log.Debug("initiate send request")
|
log.Debug("initiate send request")
|
||||||
sres, stream, err := s.sender.Send(ctx, sr)
|
sres, stream, err := s.sender.Send(ctx, sr)
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
package logic
|
package logic
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/go-playground/validator"
|
"github.com/go-playground/validator"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
@@ -8,16 +11,73 @@ import (
|
|||||||
"github.com/zrepl/zrepl/replication/logic/pdu"
|
"github.com/zrepl/zrepl/replication/logic/pdu"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
//go:generate enumer -type=InitialReplicationAutoResolution -trimprefix=InitialReplicationAutoResolution
|
||||||
|
type InitialReplicationAutoResolution uint32
|
||||||
|
|
||||||
|
const (
|
||||||
|
InitialReplicationAutoResolutionMostRecent InitialReplicationAutoResolution = 1 << iota
|
||||||
|
InitialReplicationAutoResolutionAll
|
||||||
|
InitialReplicationAutoResolutionFail
|
||||||
|
)
|
||||||
|
|
||||||
|
var initialReplicationAutoResolutionConfigMap = map[InitialReplicationAutoResolution]string{
|
||||||
|
InitialReplicationAutoResolutionMostRecent: "most_recent",
|
||||||
|
InitialReplicationAutoResolutionAll: "all",
|
||||||
|
InitialReplicationAutoResolutionFail: "fail",
|
||||||
|
}
|
||||||
|
|
||||||
|
func InitialReplicationAutoResolutionFromConfig(in string) (InitialReplicationAutoResolution, error) {
|
||||||
|
for v, s := range initialReplicationAutoResolutionConfigMap {
|
||||||
|
if s == in {
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
l := make([]string, 0, len(initialReplicationAutoResolutionConfigMap))
|
||||||
|
for _, v := range InitialReplicationAutoResolutionValues() {
|
||||||
|
l = append(l, initialReplicationAutoResolutionConfigMap[v])
|
||||||
|
}
|
||||||
|
return 0, fmt.Errorf("invalid value %q, must be one of %s", in, strings.Join(l, ", "))
|
||||||
|
}
|
||||||
|
|
||||||
|
type ConflictResolution struct {
|
||||||
|
InitialReplication InitialReplicationAutoResolution
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConflictResolution) Validate() error {
|
||||||
|
if !c.InitialReplication.IsAInitialReplicationAutoResolution() {
|
||||||
|
return errors.Errorf("must be one of %s", InitialReplicationAutoResolutionValues())
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ConflictResolutionFromConfig(in *config.ConflictResolution) (*ConflictResolution, error) {
|
||||||
|
|
||||||
|
initialReplication, err := InitialReplicationAutoResolutionFromConfig(in.InitialReplication)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Errorf("field `initial_replication` is invalid: %q is not one of %v", in.InitialReplication, InitialReplicationAutoResolutionValues())
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ConflictResolution{
|
||||||
|
InitialReplication: initialReplication,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
type PlannerPolicy struct {
|
type PlannerPolicy struct {
|
||||||
EncryptedSend tri // all sends must be encrypted (send -w, and encryption!=off)
|
ConflictResolution *ConflictResolution `validate:"ne=nil"`
|
||||||
ReplicationConfig *pdu.ReplicationConfig
|
ReplicationConfig *pdu.ReplicationConfig `validate:"ne=nil"`
|
||||||
SizeEstimationConcurrency int `validate:"gte=1"`
|
SizeEstimationConcurrency int `validate:"gte=1"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var validate = validator.New()
|
var validate = validator.New()
|
||||||
|
|
||||||
func (p PlannerPolicy) Validate() error {
|
func (p PlannerPolicy) Validate() error {
|
||||||
return validate.Struct(p)
|
if err := validate.Struct(p); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := p.ConflictResolution.Validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ReplicationConfigFromConfig(in *config.Replication) (*pdu.ReplicationConfig, error) {
|
func ReplicationConfigFromConfig(in *config.Replication) (*pdu.ReplicationConfig, error) {
|
||||||
|
|||||||
@@ -1,46 +0,0 @@
|
|||||||
package logic
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/replication/logic/pdu"
|
|
||||||
)
|
|
||||||
|
|
||||||
type tri int
|
|
||||||
|
|
||||||
const (
|
|
||||||
DontCare = 0x0
|
|
||||||
False = 0x1
|
|
||||||
True = 0x2
|
|
||||||
)
|
|
||||||
|
|
||||||
func (t tri) String() string {
|
|
||||||
switch t {
|
|
||||||
case DontCare:
|
|
||||||
return "dontcare"
|
|
||||||
case False:
|
|
||||||
return "false"
|
|
||||||
case True:
|
|
||||||
return "true"
|
|
||||||
}
|
|
||||||
panic(fmt.Sprintf("unknown variant %v", int(t)))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t tri) ToPDU() pdu.Tri {
|
|
||||||
switch t {
|
|
||||||
case DontCare:
|
|
||||||
return pdu.Tri_DontCare
|
|
||||||
case False:
|
|
||||||
return pdu.Tri_False
|
|
||||||
case True:
|
|
||||||
return pdu.Tri_True
|
|
||||||
}
|
|
||||||
panic(fmt.Sprintf("unknown variant %v", int(t)))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TriFromBool(b bool) tri {
|
|
||||||
if b {
|
|
||||||
return True
|
|
||||||
}
|
|
||||||
return False
|
|
||||||
}
|
|
||||||
@@ -62,11 +62,23 @@ const (
|
|||||||
FilesystemDone FilesystemState = "done"
|
FilesystemDone FilesystemState = "done"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type FsBlockedOn string
|
||||||
|
|
||||||
|
const (
|
||||||
|
FsBlockedOnNothing FsBlockedOn = "nothing"
|
||||||
|
FsBlockedOnPlanningStepQueue FsBlockedOn = "plan-queue"
|
||||||
|
FsBlockedOnParentInitialRepl FsBlockedOn = "parent-initial-repl"
|
||||||
|
FsBlockedOnReplStepQueue FsBlockedOn = "repl-queue"
|
||||||
|
)
|
||||||
|
|
||||||
type FilesystemReport struct {
|
type FilesystemReport struct {
|
||||||
Info *FilesystemInfo
|
Info *FilesystemInfo
|
||||||
|
|
||||||
State FilesystemState
|
State FilesystemState
|
||||||
|
|
||||||
|
// Always valid.
|
||||||
|
BlockedOn FsBlockedOn
|
||||||
|
|
||||||
// Valid in State = FilesystemPlanningErrored
|
// Valid in State = FilesystemPlanningErrored
|
||||||
PlanError *TimedError
|
PlanError *TimedError
|
||||||
// Valid in State = FilesystemSteppingErrored
|
// Valid in State = FilesystemSteppingErrored
|
||||||
@@ -85,23 +97,14 @@ type StepReport struct {
|
|||||||
Info *StepInfo
|
Info *StepInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
type EncryptedEnum string
|
|
||||||
|
|
||||||
const (
|
|
||||||
EncryptedTrue EncryptedEnum = "yes"
|
|
||||||
EncryptedFalse EncryptedEnum = "no"
|
|
||||||
EncryptedSenderDependent EncryptedEnum = "sender-dependent"
|
|
||||||
)
|
|
||||||
|
|
||||||
type StepInfo struct {
|
type StepInfo struct {
|
||||||
From, To string
|
From, To string
|
||||||
Resumed bool
|
Resumed bool
|
||||||
Encrypted EncryptedEnum
|
BytesExpected uint64
|
||||||
BytesExpected int64
|
BytesReplicated uint64
|
||||||
BytesReplicated int64
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *AttemptReport) BytesSum() (expected, replicated int64, containsInvalidSizeEstimates bool) {
|
func (a *AttemptReport) BytesSum() (expected, replicated uint64, containsInvalidSizeEstimates bool) {
|
||||||
for _, fs := range a.Filesystems {
|
for _, fs := range a.Filesystems {
|
||||||
e, r, fsContainsInvalidEstimate := fs.BytesSum()
|
e, r, fsContainsInvalidEstimate := fs.BytesSum()
|
||||||
containsInvalidSizeEstimates = containsInvalidSizeEstimates || fsContainsInvalidEstimate
|
containsInvalidSizeEstimates = containsInvalidSizeEstimates || fsContainsInvalidEstimate
|
||||||
@@ -111,7 +114,7 @@ func (a *AttemptReport) BytesSum() (expected, replicated int64, containsInvalidS
|
|||||||
return expected, replicated, containsInvalidSizeEstimates
|
return expected, replicated, containsInvalidSizeEstimates
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *FilesystemReport) BytesSum() (expected, replicated int64, containsInvalidSizeEstimates bool) {
|
func (f *FilesystemReport) BytesSum() (expected, replicated uint64, containsInvalidSizeEstimates bool) {
|
||||||
for _, step := range f.Steps {
|
for _, step := range f.Steps {
|
||||||
expected += step.Info.BytesExpected
|
expected += step.Info.BytesExpected
|
||||||
replicated += step.Info.BytesReplicated
|
replicated += step.Info.BytesReplicated
|
||||||
|
|||||||
@@ -114,12 +114,6 @@ func (c *Client) ReqSend(ctx context.Context, req *pdu.SendReq) (*pdu.SendRes, i
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
putWireOnReturn := true
|
|
||||||
defer func() {
|
|
||||||
if putWireOnReturn {
|
|
||||||
c.putWire(conn)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
if err := c.send(ctx, conn, EndpointSend, req, nil); err != nil {
|
if err := c.send(ctx, conn, EndpointSend, req, nil); err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
@@ -131,14 +125,10 @@ func (c *Client) ReqSend(ctx context.Context, req *pdu.SendReq) (*pdu.SendRes, i
|
|||||||
}
|
}
|
||||||
|
|
||||||
var stream io.ReadCloser
|
var stream io.ReadCloser
|
||||||
if !req.DryRun {
|
stream, err = conn.ReadStream(ZFSStream, true) // no shadow
|
||||||
putWireOnReturn = false
|
if err != nil {
|
||||||
stream, err = conn.ReadStream(ZFSStream, true) // no shadow
|
return nil, nil, err
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return &res, stream, nil
|
return &res, stream, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -172,6 +172,15 @@ func (s *Server) serveConnRequest(ctx context.Context, endpoint string, c *strea
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
res, sendStream, handlerErr = s.h.Send(ctx, &req) // SHADOWING
|
res, sendStream, handlerErr = s.h.Send(ctx, &req) // SHADOWING
|
||||||
|
// ensure that we always close the sendStream
|
||||||
|
if sendStream != nil {
|
||||||
|
defer func() {
|
||||||
|
err := sendStream.Close()
|
||||||
|
if err != nil {
|
||||||
|
s.log.WithError(err).Error("cannot close send stream")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
case EndpointRecv:
|
case EndpointRecv:
|
||||||
var req pdu.ReceiveReq
|
var req pdu.ReceiveReq
|
||||||
if err := proto.Unmarshal(reqStructured, &req); err != nil {
|
if err := proto.Unmarshal(reqStructured, &req); err != nil {
|
||||||
@@ -239,12 +248,9 @@ func (s *Server) serveConnRequest(ctx context.Context, endpoint string, c *strea
|
|||||||
|
|
||||||
if sendStream != nil {
|
if sendStream != nil {
|
||||||
err := c.SendStream(ctx, sendStream, ZFSStream)
|
err := c.SendStream(ctx, sendStream, ZFSStream)
|
||||||
closeErr := sendStream.Close()
|
|
||||||
if closeErr != nil {
|
|
||||||
s.log.WithError(err).Error("cannot close send stream")
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.log.WithError(err).Error("cannot write send stream")
|
s.log.WithError(err).Error("cannot write send stream")
|
||||||
}
|
}
|
||||||
|
// sendStream.Close() done via defer above
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ func main() {
|
|||||||
case "connect":
|
case "connect":
|
||||||
tc, err := getTestCase(args.testCase)
|
tc, err := getTestCase(args.testCase)
|
||||||
noerror(err)
|
noerror(err)
|
||||||
connecter, err := transportconfig.ConnecterFromConfig(global, conf.Connect)
|
connecter, err := transportconfig.ConnecterFromConfig(global, conf.Connect, config.ParseFlagsNone)
|
||||||
noerror(err)
|
noerror(err)
|
||||||
wire, err := connecter.Connect(ctx)
|
wire, err := connecter.Connect(ctx)
|
||||||
noerror(err)
|
noerror(err)
|
||||||
@@ -75,7 +75,7 @@ func main() {
|
|||||||
case "serve":
|
case "serve":
|
||||||
tc, err := getTestCase(args.testCase)
|
tc, err := getTestCase(args.testCase)
|
||||||
noerror(err)
|
noerror(err)
|
||||||
lf, err := transportconfig.ListenerFactoryFromConfig(global, conf.Serve)
|
lf, err := transportconfig.ListenerFactoryFromConfig(global, conf.Serve, config.ParseFlagsNone)
|
||||||
noerror(err)
|
noerror(err)
|
||||||
l, err := lf()
|
l, err := lf()
|
||||||
noerror(err)
|
noerror(err)
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
//go:build illumos || solaris
|
||||||
// +build illumos solaris
|
// +build illumos solaris
|
||||||
|
|
||||||
package timeoutconn
|
package timeoutconn
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/util/socketpair"
|
"github.com/zrepl/zrepl/util/socketpair"
|
||||||
|
"github.com/zrepl/zrepl/util/zreplcircleci"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestReadTimeout(t *testing.T) {
|
func TestReadTimeout(t *testing.T) {
|
||||||
@@ -81,6 +82,8 @@ func TestWriteTimeout(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestNoPartialReadsDueToDeadline(t *testing.T) {
|
func TestNoPartialReadsDueToDeadline(t *testing.T) {
|
||||||
|
zreplcircleci.SkipOnCircleCI(t, "needs predictable low scheduling latency")
|
||||||
|
|
||||||
a, b, err := socketpair.SocketPair()
|
a, b, err := socketpair.SocketPair()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer a.Close()
|
defer a.Close()
|
||||||
@@ -151,6 +154,7 @@ func (c *partialWriteMockConn) Write(p []byte) (int, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestPartialWriteMockConn(t *testing.T) {
|
func TestPartialWriteMockConn(t *testing.T) {
|
||||||
|
zreplcircleci.SkipOnCircleCI(t, "because it relies on scheduler responsiveness < 50ms")
|
||||||
mc := newPartialWriteMockConn(100*time.Millisecond, 5)
|
mc := newPartialWriteMockConn(100*time.Millisecond, 5)
|
||||||
buf := []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
|
buf := []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
|
||||||
begin := time.Now()
|
begin := time.Now()
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// +build !illumos
|
//go:build !illumos && !solaris
|
||||||
// +build !solaris
|
// +build !illumos,!solaris
|
||||||
|
|
||||||
package timeoutconn
|
package timeoutconn
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user