Compare commits
80 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3cf48852e9 | |||
| 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 | |||
| bcfcd7a134 | |||
| bf1276f767 | |||
| 9fa7a18351 | |||
| 50e8ee4549 | |||
| 3b5a1a8b9a | |||
| f661d9429f | |||
| ac4b109872 | |||
| b2c6e51a43 | |||
| ee2336a24b | |||
| 1e85b1cb5f | |||
| 5c6d69a69c | |||
| 299808aaaf | |||
| 7071adc774 | |||
| 8d678eed19 | |||
| ab7abd9686 | |||
| a58ce74ed0 | |||
| 2c8c2cfa14 |
+11
-10
@@ -150,7 +150,7 @@ parameters:
|
||||
|
||||
release_docker_baseimage_tag:
|
||||
type: string
|
||||
default: "1.16"
|
||||
default: "1.17"
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
@@ -160,15 +160,15 @@ workflows:
|
||||
jobs:
|
||||
- quickcheck-docs
|
||||
- quickcheck-go: &quickcheck-go-smoketest
|
||||
name: quickcheck-go-amd64-linux-1.16
|
||||
goversion: &latest-go-release "1.16"
|
||||
name: quickcheck-go-amd64-linux-1.17
|
||||
goversion: &latest-go-release "1.17"
|
||||
goos: linux
|
||||
goarch: amd64
|
||||
- test-go-on-latest-go-release:
|
||||
goversion: *latest-go-release
|
||||
- quickcheck-go:
|
||||
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
|
||||
alias: quickcheck-go-matrix
|
||||
parameters:
|
||||
@@ -231,8 +231,6 @@ jobs:
|
||||
- install-docdep
|
||||
- run: make docs
|
||||
|
||||
- store_artifacts:
|
||||
path: artifacts
|
||||
- download-and-install-minio-client
|
||||
- upload-minio:
|
||||
src: artifacts
|
||||
@@ -296,7 +294,8 @@ jobs:
|
||||
# don't save-cache-gomod here, test-go doesn't pull all the dependencies
|
||||
|
||||
release-build:
|
||||
machine: true
|
||||
machine:
|
||||
image: ubuntu-2004:202201-02
|
||||
steps:
|
||||
- checkout
|
||||
- run: make release-docker RELEASE_DOCKER_BASEIMAGE_TAG=<<pipeline.parameters.release_docker_baseimage_tag>>
|
||||
@@ -304,7 +303,8 @@ jobs:
|
||||
root: .
|
||||
paths: [.]
|
||||
release-deb:
|
||||
machine: true
|
||||
machine:
|
||||
image: ubuntu-2004:202201-02
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: .
|
||||
@@ -315,7 +315,8 @@ jobs:
|
||||
- "artifacts/*.deb"
|
||||
|
||||
release-rpm:
|
||||
machine: true
|
||||
machine:
|
||||
image: ubuntu-2004:202201-02
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: .
|
||||
@@ -358,7 +359,7 @@ jobs:
|
||||
subcommand: docdep
|
||||
- run:
|
||||
command: |
|
||||
git config --global user.email "me@cschwarz.com"
|
||||
git config --global user.email "zreplbot@cschwarz.com"
|
||||
git config --global user.name "zrepl-github-io-ci"
|
||||
|
||||
# https://circleci.com/docs/2.0/add-ssh-key/#adding-multiple-keys-with-blank-hostnames
|
||||
|
||||
@@ -28,7 +28,7 @@ GO_BUILDFLAGS := $(GO_MOD_READONLY) $(GO_EXTRA_BUILDFLAGS)
|
||||
GO_BUILD := $(GO_ENV_VARS) $(GO) build $(GO_BUILDFLAGS) -ldflags $(GO_LDFLAGS)
|
||||
GOLANGCI_LINT := golangci-lint
|
||||
GOCOVMERGE := gocovmerge
|
||||
RELEASE_DOCKER_BASEIMAGE_TAG ?= 1.16
|
||||
RELEASE_DOCKER_BASEIMAGE_TAG ?= 1.17
|
||||
RELEASE_DOCKER_BASEIMAGE ?= golang:$(RELEASE_DOCKER_BASEIMAGE_TAG)
|
||||
|
||||
ifneq ($(GOARM),)
|
||||
@@ -173,6 +173,10 @@ check-git-clean:
|
||||
fi; \
|
||||
fi;
|
||||
|
||||
tag-release:
|
||||
test -n "$(ZREPL_TAG_VERSION)" || exit 1
|
||||
git tag -u E27CA5FC -m "$(ZREPL_TAG_VERSION)" "$(ZREPL_TAG_VERSION)"
|
||||
|
||||
sign:
|
||||
gpg -u "89BC 5D89 C845 568B F578 B306 CDBD 8EC8 E27C A5FC" \
|
||||
--armor \
|
||||
@@ -189,6 +193,8 @@ GO_SUPPORTS_ILLUMOS := $(shell $(GO) version | gawk -F '.' '/^go version /{split
|
||||
bins-all:
|
||||
$(MAKE) $(BINS_ALL_TARGETS) GOOS=freebsd GOARCH=amd64
|
||||
$(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=arm64
|
||||
$(MAKE) $(BINS_ALL_TARGETS) GOOS=linux GOARCH=arm GOARM=7
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
[](https://github.com/zrepl/zrepl/blob/master/LICENSE)
|
||||
[](https://golang.org/)
|
||||
[](https://golang.org/)
|
||||
[](https://zrepl.github.io)
|
||||
[](https://patreon.com/zrepl)
|
||||
[](https://patreon.com/zrepl)
|
||||
[](https://github.com/sponsors/problame)
|
||||
[](https://liberapay.com/zrepl/donate)
|
||||
[](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=R5QSXJVYHGX96)
|
||||
[](https://twitter.com/intent/tweet?text=Wow:&url=https%3A%2F%2Fgithub.com%2Fzrepl%2Fzrepl)
|
||||
[](https://matrix.to/#/#zrepl:matrix.org)
|
||||
|
||||
# zrepl
|
||||
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
|
||||
|
||||
* 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 service manager file and _please_ try to upstream it to this repository.
|
||||
* `dist/systemd` contains a Systemd unit template.
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
//go:build tools
|
||||
// +build tools
|
||||
|
||||
package main
|
||||
|
||||
+11
-1
@@ -21,6 +21,7 @@ import (
|
||||
var configcheckArgs struct {
|
||||
format string
|
||||
what string
|
||||
skipCertCheck bool
|
||||
}
|
||||
|
||||
var ConfigcheckCmd = &cli.Subcommand{
|
||||
@@ -29,6 +30,7 @@ var ConfigcheckCmd = &cli.Subcommand{
|
||||
SetupFlags: func(f *pflag.FlagSet) {
|
||||
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.BoolVar(&configcheckArgs.skipCertCheck, "skip-cert-check", false, "skip checking cert files")
|
||||
},
|
||||
Run: func(ctx context.Context, subcommand *cli.Subcommand, args []string) error {
|
||||
formatMap := map[string]func(interface{}){
|
||||
@@ -56,8 +58,16 @@ var ConfigcheckCmd = &cli.Subcommand{
|
||||
}
|
||||
|
||||
var hadErr bool
|
||||
|
||||
parseFlags := config.ParseFlagsNone
|
||||
|
||||
if configcheckArgs.skipCertCheck {
|
||||
parseFlags |= config.ParseFlagsNoCertCheck
|
||||
}
|
||||
|
||||
// further: try to build jobs
|
||||
confJobs, err := job.JobsFromConfig(subcommand.Config())
|
||||
confJobs, err := job.JobsFromConfig(subcommand.Config(), parseFlags)
|
||||
|
||||
if err != nil {
|
||||
err := errors.Wrap(err, "cannot build jobs from config")
|
||||
if configcheckArgs.what == "jobs" {
|
||||
|
||||
+1
-1
@@ -129,7 +129,7 @@ func doMigrateReplicationCursor(ctx context.Context, sc *cli.Subcommand, args []
|
||||
}
|
||||
|
||||
cfg := sc.Config()
|
||||
jobs, err := job.JobsFromConfig(cfg)
|
||||
jobs, err := job.JobsFromConfig(cfg, config.ParseFlagsNone)
|
||||
if err != nil {
|
||||
fmt.Printf("cannot parse config:\n%s\n\n", err)
|
||||
fmt.Printf("NOTE: this migration was released together with a change in job name requirements.\n")
|
||||
|
||||
@@ -1,816 +0,0 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"net/http"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
// tcell is the termbox-compatible library for abstracting away escape sequences, etc.
|
||||
// as of tcell#252, the number of default distributed terminals is relatively limited
|
||||
// additional terminal definitions can be included via side-effect import
|
||||
// See https://github.com/gdamore/tcell/blob/master/terminfo/base/base.go
|
||||
// See https://github.com/gdamore/tcell/issues/252#issuecomment-533836078
|
||||
"github.com/gdamore/tcell/termbox"
|
||||
_ "github.com/gdamore/tcell/terminfo/s/screen" // tmux on FreeBSD 11 & 12 without ncurses
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/pflag"
|
||||
"github.com/zrepl/yaml-config"
|
||||
|
||||
"github.com/zrepl/zrepl/cli"
|
||||
"github.com/zrepl/zrepl/daemon"
|
||||
"github.com/zrepl/zrepl/daemon/job"
|
||||
"github.com/zrepl/zrepl/daemon/pruner"
|
||||
"github.com/zrepl/zrepl/daemon/snapper"
|
||||
"github.com/zrepl/zrepl/replication/report"
|
||||
)
|
||||
|
||||
type byteProgressMeasurement struct {
|
||||
time time.Time
|
||||
val int64
|
||||
}
|
||||
|
||||
type bytesProgressHistory struct {
|
||||
last *byteProgressMeasurement // pointer as poor man's optional
|
||||
changeCount int
|
||||
lastChange time.Time
|
||||
bpsAvg float64
|
||||
}
|
||||
|
||||
func (p *bytesProgressHistory) Update(currentVal int64) (bytesPerSecondAvg int64, changeCount int) {
|
||||
|
||||
if p.last == nil {
|
||||
p.last = &byteProgressMeasurement{
|
||||
time: time.Now(),
|
||||
val: currentVal,
|
||||
}
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
if p.last.val != currentVal {
|
||||
p.changeCount++
|
||||
p.lastChange = time.Now()
|
||||
}
|
||||
|
||||
if time.Since(p.lastChange) > 3*time.Second {
|
||||
p.last = nil
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
deltaV := currentVal - p.last.val
|
||||
deltaT := time.Since(p.last.time)
|
||||
rate := float64(deltaV) / deltaT.Seconds()
|
||||
|
||||
factor := 0.3
|
||||
p.bpsAvg = (1-factor)*p.bpsAvg + factor*rate
|
||||
|
||||
p.last.time = time.Now()
|
||||
p.last.val = currentVal
|
||||
|
||||
return int64(p.bpsAvg), p.changeCount
|
||||
}
|
||||
|
||||
type tui struct {
|
||||
x, y int
|
||||
indent int
|
||||
|
||||
lock sync.Mutex //For report and error
|
||||
report map[string]*job.Status
|
||||
err error
|
||||
|
||||
jobFilter string
|
||||
|
||||
replicationProgress map[string]*bytesProgressHistory // by job name
|
||||
}
|
||||
|
||||
func newTui() tui {
|
||||
return tui{
|
||||
replicationProgress: make(map[string]*bytesProgressHistory),
|
||||
}
|
||||
}
|
||||
|
||||
const INDENT_MULTIPLIER = 4
|
||||
|
||||
func (t *tui) moveLine(dl int, col int) {
|
||||
t.y += dl
|
||||
t.x = t.indent*INDENT_MULTIPLIER + col
|
||||
}
|
||||
|
||||
func (t *tui) write(text string) {
|
||||
for _, c := range text {
|
||||
if c == '\n' {
|
||||
t.newline()
|
||||
continue
|
||||
}
|
||||
termbox.SetCell(t.x, t.y, c, termbox.ColorDefault, termbox.ColorDefault)
|
||||
t.x += 1
|
||||
}
|
||||
}
|
||||
|
||||
func (t *tui) printf(text string, a ...interface{}) {
|
||||
t.write(fmt.Sprintf(text, a...))
|
||||
}
|
||||
|
||||
func wrap(s string, width int) string {
|
||||
var b strings.Builder
|
||||
for len(s) > 0 {
|
||||
rem := width
|
||||
if rem > len(s) {
|
||||
rem = len(s)
|
||||
}
|
||||
if idx := strings.IndexAny(s, "\n\r"); idx != -1 && idx < rem {
|
||||
rem = idx + 1
|
||||
}
|
||||
untilNewline := strings.TrimRight(s[:rem], "\n\r")
|
||||
s = s[rem:]
|
||||
if len(untilNewline) == 0 {
|
||||
continue
|
||||
}
|
||||
b.WriteString(untilNewline)
|
||||
b.WriteString("\n")
|
||||
}
|
||||
return strings.TrimRight(b.String(), "\n\r")
|
||||
}
|
||||
|
||||
func (t *tui) printfDrawIndentedAndWrappedIfMultiline(format string, a ...interface{}) {
|
||||
whole := fmt.Sprintf(format, a...)
|
||||
width, _ := termbox.Size()
|
||||
if !strings.ContainsAny(whole, "\n\r") && t.x+len(whole) <= width {
|
||||
t.printf(format, a...)
|
||||
} else {
|
||||
t.addIndent(1)
|
||||
t.newline()
|
||||
t.write(wrap(whole, width-INDENT_MULTIPLIER*t.indent))
|
||||
t.addIndent(-1)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *tui) newline() {
|
||||
t.moveLine(1, 0)
|
||||
}
|
||||
|
||||
func (t *tui) setIndent(indent int) {
|
||||
t.indent = indent
|
||||
t.moveLine(0, 0)
|
||||
}
|
||||
|
||||
func (t *tui) addIndent(indent int) {
|
||||
t.indent += indent
|
||||
t.moveLine(0, 0)
|
||||
}
|
||||
|
||||
var statusFlags struct {
|
||||
Raw bool
|
||||
Job string
|
||||
}
|
||||
|
||||
var StatusCmd = &cli.Subcommand{
|
||||
Use: "status",
|
||||
Short: "show job activity or dump as JSON for monitoring",
|
||||
SetupFlags: func(f *pflag.FlagSet) {
|
||||
f.BoolVar(&statusFlags.Raw, "raw", false, "dump raw status description from zrepl daemon")
|
||||
f.StringVar(&statusFlags.Job, "job", "", "only dump specified job")
|
||||
},
|
||||
Run: runStatus,
|
||||
}
|
||||
|
||||
func runStatus(ctx context.Context, s *cli.Subcommand, args []string) error {
|
||||
httpc, err := controlHttpClient(s.Config().Global.Control.SockPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if statusFlags.Raw {
|
||||
resp, err := httpc.Get("http://unix" + daemon.ControlJobEndpointStatus)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
fmt.Fprintf(os.Stderr, "Received error response:\n")
|
||||
_, err := io.CopyN(os.Stderr, resp.Body, 4096)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return errors.Errorf("exit")
|
||||
}
|
||||
if _, err := io.Copy(os.Stdout, resp.Body); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
t := newTui()
|
||||
t.lock.Lock()
|
||||
t.err = errors.New("Got no report yet")
|
||||
t.lock.Unlock()
|
||||
t.jobFilter = statusFlags.Job
|
||||
|
||||
err = termbox.Init()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer termbox.Close()
|
||||
|
||||
update := func() {
|
||||
var m daemon.Status
|
||||
|
||||
err2 := jsonRequestResponse(httpc, daemon.ControlJobEndpointStatus,
|
||||
struct{}{},
|
||||
&m,
|
||||
)
|
||||
|
||||
t.lock.Lock()
|
||||
t.err = err2
|
||||
t.report = m.Jobs
|
||||
t.lock.Unlock()
|
||||
t.draw()
|
||||
}
|
||||
update()
|
||||
|
||||
ticker := time.NewTicker(500 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
go func() {
|
||||
for range ticker.C {
|
||||
update()
|
||||
}
|
||||
}()
|
||||
|
||||
termbox.HideCursor()
|
||||
termbox.Clear(termbox.ColorDefault, termbox.ColorDefault)
|
||||
|
||||
loop:
|
||||
for {
|
||||
switch ev := termbox.PollEvent(); ev.Type {
|
||||
case termbox.EventKey:
|
||||
switch ev.Key {
|
||||
case termbox.KeyEsc:
|
||||
break loop
|
||||
case termbox.KeyCtrlC:
|
||||
break loop
|
||||
}
|
||||
case termbox.EventResize:
|
||||
t.draw()
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func (t *tui) getReplicationProgressHistory(jobName string) *bytesProgressHistory {
|
||||
p, ok := t.replicationProgress[jobName]
|
||||
if !ok {
|
||||
p = &bytesProgressHistory{}
|
||||
t.replicationProgress[jobName] = p
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func (t *tui) draw() {
|
||||
t.lock.Lock()
|
||||
defer t.lock.Unlock()
|
||||
|
||||
termbox.Clear(termbox.ColorDefault, termbox.ColorDefault)
|
||||
t.x = 0
|
||||
t.y = 0
|
||||
t.indent = 0
|
||||
|
||||
if t.err != nil {
|
||||
t.write(t.err.Error())
|
||||
} else {
|
||||
//Iterate over map in alphabetical order
|
||||
keys := make([]string, 0, len(t.report))
|
||||
for k := range t.report {
|
||||
if len(k) == 0 || daemon.IsInternalJobName(k) { //Internal job
|
||||
continue
|
||||
}
|
||||
if t.jobFilter != "" && k != t.jobFilter {
|
||||
continue
|
||||
}
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
if len(keys) == 0 {
|
||||
t.setIndent(0)
|
||||
t.printf("no jobs to display")
|
||||
t.newline()
|
||||
termbox.Flush()
|
||||
return
|
||||
}
|
||||
|
||||
for _, k := range keys {
|
||||
v := t.report[k]
|
||||
|
||||
t.setIndent(0)
|
||||
|
||||
t.printf("Job: %s", k)
|
||||
t.setIndent(1)
|
||||
t.newline()
|
||||
t.printf("Type: %s", v.Type)
|
||||
t.setIndent(1)
|
||||
t.newline()
|
||||
|
||||
if v.Type == job.TypePush || v.Type == job.TypePull {
|
||||
activeStatus, ok := v.JobSpecific.(*job.ActiveSideStatus)
|
||||
if !ok || activeStatus == nil {
|
||||
t.printf("ActiveSideStatus is null")
|
||||
t.newline()
|
||||
continue
|
||||
}
|
||||
|
||||
t.printf("Replication:")
|
||||
t.newline()
|
||||
t.addIndent(1)
|
||||
t.renderReplicationReport(activeStatus.Replication, t.getReplicationProgressHistory(k))
|
||||
t.addIndent(-1)
|
||||
|
||||
t.printf("Pruning Sender:")
|
||||
t.newline()
|
||||
t.addIndent(1)
|
||||
t.renderPrunerReport(activeStatus.PruningSender)
|
||||
t.addIndent(-1)
|
||||
|
||||
t.printf("Pruning Receiver:")
|
||||
t.newline()
|
||||
t.addIndent(1)
|
||||
t.renderPrunerReport(activeStatus.PruningReceiver)
|
||||
t.addIndent(-1)
|
||||
|
||||
if v.Type == job.TypePush {
|
||||
t.printf("Snapshotting:")
|
||||
t.newline()
|
||||
t.addIndent(1)
|
||||
t.renderSnapperReport(activeStatus.Snapshotting)
|
||||
t.addIndent(-1)
|
||||
}
|
||||
|
||||
} else if v.Type == job.TypeSnap {
|
||||
snapStatus, ok := v.JobSpecific.(*job.SnapJobStatus)
|
||||
if !ok || snapStatus == nil {
|
||||
t.printf("SnapJobStatus is null")
|
||||
t.newline()
|
||||
continue
|
||||
}
|
||||
t.printf("Pruning snapshots:")
|
||||
t.newline()
|
||||
t.addIndent(1)
|
||||
t.renderPrunerReport(snapStatus.Pruning)
|
||||
t.addIndent(-1)
|
||||
t.printf("Snapshotting:")
|
||||
t.newline()
|
||||
t.addIndent(1)
|
||||
t.renderSnapperReport(snapStatus.Snapshotting)
|
||||
t.addIndent(-1)
|
||||
} else if v.Type == job.TypeSource {
|
||||
|
||||
st := v.JobSpecific.(*job.PassiveStatus)
|
||||
t.printf("Snapshotting:\n")
|
||||
t.addIndent(1)
|
||||
t.renderSnapperReport(st.Snapper)
|
||||
t.addIndent(-1)
|
||||
|
||||
} else {
|
||||
t.printf("No status representation for job type '%s', dumping as YAML", v.Type)
|
||||
t.newline()
|
||||
asYaml, err := yaml.Marshal(v.JobSpecific)
|
||||
if err != nil {
|
||||
t.printf("Error marshaling status to YAML: %s", err)
|
||||
t.newline()
|
||||
continue
|
||||
}
|
||||
t.write(string(asYaml))
|
||||
t.newline()
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
termbox.Flush()
|
||||
}
|
||||
|
||||
func (t *tui) renderReplicationReport(rep *report.Report, history *bytesProgressHistory) {
|
||||
if rep == nil {
|
||||
t.printf("...\n")
|
||||
return
|
||||
}
|
||||
|
||||
if rep.WaitReconnectError != nil {
|
||||
t.printfDrawIndentedAndWrappedIfMultiline("Connectivity: %s", rep.WaitReconnectError)
|
||||
t.newline()
|
||||
}
|
||||
if !rep.WaitReconnectSince.IsZero() {
|
||||
delta := time.Until(rep.WaitReconnectUntil).Round(time.Second)
|
||||
if rep.WaitReconnectUntil.IsZero() || delta > 0 {
|
||||
var until string
|
||||
if rep.WaitReconnectUntil.IsZero() {
|
||||
until = "waiting indefinitely"
|
||||
} else {
|
||||
until = fmt.Sprintf("hard fail in %s @ %s", delta, rep.WaitReconnectUntil)
|
||||
}
|
||||
t.printfDrawIndentedAndWrappedIfMultiline("Connectivity: reconnecting with exponential backoff (since %s) (%s)",
|
||||
rep.WaitReconnectSince, until)
|
||||
} else {
|
||||
t.printfDrawIndentedAndWrappedIfMultiline("Connectivity: reconnects reached hard-fail timeout @ %s", rep.WaitReconnectUntil)
|
||||
}
|
||||
t.newline()
|
||||
}
|
||||
|
||||
// TODO visualize more than the latest attempt by folding all attempts into one
|
||||
if len(rep.Attempts) == 0 {
|
||||
t.printf("no attempts made yet")
|
||||
return
|
||||
} else {
|
||||
t.printf("Attempt #%d", len(rep.Attempts))
|
||||
if len(rep.Attempts) > 1 {
|
||||
t.printf(". Previous attempts failed with the following statuses:")
|
||||
t.newline()
|
||||
t.addIndent(1)
|
||||
for i, a := range rep.Attempts[:len(rep.Attempts)-1] {
|
||||
t.printfDrawIndentedAndWrappedIfMultiline("#%d: %s (failed at %s) (ran %s)", i+1, a.State, a.FinishAt, a.FinishAt.Sub(a.StartAt))
|
||||
t.newline()
|
||||
}
|
||||
t.addIndent(-1)
|
||||
} else {
|
||||
t.newline()
|
||||
}
|
||||
}
|
||||
|
||||
latest := rep.Attempts[len(rep.Attempts)-1]
|
||||
sort.Slice(latest.Filesystems, func(i, j int) bool {
|
||||
return latest.Filesystems[i].Info.Name < latest.Filesystems[j].Info.Name
|
||||
})
|
||||
|
||||
t.printf("Status: %s", latest.State)
|
||||
t.newline()
|
||||
if latest.State == report.AttemptPlanningError {
|
||||
t.printf("Problem: ")
|
||||
t.printfDrawIndentedAndWrappedIfMultiline("%s", latest.PlanError)
|
||||
t.newline()
|
||||
} else if latest.State == report.AttemptFanOutError {
|
||||
t.printf("Problem: one or more of the filesystems encountered errors")
|
||||
t.newline()
|
||||
}
|
||||
|
||||
if latest.State != report.AttemptPlanning && latest.State != report.AttemptPlanningError {
|
||||
// Draw global progress bar
|
||||
// Progress: [---------------]
|
||||
expected, replicated, containsInvalidSizeEstimates := latest.BytesSum()
|
||||
rate, changeCount := history.Update(replicated)
|
||||
eta := time.Duration(0)
|
||||
if rate > 0 {
|
||||
eta = time.Duration((expected-replicated)/rate) * time.Second
|
||||
}
|
||||
t.write("Progress: ")
|
||||
t.drawBar(50, replicated, expected, changeCount)
|
||||
t.write(fmt.Sprintf(" %s / %s @ %s/s", ByteCountBinary(replicated), ByteCountBinary(expected), ByteCountBinary(rate)))
|
||||
if eta != 0 {
|
||||
t.write(fmt.Sprintf(" (%s remaining)", humanizeDuration(eta)))
|
||||
}
|
||||
t.newline()
|
||||
if containsInvalidSizeEstimates {
|
||||
t.write("NOTE: not all steps could be size-estimated, total estimate is likely imprecise!")
|
||||
t.newline()
|
||||
}
|
||||
|
||||
if len(latest.Filesystems) == 0 {
|
||||
t.write("NOTE: no filesystems were considered for replication!")
|
||||
t.newline()
|
||||
}
|
||||
|
||||
var maxFSLen int
|
||||
for _, fs := range latest.Filesystems {
|
||||
if len(fs.Info.Name) > maxFSLen {
|
||||
maxFSLen = len(fs.Info.Name)
|
||||
}
|
||||
}
|
||||
for _, fs := range latest.Filesystems {
|
||||
t.printFilesystemStatus(fs, false, maxFSLen) // FIXME bring 'active' flag back
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (t *tui) renderPrunerReport(r *pruner.Report) {
|
||||
if r == nil {
|
||||
t.printf("...\n")
|
||||
return
|
||||
}
|
||||
|
||||
state, err := pruner.StateString(r.State)
|
||||
if err != nil {
|
||||
t.printf("Status: %q (parse error: %q)\n", r.State, err)
|
||||
return
|
||||
}
|
||||
|
||||
t.printf("Status: %s", state)
|
||||
t.newline()
|
||||
|
||||
if r.Error != "" {
|
||||
t.printf("Error: %s\n", r.Error)
|
||||
}
|
||||
|
||||
type commonFS struct {
|
||||
*pruner.FSReport
|
||||
completed bool
|
||||
}
|
||||
all := make([]commonFS, 0, len(r.Pending)+len(r.Completed))
|
||||
for i := range r.Pending {
|
||||
all = append(all, commonFS{&r.Pending[i], false})
|
||||
}
|
||||
for i := range r.Completed {
|
||||
all = append(all, commonFS{&r.Completed[i], true})
|
||||
}
|
||||
|
||||
switch state {
|
||||
case pruner.Plan:
|
||||
fallthrough
|
||||
case pruner.PlanErr:
|
||||
return
|
||||
}
|
||||
|
||||
if len(all) == 0 {
|
||||
t.printf("nothing to do\n")
|
||||
return
|
||||
}
|
||||
|
||||
var totalDestroyCount, completedDestroyCount int
|
||||
var maxFSname int
|
||||
for _, fs := range all {
|
||||
totalDestroyCount += len(fs.DestroyList)
|
||||
if fs.completed {
|
||||
completedDestroyCount += len(fs.DestroyList)
|
||||
}
|
||||
if maxFSname < len(fs.Filesystem) {
|
||||
maxFSname = len(fs.Filesystem)
|
||||
}
|
||||
}
|
||||
|
||||
// global progress bar
|
||||
progress := int(math.Round(80 * float64(completedDestroyCount) / float64(totalDestroyCount)))
|
||||
t.write("Progress: ")
|
||||
t.write("[")
|
||||
t.write(times("=", progress))
|
||||
t.write(">")
|
||||
t.write(times("-", 80-progress))
|
||||
t.write("]")
|
||||
t.printf(" %d/%d snapshots", completedDestroyCount, totalDestroyCount)
|
||||
t.newline()
|
||||
|
||||
sort.SliceStable(all, func(i, j int) bool {
|
||||
return strings.Compare(all[i].Filesystem, all[j].Filesystem) == -1
|
||||
})
|
||||
|
||||
// Draw a table-like representation of 'all'
|
||||
for _, fs := range all {
|
||||
t.write(rightPad(fs.Filesystem, maxFSname, " "))
|
||||
t.write(" ")
|
||||
if !fs.SkipReason.NotSkipped() {
|
||||
t.printf("skipped: %s\n", fs.SkipReason)
|
||||
continue
|
||||
}
|
||||
if fs.LastError != "" {
|
||||
if strings.ContainsAny(fs.LastError, "\r\n") {
|
||||
t.printf("ERROR:")
|
||||
t.printfDrawIndentedAndWrappedIfMultiline("%s\n", fs.LastError)
|
||||
} else {
|
||||
t.printfDrawIndentedAndWrappedIfMultiline("ERROR: %s\n", fs.LastError)
|
||||
}
|
||||
t.newline()
|
||||
continue
|
||||
}
|
||||
|
||||
pruneRuleActionStr := fmt.Sprintf("(destroy %d of %d snapshots)",
|
||||
len(fs.DestroyList), len(fs.SnapshotList))
|
||||
|
||||
if fs.completed {
|
||||
t.printf("Completed %s\n", pruneRuleActionStr)
|
||||
continue
|
||||
}
|
||||
|
||||
t.write("Pending ") // whitespace is padding 10
|
||||
if len(fs.DestroyList) == 1 {
|
||||
t.write(fs.DestroyList[0].Name)
|
||||
} else {
|
||||
t.write(pruneRuleActionStr)
|
||||
}
|
||||
t.newline()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (t *tui) renderSnapperReport(r *snapper.Report) {
|
||||
if r == nil {
|
||||
t.printf("<snapshot type does not have a report>\n")
|
||||
return
|
||||
}
|
||||
|
||||
t.printf("Status: %s", r.State)
|
||||
t.newline()
|
||||
|
||||
if r.Error != "" {
|
||||
t.printf("Error: %s\n", r.Error)
|
||||
}
|
||||
if !r.SleepUntil.IsZero() {
|
||||
t.printf("Sleep until: %s\n", r.SleepUntil)
|
||||
}
|
||||
|
||||
sort.Slice(r.Progress, func(i, j int) bool {
|
||||
return strings.Compare(r.Progress[i].Path, r.Progress[j].Path) == -1
|
||||
})
|
||||
|
||||
t.addIndent(1)
|
||||
defer t.addIndent(-1)
|
||||
dur := func(d time.Duration) string {
|
||||
return d.Round(100 * time.Millisecond).String()
|
||||
}
|
||||
|
||||
type row struct {
|
||||
path, state, duration, remainder, hookReport string
|
||||
}
|
||||
var widths struct {
|
||||
path, state, duration int
|
||||
}
|
||||
rows := make([]*row, len(r.Progress))
|
||||
for i, fs := range r.Progress {
|
||||
r := &row{
|
||||
path: fs.Path,
|
||||
state: fs.State.String(),
|
||||
}
|
||||
if fs.HooksHadError {
|
||||
r.hookReport = fs.Hooks // FIXME render here, not in daemon
|
||||
}
|
||||
switch fs.State {
|
||||
case snapper.SnapPending:
|
||||
r.duration = "..."
|
||||
r.remainder = ""
|
||||
case snapper.SnapStarted:
|
||||
r.duration = dur(time.Since(fs.StartAt))
|
||||
r.remainder = fmt.Sprintf("snap name: %q", fs.SnapName)
|
||||
case snapper.SnapDone:
|
||||
fallthrough
|
||||
case snapper.SnapError:
|
||||
r.duration = dur(fs.DoneAt.Sub(fs.StartAt))
|
||||
r.remainder = fmt.Sprintf("snap name: %q", fs.SnapName)
|
||||
}
|
||||
rows[i] = r
|
||||
if len(r.path) > widths.path {
|
||||
widths.path = len(r.path)
|
||||
}
|
||||
if len(r.state) > widths.state {
|
||||
widths.state = len(r.state)
|
||||
}
|
||||
if len(r.duration) > widths.duration {
|
||||
widths.duration = len(r.duration)
|
||||
}
|
||||
}
|
||||
|
||||
for _, r := range rows {
|
||||
path := rightPad(r.path, widths.path, " ")
|
||||
state := rightPad(r.state, widths.state, " ")
|
||||
duration := rightPad(r.duration, widths.duration, " ")
|
||||
t.printf("%s %s %s", path, state, duration)
|
||||
t.printfDrawIndentedAndWrappedIfMultiline(" %s", r.remainder)
|
||||
if r.hookReport != "" {
|
||||
t.printfDrawIndentedAndWrappedIfMultiline("%s", r.hookReport)
|
||||
}
|
||||
t.newline()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func times(str string, n int) (out string) {
|
||||
for i := 0; i < n; i++ {
|
||||
out += str
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func rightPad(str string, length int, pad string) string {
|
||||
if len(str) > length {
|
||||
return str[:length]
|
||||
}
|
||||
return str + strings.Repeat(pad, length-len(str))
|
||||
}
|
||||
|
||||
var arrowPositions = `>\|/`
|
||||
|
||||
// changeCount = 0 indicates stall / no progress
|
||||
func (t *tui) drawBar(length int, bytes, totalBytes int64, changeCount int) {
|
||||
var completedLength int
|
||||
if totalBytes > 0 {
|
||||
completedLength = int(int64(length) * bytes / totalBytes)
|
||||
if completedLength > length {
|
||||
completedLength = length
|
||||
}
|
||||
} else if totalBytes == bytes {
|
||||
completedLength = length
|
||||
}
|
||||
|
||||
t.write("[")
|
||||
t.write(times("=", completedLength))
|
||||
t.write(string(arrowPositions[changeCount%len(arrowPositions)]))
|
||||
t.write(times("-", length-completedLength))
|
||||
t.write("]")
|
||||
}
|
||||
|
||||
func (t *tui) printFilesystemStatus(rep *report.FilesystemReport, active bool, maxFS int) {
|
||||
|
||||
expected, replicated, containsInvalidSizeEstimates := rep.BytesSum()
|
||||
sizeEstimationImpreciseNotice := ""
|
||||
if containsInvalidSizeEstimates {
|
||||
sizeEstimationImpreciseNotice = " (some steps lack size estimation)"
|
||||
}
|
||||
if rep.CurrentStep < len(rep.Steps) && rep.Steps[rep.CurrentStep].Info.BytesExpected == 0 {
|
||||
sizeEstimationImpreciseNotice = " (step lacks size estimation)"
|
||||
}
|
||||
|
||||
status := fmt.Sprintf("%s (step %d/%d, %s/%s)%s",
|
||||
strings.ToUpper(string(rep.State)),
|
||||
rep.CurrentStep, len(rep.Steps),
|
||||
ByteCountBinary(replicated), ByteCountBinary(expected),
|
||||
sizeEstimationImpreciseNotice,
|
||||
)
|
||||
|
||||
activeIndicator := " "
|
||||
if active {
|
||||
activeIndicator = "*"
|
||||
}
|
||||
t.printf("%s %s %s ",
|
||||
activeIndicator,
|
||||
rightPad(rep.Info.Name, maxFS, " "),
|
||||
status)
|
||||
|
||||
next := ""
|
||||
if err := rep.Error(); err != nil {
|
||||
next = err.Err
|
||||
} else if rep.State != report.FilesystemDone {
|
||||
if nextStep := rep.NextStep(); nextStep != nil {
|
||||
if nextStep.IsIncremental() {
|
||||
next = fmt.Sprintf("next: %s => %s", nextStep.Info.From, nextStep.Info.To)
|
||||
} else {
|
||||
next = fmt.Sprintf("next: full send %s", nextStep.Info.To)
|
||||
}
|
||||
attribs := []string{}
|
||||
|
||||
if nextStep.Info.Resumed {
|
||||
attribs = append(attribs, "resumed")
|
||||
}
|
||||
|
||||
attribs = append(attribs, fmt.Sprintf("encrypted=%s", nextStep.Info.Encrypted))
|
||||
|
||||
next += fmt.Sprintf(" (%s)", strings.Join(attribs, ", "))
|
||||
} else {
|
||||
next = "" // individual FSes may still be in planning state
|
||||
}
|
||||
|
||||
}
|
||||
t.printfDrawIndentedAndWrappedIfMultiline("%s", next)
|
||||
|
||||
t.newline()
|
||||
}
|
||||
|
||||
func ByteCountBinary(b int64) string {
|
||||
const unit = 1024
|
||||
if b < unit {
|
||||
return fmt.Sprintf("%d B", b)
|
||||
}
|
||||
div, exp := int64(unit), 0
|
||||
for n := b / unit; n >= unit; n /= unit {
|
||||
div *= unit
|
||||
exp++
|
||||
}
|
||||
return fmt.Sprintf("%.1f %ciB", float64(b)/float64(div), "KMGTPE"[exp])
|
||||
}
|
||||
|
||||
func humanizeDuration(duration time.Duration) string {
|
||||
days := int64(duration.Hours() / 24)
|
||||
hours := int64(math.Mod(duration.Hours(), 24))
|
||||
minutes := int64(math.Mod(duration.Minutes(), 60))
|
||||
seconds := int64(math.Mod(duration.Seconds(), 60))
|
||||
|
||||
var parts []string
|
||||
|
||||
force := false
|
||||
chunks := []int64{days, hours, minutes, seconds}
|
||||
for i, chunk := range chunks {
|
||||
if force || chunk > 0 {
|
||||
padding := 0
|
||||
if force {
|
||||
padding = 2
|
||||
}
|
||||
parts = append(parts, fmt.Sprintf("%*d%c", padding, chunk, "dhms"[i]))
|
||||
force = true
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(parts, " ")
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/zrepl/zrepl/daemon"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
h *http.Client
|
||||
}
|
||||
|
||||
func New(network, addr string) (*Client, error) {
|
||||
httpc, err := makeControlHttpClient(func(_ context.Context) (net.Conn, error) { return net.Dial(network, addr) })
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Client{httpc}, nil
|
||||
}
|
||||
|
||||
func (c *Client) Status() (s daemon.Status, _ error) {
|
||||
err := jsonRequestResponse(c.h, daemon.ControlJobEndpointStatus,
|
||||
struct{}{},
|
||||
&s,
|
||||
)
|
||||
return s, err
|
||||
}
|
||||
|
||||
func (c *Client) StatusRaw() ([]byte, error) {
|
||||
var r json.RawMessage
|
||||
err := jsonRequestResponse(c.h, daemon.ControlJobEndpointStatus, struct{}{}, &r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func (c *Client) signal(job, sig string) error {
|
||||
return jsonRequestResponse(c.h, daemon.ControlJobEndpointSignal,
|
||||
struct {
|
||||
Name string
|
||||
Op string
|
||||
}{
|
||||
Name: job,
|
||||
Op: sig,
|
||||
},
|
||||
struct{}{},
|
||||
)
|
||||
}
|
||||
|
||||
func (c *Client) SignalWakeup(job string) error {
|
||||
return c.signal(job, "wakeup")
|
||||
}
|
||||
|
||||
func (c *Client) SignalReset(job string) error {
|
||||
return c.signal(job, "reset")
|
||||
}
|
||||
|
||||
func makeControlHttpClient(dialfunc func(context.Context) (net.Conn, error)) (client *http.Client, err error) {
|
||||
return &http.Client{
|
||||
Transport: &http.Transport{
|
||||
DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
|
||||
return dialfunc(ctx)
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func jsonRequestResponse(c *http.Client, endpoint string, req interface{}, res interface{}) error {
|
||||
var buf bytes.Buffer
|
||||
encodeErr := json.NewEncoder(&buf).Encode(req)
|
||||
if encodeErr != nil {
|
||||
return encodeErr
|
||||
}
|
||||
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
var msg bytes.Buffer
|
||||
_, _ = io.CopyN(&msg, resp.Body, 4096) // ignore error, just display what we got
|
||||
return errors.Errorf("%s", msg.String())
|
||||
}
|
||||
|
||||
decodeError := json.NewDecoder(resp.Body).Decode(&res)
|
||||
if decodeError != nil {
|
||||
return decodeError
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package status
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/mattn/go-isatty"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
"github.com/zrepl/zrepl/cli"
|
||||
"github.com/zrepl/zrepl/client/status/client"
|
||||
"github.com/zrepl/zrepl/config"
|
||||
"github.com/zrepl/zrepl/daemon"
|
||||
"github.com/zrepl/zrepl/util/choices"
|
||||
)
|
||||
|
||||
type Client interface {
|
||||
Status() (daemon.Status, error)
|
||||
StatusRaw() ([]byte, error)
|
||||
SignalWakeup(job string) error
|
||||
SignalReset(job string) error
|
||||
}
|
||||
|
||||
type statusFlags struct {
|
||||
Mode choices.Choices
|
||||
Job string
|
||||
Delay time.Duration
|
||||
}
|
||||
|
||||
var statusv2Flags statusFlags
|
||||
|
||||
type statusv2Mode int
|
||||
|
||||
const (
|
||||
StatusV2ModeInteractive statusv2Mode = 1 + iota
|
||||
StatusV2ModeDump
|
||||
StatusV2ModeRaw
|
||||
StatusV2ModeLegacy
|
||||
)
|
||||
|
||||
var Subcommand = &cli.Subcommand{
|
||||
Use: "status",
|
||||
Short: "retrieve & display daemon status information",
|
||||
SetupFlags: func(f *pflag.FlagSet) {
|
||||
statusv2Flags.Mode.Init(
|
||||
"interactive", StatusV2ModeInteractive,
|
||||
"dump", StatusV2ModeDump,
|
||||
"raw", StatusV2ModeRaw,
|
||||
"legacy", StatusV2ModeLegacy,
|
||||
)
|
||||
statusv2Flags.Mode.SetTypeString("mode")
|
||||
statusv2Flags.Mode.SetDefaultValue(StatusV2ModeInteractive)
|
||||
f.Var(&statusv2Flags.Mode, "mode", statusv2Flags.Mode.Usage())
|
||||
f.StringVar(&statusv2Flags.Job, "job", "", "only show specified job (works in \"dump\" and \"interactive\" mode)")
|
||||
f.DurationVarP(&statusv2Flags.Delay, "delay", "d", 1*time.Second, "use -d 3s for 3 seconds delay (minimum delay is 1s)")
|
||||
},
|
||||
Run: func(ctx context.Context, subcommand *cli.Subcommand, args []string) error {
|
||||
return runStatusV2Command(ctx, subcommand.Config(), args)
|
||||
},
|
||||
}
|
||||
|
||||
func runStatusV2Command(ctx context.Context, config *config.Config, args []string) error {
|
||||
|
||||
c, err := client.New("unix", config.Global.Control.SockPath)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "connect to daemon socket at %q", config.Global.Control.SockPath)
|
||||
}
|
||||
|
||||
mode := statusv2Flags.Mode.Value().(statusv2Mode)
|
||||
|
||||
if !isatty.IsTerminal(os.Stdout.Fd()) && mode != StatusV2ModeDump && mode != StatusV2ModeRaw {
|
||||
dumpmode, err := statusv2Flags.Mode.InputForChoice(StatusV2ModeDump)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
rawmode, err := statusv2Flags.Mode.InputForChoice(StatusV2ModeRaw)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return errors.Errorf("error: stdout is not a tty, please use --mode %s or --mode %s", dumpmode, rawmode)
|
||||
}
|
||||
|
||||
switch mode {
|
||||
case StatusV2ModeInteractive:
|
||||
return interactive(c, statusv2Flags)
|
||||
case StatusV2ModeDump:
|
||||
return dump(c, statusv2Flags.Job)
|
||||
case StatusV2ModeRaw:
|
||||
return raw(c)
|
||||
case StatusV2ModeLegacy:
|
||||
return legacy(c, statusv2Flags)
|
||||
default:
|
||||
panic("unreachable")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package status
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/gdamore/tcell"
|
||||
"github.com/mattn/go-isatty"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/zrepl/zrepl/client/status/viewmodel"
|
||||
)
|
||||
|
||||
func dump(c Client, job string) error {
|
||||
s, err := c.Status()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if job != "" {
|
||||
if _, ok := s.Jobs[job]; !ok {
|
||||
return errors.Errorf("job %q not found", job)
|
||||
}
|
||||
}
|
||||
|
||||
width := (1 << 31) - 1
|
||||
wrap := false
|
||||
hline := strings.Repeat("-", 80)
|
||||
if isatty.IsTerminal(os.Stdout.Fd()) {
|
||||
wrap = true
|
||||
screen, err := tcell.NewScreen()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "get terminal dimensions")
|
||||
}
|
||||
if err := screen.Init(); err != nil {
|
||||
return errors.Wrap(err, "init screen")
|
||||
}
|
||||
width, _ = screen.Size()
|
||||
screen.Fini()
|
||||
hline = strings.Repeat("-", width)
|
||||
}
|
||||
|
||||
m := viewmodel.New()
|
||||
params := viewmodel.Params{
|
||||
Report: s.Jobs,
|
||||
ReportFetchError: nil,
|
||||
SelectedJob: nil,
|
||||
FSFilter: func(s string) bool { return true },
|
||||
DetailViewWidth: width,
|
||||
DetailViewWrap: wrap,
|
||||
ShortKeybindingOverview: "",
|
||||
}
|
||||
m.Update(params)
|
||||
for _, j := range m.Jobs() {
|
||||
if job != "" && j.Name() != job {
|
||||
continue
|
||||
}
|
||||
params.SelectedJob = j
|
||||
m.Update(params)
|
||||
fmt.Println(m.SelectedJob().FullDescription())
|
||||
if job != "" {
|
||||
return nil
|
||||
} else {
|
||||
fmt.Println(hline)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,347 @@
|
||||
package status
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
tview "gitlab.com/tslocum/cview"
|
||||
|
||||
"github.com/zrepl/zrepl/client/status/viewmodel"
|
||||
)
|
||||
|
||||
func interactive(c Client, flag statusFlags) error {
|
||||
|
||||
// Set this so we don't overwrite the default terminal colors
|
||||
// See https://github.com/rivo/tview/blob/master/styles.go
|
||||
tview.Styles.PrimitiveBackgroundColor = tcell.ColorDefault
|
||||
tview.Styles.ContrastBackgroundColor = tcell.ColorDefault
|
||||
tview.Styles.PrimaryTextColor = tcell.ColorDefault
|
||||
tview.Styles.BorderColor = tcell.ColorDefault
|
||||
app := tview.NewApplication()
|
||||
|
||||
jobDetailSplit := tview.NewFlex()
|
||||
jobMenu := tview.NewTreeView()
|
||||
jobMenuRoot := tview.NewTreeNode("jobs")
|
||||
jobMenuRoot.SetSelectable(true)
|
||||
jobMenu.SetRoot(jobMenuRoot)
|
||||
jobMenu.SetCurrentNode(jobMenuRoot)
|
||||
jobMenu.SetSelectedTextColor(tcell.ColorGreen)
|
||||
jobTextDetail := tview.NewTextView()
|
||||
jobTextDetail.SetWrap(false)
|
||||
|
||||
jobMenu.SetBorder(true)
|
||||
jobTextDetail.SetBorder(true)
|
||||
|
||||
toolbarSplit := tview.NewFlex()
|
||||
toolbarSplit.SetDirection(tview.FlexRow)
|
||||
inputBarContainer := tview.NewFlex()
|
||||
fsFilterInput := tview.NewInputField()
|
||||
fsFilterInput.SetBorder(false)
|
||||
fsFilterInput.SetFieldBackgroundColor(tcell.ColorDefault)
|
||||
inputBarLabel := tview.NewTextView()
|
||||
inputBarLabel.SetText("[::b]FILTER ")
|
||||
inputBarLabel.SetDynamicColors(true)
|
||||
inputBarContainer.AddItem(inputBarLabel, 7, 1, false)
|
||||
inputBarContainer.AddItem(fsFilterInput, 0, 10, false)
|
||||
toolbarSplit.AddItem(inputBarContainer, 1, 0, false)
|
||||
toolbarSplit.AddItem(jobDetailSplit, 0, 10, false)
|
||||
|
||||
bottombar := tview.NewFlex()
|
||||
bottombar.SetDirection(tview.FlexColumn)
|
||||
bottombarDateView := tview.NewTextView()
|
||||
bottombar.AddItem(bottombarDateView, len(time.Now().String()), 0, false)
|
||||
bottomBarStatus := tview.NewTextView()
|
||||
bottomBarStatus.SetDynamicColors(true)
|
||||
bottomBarStatus.SetTextAlign(tview.AlignRight)
|
||||
bottombar.AddItem(bottomBarStatus, 0, 10, false)
|
||||
toolbarSplit.AddItem(bottombar, 1, 0, false)
|
||||
|
||||
tabbableWithJobMenu := []tview.Primitive{jobMenu, jobTextDetail, fsFilterInput}
|
||||
tabbableWithoutJobMenu := []tview.Primitive{jobTextDetail, fsFilterInput}
|
||||
var tabbable []tview.Primitive
|
||||
tabbableActiveIndex := 0
|
||||
tabbableRedraw := func() {
|
||||
if len(tabbable) == 0 {
|
||||
app.SetFocus(nil)
|
||||
return
|
||||
}
|
||||
if tabbableActiveIndex >= len(tabbable) {
|
||||
app.SetFocus(tabbable[0])
|
||||
return
|
||||
}
|
||||
app.SetFocus(tabbable[tabbableActiveIndex])
|
||||
}
|
||||
tabbableCycle := func() {
|
||||
if len(tabbable) == 0 {
|
||||
return
|
||||
}
|
||||
tabbableActiveIndex = (tabbableActiveIndex + 1) % len(tabbable)
|
||||
app.SetFocus(tabbable[tabbableActiveIndex])
|
||||
tabbableRedraw()
|
||||
}
|
||||
|
||||
jobMenuVisisble := false
|
||||
reconfigureJobDetailSplit := func(setJobMenuVisible bool) {
|
||||
if jobMenuVisisble == setJobMenuVisible {
|
||||
return
|
||||
}
|
||||
jobMenuVisisble = setJobMenuVisible
|
||||
if setJobMenuVisible {
|
||||
jobDetailSplit.RemoveItem(jobTextDetail)
|
||||
jobDetailSplit.AddItem(jobMenu, 0, 1, true)
|
||||
jobDetailSplit.AddItem(jobTextDetail, 0, 5, false)
|
||||
tabbable = tabbableWithJobMenu
|
||||
} else {
|
||||
jobDetailSplit.RemoveItem(jobMenu)
|
||||
tabbable = tabbableWithoutJobMenu
|
||||
}
|
||||
tabbableRedraw()
|
||||
}
|
||||
|
||||
showModal := func(m *tview.Modal, modalDoneFunc func(idx int, label string)) {
|
||||
preModalFocus := app.GetFocus()
|
||||
m.SetDoneFunc(func(idx int, label string) {
|
||||
if modalDoneFunc != nil {
|
||||
modalDoneFunc(idx, label)
|
||||
}
|
||||
app.SetRoot(toolbarSplit, true)
|
||||
app.SetFocus(preModalFocus)
|
||||
app.Draw()
|
||||
})
|
||||
app.SetRoot(m, true)
|
||||
app.Draw()
|
||||
}
|
||||
|
||||
app.SetRoot(toolbarSplit, true)
|
||||
// initial focus
|
||||
tabbableActiveIndex = len(tabbable)
|
||||
tabbableCycle()
|
||||
reconfigureJobDetailSplit(true)
|
||||
|
||||
m := viewmodel.New()
|
||||
params := &viewmodel.Params{
|
||||
Report: nil,
|
||||
SelectedJob: nil,
|
||||
FSFilter: func(_ string) bool { return true },
|
||||
DetailViewWidth: 100,
|
||||
DetailViewWrap: false,
|
||||
ShortKeybindingOverview: "[::b]Q[::-] quit [::b]<TAB>[::-] switch panes [::b]Shift+M[::-] toggle navbar [::b]Shift+S[::-] signal job [::b]</>[::-] filter filesystems",
|
||||
}
|
||||
paramsMtx := &sync.Mutex{}
|
||||
var redraw func()
|
||||
viewmodelupdate := func(cb func(*viewmodel.Params)) {
|
||||
paramsMtx.Lock()
|
||||
defer paramsMtx.Unlock()
|
||||
cb(params)
|
||||
m.Update(*params)
|
||||
}
|
||||
redraw = func() {
|
||||
jobs := m.Jobs()
|
||||
if flag.Job != "" {
|
||||
job_found := false
|
||||
for _, job := range jobs {
|
||||
if strings.Compare(flag.Job, job.Name()) == 0 {
|
||||
jobs = []*viewmodel.Job{job}
|
||||
job_found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !job_found {
|
||||
jobs = nil
|
||||
}
|
||||
}
|
||||
redrawJobsList := false
|
||||
var selectedJobN *tview.TreeNode
|
||||
if len(jobMenuRoot.GetChildren()) == len(jobs) {
|
||||
for i, jobN := range jobMenuRoot.GetChildren() {
|
||||
if jobN.GetReference().(*viewmodel.Job) != jobs[i] {
|
||||
redrawJobsList = true
|
||||
break
|
||||
}
|
||||
if jobN.GetReference().(*viewmodel.Job) == m.SelectedJob() {
|
||||
selectedJobN = jobN
|
||||
}
|
||||
}
|
||||
} else {
|
||||
redrawJobsList = true
|
||||
}
|
||||
if redrawJobsList {
|
||||
selectedJobN = nil
|
||||
children := make([]*tview.TreeNode, len(jobs))
|
||||
for i := range jobs {
|
||||
jobN := tview.NewTreeNode(jobs[i].JobTreeTitle())
|
||||
jobN.SetReference(jobs[i])
|
||||
jobN.SetSelectable(true)
|
||||
children[i] = jobN
|
||||
jobN.SetSelectedFunc(func() {
|
||||
viewmodelupdate(func(p *viewmodel.Params) {
|
||||
p.SelectedJob = jobN.GetReference().(*viewmodel.Job)
|
||||
})
|
||||
})
|
||||
if jobs[i] == m.SelectedJob() {
|
||||
selectedJobN = jobN
|
||||
}
|
||||
}
|
||||
jobMenuRoot.SetChildren(children)
|
||||
}
|
||||
|
||||
if selectedJobN != nil && jobMenu.GetCurrentNode() != selectedJobN {
|
||||
jobMenu.SetCurrentNode(selectedJobN)
|
||||
} else if selectedJobN == nil {
|
||||
// select something, otherwise selection breaks (likely bug in tview)
|
||||
jobMenu.SetCurrentNode(jobMenuRoot)
|
||||
}
|
||||
|
||||
if selJ := m.SelectedJob(); selJ != nil {
|
||||
jobTextDetail.SetText(selJ.FullDescription())
|
||||
} else {
|
||||
jobTextDetail.SetText("please select a job")
|
||||
}
|
||||
|
||||
bottombardatestring := m.DateString()
|
||||
bottombarDateView.SetText(bottombardatestring)
|
||||
bottombar.ResizeItem(bottombarDateView, len(bottombardatestring), 0)
|
||||
|
||||
bottomBarStatus.SetText(m.BottomBarStatus())
|
||||
|
||||
app.Draw()
|
||||
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
app.Suspend(func() {
|
||||
panic(err)
|
||||
})
|
||||
}
|
||||
}()
|
||||
for {
|
||||
st, err := c.Status()
|
||||
viewmodelupdate(func(p *viewmodel.Params) {
|
||||
p.Report = st.Jobs
|
||||
p.ReportFetchError = err
|
||||
})
|
||||
app.QueueUpdateDraw(redraw)
|
||||
|
||||
time.Sleep(flag.Delay)
|
||||
}
|
||||
}()
|
||||
|
||||
jobMenu.SetChangedFunc(func(jobN *tview.TreeNode) {
|
||||
viewmodelupdate(func(p *viewmodel.Params) {
|
||||
p.SelectedJob, _ = jobN.GetReference().(*viewmodel.Job)
|
||||
})
|
||||
redraw()
|
||||
jobTextDetail.ScrollToBeginning()
|
||||
})
|
||||
jobMenu.SetSelectedFunc(func(jobN *tview.TreeNode) {
|
||||
app.SetFocus(jobTextDetail)
|
||||
})
|
||||
|
||||
app.SetBeforeDrawFunc(func(screen tcell.Screen) bool {
|
||||
viewmodelupdate(func(p *viewmodel.Params) {
|
||||
_, _, p.DetailViewWidth, _ = jobTextDetail.GetInnerRect()
|
||||
})
|
||||
return false
|
||||
})
|
||||
|
||||
app.SetInputCapture(func(e *tcell.EventKey) *tcell.EventKey {
|
||||
if e.Key() == tcell.KeyTab {
|
||||
tabbableCycle()
|
||||
return nil
|
||||
}
|
||||
|
||||
if e.Key() == tcell.KeyRune && app.GetFocus() == fsFilterInput {
|
||||
return e
|
||||
}
|
||||
|
||||
if e.Key() == tcell.KeyRune && e.Rune() == '/' {
|
||||
if app.GetFocus() != fsFilterInput {
|
||||
app.SetFocus(fsFilterInput)
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
if e.Key() == tcell.KeyRune && e.Rune() == 'M' {
|
||||
reconfigureJobDetailSplit(!jobMenuVisisble)
|
||||
return nil
|
||||
}
|
||||
|
||||
if e.Key() == tcell.KeyRune && e.Rune() == 'q' {
|
||||
app.Stop()
|
||||
}
|
||||
|
||||
if e.Key() == tcell.KeyRune && e.Rune() == 'S' {
|
||||
job, ok := jobMenu.GetCurrentNode().GetReference().(*viewmodel.Job)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
signals := []string{"wakeup", "reset"}
|
||||
clientFuncs := []func(job string) error{c.SignalWakeup, c.SignalReset}
|
||||
sigMod := tview.NewModal()
|
||||
sigMod.SetBackgroundColor(tcell.ColorDefault)
|
||||
sigMod.SetBorder(true)
|
||||
sigMod.GetForm().SetButtonTextColorFocused(tcell.ColorGreen)
|
||||
sigMod.AddButtons(signals)
|
||||
sigMod.SetText(fmt.Sprintf("Send a signal to job %q", job.Name()))
|
||||
showModal(sigMod, func(idx int, _ string) {
|
||||
go func() {
|
||||
if idx == -1 {
|
||||
return
|
||||
}
|
||||
err := clientFuncs[idx](job.Name())
|
||||
if err != nil {
|
||||
app.QueueUpdate(func() {
|
||||
me := tview.NewModal()
|
||||
me.SetText(fmt.Sprintf("signal error: %s", err))
|
||||
me.AddButtons([]string{"Close"})
|
||||
showModal(me, nil)
|
||||
})
|
||||
}
|
||||
}()
|
||||
})
|
||||
}
|
||||
|
||||
return e
|
||||
})
|
||||
|
||||
fsFilterInput.SetChangedFunc(func(searchterm string) {
|
||||
viewmodelupdate(func(p *viewmodel.Params) {
|
||||
p.FSFilter = func(fs string) bool {
|
||||
r, err := regexp.Compile(searchterm)
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
return r.MatchString(fs)
|
||||
}
|
||||
})
|
||||
redraw()
|
||||
jobTextDetail.ScrollToBeginning()
|
||||
})
|
||||
fsFilterInput.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||
if event.Key() == tcell.KeyEnter {
|
||||
app.SetFocus(jobTextDetail)
|
||||
return nil
|
||||
}
|
||||
return event
|
||||
})
|
||||
|
||||
jobTextDetail.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||
if event.Key() == tcell.KeyRune && event.Rune() == 'w' {
|
||||
// toggle wrapping
|
||||
viewmodelupdate(func(p *viewmodel.Params) {
|
||||
p.DetailViewWrap = !p.DetailViewWrap
|
||||
})
|
||||
redraw()
|
||||
return nil
|
||||
}
|
||||
return event
|
||||
})
|
||||
|
||||
return app.Run()
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
package status
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/mattn/go-isatty"
|
||||
"github.com/pkg/errors"
|
||||
tview "gitlab.com/tslocum/cview"
|
||||
|
||||
"github.com/zrepl/zrepl/client/status/viewmodel"
|
||||
)
|
||||
|
||||
func legacy(c Client, flag statusFlags) error {
|
||||
|
||||
// Set this so we don't overwrite the default terminal colors
|
||||
// See https://github.com/rivo/tview/blob/master/styles.go
|
||||
tview.Styles.PrimitiveBackgroundColor = tcell.ColorDefault
|
||||
tview.Styles.ContrastBackgroundColor = tcell.ColorDefault
|
||||
tview.Styles.PrimaryTextColor = tcell.ColorDefault
|
||||
tview.Styles.BorderColor = tcell.ColorDefault
|
||||
app := tview.NewApplication()
|
||||
|
||||
textView := tview.NewTextView()
|
||||
textView.SetWrap(true)
|
||||
textView.SetScrollable(true) // so that it allows us to set scroll position
|
||||
textView.SetScrollBarVisibility(tview.ScrollBarNever)
|
||||
|
||||
app.SetRoot(textView, true)
|
||||
|
||||
width := (1 << 31) - 1
|
||||
wrap := false
|
||||
if isatty.IsTerminal(os.Stdout.Fd()) {
|
||||
wrap = true
|
||||
screen, err := tcell.NewScreen()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "get terminal dimensions")
|
||||
}
|
||||
if err := screen.Init(); err != nil {
|
||||
return errors.Wrap(err, "init screen")
|
||||
}
|
||||
width, _ = screen.Size()
|
||||
screen.Fini()
|
||||
}
|
||||
|
||||
paramsMtx := &sync.Mutex{}
|
||||
params := viewmodel.Params{
|
||||
Report: nil,
|
||||
ReportFetchError: nil,
|
||||
SelectedJob: nil,
|
||||
FSFilter: func(s string) bool { return true },
|
||||
DetailViewWidth: width,
|
||||
DetailViewWrap: wrap,
|
||||
ShortKeybindingOverview: "",
|
||||
}
|
||||
|
||||
redraw := func() {
|
||||
textView.Clear()
|
||||
|
||||
paramsMtx.Lock()
|
||||
defer paramsMtx.Unlock()
|
||||
|
||||
if params.ReportFetchError != nil {
|
||||
fmt.Fprintln(textView, params.ReportFetchError.Error())
|
||||
} else if params.Report != nil {
|
||||
m := viewmodel.New()
|
||||
m.Update(params)
|
||||
for _, j := range m.Jobs() {
|
||||
if flag.Job != "" && j.Name() != flag.Job {
|
||||
continue
|
||||
}
|
||||
params.SelectedJob = j
|
||||
m.Update(params)
|
||||
fmt.Fprintln(textView, m.SelectedJob().FullDescription())
|
||||
if flag.Job != "" {
|
||||
break
|
||||
} else {
|
||||
hline := strings.Repeat("-", params.DetailViewWidth)
|
||||
fmt.Fprintln(textView, hline)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
fmt.Fprintln(textView, "waiting for request results")
|
||||
}
|
||||
textView.ScrollToBeginning()
|
||||
}
|
||||
|
||||
app.SetBeforeDrawFunc(func(screen tcell.Screen) bool {
|
||||
// sync resizes to `params`
|
||||
paramsMtx.Lock()
|
||||
_, _, newWidth, _ := textView.GetInnerRect()
|
||||
if newWidth != params.DetailViewWidth {
|
||||
params.DetailViewWidth = newWidth
|
||||
app.QueueUpdateDraw(redraw)
|
||||
}
|
||||
paramsMtx.Unlock()
|
||||
|
||||
textView.ScrollToBeginning() // has the effect of inhibiting user scrolls
|
||||
return false
|
||||
})
|
||||
|
||||
go func() {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
app.Suspend(func() {
|
||||
panic(err)
|
||||
})
|
||||
}
|
||||
}()
|
||||
for {
|
||||
st, err := c.Status()
|
||||
paramsMtx.Lock()
|
||||
params.Report = st.Jobs
|
||||
params.ReportFetchError = err
|
||||
paramsMtx.Unlock()
|
||||
|
||||
app.QueueUpdateDraw(redraw)
|
||||
|
||||
time.Sleep(flag.Delay)
|
||||
}
|
||||
}()
|
||||
|
||||
return app.Run()
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package status
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
func raw(c Client) error {
|
||||
b, err := c.StatusRaw()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := io.Copy(os.Stdout, bytes.NewReader(b)); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package viewmodel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
)
|
||||
|
||||
func ByteCountBinaryUint(b uint64) string {
|
||||
if b > math.MaxInt64 {
|
||||
panic(b)
|
||||
}
|
||||
return ByteCountBinary(int64(b))
|
||||
}
|
||||
|
||||
func ByteCountBinary(b int64) string {
|
||||
const unit = 1024
|
||||
if b < unit {
|
||||
return fmt.Sprintf("%d B", b)
|
||||
}
|
||||
div, exp := unit, 0
|
||||
for n := b / unit; n >= unit; n /= unit {
|
||||
div *= unit
|
||||
exp++
|
||||
}
|
||||
return fmt.Sprintf("%.1f %ciB", float64(b)/float64(div), "KMGTPE"[exp])
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package viewmodel
|
||||
|
||||
import "time"
|
||||
|
||||
type byteProgressMeasurement struct {
|
||||
time time.Time
|
||||
val uint64
|
||||
}
|
||||
|
||||
type bytesProgressHistory struct {
|
||||
last *byteProgressMeasurement // pointer as poor man's optional
|
||||
changeCount int
|
||||
lastChange time.Time
|
||||
}
|
||||
|
||||
func (p *bytesProgressHistory) Update(currentVal uint64) (bytesPerSecondAvg int64, changeCount int) {
|
||||
|
||||
if p.last == nil {
|
||||
p.last = &byteProgressMeasurement{
|
||||
time: time.Now(),
|
||||
val: currentVal,
|
||||
}
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
if p.last.val != currentVal {
|
||||
p.changeCount++
|
||||
p.lastChange = time.Now()
|
||||
}
|
||||
|
||||
if time.Since(p.lastChange) > 3*time.Second {
|
||||
p.last = nil
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
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)
|
||||
rate := float64(deltaV) / deltaT.Seconds()
|
||||
|
||||
p.last.time = time.Now()
|
||||
p.last.val = currentVal
|
||||
|
||||
return int64(rate), p.changeCount
|
||||
}
|
||||
@@ -0,0 +1,627 @@
|
||||
package viewmodel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
yaml "github.com/zrepl/yaml-config"
|
||||
|
||||
"github.com/zrepl/zrepl/client/status/viewmodel/stringbuilder"
|
||||
"github.com/zrepl/zrepl/daemon"
|
||||
"github.com/zrepl/zrepl/daemon/job"
|
||||
"github.com/zrepl/zrepl/daemon/pruner"
|
||||
"github.com/zrepl/zrepl/daemon/snapper"
|
||||
"github.com/zrepl/zrepl/replication/report"
|
||||
)
|
||||
|
||||
type M struct {
|
||||
jobs map[string]*Job
|
||||
jobsList []*Job
|
||||
selectedJob *Job
|
||||
dateString string
|
||||
bottomBarStatus string
|
||||
}
|
||||
|
||||
type Job struct {
|
||||
// long-lived
|
||||
name string
|
||||
byteProgress *bytesProgressHistory
|
||||
|
||||
lastStatus *job.Status
|
||||
fulldescription string
|
||||
}
|
||||
|
||||
func New() *M {
|
||||
return &M{
|
||||
jobs: make(map[string]*Job),
|
||||
jobsList: make([]*Job, 0),
|
||||
selectedJob: nil,
|
||||
}
|
||||
}
|
||||
|
||||
type FilterFunc func(string) bool
|
||||
|
||||
type Params struct {
|
||||
Report map[string]*job.Status
|
||||
ReportFetchError error
|
||||
SelectedJob *Job
|
||||
FSFilter FilterFunc `validate:"required"`
|
||||
DetailViewWidth int `validate:"gte=1"`
|
||||
DetailViewWrap bool
|
||||
ShortKeybindingOverview string
|
||||
}
|
||||
|
||||
var validate = validator.New()
|
||||
|
||||
func (m *M) Update(p Params) {
|
||||
|
||||
if err := validate.Struct(p); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if p.ReportFetchError != nil {
|
||||
m.bottomBarStatus = fmt.Sprintf("[red::]status fetch: %s", p.ReportFetchError)
|
||||
} else {
|
||||
m.bottomBarStatus = p.ShortKeybindingOverview
|
||||
for jobname, st := range p.Report {
|
||||
// TODO handle job renames & deletions
|
||||
j, ok := m.jobs[jobname]
|
||||
if !ok {
|
||||
j = &Job{
|
||||
name: jobname,
|
||||
byteProgress: &bytesProgressHistory{},
|
||||
}
|
||||
m.jobs[jobname] = j
|
||||
m.jobsList = append(m.jobsList, j)
|
||||
}
|
||||
j.lastStatus = st
|
||||
}
|
||||
}
|
||||
|
||||
// filter out internal jobs
|
||||
var jobsList []*Job
|
||||
for _, j := range m.jobsList {
|
||||
if daemon.IsInternalJobName(j.name) {
|
||||
continue
|
||||
}
|
||||
jobsList = append(jobsList, j)
|
||||
}
|
||||
m.jobsList = jobsList
|
||||
|
||||
// determinism!
|
||||
sort.Slice(m.jobsList, func(i, j int) bool {
|
||||
return strings.Compare(m.jobsList[i].name, m.jobsList[j].name) < 0
|
||||
})
|
||||
|
||||
// try to not lose the selected job
|
||||
m.selectedJob = nil
|
||||
for _, j := range m.jobsList {
|
||||
j.updateFullDescription(p)
|
||||
if j == p.SelectedJob {
|
||||
m.selectedJob = j
|
||||
}
|
||||
}
|
||||
|
||||
m.dateString = time.Now().Format(time.RFC3339)
|
||||
|
||||
}
|
||||
|
||||
func (m *M) BottomBarStatus() string { return m.bottomBarStatus }
|
||||
|
||||
func (m *M) Jobs() []*Job { return m.jobsList }
|
||||
|
||||
// may be nil
|
||||
func (m *M) SelectedJob() *Job { return m.selectedJob }
|
||||
|
||||
func (m *M) DateString() string { return m.dateString }
|
||||
|
||||
func (j *Job) updateFullDescription(p Params) {
|
||||
width := p.DetailViewWidth
|
||||
if !p.DetailViewWrap {
|
||||
width = 10000000 // FIXME
|
||||
}
|
||||
b := stringbuilder.New(stringbuilder.Config{
|
||||
IndentMultiplier: 3,
|
||||
Width: width,
|
||||
})
|
||||
drawJob(b, j.name, j.lastStatus, j.byteProgress, p.FSFilter)
|
||||
j.fulldescription = b.String()
|
||||
}
|
||||
|
||||
func (j *Job) JobTreeTitle() string {
|
||||
return j.name
|
||||
}
|
||||
|
||||
func (j *Job) FullDescription() string {
|
||||
return j.fulldescription
|
||||
}
|
||||
|
||||
func (j *Job) Name() string {
|
||||
return j.name
|
||||
}
|
||||
|
||||
func drawJob(t *stringbuilder.B, name string, v *job.Status, history *bytesProgressHistory, fsfilter FilterFunc) {
|
||||
|
||||
t.Printf("Job: %s\n", name)
|
||||
t.Printf("Type: %s\n\n", v.Type)
|
||||
|
||||
if v.Type == job.TypePush || v.Type == job.TypePull {
|
||||
activeStatus, ok := v.JobSpecific.(*job.ActiveSideStatus)
|
||||
if !ok || activeStatus == nil {
|
||||
t.Printf("ActiveSideStatus is null")
|
||||
t.Newline()
|
||||
return
|
||||
}
|
||||
|
||||
t.Printf("Replication:")
|
||||
t.AddIndentAndNewline(1)
|
||||
renderReplicationReport(t, activeStatus.Replication, history, fsfilter)
|
||||
t.AddIndentAndNewline(-1)
|
||||
|
||||
t.Printf("Pruning Sender:")
|
||||
t.AddIndentAndNewline(1)
|
||||
renderPrunerReport(t, activeStatus.PruningSender, fsfilter)
|
||||
t.AddIndentAndNewline(-1)
|
||||
|
||||
t.Printf("Pruning Receiver:")
|
||||
t.AddIndentAndNewline(1)
|
||||
renderPrunerReport(t, activeStatus.PruningReceiver, fsfilter)
|
||||
t.AddIndentAndNewline(-1)
|
||||
|
||||
if v.Type == job.TypePush {
|
||||
t.Printf("Snapshotting:")
|
||||
t.AddIndentAndNewline(1)
|
||||
renderSnapperReport(t, activeStatus.Snapshotting, fsfilter)
|
||||
t.AddIndentAndNewline(-1)
|
||||
}
|
||||
|
||||
} else if v.Type == job.TypeSnap {
|
||||
snapStatus, ok := v.JobSpecific.(*job.SnapJobStatus)
|
||||
if !ok || snapStatus == nil {
|
||||
t.Printf("SnapJobStatus is null")
|
||||
t.Newline()
|
||||
return
|
||||
}
|
||||
t.Printf("Pruning snapshots:")
|
||||
t.AddIndentAndNewline(1)
|
||||
renderPrunerReport(t, snapStatus.Pruning, fsfilter)
|
||||
t.AddIndentAndNewline(-1)
|
||||
t.Printf("Snapshotting:")
|
||||
t.AddIndentAndNewline(1)
|
||||
renderSnapperReport(t, snapStatus.Snapshotting, fsfilter)
|
||||
t.AddIndentAndNewline(-1)
|
||||
} else if v.Type == job.TypeSource {
|
||||
|
||||
st := v.JobSpecific.(*job.PassiveStatus)
|
||||
t.Printf("Snapshotting:\n")
|
||||
t.AddIndent(1)
|
||||
renderSnapperReport(t, st.Snapper, fsfilter)
|
||||
t.AddIndentAndNewline(-1)
|
||||
|
||||
} else {
|
||||
t.Printf("No status representation for job type '%s', dumping as YAML", v.Type)
|
||||
t.Newline()
|
||||
asYaml, err := yaml.Marshal(v.JobSpecific)
|
||||
if err != nil {
|
||||
t.Printf("Error marshaling status to YAML: %s", err)
|
||||
t.Newline()
|
||||
return
|
||||
}
|
||||
t.Write(string(asYaml))
|
||||
t.Newline()
|
||||
}
|
||||
}
|
||||
|
||||
func printFilesystemStatus(t *stringbuilder.B, rep *report.FilesystemReport, maxFS int) {
|
||||
|
||||
expected, replicated, containsInvalidSizeEstimates := rep.BytesSum()
|
||||
sizeEstimationImpreciseNotice := ""
|
||||
if containsInvalidSizeEstimates {
|
||||
sizeEstimationImpreciseNotice = " (some steps lack size estimation)"
|
||||
}
|
||||
if rep.CurrentStep < len(rep.Steps) && rep.Steps[rep.CurrentStep].Info.BytesExpected == 0 {
|
||||
sizeEstimationImpreciseNotice = " (step lacks size estimation)"
|
||||
}
|
||||
|
||||
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",
|
||||
strings.ToUpper(string(rep.State)),
|
||||
userVisisbleCurrentStep, userVisibleTotalSteps,
|
||||
ByteCountBinaryUint(replicated), ByteCountBinaryUint(expected),
|
||||
sizeEstimationImpreciseNotice,
|
||||
)
|
||||
|
||||
activeIndicator := " "
|
||||
if rep.BlockedOn == report.FsBlockedOnNothing &&
|
||||
(rep.State == report.FilesystemPlanning || rep.State == report.FilesystemStepping) {
|
||||
activeIndicator = "*"
|
||||
}
|
||||
t.AddIndent(1)
|
||||
t.Printf("%s %s %s ",
|
||||
activeIndicator,
|
||||
stringbuilder.RightPad(rep.Info.Name, maxFS, " "),
|
||||
status)
|
||||
|
||||
next := ""
|
||||
if err := rep.Error(); err != nil {
|
||||
next = err.Err
|
||||
} else if rep.State != report.FilesystemDone {
|
||||
if nextStep := rep.NextStep(); nextStep != nil {
|
||||
if nextStep.IsIncremental() {
|
||||
next = fmt.Sprintf("next: %s => %s", nextStep.Info.From, nextStep.Info.To)
|
||||
} else {
|
||||
next = fmt.Sprintf("next: full send %s", nextStep.Info.To)
|
||||
}
|
||||
attribs := []string{}
|
||||
|
||||
if nextStep.Info.Resumed {
|
||||
attribs = append(attribs, "resumed")
|
||||
}
|
||||
|
||||
attribs = append(attribs, fmt.Sprintf("encrypted=%s", nextStep.Info.Encrypted))
|
||||
|
||||
next += fmt.Sprintf(" (%s)", strings.Join(attribs, ", "))
|
||||
} else {
|
||||
next = "" // individual FSes may still be in planning state
|
||||
}
|
||||
|
||||
}
|
||||
t.Printf("%s", next)
|
||||
|
||||
t.AddIndent(-1)
|
||||
t.Newline()
|
||||
}
|
||||
|
||||
func renderReplicationReport(t *stringbuilder.B, rep *report.Report, history *bytesProgressHistory, fsfilter FilterFunc) {
|
||||
if rep == nil {
|
||||
t.Printf("...\n")
|
||||
return
|
||||
}
|
||||
|
||||
if rep.WaitReconnectError != nil {
|
||||
t.PrintfDrawIndentedAndWrappedIfMultiline("Connectivity: %s", rep.WaitReconnectError)
|
||||
t.Newline()
|
||||
}
|
||||
if !rep.WaitReconnectSince.IsZero() {
|
||||
delta := time.Until(rep.WaitReconnectUntil).Round(time.Second)
|
||||
if rep.WaitReconnectUntil.IsZero() || delta > 0 {
|
||||
var until string
|
||||
if rep.WaitReconnectUntil.IsZero() {
|
||||
until = "waiting indefinitely"
|
||||
} else {
|
||||
until = fmt.Sprintf("hard fail in %s @ %s", delta, rep.WaitReconnectUntil)
|
||||
}
|
||||
t.PrintfDrawIndentedAndWrappedIfMultiline("Connectivity: reconnecting with exponential backoff (since %s) (%s)",
|
||||
rep.WaitReconnectSince, until)
|
||||
} else {
|
||||
t.PrintfDrawIndentedAndWrappedIfMultiline("Connectivity: reconnects reached hard-fail timeout @ %s", rep.WaitReconnectUntil)
|
||||
}
|
||||
t.Newline()
|
||||
}
|
||||
|
||||
// TODO visualize more than the latest attempt by folding all attempts into one
|
||||
if len(rep.Attempts) == 0 {
|
||||
t.Printf("no attempts made yet")
|
||||
return
|
||||
} else {
|
||||
t.Printf("Attempt #%d", len(rep.Attempts))
|
||||
if len(rep.Attempts) > 1 {
|
||||
t.Printf(". Previous attempts failed with the following statuses:")
|
||||
t.AddIndentAndNewline(1)
|
||||
for i, a := range rep.Attempts[:len(rep.Attempts)-1] {
|
||||
t.PrintfDrawIndentedAndWrappedIfMultiline("#%d: %s (failed at %s) (ran %s)\n", i+1, a.State, a.FinishAt, a.FinishAt.Sub(a.StartAt))
|
||||
}
|
||||
t.AddIndentAndNewline(-1)
|
||||
} else {
|
||||
t.Newline()
|
||||
}
|
||||
}
|
||||
|
||||
latest := rep.Attempts[len(rep.Attempts)-1]
|
||||
sort.Slice(latest.Filesystems, func(i, j int) bool {
|
||||
return latest.Filesystems[i].Info.Name < latest.Filesystems[j].Info.Name
|
||||
})
|
||||
|
||||
// apply filter
|
||||
filtered := make([]*report.FilesystemReport, 0, len(latest.Filesystems))
|
||||
for _, fs := range latest.Filesystems {
|
||||
if !fsfilter(fs.Info.Name) {
|
||||
continue
|
||||
}
|
||||
filtered = append(filtered, fs)
|
||||
}
|
||||
latest.Filesystems = filtered
|
||||
|
||||
t.Printf("Status: %s", latest.State)
|
||||
t.Newline()
|
||||
if !latest.FinishAt.IsZero() {
|
||||
t.Printf("Last Run: %s (lasted %s)\n", latest.FinishAt.Round(time.Second), latest.FinishAt.Sub(latest.StartAt).Round(time.Second))
|
||||
} else {
|
||||
t.Printf("Started: %s (lasting %s)\n", latest.StartAt.Round(time.Second), time.Since(latest.StartAt).Round(time.Second))
|
||||
}
|
||||
|
||||
if latest.State == report.AttemptPlanningError {
|
||||
t.Printf("Problem: ")
|
||||
t.PrintfDrawIndentedAndWrappedIfMultiline("%s", latest.PlanError)
|
||||
t.Newline()
|
||||
} else if latest.State == report.AttemptFanOutError {
|
||||
t.Printf("Problem: one or more of the filesystems encountered errors")
|
||||
t.Newline()
|
||||
}
|
||||
|
||||
if latest.State != report.AttemptPlanning && latest.State != report.AttemptPlanningError {
|
||||
// Draw global progress bar
|
||||
// Progress: [---------------]
|
||||
expected, replicated, containsInvalidSizeEstimates := latest.BytesSum()
|
||||
rate, changeCount := history.Update(replicated)
|
||||
eta := time.Duration(0)
|
||||
if rate > 0 {
|
||||
eta = time.Duration((float64(expected)-float64(replicated))/float64(rate)) * time.Second
|
||||
}
|
||||
|
||||
t.Write("Progress: ")
|
||||
t.DrawBar(50, replicated, expected, changeCount)
|
||||
t.Write(fmt.Sprintf(" %s / %s @ %s/s", ByteCountBinaryUint(replicated), ByteCountBinaryUint(expected), ByteCountBinary(rate)))
|
||||
if eta != 0 {
|
||||
t.Write(fmt.Sprintf(" (%s remaining)", humanizeDuration(eta)))
|
||||
}
|
||||
t.Newline()
|
||||
if containsInvalidSizeEstimates {
|
||||
t.Write("NOTE: not all steps could be size-estimated, total estimate is likely imprecise!")
|
||||
t.Newline()
|
||||
}
|
||||
|
||||
if len(latest.Filesystems) == 0 {
|
||||
t.Write("NOTE: no filesystems were considered for replication!")
|
||||
t.Newline()
|
||||
}
|
||||
|
||||
var maxFSLen int
|
||||
for _, fs := range latest.Filesystems {
|
||||
if len(fs.Info.Name) > maxFSLen {
|
||||
maxFSLen = len(fs.Info.Name)
|
||||
}
|
||||
}
|
||||
for _, fs := range latest.Filesystems {
|
||||
printFilesystemStatus(t, fs, maxFSLen)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func humanizeDuration(duration time.Duration) string {
|
||||
days := int64(duration.Hours() / 24)
|
||||
hours := int64(math.Mod(duration.Hours(), 24))
|
||||
minutes := int64(math.Mod(duration.Minutes(), 60))
|
||||
seconds := int64(math.Mod(duration.Seconds(), 60))
|
||||
|
||||
var parts []string
|
||||
|
||||
force := false
|
||||
chunks := []int64{days, hours, minutes, seconds}
|
||||
for i, chunk := range chunks {
|
||||
if force || chunk > 0 {
|
||||
padding := 0
|
||||
if force {
|
||||
padding = 2
|
||||
}
|
||||
parts = append(parts, fmt.Sprintf("%*d%c", padding, chunk, "dhms"[i]))
|
||||
force = true
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(parts, " ")
|
||||
}
|
||||
|
||||
func renderPrunerReport(t *stringbuilder.B, r *pruner.Report, fsfilter FilterFunc) {
|
||||
if r == nil {
|
||||
t.Printf("...\n")
|
||||
return
|
||||
}
|
||||
|
||||
state, err := pruner.StateString(r.State)
|
||||
if err != nil {
|
||||
t.Printf("Status: %q (parse error: %q)\n", r.State, err)
|
||||
return
|
||||
}
|
||||
|
||||
t.Printf("Status: %s", state)
|
||||
t.Newline()
|
||||
|
||||
if r.Error != "" {
|
||||
t.Printf("Error: %s\n", r.Error)
|
||||
}
|
||||
|
||||
type commonFS struct {
|
||||
*pruner.FSReport
|
||||
completed bool
|
||||
}
|
||||
all := make([]commonFS, 0, len(r.Pending)+len(r.Completed))
|
||||
for i := range r.Pending {
|
||||
all = append(all, commonFS{&r.Pending[i], false})
|
||||
}
|
||||
for i := range r.Completed {
|
||||
all = append(all, commonFS{&r.Completed[i], true})
|
||||
}
|
||||
|
||||
// filter all
|
||||
filtered := make([]commonFS, 0, len(all))
|
||||
for _, fs := range all {
|
||||
if fsfilter(fs.FSReport.Filesystem) {
|
||||
filtered = append(filtered, fs)
|
||||
}
|
||||
}
|
||||
all = filtered
|
||||
|
||||
switch state {
|
||||
case pruner.Plan:
|
||||
fallthrough
|
||||
case pruner.PlanErr:
|
||||
return
|
||||
}
|
||||
|
||||
if len(all) == 0 {
|
||||
t.Printf("nothing to do\n")
|
||||
return
|
||||
}
|
||||
|
||||
var totalDestroyCount, completedDestroyCount int
|
||||
var maxFSname int
|
||||
for _, fs := range all {
|
||||
totalDestroyCount += len(fs.DestroyList)
|
||||
if fs.completed {
|
||||
completedDestroyCount += len(fs.DestroyList)
|
||||
}
|
||||
if maxFSname < len(fs.Filesystem) {
|
||||
maxFSname = len(fs.Filesystem)
|
||||
}
|
||||
}
|
||||
|
||||
// global progress bar
|
||||
progress := int(math.Round(80 * float64(completedDestroyCount) / float64(totalDestroyCount)))
|
||||
t.Write("Progress: ")
|
||||
t.Write("[")
|
||||
t.Write(stringbuilder.Times("=", progress))
|
||||
t.Write(">")
|
||||
t.Write(stringbuilder.Times("-", 80-progress))
|
||||
t.Write("]")
|
||||
t.Printf(" %d/%d snapshots", completedDestroyCount, totalDestroyCount)
|
||||
t.Newline()
|
||||
|
||||
sort.SliceStable(all, func(i, j int) bool {
|
||||
return strings.Compare(all[i].Filesystem, all[j].Filesystem) == -1
|
||||
})
|
||||
|
||||
// Draw a table-like representation of 'all'
|
||||
for _, fs := range all {
|
||||
t.Write(stringbuilder.RightPad(fs.Filesystem, maxFSname, " "))
|
||||
t.Write(" ")
|
||||
if !fs.SkipReason.NotSkipped() {
|
||||
t.Printf("skipped: %s\n", fs.SkipReason)
|
||||
continue
|
||||
}
|
||||
if fs.LastError != "" {
|
||||
if strings.ContainsAny(fs.LastError, "\r\n") {
|
||||
t.Printf("ERROR:")
|
||||
t.PrintfDrawIndentedAndWrappedIfMultiline("%s\n", fs.LastError)
|
||||
} else {
|
||||
t.PrintfDrawIndentedAndWrappedIfMultiline("ERROR: %s\n", fs.LastError)
|
||||
}
|
||||
t.Newline()
|
||||
continue
|
||||
}
|
||||
|
||||
pruneRuleActionStr := fmt.Sprintf("(destroy %d of %d snapshots)",
|
||||
len(fs.DestroyList), len(fs.SnapshotList))
|
||||
|
||||
if fs.completed {
|
||||
t.Printf("Completed %s\n", pruneRuleActionStr)
|
||||
continue
|
||||
}
|
||||
|
||||
t.Write("Pending ") // whitespace is padding 10
|
||||
if len(fs.DestroyList) == 1 {
|
||||
t.Write(fs.DestroyList[0].Name)
|
||||
} else {
|
||||
t.Write(pruneRuleActionStr)
|
||||
}
|
||||
t.Newline()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func renderSnapperReport(t *stringbuilder.B, r *snapper.Report, fsfilter FilterFunc) {
|
||||
if r == nil {
|
||||
t.Printf("<snapshot type does not have a report>\n")
|
||||
return
|
||||
}
|
||||
|
||||
t.Printf("Status: %s", r.State)
|
||||
t.Newline()
|
||||
|
||||
if r.Error != "" {
|
||||
t.Printf("Error: %s\n", r.Error)
|
||||
}
|
||||
if !r.SleepUntil.IsZero() {
|
||||
t.Printf("Sleep until: %s\n", r.SleepUntil)
|
||||
}
|
||||
|
||||
sort.Slice(r.Progress, func(i, j int) bool {
|
||||
return strings.Compare(r.Progress[i].Path, r.Progress[j].Path) == -1
|
||||
})
|
||||
|
||||
dur := func(d time.Duration) string {
|
||||
return d.Round(100 * time.Millisecond).String()
|
||||
}
|
||||
|
||||
type row struct {
|
||||
path, state, duration, remainder, hookReport string
|
||||
}
|
||||
var widths struct {
|
||||
path, state, duration int
|
||||
}
|
||||
rows := make([]*row, 0, len(r.Progress))
|
||||
for _, fs := range r.Progress {
|
||||
if !fsfilter(fs.Path) {
|
||||
continue
|
||||
}
|
||||
r := &row{
|
||||
path: fs.Path,
|
||||
state: fs.State.String(),
|
||||
}
|
||||
if fs.HooksHadError {
|
||||
r.hookReport = fs.Hooks // FIXME render here, not in daemon
|
||||
}
|
||||
switch fs.State {
|
||||
case snapper.SnapPending:
|
||||
r.duration = "..."
|
||||
r.remainder = ""
|
||||
case snapper.SnapStarted:
|
||||
r.duration = dur(time.Since(fs.StartAt))
|
||||
r.remainder = fmt.Sprintf("snap name: %q", fs.SnapName)
|
||||
case snapper.SnapDone:
|
||||
fallthrough
|
||||
case snapper.SnapError:
|
||||
r.duration = dur(fs.DoneAt.Sub(fs.StartAt))
|
||||
r.remainder = fmt.Sprintf("snap name: %q", fs.SnapName)
|
||||
}
|
||||
rows = append(rows, r)
|
||||
if len(r.path) > widths.path {
|
||||
widths.path = len(r.path)
|
||||
}
|
||||
if len(r.state) > widths.state {
|
||||
widths.state = len(r.state)
|
||||
}
|
||||
if len(r.duration) > widths.duration {
|
||||
widths.duration = len(r.duration)
|
||||
}
|
||||
}
|
||||
|
||||
for _, r := range rows {
|
||||
path := stringbuilder.RightPad(r.path, widths.path, " ")
|
||||
state := stringbuilder.RightPad(r.state, widths.state, " ")
|
||||
duration := stringbuilder.RightPad(r.duration, widths.duration, " ")
|
||||
t.Printf("%s %s %s", path, state, duration)
|
||||
t.PrintfDrawIndentedAndWrappedIfMultiline(" %s", r.remainder)
|
||||
if r.hookReport != "" {
|
||||
t.PrintfDrawIndentedAndWrappedIfMultiline("%s", r.hookReport)
|
||||
}
|
||||
t.Newline()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package stringbuilder
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
)
|
||||
|
||||
type B struct {
|
||||
// const
|
||||
indentMultiplier int
|
||||
|
||||
// mut
|
||||
sb *strings.Builder
|
||||
indent int
|
||||
width int
|
||||
x, y int
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
IndentMultiplier int `validate:"gte=1"`
|
||||
Width int `validate:"gte=1"`
|
||||
}
|
||||
|
||||
var validate = validator.New()
|
||||
|
||||
func New(config Config) *B {
|
||||
|
||||
if err := validate.Struct(config); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return &B{sb: &strings.Builder{}, width: config.Width, indentMultiplier: config.IndentMultiplier}
|
||||
}
|
||||
|
||||
func (b *B) String() string { return b.sb.String() }
|
||||
|
||||
func (w *B) Newline() {
|
||||
w.Write("\n")
|
||||
}
|
||||
|
||||
func (w *B) PrintfDrawIndentedAndWrappedIfMultiline(format string, args ...interface{}) {
|
||||
whole := fmt.Sprintf(format, args...)
|
||||
if strings.ContainsAny(whole, "\n\r") {
|
||||
w.AddIndent(1)
|
||||
defer w.AddIndent(-1)
|
||||
}
|
||||
w.Write(whole)
|
||||
}
|
||||
|
||||
func (w *B) Printf(format string, args ...interface{}) {
|
||||
whole := fmt.Sprintf(format, args...)
|
||||
w.Write(whole)
|
||||
}
|
||||
|
||||
func (t *B) AddIndent(delta int) {
|
||||
t.indent += delta * t.indentMultiplier
|
||||
}
|
||||
|
||||
func (t *B) AddIndentAndNewline(delta int) {
|
||||
t.indent += delta * t.indentMultiplier
|
||||
t.Write("\n")
|
||||
}
|
||||
|
||||
func (w *B) Write(s string) {
|
||||
for _, c := range s {
|
||||
if c == '\n' {
|
||||
fmt.Fprint(w.sb, "\n")
|
||||
w.x = 0
|
||||
fmt.Fprint(w.sb, Times(" ", w.indent-w.x))
|
||||
w.x = w.indent
|
||||
w.y++
|
||||
continue
|
||||
}
|
||||
if w.x >= w.width {
|
||||
fmt.Fprint(w.sb, "\n")
|
||||
w.x = 0
|
||||
fmt.Fprint(w.sb, Times(" ", w.indent-w.x))
|
||||
w.x = w.indent
|
||||
}
|
||||
fmt.Fprintf(w.sb, "%c", c)
|
||||
w.x++
|
||||
}
|
||||
}
|
||||
|
||||
func Times(str string, n int) (out string) {
|
||||
for i := 0; i < n; i++ {
|
||||
out += str
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func RightPad(str string, length int, pad string) string {
|
||||
if len(str) > length {
|
||||
return str[:length]
|
||||
}
|
||||
return str + strings.Repeat(pad, length-len(str))
|
||||
}
|
||||
|
||||
// changeCount = 0 indicates stall / no progress
|
||||
func (w *B) DrawBar(length int, bytes, totalBytes uint64, changeCount int) {
|
||||
const arrowPositions = `>\|/`
|
||||
var completedLength int
|
||||
if totalBytes > 0 {
|
||||
completedLength = int(uint64(length) * bytes / totalBytes)
|
||||
if completedLength > length {
|
||||
completedLength = length
|
||||
}
|
||||
} else if totalBytes == bytes {
|
||||
completedLength = length
|
||||
}
|
||||
|
||||
w.Write("[")
|
||||
w.Write(Times("=", completedLength))
|
||||
w.Write(string(arrowPositions[changeCount%len(arrowPositions)]))
|
||||
w.Write(Times("-", length-completedLength))
|
||||
w.Write("]")
|
||||
}
|
||||
+9
-1
@@ -139,9 +139,11 @@ var testPlaceholder = &cli.Subcommand{
|
||||
func runTestPlaceholder(ctx context.Context, subcommand *cli.Subcommand, args []string) error {
|
||||
|
||||
var checkDPs []*zfs.DatasetPath
|
||||
var datasetWasExplicitArgument bool
|
||||
|
||||
// all actions first
|
||||
if testPlaceholderArgs.all {
|
||||
datasetWasExplicitArgument = false
|
||||
out, err := zfs.ZFSList(ctx, []string{"name"})
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
||||
} else {
|
||||
datasetWasExplicitArgument = true
|
||||
dp, err := zfs.NewDatasetPath(testPlaceholderArgs.ds)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -171,7 +174,12 @@ func runTestPlaceholder(ctx context.Context, subcommand *cli.Subcommand, args []
|
||||
return errors.Wrap(err, "cannot get placeholder state")
|
||||
}
|
||||
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"
|
||||
if !ph.IsPlaceholder {
|
||||
|
||||
+31
-5
@@ -13,9 +13,17 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
"github.com/zrepl/yaml-config"
|
||||
|
||||
"github.com/zrepl/zrepl/util/datasizeunit"
|
||||
zfsprop "github.com/zrepl/zrepl/zfs/property"
|
||||
)
|
||||
|
||||
type ParseFlags uint
|
||||
|
||||
const (
|
||||
ParseFlagsNone ParseFlags = 0
|
||||
ParseFlagsNoCertCheck ParseFlags = 1 << iota
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Jobs []JobEnum `yaml:"jobs"`
|
||||
Global *Global `yaml:"global,optional,fromdefaults"`
|
||||
@@ -60,6 +68,11 @@ type ActiveJob struct {
|
||||
Pruning PruningSenderReceiver `yaml:"pruning"`
|
||||
Debug JobDebugSettings `yaml:"debug,optional"`
|
||||
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 {
|
||||
@@ -85,8 +98,10 @@ type SendOptions struct {
|
||||
BackupProperties bool `yaml:"backup_properties,optional,default=false"`
|
||||
LargeBlocks bool `yaml:"large_blocks,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"`
|
||||
|
||||
BandwidthLimit *BandwidthLimit `yaml:"bandwidth_limit,optional,fromdefaults"`
|
||||
}
|
||||
|
||||
type RecvOptions struct {
|
||||
@@ -96,6 +111,17 @@ type RecvOptions struct {
|
||||
// Reencrypt bool `yaml:"reencrypt"`
|
||||
|
||||
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 {
|
||||
@@ -113,15 +139,15 @@ type ReplicationOptionsConcurrency struct {
|
||||
SizeEstimates int `yaml:"size_estimates,optional,default=4"`
|
||||
}
|
||||
|
||||
func (l *RecvOptions) SetDefault() {
|
||||
*l = RecvOptions{Properties: &PropertyRecvOptions{}}
|
||||
}
|
||||
|
||||
type PropertyRecvOptions struct {
|
||||
Inherit []zfsprop.Property `yaml:"inherit,optional"`
|
||||
Override map[zfsprop.Property]string `yaml:"override,optional"`
|
||||
}
|
||||
|
||||
type PlaceholderRecvOptions struct {
|
||||
Encryption string `yaml:"encryption,default=unspecified"`
|
||||
}
|
||||
|
||||
type PushJob struct {
|
||||
ActiveJob `yaml:",inline"`
|
||||
Snapshotting SnapshottingEnum `yaml:"snapshotting"`
|
||||
|
||||
@@ -70,9 +70,9 @@ func TestPrometheusMonitoring(t *testing.T) {
|
||||
global:
|
||||
monitoring:
|
||||
- type: prometheus
|
||||
listen: ':9091'
|
||||
listen: ':9811'
|
||||
`)
|
||||
assert.Equal(t, ":9091", conf.Global.Monitoring[0].Ret.(*PrometheusMonitoring).Listen)
|
||||
assert.Equal(t, ":9811", conf.Global.Monitoring[0].Ret.(*PrometheusMonitoring).Listen)
|
||||
}
|
||||
|
||||
func TestSyslogLoggingOutletFacility(t *testing.T) {
|
||||
|
||||
@@ -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
|
||||
@@ -5,7 +5,7 @@
|
||||
# quick start section which inlines this example.
|
||||
#
|
||||
# CUSTOMIZATIONS YOU WILL LIKELY WANT TO APPLY:
|
||||
# - adjust the name of the production pool `system` in the `filesystems` filter of jobs `snapjob` and `push_to_derive`
|
||||
# - adjust the name of the production pool `system` in the `filesystems` filter of jobs `snapjob` and `push_to_drive`
|
||||
# - adjust the name of the backup pool `backuppool` in the `backuppool_sink` job
|
||||
# - adjust the occurences of `myhostname` to the name of the system you are backing up (cannot be easily changed once you start replicating)
|
||||
# - make sure the `zrepl_` prefix is not being used by any other zfs tools you might have installed (it likely isn't)
|
||||
|
||||
@@ -9,8 +9,8 @@ jobs:
|
||||
key: /etc/zrepl/prod.key
|
||||
server_cn: "backups"
|
||||
filesystems: {
|
||||
"zroot/var/db": true,
|
||||
"zroot/usr/home<": true,
|
||||
"zroot<": true,
|
||||
"zroot/var/tmp<": false,
|
||||
"zroot/usr/home/paranoid": false
|
||||
}
|
||||
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_'
|
||||
+4
-2
@@ -8,6 +8,7 @@ import (
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
@@ -126,6 +127,7 @@ func (j *controlJob) Run(ctx context.Context) {
|
||||
Global: GlobalStatus{
|
||||
ZFSCmds: globalZFS,
|
||||
Envconst: envconstReport,
|
||||
OsEnviron: os.Environ(),
|
||||
}}
|
||||
return s, nil
|
||||
}})
|
||||
@@ -156,8 +158,8 @@ func (j *controlJob) Run(ctx context.Context) {
|
||||
server := http.Server{
|
||||
Handler: mux,
|
||||
// control socket is local, 1s timeout should be more than sufficient, even on a loaded system
|
||||
WriteTimeout: 1 * time.Second,
|
||||
ReadTimeout: 1 * time.Second,
|
||||
WriteTimeout: envconst.Duration("ZREPL_DAEMON_CONTROL_SERVER_WRITE_TIMEOUT", 1*time.Second),
|
||||
ReadTimeout: envconst.Duration("ZREPL_DAEMON_CONTROL_SERVER_READ_TIMEOUT", 1*time.Second),
|
||||
}
|
||||
|
||||
outer:
|
||||
|
||||
+2
-1
@@ -51,7 +51,7 @@ func Run(ctx context.Context, conf *config.Config) error {
|
||||
}
|
||||
outlets.Add(newPrometheusLogOutlet(), logger.Debug)
|
||||
|
||||
confJobs, err := job.JobsFromConfig(conf)
|
||||
confJobs, err := job.JobsFromConfig(conf, config.ParseFlagsNone)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "cannot build jobs from config")
|
||||
}
|
||||
@@ -162,6 +162,7 @@ type Status struct {
|
||||
type GlobalStatus struct {
|
||||
ZFSCmds *zfscmd.Report
|
||||
Envconst *envconst.Report
|
||||
OsEnviron []string
|
||||
}
|
||||
|
||||
func (s *jobs) status() map[string]*job.Status {
|
||||
|
||||
+28
-4
@@ -42,6 +42,7 @@ type ActiveSide struct {
|
||||
promPruneSecs *prometheus.HistogramVec // labels: prune_side
|
||||
promBytesReplicated *prometheus.CounterVec // labels: filesystem
|
||||
promReplicationErrors prometheus.Gauge
|
||||
promLastSuccessful prometheus.Gauge
|
||||
|
||||
tasksMtx sync.Mutex
|
||||
tasks activeSideTasks
|
||||
@@ -161,8 +162,14 @@ func modePushFromConfig(g *config.Global, in *config.PushJob, jobID endpoint.Job
|
||||
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{
|
||||
EncryptedSend: logic.TriFromBool(in.Send.Encrypted),
|
||||
ConflictResolution: conflictResolution,
|
||||
ReplicationConfig: replicationConfig,
|
||||
SizeEstimationConcurrency: in.Replication.Concurrency.SizeEstimates,
|
||||
}
|
||||
@@ -260,8 +267,14 @@ func modePullFromConfig(g *config.Global, in *config.PullJob, jobID endpoint.Job
|
||||
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{
|
||||
EncryptedSend: logic.DontCare,
|
||||
ConflictResolution: conflictResolution,
|
||||
ReplicationConfig: replicationConfig,
|
||||
SizeEstimationConcurrency: in.Replication.Concurrency.SizeEstimates,
|
||||
}
|
||||
@@ -287,7 +300,7 @@ func replicationDriverConfigFromConfig(in *config.Replication) (c driver.Config,
|
||||
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.name, err = endpoint.MakeJobID(in.Name)
|
||||
@@ -321,7 +334,6 @@ func activeSide(g *config.Global, in *config.ActiveJob, configJob interface{}) (
|
||||
Help: "number of bytes replicated from sender to receiver per filesystem",
|
||||
ConstLabels: prometheus.Labels{"zrepl_job": j.name.String()},
|
||||
}, []string{"filesystem"})
|
||||
|
||||
j.promReplicationErrors = prometheus.NewGauge(prometheus.GaugeOpts{
|
||||
Namespace: "zrepl",
|
||||
Subsystem: "replication",
|
||||
@@ -329,8 +341,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",
|
||||
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 {
|
||||
return nil, errors.Wrap(err, "cannot build client")
|
||||
}
|
||||
@@ -360,6 +379,7 @@ func (j *ActiveSide) RegisterMetrics(registerer prometheus.Registerer) {
|
||||
registerer.MustRegister(j.promPruneSecs)
|
||||
registerer.MustRegister(j.promBytesReplicated)
|
||||
registerer.MustRegister(j.promReplicationErrors)
|
||||
registerer.MustRegister(j.promLastSuccessful)
|
||||
}
|
||||
|
||||
func (j *ActiveSide) Name() string { return j.name.String() }
|
||||
@@ -494,7 +514,11 @@ func (j *ActiveSide) do(ctx context.Context) {
|
||||
repCancel() // always cancel to free up context resources
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -8,12 +8,13 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"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))
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
@@ -41,19 +42,19 @@ func JobsFromConfig(c *config.Config) ([]Job, error) {
|
||||
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) {
|
||||
return nil, errors.Wrapf(e, "cannot build job %q", name)
|
||||
}
|
||||
// FIXME prettify this
|
||||
switch v := in.Ret.(type) {
|
||||
case *config.SinkJob:
|
||||
j, err = passiveSideFromConfig(c, &v.PassiveJob, v)
|
||||
j, err = passiveSideFromConfig(c, &v.PassiveJob, v, parseFlags)
|
||||
if err != nil {
|
||||
return cannotBuildJob(err, v.Name)
|
||||
}
|
||||
case *config.SourceJob:
|
||||
j, err = passiveSideFromConfig(c, &v.PassiveJob, v)
|
||||
j, err = passiveSideFromConfig(c, &v.PassiveJob, v, parseFlags)
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
||||
case *config.PushJob:
|
||||
j, err = activeSide(c, &v.ActiveJob, v)
|
||||
j, err = activeSide(c, &v.ActiveJob, v, parseFlags)
|
||||
if err != nil {
|
||||
return cannotBuildJob(err, v.Name)
|
||||
}
|
||||
case *config.PullJob:
|
||||
j, err = activeSide(c, &v.ActiveJob, v)
|
||||
j, err = activeSide(c, &v.ActiveJob, v, parseFlags)
|
||||
if err != nil {
|
||||
return cannotBuildJob(err, v.Name)
|
||||
}
|
||||
@@ -107,3 +108,13 @@ func validateReceivingSidesDoNotOverlap(receivingRootFSs []string) error {
|
||||
}
|
||||
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")
|
||||
}
|
||||
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,
|
||||
JobID: jobID,
|
||||
|
||||
@@ -34,7 +39,15 @@ func buildSenderConfig(in SendingJobConfig, jobID endpoint.JobID) (*endpoint.Sen
|
||||
SendCompressed: sendOpts.Compressed,
|
||||
SendEmbeddedData: sendOpts.EmbeddedData,
|
||||
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 {
|
||||
@@ -53,6 +66,22 @@ func buildReceiverConfig(in ReceivingJobConfig, jobID endpoint.JobID) (rc endpoi
|
||||
}
|
||||
|
||||
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{
|
||||
JobID: jobID,
|
||||
RootWithoutClientComponent: rootFs,
|
||||
@@ -60,6 +89,10 @@ func buildReceiverConfig(in ReceivingJobConfig, jobID endpoint.JobID) (rc endpoi
|
||||
|
||||
InheritProperties: recvOpts.Properties.Inherit,
|
||||
OverrideProperties: recvOpts.Properties.Override,
|
||||
|
||||
BandwidthLimit: bwlim,
|
||||
|
||||
PlaceholderEncryption: placeholderEncryption,
|
||||
}
|
||||
if err := rc.Validate(); err != nil {
|
||||
return rc, errors.Wrap(err, "cannot build receiver config")
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/zrepl/zrepl/config"
|
||||
"github.com/zrepl/zrepl/transport/tls"
|
||||
)
|
||||
|
||||
func TestValidateReceivingSidesDoNotOverlap(t *testing.T) {
|
||||
@@ -96,7 +95,7 @@ jobs:
|
||||
conf, err := config.ParseConfigBytes([]byte(fill(c.jobName)))
|
||||
require.NoError(t, err, "not expecting yaml-config to know about job ids")
|
||||
require.NotNil(t, conf)
|
||||
jobs, err := JobsFromConfig(conf)
|
||||
jobs, err := JobsFromConfig(conf, config.ParseFlagsNone)
|
||||
|
||||
if c.valid {
|
||||
assert.NoError(t, err)
|
||||
@@ -119,6 +118,14 @@ func TestSampleConfigsAreBuiltWithoutErrors(t *testing.T) {
|
||||
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 {
|
||||
|
||||
if path.Ext(p) != ".yml" {
|
||||
@@ -126,23 +133,78 @@ func TestSampleConfigsAreBuiltWithoutErrors(t *testing.T) {
|
||||
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) {
|
||||
c, err := config.ParseConfig(p)
|
||||
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.Log(pretty.Sprint(c))
|
||||
|
||||
tls.FakeCertificateLoading(t)
|
||||
jobs, err := JobsFromConfig(c)
|
||||
jobs, err := JobsFromConfig(c, config.ParseFlagsNoCertCheck)
|
||||
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) {
|
||||
@@ -235,7 +297,7 @@ jobs:
|
||||
t.Logf("testing config:\n%s", cstr)
|
||||
c, err := config.ParseConfigBytes([]byte(cstr))
|
||||
require.NoError(t, err)
|
||||
jobs, err := JobsFromConfig(c)
|
||||
jobs, err := JobsFromConfig(c, config.ParseFlagsNone)
|
||||
if ts.expectOk != nil {
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, c)
|
||||
|
||||
@@ -91,7 +91,7 @@ func (m *modeSource) SnapperReport() *snapper.Report {
|
||||
return m.snapper.Report()
|
||||
}
|
||||
|
||||
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{}
|
||||
|
||||
@@ -110,7 +110,7 @@ func passiveSideFromConfig(g *config.Global, in *config.PassiveJob, configJob in
|
||||
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")
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
|
||||
"github.com/zrepl/zrepl/daemon/logging/trace"
|
||||
"github.com/zrepl/zrepl/util/bandwidthlimit"
|
||||
"github.com/zrepl/zrepl/util/nodefault"
|
||||
|
||||
"github.com/zrepl/zrepl/config"
|
||||
@@ -146,7 +147,7 @@ type alwaysUpToDateReplicationCursorHistory struct {
|
||||
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) {
|
||||
fsvReq := &pdu.ListFilesystemVersionsReq{
|
||||
@@ -179,8 +180,11 @@ func (j *SnapJob) doPrune(ctx context.Context) {
|
||||
sender := endpoint.NewSender(endpoint.SenderConfig{
|
||||
JobID: j.name,
|
||||
FSF: j.fsfilter,
|
||||
// FIXME encryption setting is irrelevant for SnapJob because the endpoint is only used as pruner.Target
|
||||
// FIXME the following config fields are irrelevant for SnapJob
|
||||
// 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.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
|
||||
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
|
||||
|
||||
@@ -7,7 +7,9 @@ import (
|
||||
"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{}) {
|
||||
if !debugEnabled {
|
||||
|
||||
+20
-12
@@ -19,12 +19,20 @@ import (
|
||||
"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
|
||||
type History interface {
|
||||
type Sender interface {
|
||||
ReplicationCursor(ctx context.Context, req *pdu.ReplicationCursorReq) (*pdu.ReplicationCursorRes, 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
|
||||
type Target interface {
|
||||
ListFilesystems(ctx context.Context, req *pdu.ListFilesystemReq) (*pdu.ListFilesystemRes, error)
|
||||
@@ -48,7 +56,7 @@ func GetLogger(ctx context.Context) Logger {
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
target Target
|
||||
receiver History
|
||||
sender Sender
|
||||
rules []pruning.KeepRule
|
||||
retryWait time.Duration
|
||||
considerSnapAtCursorReplicated bool
|
||||
@@ -132,12 +140,12 @@ func NewPrunerFactory(in config.PruningSenderReceiver, promPruneSecs *prometheus
|
||||
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{
|
||||
args: args{
|
||||
context.WithValue(ctx, contextKeyPruneSide, "sender"),
|
||||
target,
|
||||
receiver,
|
||||
sender,
|
||||
f.senderRules,
|
||||
f.retryWait,
|
||||
f.considerSnapAtCursorReplicated,
|
||||
@@ -148,12 +156,12 @@ func (f *PrunerFactory) BuildSenderPruner(ctx context.Context, target Target, re
|
||||
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{
|
||||
args: args{
|
||||
context.WithValue(ctx, contextKeyPruneSide, "receiver"),
|
||||
target,
|
||||
receiver,
|
||||
sender,
|
||||
f.receiverRules,
|
||||
f.retryWait,
|
||||
false, // senseless here anyways
|
||||
@@ -164,12 +172,12 @@ func (f *PrunerFactory) BuildReceiverPruner(ctx context.Context, target Target,
|
||||
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{
|
||||
args: args{
|
||||
context.WithValue(ctx, contextKeyPruneSide, "local"),
|
||||
target,
|
||||
receiver,
|
||||
history,
|
||||
f.keepRules,
|
||||
f.retryWait,
|
||||
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) {
|
||||
|
||||
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 {
|
||||
u(func(p *Pruner) {
|
||||
p.state = PlanErr
|
||||
@@ -410,7 +418,7 @@ tfss_loop:
|
||||
rcReq := &pdu.ReplicationCursorReq{
|
||||
Filesystem: tfs.Path,
|
||||
}
|
||||
rc, err := receiver.ReplicationCursor(ctx, rcReq)
|
||||
rc, err := sender.ReplicationCursor(ctx, rcReq)
|
||||
if err != nil {
|
||||
pfsPlanErrAndLog(err, "cannot get replication cursor bookmark")
|
||||
continue tfss_loop
|
||||
@@ -456,7 +464,7 @@ tfss_loop:
|
||||
})
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
+21
-13
@@ -203,16 +203,8 @@ func syncUp(a args, u updater) state {
|
||||
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)
|
||||
}
|
||||
|
||||
return waitUntilThenTransitionToPlanning(a, u, syncPoint)
|
||||
}
|
||||
|
||||
func plan(a args, u updater) state {
|
||||
@@ -370,17 +362,33 @@ func wait(a args, u updater) state {
|
||||
logFunc("enter wait-state after error")
|
||||
})
|
||||
|
||||
t := time.NewTimer(time.Until(sleepUntil))
|
||||
defer t.Stop()
|
||||
return waitUntilThenTransitionToPlanning(a, u, sleepUntil)
|
||||
}
|
||||
|
||||
func waitUntilThenTransitionToPlanning(a args, u updater, sleepUntil time.Time) state {
|
||||
|
||||
ticker := time.NewTicker(333 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
lastLog := time.Now()
|
||||
for {
|
||||
select {
|
||||
case <-t.C:
|
||||
case <-ticker.C:
|
||||
now := time.Now()
|
||||
if now.Before(sleepUntil) {
|
||||
// rate limit loggin to once per minute
|
||||
if (now.Sub(lastLog)) > envconst.Duration("ZREPL_SNAPPER_DEBUG_LOG_ISSUE_611", 1*time.Minute) {
|
||||
getLogger(a.ctx).WithField("sleep_until", sleepUntil).WithField("remaining", sleepUntil.Sub(now)).Error("DEBUG-LOG-ZREPL-ISSUE-611")
|
||||
lastLog = now
|
||||
}
|
||||
continue // continue with ticker
|
||||
}
|
||||
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) {
|
||||
|
||||
+1
-1
@@ -249,7 +249,7 @@
|
||||
"stack": true,
|
||||
"steppedLine": false,
|
||||
"targets": [{
|
||||
"expr": "zrepl_version_daemon{job='$prom_job_name'}",
|
||||
"expr": "sgn(zrepl_start_time{job='$prom_job_name'})",
|
||||
"format": "time_series",
|
||||
"interval": "",
|
||||
"intervalFactor": 1,
|
||||
|
||||
+76
-17
@@ -6,6 +6,7 @@
|
||||
.. |docs| replace:: [DOCS]
|
||||
.. |feature| replace:: [FEATURE]
|
||||
.. |mig| replace:: **[MIGRATION]**
|
||||
.. |maint| replace:: [MAINT]
|
||||
|
||||
.. _changelog:
|
||||
|
||||
@@ -15,17 +16,82 @@ Changelog
|
||||
The changelog summarizes bugfixes that are deemed relevant for users and package maintainers.
|
||||
Developers should consult the git commit log or GitHub issue tracker.
|
||||
|
||||
We use the following annotations for classifying changes:
|
||||
0.6 (Unreleased)
|
||||
----------------
|
||||
|
||||
* |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.
|
||||
* `Feature Wishlist on GitHub <https://github.com/zrepl/zrepl/discussions/547>`_
|
||||
|
||||
* |feature| Add ``ZREPL_DESTROY_MAX_BATCH_SIZE`` env var (default 0=unlimited).
|
||||
* |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``
|
||||
|
||||
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>`_.
|
||||
|
||||
|
||||
0.4.0
|
||||
-----
|
||||
|
||||
* |feature| support setting zfs send / recv flags in the config (send: ``-wLcepbS`` , recv: ``-ox`` ).
|
||||
Config docs :ref:`here <job-send-options>` and :ref:`here <job-recv-options>` .
|
||||
* |feature| parallel replication is now configurable (disabled by default, :ref:`config docs here <replication-option-concurrency>` ).
|
||||
* |feature| New ``zrepl status`` UI:
|
||||
|
||||
* Interactive job selection.
|
||||
* Interactively ``zrepl signal`` jobs.
|
||||
* Filter filesystems in the job view by name.
|
||||
* An approximation of the old UI is still included as `--mode legacy` but will be removed in a future release of zrepl.
|
||||
|
||||
* |bugfix| Actually use concurrency when listing zrepl abstractions & doing size estimation.
|
||||
These operations were accidentally made sequential in zrepl 0.3.
|
||||
* |bugfix| Job hang-up during second replication attempt.
|
||||
* |bugfix| Data races conditions in the dataconn rpc stack.
|
||||
* |maint| Update to protobuf v1.25 and grpc 1.35.
|
||||
|
||||
For users who skipped the 0.3.1 update: please make sure your pruning grid config is correct.
|
||||
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.
|
||||
|
||||
0.3.1
|
||||
-----
|
||||
@@ -45,13 +111,6 @@ Mostly a bugfix release for :ref:`zrepl 0.3 <release-0.3>`.
|
||||
* CI infrastructure rework
|
||||
* Continuous deployment of that new `stable` branch to zrepl.github.io.
|
||||
|
||||
.. NOTE::
|
||||
| zrepl is a spare-time project primarily developed by `Christian Schwarz <https://cschwarz.com>`_.
|
||||
| You can support maintenance and feature development through one of the following services:
|
||||
| |Donate via Patreon| |Donate via GitHub Sponsors| |Donate via Liberapay| |Donate via PayPal|
|
||||
| Note that PayPal processing fees are relatively high for small donations.
|
||||
| For SEPA wire transfer and **commercial support**, please `contact Christian directly <https://cschwarz.com>`_.
|
||||
|
||||
.. _release-0.3:
|
||||
|
||||
0.3
|
||||
|
||||
@@ -13,6 +13,7 @@ Configuration
|
||||
configuration/filter_syntax
|
||||
configuration/sendrecvoptions
|
||||
configuration/replication
|
||||
configuration/conflict_resolution
|
||||
configuration/snapshotting
|
||||
configuration/prune
|
||||
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``.
|
||||
@@ -13,7 +13,7 @@ Prometheus & Grafana
|
||||
--------------------
|
||||
|
||||
zrepl can expose `Prometheus metrics <https://prometheus.io/docs/instrumenting/exposition_formats/>`_ via HTTP.
|
||||
The ``listen`` attribute is a `net.Listen <https://golang.org/pkg/net/#Listen>`_ string for tcp, e.g. ``:9091`` or ``127.0.0.1:9091``.
|
||||
The ``listen`` attribute is a `net.Listen <https://golang.org/pkg/net/#Listen>`_ string for tcp, e.g. ``:9811`` or ``127.0.0.1:9811`` (port 9811 was reserved to zrepl `on the official list <https://github.com/prometheus/prometheus/wiki/Default-port-allocations/_compare/43e495dd251ee328ac0d08b58084665b5c0f7a7e...459195059b55b414193ebeb80c5ba463d2606951>`_).
|
||||
The ``listen_freebind`` attribute is :ref:`explained here <listen-freebind-explanation>`.
|
||||
The Prometheus monitoring job appears in the ``zrepl control`` job list and may be specified **at most once**.
|
||||
|
||||
@@ -30,7 +30,7 @@ The dashboard also contains some advice on which metrics are important to monito
|
||||
global:
|
||||
monitoring:
|
||||
- type: prometheus
|
||||
listen: ':9091'
|
||||
listen: ':9811'
|
||||
listen_freebind: true # optional, default false
|
||||
|
||||
|
||||
|
||||
@@ -233,22 +233,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`.
|
||||
|
||||
Limitations
|
||||
^^^^^^^^^^^
|
||||
|
||||
.. ATTENTION::
|
||||
Caveats With Complex Setups (More Than 2 Jobs or Machines)
|
||||
----------------------------------------------------------
|
||||
|
||||
Currently, zrepl does not replicate filesystem properties.
|
||||
When receiving a filesystem, it is never mounted (`-u` flag) and `mountpoint=none` is set.
|
||||
This is temporary and being worked on :issue:`24`.
|
||||
|
||||
|
||||
.. _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.
|
||||
Most users are served well with a single sender and a single receiver job.
|
||||
This section documents considerations for more complex setups.
|
||||
|
||||
.. ATTENTION::
|
||||
@@ -262,7 +251,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>`_.
|
||||
|
||||
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**.
|
||||
Otherwise, concurrently running jobs might interfere when operating on the same filesystem.
|
||||
@@ -281,18 +270,53 @@ On your setup, ensure that
|
||||
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).
|
||||
|
||||
**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.
|
||||
|
||||
* 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**:
|
||||
|
||||
|
||||
@@ -67,8 +67,9 @@ Policy ``not_replicated``
|
||||
...
|
||||
|
||||
``not_replicated`` keeps all snapshots that have not been replicated to the receiving side.
|
||||
It only makes sense to specify this rule on a sender (source or push job).
|
||||
The state required to evaluate this rule is stored in the :ref:`replication cursor bookmark <replication-cursor-and-last-received-hold>` on the sending side.
|
||||
It only makes sense to specify this rule for the ``keep_sender``.
|
||||
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:
|
||||
|
||||
@@ -106,7 +107,7 @@ The following procedure happens during pruning:
|
||||
#. 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.
|
||||
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.
|
||||
|
||||
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,
|
||||
we subject all snapshots to the grid pruning policy by settings `regex: .*`.
|
||||
Assume the following grid specification:
|
||||
|
||||
`
|
||||
grid: 1x1h(keep=all) | 2x2h | 1x3h
|
||||
regex: .*
|
||||
`
|
||||
|
||||
This grid specification produces the following constellation of buckets:
|
||||
|
||||
0h 1h 2h 3h 4h 5h 6h 7h 8h 9h
|
||||
| | | | | | | | | |
|
||||
|-Bucket1-|-----Bucket 2------|------Bucket 3-----|-----------Bucket 4----------|
|
||||
|-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
|
||||
Bucket 2: d,e,f,g,h,i,j
|
||||
Bucket 3: k,l,m,n,o,p
|
||||
Bucket 4: q,r, q,r,s,t,u,v,w,x,y,z
|
||||
None: A,B,C,D
|
||||
Bucket1: a,b,c
|
||||
Bucket2: d,e,f,g,h,i
|
||||
Bucket3: j,k,l,m,n,o,p
|
||||
Bucket4: q,r,s,t,u,v,w,x,y,z
|
||||
No bucket: A,B,C,D
|
||||
|
||||
It then applies the per-bucket pruning logic described above which resulting in the
|
||||
following list of remaining snapshots.
|
||||
For each bucket, we now prune snapshots until it only contains `keep` snapshots.
|
||||
Newer snapshots are destroyed first.
|
||||
Snapshots that do not fall into a bucket are always destroyed.
|
||||
|
||||
| a b c j p z |
|
||||
|
||||
Note that it only makes sense to grow (not shorten) the interval duration for buckets
|
||||
further in the past since each bucket acts like a low-pass filter for incoming snapshots
|
||||
and adding a less-low-pass-filter after a low-pass one has no effect.
|
||||
Result after pruning:
|
||||
|
||||
0h 1h 2h 3h 4h 5h 6h 7h 8h 9h
|
||||
| | | | | | | | | |
|
||||
|-Bucket1-|-----Bucket2-------|------Bucket3------|-----------Bucket4-----------|
|
||||
| | | | |
|
||||
| a b c | i | p | z |
|
||||
|
||||
.. _prune-keep-last-n:
|
||||
|
||||
|
||||
@@ -36,9 +36,12 @@ See the `upstream man page <https://openzfs.github.io/openzfs-docs/man/8/zfs-sen
|
||||
* - ``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``
|
||||
- ``-w``
|
||||
- Use ``encrypted`` to only allow encrypted sends.
|
||||
- Use ``encrypted`` to only allow encrypted sends. Mixed sends are not supported.
|
||||
* - ``send_properties``
|
||||
- ``-p``
|
||||
- **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``
|
||||
- ``-c``
|
||||
-
|
||||
* - ``embbeded_data``
|
||||
* - ``embedded_data``
|
||||
- ``-e``
|
||||
-
|
||||
* - ``saved``
|
||||
@@ -138,8 +141,16 @@ Recv Options
|
||||
override: {
|
||||
"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:
|
||||
|
||||
``properties``
|
||||
@@ -186,6 +197,12 @@ Mount behaviour
|
||||
* ``canmount``
|
||||
* ``overlay``
|
||||
|
||||
Note: inheriting or overriding the ``mountpoint`` property on ZVOLs fails in ``zfs recv``.
|
||||
This is an `issue in OpenZFS <https://github.com/openzfs/zfs/issues/11416>`_ .
|
||||
As a workaround, consider creating separate zrepl jobs for your ZVOL and filesystem datasets.
|
||||
Please comment at zrepl :issue:`430` if you encounter this issue and/or would like zrepl to automatically work around it.
|
||||
|
||||
|
||||
Systemd
|
||||
-------
|
||||
|
||||
@@ -206,3 +223,60 @@ and property replication is enabled, the receiver must :ref:`inherit the followi
|
||||
* ``keylocation``
|
||||
* ``keyformat``
|
||||
* ``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.
|
||||
|
||||
+4
-3
@@ -5,7 +5,7 @@
|
||||
|
||||
.. |GitHub license| image:: https://img.shields.io/github/license/zrepl/zrepl.svg
|
||||
: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/
|
||||
.. |User Docs| image:: https://img.shields.io/badge/docs-web-blue.svg
|
||||
:target: https://zrepl.github.io
|
||||
@@ -13,13 +13,14 @@
|
||||
: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
|
||||
: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
|
||||
.. |Donate via GitHub Sponsors| image:: https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&style=flat&color=yellow
|
||||
:target: https://github.com/sponsors/problame
|
||||
.. |Twitter| image:: https://img.shields.io/twitter/url/https/github.com/zrepl/zrepl.svg?style=social
|
||||
:target: https://twitter.com/intent/tweet?text=Wow:&url=https%3A%2F%2Fgithub.com%2Fzrepl%2Fzrepl
|
||||
|
||||
.. |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>`
|
||||
.. |connect-transport| replace:: :ref:`connect specification<transport>`
|
||||
|
||||
+5
-3
@@ -5,7 +5,7 @@
|
||||
|
||||
.. 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
|
||||
@@ -25,10 +25,10 @@ zrepl - ZFS replication
|
||||
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/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/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/audit 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] Embedded data send & receive
|
||||
* [x] Resume state send & receive
|
||||
* [x] Bandwidth limiting
|
||||
|
||||
* **Automatic snapshot management**
|
||||
|
||||
@@ -136,5 +137,6 @@ Table of Contents
|
||||
pr
|
||||
changelog
|
||||
GitHub Repository & Issue Tracker <https://github.com/zrepl/zrepl>
|
||||
Chat: Matrix <https://matrix.to/#/#zrepl:matrix.org>
|
||||
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>`_ .
|
||||
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:
|
||||
|
||||
::
|
||||
|
||||
apt update && apt install curl gnupg lsb-release; \
|
||||
ARCH="$(dpkg --print-architecture)"; \
|
||||
CODENAME="$(lsb_release -i -s | tr '[:upper:]' '[:lower:]') $(lsb_release -c -s | tr '[:upper:]' '[:lower:]')"; \
|
||||
echo "Using Distro and Codename: $CODENAME"; \
|
||||
(curl https://zrepl.cschwarz.com/apt/apt-key.asc | apt-key add -) && \
|
||||
(echo "deb [arch=$ARCH] https://zrepl.cschwarz.com/apt/$CODENAME main" > /etc/apt/sources.list.d/zrepl.list) && \
|
||||
apt update
|
||||
(
|
||||
set -ex
|
||||
zrepl_apt_key_url=https://zrepl.cschwarz.com/apt/apt-key.asc
|
||||
zrepl_apt_key_dst=/usr/share/keyrings/zrepl.gpg
|
||||
zrepl_apt_repo_file=/etc/apt/sources.list.d/zrepl.list
|
||||
|
||||
# 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::
|
||||
|
||||
|
||||
@@ -93,6 +93,7 @@ Enable the zrepl daemon to start automatically at boot:
|
||||
|
||||
sysrc zrepl_enable="YES"
|
||||
|
||||
Now jump to :ref:`the summary <installation-freebsd-jail-summary>` below.
|
||||
|
||||
Plugin
|
||||
######
|
||||
@@ -134,7 +135,18 @@ Now ``zrepl`` can be started.
|
||||
|
||||
service zrepl start
|
||||
|
||||
Now jump to :ref:`the summary <installation-freebsd-jail-summary>` below.
|
||||
|
||||
.. _installation-freebsd-jail-summary:
|
||||
|
||||
Summary
|
||||
-------
|
||||
|
||||
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.
|
||||
|
||||
+12
-9
@@ -1,6 +1,5 @@
|
||||
#!/bin/bash
|
||||
set -eo pipefail
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
NON_INTERACTIVE=false
|
||||
DO_CLONE=false
|
||||
@@ -13,7 +12,7 @@ while getopts "ca" arg; do
|
||||
DO_CLONE=true
|
||||
;;
|
||||
*)
|
||||
echo invalid option
|
||||
echo "invalid option '-$arg'"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
@@ -27,11 +26,6 @@ checkout_repo_msg() {
|
||||
echo "clone ${GHPAGESREPO} to ${PUBLICDIR}:"
|
||||
}
|
||||
|
||||
exit_msg() {
|
||||
echo "error, exiting..."
|
||||
}
|
||||
trap exit_msg EXIT
|
||||
|
||||
if ! type sphinx-versioning >/dev/null; then
|
||||
echo "install sphinx-versioning and come back"
|
||||
exit 1
|
||||
@@ -71,6 +65,9 @@ git reset --hard origin/master
|
||||
|
||||
echo "cleaning GitHub pages repo"
|
||||
git rm -rf .
|
||||
cat > .gitignore <<EOF
|
||||
**/.doctrees
|
||||
EOF
|
||||
|
||||
popd
|
||||
|
||||
@@ -96,7 +93,13 @@ COMMIT_MSG="sphinx-versioning render from publish.sh - $(date -u) - ${CURRENT_CO
|
||||
pushd "$PUBLICDIR"
|
||||
|
||||
echo "adding and commiting all changes in GitHub pages repo"
|
||||
git add .gitignore
|
||||
git add -A
|
||||
git commit -m "$COMMIT_MSG"
|
||||
if [ "$(git status --porcelain)" != "" ]; then
|
||||
git commit -m "$COMMIT_MSG"
|
||||
else
|
||||
echo "nothing to commit"
|
||||
fi
|
||||
echo "pushing to GitHub pages repo"
|
||||
git push origin master
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ Keep the :ref:`full config documentation <configuration_toc>` handy if a config
|
||||
|
||||
quickstart/continuous_server_backup
|
||||
quickstart/backup_to_external_disk
|
||||
quickstart/fan_out_replication
|
||||
|
||||
Use ``zrepl configcheck`` to validate your configuration.
|
||||
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).
|
||||
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
|
||||
=============
|
||||
|
||||
|
||||
@@ -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.
|
||||
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:
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
* ``zroot/var/db``
|
||||
* ``zroot/usr/home`` and all its child filesystems
|
||||
* **except** ``zroot/usr/home/paranoid`` belonging to a user doing backups themselves
|
||||
* The entire pool ``zroot``
|
||||
* except ``zroot/var/tmp`` and all child datasets of it
|
||||
* 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:
|
||||
|
||||
|
||||
@@ -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-qthelp==1.0.2
|
||||
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
|
||||
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>
|
||||
|
||||
* |supporter-gold| Prominic.NET, Inc.
|
||||
* |supporter-std| Torsten Blum
|
||||
* |supporter-gold| Cyberiada GmbH
|
||||
* |supporter-std| `Gordon Schulz <https://github.com/azmodude>`_
|
||||
* |supporter-std| `@jwittlincohen <https://github.com/jwittlincohen>`_
|
||||
|
||||
+71
-1
@@ -26,7 +26,7 @@ CLI Overview
|
||||
* - ``zrepl daemon``
|
||||
- run the daemon, required for all zrepl functionality
|
||||
* - ``zrepl status``
|
||||
- show job activity, or with ``--raw`` for JSON output
|
||||
- show job activity, or with ``--mode raw`` for JSON output
|
||||
* - ``zrepl stdinserver``
|
||||
- see :ref:`transport-ssh+stdinserver`
|
||||
* - ``zrepl signal wakeup JOB``
|
||||
@@ -78,3 +78,73 @@ Systemd Unit File
|
||||
A systemd service definition template is available in :repomasterlink:`dist/systemd`.
|
||||
Note that some of the options only work on recent versions of systemd.
|
||||
Any help & improvements are very welcome, see :issue:`145`.
|
||||
|
||||
|
||||
|
||||
============
|
||||
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.
|
||||
+159
-42
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/zrepl/zrepl/daemon/logging/trace"
|
||||
|
||||
"github.com/zrepl/zrepl/replication/logic/pdu"
|
||||
"github.com/zrepl/zrepl/util/bandwidthlimit"
|
||||
"github.com/zrepl/zrepl/util/chainedio"
|
||||
"github.com/zrepl/zrepl/util/chainlock"
|
||||
"github.com/zrepl/zrepl/util/envconst"
|
||||
@@ -34,6 +35,8 @@ type SenderConfig struct {
|
||||
SendCompressed bool
|
||||
SendEmbeddedData bool
|
||||
SendSaved bool
|
||||
|
||||
BandwidthLimit bandwidthlimit.Config
|
||||
}
|
||||
|
||||
func (c *SenderConfig) Validate() error {
|
||||
@@ -44,6 +47,9 @@ func (c *SenderConfig) Validate() error {
|
||||
if _, err := StepHoldTag(c.JobID); err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -54,16 +60,21 @@ type Sender struct {
|
||||
FSFilter zfs.DatasetFilter
|
||||
jobId JobID
|
||||
config SenderConfig
|
||||
bwLimit bandwidthlimit.Wrapper
|
||||
}
|
||||
|
||||
func NewSender(conf SenderConfig) *Sender {
|
||||
if err := conf.Validate(); err != nil {
|
||||
panic("invalid config" + err.Error())
|
||||
}
|
||||
|
||||
ratelimiter := bandwidthlimit.WrapperFromConfig(conf.BandwidthLimit)
|
||||
|
||||
return &Sender{
|
||||
FSFilter: conf.FSF,
|
||||
jobId: conf.JobID,
|
||||
config: conf,
|
||||
bwLimit: ratelimiter,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,12 +159,11 @@ func sendArgsFromPDUAndValidateExistsAndGetVersion(ctx context.Context, fs strin
|
||||
return version, nil
|
||||
}
|
||||
|
||||
func (s *Sender) Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, io.ReadCloser, error) {
|
||||
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
|
||||
func (s *Sender) sendMakeArgs(ctx context.Context, r *pdu.SendReq) (sendArgs zfs.ZFSSendArgsValidated, _ error) {
|
||||
|
||||
_, err := s.filterCheckFS(r.Filesystem)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return sendArgs, err
|
||||
}
|
||||
switch r.Encrypted {
|
||||
case pdu.Tri_DontCare:
|
||||
@@ -161,16 +171,16 @@ func (s *Sender) Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, io.Rea
|
||||
// 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")
|
||||
return sendArgs, 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")
|
||||
return sendArgs, 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)
|
||||
return sendArgs, fmt.Errorf("unknown pdu.Tri variant %q", r.Encrypted)
|
||||
}
|
||||
|
||||
sendArgsUnvalidated := zfs.ZFSSendArgsUnvalidated{
|
||||
@@ -190,30 +200,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 {
|
||||
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 {
|
||||
return nil, nil, errors.Wrap(err, "zfs send dry failed")
|
||||
}
|
||||
|
||||
// From now on, assume that sendArgs has been validated by ZFSSendDry
|
||||
// (because validation involves shelling out, it's actually a little expensive)
|
||||
|
||||
var expSize int64 = 0 // protocol says 0 means no estimate
|
||||
if si.SizeEstimate != -1 { // but si returns -1 for no size estimate
|
||||
expSize = si.SizeEstimate
|
||||
}
|
||||
res := &pdu.SendRes{
|
||||
ExpectedSize: expSize,
|
||||
UsedResumeToken: r.ResumeToken != "",
|
||||
}
|
||||
|
||||
if r.DryRun {
|
||||
return res, nil, nil
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// create holds or bookmarks of `From` and `To` to guarantee one of the following:
|
||||
@@ -301,15 +300,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)
|
||||
}()
|
||||
|
||||
sendStream, err := zfs.ZFSSend(ctx, sendArgs)
|
||||
var sendStream io.ReadCloser
|
||||
sendStream, err = zfs.ZFSSend(ctx, sendArgs)
|
||||
if err != nil {
|
||||
// it's ok to not destroy the abstractions we just created here, a new send attempt will take care of it
|
||||
return nil, nil, errors.Wrap(err, "zfs send failed")
|
||||
}
|
||||
|
||||
// apply rate limit
|
||||
sendStream = s.bwLimit.WrapReadCloser(sendStream)
|
||||
|
||||
res := &pdu.SendRes{
|
||||
ExpectedSize: 0,
|
||||
UsedResumeToken: r.ResumeToken != "",
|
||||
}
|
||||
|
||||
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) {
|
||||
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
|
||||
|
||||
@@ -439,8 +470,22 @@ type ReceiverConfig struct {
|
||||
|
||||
InheritProperties []zfsprop.Property
|
||||
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() {
|
||||
c.RootWithoutClientComponent = c.RootWithoutClientComponent.Copy()
|
||||
|
||||
@@ -475,6 +520,15 @@ func (c *ReceiverConfig) Validate() error {
|
||||
if c.RootWithoutClientComponent.Length() <= 0 {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -484,7 +538,11 @@ type Receiver struct {
|
||||
|
||||
conf ReceiverConfig // validated
|
||||
|
||||
bwLimit bandwidthlimit.Wrapper
|
||||
|
||||
recvParentCreationMtx *chainlock.L
|
||||
|
||||
Test_OverrideClientIdentityFunc func() string // for use by platformtest
|
||||
}
|
||||
|
||||
func NewReceiver(config ReceiverConfig) *Receiver {
|
||||
@@ -495,6 +553,7 @@ func NewReceiver(config ReceiverConfig) *Receiver {
|
||||
return &Receiver{
|
||||
conf: config,
|
||||
recvParentCreationMtx: chainlock.New(),
|
||||
bwLimit: bandwidthlimit.WrapperFromConfig(config.BandwidthLimit),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -521,10 +580,16 @@ func (s *Receiver) clientRootFromCtx(ctx context.Context) *zfs.DatasetPath {
|
||||
return s.conf.RootWithoutClientComponent.Copy()
|
||||
}
|
||||
|
||||
clientIdentity, ok := ctx.Value(ClientIdentityKey).(string)
|
||||
var clientIdentity string
|
||||
if s.Test_OverrideClientIdentityFunc != nil {
|
||||
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)
|
||||
if err != nil {
|
||||
@@ -564,7 +629,8 @@ func (s *Receiver) ListFilesystems(ctx context.Context, req *pdu.ListFilesystemR
|
||||
if rphs, err := zfs.ZFSGetFilesystemPlaceholderState(ctx, s.conf.RootWithoutClientComponent); err != nil {
|
||||
return nil, errors.Wrap(err, "cannot determine whether root_fs exists")
|
||||
} 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)
|
||||
@@ -668,6 +734,38 @@ func (s *Receiver) Send(ctx context.Context, req *pdu.SendReq) (*pdu.SendRes, io
|
||||
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) {
|
||||
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
|
||||
|
||||
@@ -708,15 +806,18 @@ func (s *Receiver) Receive(ctx context.Context, req *pdu.ReceiveReq, receive io.
|
||||
if v.Path.Equal(lp) {
|
||||
return false
|
||||
}
|
||||
|
||||
l := getLogger(ctx).
|
||||
WithField("placeholder_fs", v.Path.ToString()).
|
||||
WithField("receive_fs", lp.ToString())
|
||||
|
||||
ph, err := zfs.ZFSGetFilesystemPlaceholderState(ctx, v.Path)
|
||||
getLogger(ctx).
|
||||
WithField("fs", v.Path.ToString()).
|
||||
WithField("placeholder_state", fmt.Sprintf("%#v", ph)).
|
||||
l.WithField("placeholder_state", fmt.Sprintf("%#v", ph)).
|
||||
WithField("err", fmt.Sprintf("%s", err)).
|
||||
WithField("errType", fmt.Sprintf("%T", err)).
|
||||
Debug("placeholder state for filesystem")
|
||||
Debug("get placeholder state for filesystem")
|
||||
if err != nil {
|
||||
visitErr = err
|
||||
visitErr = errors.Wrapf(err, "cannot get placeholder state of %s", v.Path.ToString())
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -727,21 +828,34 @@ func (s *Receiver) Receive(ctx context.Context, req *pdu.ReceiveReq, receive io.
|
||||
} else {
|
||||
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
|
||||
}
|
||||
l := getLogger(ctx).WithField("placeholder_fs", v.Path)
|
||||
l.Debug("create placeholder filesystem")
|
||||
err := zfs.ZFSCreatePlaceholderFilesystem(ctx, v.Path, v.Parent.Path)
|
||||
|
||||
// compute the value lazily so that users who don't rely on
|
||||
// placeholders can use the default value PlaceholderCreationEncryptionPropertyUnspecified
|
||||
placeholderEncryption, err := s.receive_GetPlaceholderCreationEncryptionValue(root, v.Path)
|
||||
if err != nil {
|
||||
l.WithError(err).Error("cannot create placeholder filesystem")
|
||||
visitErr = err
|
||||
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
|
||||
}
|
||||
return true
|
||||
|
||||
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
|
||||
}
|
||||
getLogger(ctx).WithField("filesystem", v.Path.ToString()).Debug("exists")
|
||||
l.Info("created placeholder filesystem")
|
||||
return true
|
||||
} else {
|
||||
l.Debug("filesystem exists")
|
||||
return true // leave this fs as is
|
||||
}
|
||||
})
|
||||
}()
|
||||
getLogger(ctx).WithField("visitErr", visitErr).Debug("complete tree-walk")
|
||||
@@ -787,6 +901,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")
|
||||
}
|
||||
|
||||
// apply rate limit
|
||||
receive = s.bwLimit.WrapReadCloser(receive)
|
||||
|
||||
var peek bytes.Buffer
|
||||
var MaxPeek = envconst.Int64("ZREPL_ENDPOINT_RECV_PEEK_SIZE", 1<<20)
|
||||
log.WithField("max_peek_bytes", MaxPeek).Info("peeking incoming stream")
|
||||
|
||||
@@ -813,7 +813,7 @@ func listStaleFiltering(abs []Abstraction, sinceBound *CreateTXGRangeBound) *Sta
|
||||
l := by[k]
|
||||
|
||||
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
|
||||
// to consider all steps stale (...at first)
|
||||
@@ -821,11 +821,10 @@ func listStaleFiltering(abs []Abstraction, sinceBound *CreateTXGRangeBound) *Sta
|
||||
{
|
||||
sfnsc := stepFirstNotStaleCandidates[k.fsAndJobId]
|
||||
|
||||
// if there's a replication cursor, use it as a cutoff between live and stale
|
||||
// if there's none, we are in initial replication and only need to keep
|
||||
// the most recent step hold live, since that's what our initial replication strategy
|
||||
// uses (both initially and on resume)
|
||||
// (FIXME hardcoded replication strategy)
|
||||
// If there's a replication cursor, use it as a cutoff between live and stale
|
||||
// for both cursors and holds.
|
||||
// If there's no cursor, we are in initial replication and only need to keep
|
||||
// the most recent step hold live, which hold the .To of the initial send step.
|
||||
if sfnsc.cursor != nil {
|
||||
untilBound = &CreateTXGRangeBound{
|
||||
CreateTXG: (*sfnsc.cursor).GetCreateTXG(),
|
||||
@@ -858,7 +857,7 @@ func listStaleFiltering(abs []Abstraction, sinceBound *CreateTXGRangeBound) *Sta
|
||||
}
|
||||
|
||||
} 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
|
||||
|
||||
// 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
|
||||
}
|
||||
@@ -5,22 +5,22 @@ go 1.12
|
||||
require (
|
||||
github.com/fatih/color v1.7.0
|
||||
github.com/gdamore/tcell v1.2.0
|
||||
github.com/gdamore/tcell/v2 v2.2.0
|
||||
github.com/gitchander/permutation v0.0.0-20181107151852-9e56b92e9909
|
||||
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/v10 v10.4.1
|
||||
github.com/go-sql-driver/mysql v1.4.1-0.20190907122137-b2c03bcae3d4
|
||||
github.com/golang/protobuf v1.4.3
|
||||
github.com/google/uuid v1.1.2
|
||||
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/leodido/go-urn v1.2.1 // indirect
|
||||
github.com/lib/pq v1.2.0
|
||||
github.com/mattn/go-colorable v0.1.4 // indirect
|
||||
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/onsi/ginkgo v1.10.2 // indirect
|
||||
github.com/onsi/gomega v1.7.0 // indirect
|
||||
@@ -29,21 +29,22 @@ require (
|
||||
github.com/problame/go-netssh v0.0.0-20200601114649-26439f9f0dc5
|
||||
github.com/prometheus/client_golang v1.2.1
|
||||
github.com/prometheus/common v0.7.0
|
||||
github.com/sergi/go-diff v1.0.1-0.20180205163309-da645544ed44 // go1.12 thinks it needs this
|
||||
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/pflag v1.0.5
|
||||
github.com/stretchr/testify v1.6.1
|
||||
github.com/willf/bitset v1.1.10
|
||||
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
|
||||
gitlab.com/tslocum/cview v1.5.3
|
||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58
|
||||
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
|
||||
gonum.org/v1/gonum v0.7.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20210122163508-8081c04a3579 // indirect
|
||||
google.golang.org/grpc v1.35.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=
|
||||
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/OpenPeeDeeP/depguard v0.0.0-20180806142446-a69c782687b2/go.mod h1:7/4sitnI9YlQgTLLk734QlzXT8DuHVnAyztLplQjk+o=
|
||||
github.com/OpenPeeDeeP/depguard v0.0.0-20181229194401-1f388ab2d810/go.mod h1:7/4sitnI9YlQgTLLk734QlzXT8DuHVnAyztLplQjk+o=
|
||||
github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
|
||||
github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
|
||||
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-20190717042225-c3de453c63f4 h1:Hs82Z41s6SdL1CELW+XaDYmOH4hkBN4/N9og/AsOv7E=
|
||||
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 v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
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/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/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.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
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.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/fatih/color v1.6.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
|
||||
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/ftrvxmtrx/fd v0.0.0-20150925145434-c6d800382fff h1:zk1wwii7uXmI0znwU+lqg+wFL9G5+vm5I+9rv2let60=
|
||||
@@ -46,108 +33,54 @@ 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/tcell v1.2.0 h1:ikixzsxc8K8o3V2/CEmyoEW8mJZaNYQQ3NP3VIQdUe4=
|
||||
github.com/gdamore/tcell v1.2.0/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebKS4zMM=
|
||||
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.2.0 h1:vSyEgKwraXPSOkvCk7IwOSyX+Pv3V2cV9CikJMXg4U4=
|
||||
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/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.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.4.0 h1:MP4Eh7ZCb31lleYCFuwm0oe4/YGak+5l1vA2NOE80nA=
|
||||
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/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/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
|
||||
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
||||
github.com/go-playground/validator v9.31.0+incompatible h1:UA72EPEogEnq76ehGdEDp4Mit+3FDh548oRqwVgNsHA=
|
||||
github.com/go-playground/validator v9.31.0+incompatible/go.mod h1:yrEkQXlcI+PugkyDjY2bRrL/UBU4f3rvrgkN3V8JEig=
|
||||
github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
|
||||
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
|
||||
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-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.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
|
||||
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.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.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.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.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/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.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
|
||||
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/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.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.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.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w=
|
||||
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/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/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/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
||||
@@ -156,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/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/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/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
|
||||
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88 h1:uC1QfSlInpQF+M0ao65imhwqKnz3Q2z/d8PWZRMQvDM=
|
||||
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k=
|
||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||
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 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
|
||||
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/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
@@ -177,58 +103,37 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||
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/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0=
|
||||
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/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/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/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
|
||||
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/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/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/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.10 h1:CoZ3S2P7pvtP45xOtBw+/mDL2z0RKI576gSkzRRpdGg=
|
||||
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
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/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-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 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/montanaflynn/stats v0.5.0 h1:2EkzeTSqBB4V4bJwWrt5gIIrZmpJBcoIRGS2kWLgzmk=
|
||||
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/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.10.2 h1:uqH7bpe+ERSiDa34FDOF7RikN6RzXgduUF8yarlZp94=
|
||||
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/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.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
@@ -236,128 +141,74 @@ 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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
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/go.mod h1:Ba6RaFpK1+IHWs1wLZ6sCBuXkt4iAVLeOG5GinXHusM=
|
||||
github.com/prometheus/client_golang v0.0.0-20180410130117-e11c6ff8170b/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||
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_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-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/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.7.0 h1:L+1lyG48J1zAQXA3RBX/nG/B3gjlHq0zTt2tlbJLyCY=
|
||||
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.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
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/quasilyte/go-consistent v0.0.0-20190521200055-c6f3937de18c/go.mod h1:5STLWrekHfjyYwxBRVRXNOSewLJ3PWfDJd1VyTS21fI=
|
||||
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.2.1/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
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/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/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
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/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.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
|
||||
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/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/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.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
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.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
|
||||
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/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
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/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/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/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/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/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc=
|
||||
github.com/zrepl/yaml-config v0.0.0-20190928121844-af7ca3f8448f h1:3MuiGfgMHCSwKUcsuI7ODbi50j+evTB7SsoOBMNC5Fk=
|
||||
github.com/zrepl/yaml-config v0.0.0-20190928121844-af7ca3f8448f/go.mod h1:JmNwisZzOvW4GfpfLvhZ+gtyKLsIiA+WC+wNKJGJaFg=
|
||||
github.com/zrepl/yaml-config v0.0.0-20191220194647-cbb6b0cf4bdd h1:SSo67WLS+99QESvbW8Meibz7zCrxshP71U9dH5KOCXM=
|
||||
github.com/zrepl/yaml-config v0.0.0-20191220194647-cbb6b0cf4bdd/go.mod h1:JmNwisZzOvW4GfpfLvhZ+gtyKLsIiA+WC+wNKJGJaFg=
|
||||
github.com/zrepl/zrepl v0.2.0/go.mod h1:M3Zv2IGSO8iYpUjsZD6ayZ2LHy7zyMfzet9XatKOrZ8=
|
||||
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/cview v1.5.3 h1:6OTCtIUp1EkfGeLqQFRHtW8ynMJ66BhoBwuW8oZ84AQ=
|
||||
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-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
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-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
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-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-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-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-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980 h1:dfGZHvZk057jK2MCeWus/TowKpJ8y4AmooUzdBSR9GU=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
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/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
@@ -367,79 +218,48 @@ 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-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
|
||||
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-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-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-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-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-20191010194322-b09406accb47 h1:/XfQ9z7ib8eEJX2hdgFTZJ/ntt0swNk5oYBziWeTCvY=
|
||||
golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/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-20210113181707-4bcb84eeeb78/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c h1:VwygUrnw9jn88c4u8GD3rZQbqrP/tgas88tPUbBxQrk=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/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/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/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.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
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/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-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-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-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/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=
|
||||
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.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
|
||||
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-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-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-20210122163508-8081c04a3579 h1:Iwh0ba2kTgq2Q6mJiXhzrrjD7h11nEVnbMHFmp0/HsQ=
|
||||
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.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
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.35.0 h1:TwIQcH3es+MojMVojxxfQ3l3OF2KzlRxML2xZq0kRo8=
|
||||
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||
@@ -454,32 +274,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.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
|
||||
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/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
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/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/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.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
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/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-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=
|
||||
|
||||
@@ -4,12 +4,13 @@ package main
|
||||
import (
|
||||
"github.com/zrepl/zrepl/cli"
|
||||
"github.com/zrepl/zrepl/client"
|
||||
"github.com/zrepl/zrepl/client/status"
|
||||
"github.com/zrepl/zrepl/daemon"
|
||||
)
|
||||
|
||||
func init() {
|
||||
cli.AddSubcommand(daemon.DaemonCmd)
|
||||
cli.AddSubcommand(client.StatusCmd)
|
||||
cli.AddSubcommand(status.Subcommand)
|
||||
cli.AddSubcommand(client.SignalCmd)
|
||||
cli.AddSubcommand(client.StdinserverCmd)
|
||||
cli.AddSubcommand(client.ConfigcheckCmd)
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
FROM debian:latest
|
||||
|
||||
# binutils are for cross-compilation to work in bullseye
|
||||
RUN apt-get update && apt-get install -y \
|
||||
build-essential \
|
||||
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
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ func main() {
|
||||
flag.StringVar(&args.CreateArgs.Mountpoint, "mountpoint", "", "")
|
||||
flag.BoolVar(&args.StopAndKeepPoolOnFail, "failure.stop-and-keep-pool", false, "if a test case fails, stop test execution and keep pool as it was when the test failed")
|
||||
flag.StringVar(&args.Run, "run", "", "")
|
||||
flag.DurationVar(&platformtest.ZpoolExportTimeout, "zfs.zpool-export-timeout", platformtest.ZpoolExportTimeout, "")
|
||||
flag.Parse()
|
||||
|
||||
if err := HarnessRun(args); err != nil {
|
||||
|
||||
@@ -5,12 +5,17 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
|
||||
var ZpoolExportTimeout time.Duration = 500 * time.Millisecond
|
||||
|
||||
type Zpool struct {
|
||||
args ZpoolCreateArgs
|
||||
}
|
||||
@@ -93,9 +98,24 @@ func (p *Zpool) Name() string { return p.args.PoolName }
|
||||
|
||||
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)
|
||||
|
||||
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 {
|
||||
return errors.Wrapf(err, "remove pool image")
|
||||
|
||||
@@ -21,9 +21,15 @@ var Cases = []Case{BatchDestroy,
|
||||
ReplicationIncrementalCleansUpStaleAbstractionsWithoutCacheOnSecondReplication,
|
||||
ReplicationIncrementalDestroysStepHoldsIffIncrementalStepHoldsAreDisabledButStepHoldsExist,
|
||||
ReplicationIncrementalIsPossibleIfCommonSnapshotIsDestroyed,
|
||||
ReplicationInitialAll,
|
||||
ReplicationInitialFail,
|
||||
ReplicationInitialMostRecent,
|
||||
ReplicationIsResumableFullSend__both_GuaranteeResumability,
|
||||
ReplicationIsResumableFullSend__initial_GuaranteeIncrementalReplication_incremental_GuaranteeIncrementalReplication,
|
||||
ReplicationIsResumableFullSend__initial_GuaranteeResumability_incremental_GuaranteeIncrementalReplication,
|
||||
ReplicationPlaceholderEncryption__EncryptOnReceiverUseCase__WorksIfConfiguredWithInherit,
|
||||
ReplicationPlaceholderEncryption__UnspecifiedIsOkForClientIdentityPlaceholder,
|
||||
ReplicationPlaceholderEncryption__UnspecifiedLeadsToFailureAtRuntimeWhenCreatingPlaceholders,
|
||||
ReplicationPropertyReplicationWorks,
|
||||
ReplicationReceiverErrorWhileStillSending,
|
||||
ReplicationStepCompletedLostBehavior__GuaranteeIncrementalReplication,
|
||||
@@ -34,5 +40,10 @@ var Cases = []Case{BatchDestroy,
|
||||
SendArgsValidationEncryptedSendOfUnencryptedDatasetForbidden__EncryptionSupported_true,
|
||||
SendArgsValidationResumeTokenDifferentFilesystemForbidden,
|
||||
SendArgsValidationResumeTokenEncryptionMismatchForbidden,
|
||||
SendStreamCloseAfterBlockedOnPipeWrite,
|
||||
SendStreamCloseAfterEOFRead,
|
||||
SendStreamMultipleCloseAfterEOF,
|
||||
SendStreamMultipleCloseBeforeEOF,
|
||||
SendStreamNonEOFReadErrorHandling,
|
||||
UndestroyableSnapshotParsing,
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/zrepl/zrepl/daemon/filters"
|
||||
"github.com/zrepl/zrepl/platformtest"
|
||||
"github.com/zrepl/zrepl/util/limitio"
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
@@ -174,3 +175,8 @@ func datasetToStringSortedTrimPrefix(prefix *zfs.DatasetPath, paths []*zfs.Datas
|
||||
sort.Strings(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)
|
||||
require.NoError(ctx, err)
|
||||
defer sendStream.Close()
|
||||
|
||||
recvOpts := zfs.RecvOptions{
|
||||
RollbackAndForceRecv: true,
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"os"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/kr/pretty"
|
||||
@@ -20,6 +21,7 @@ import (
|
||||
"github.com/zrepl/zrepl/replication/logic"
|
||||
"github.com/zrepl/zrepl/replication/logic/pdu"
|
||||
"github.com/zrepl/zrepl/replication/report"
|
||||
"github.com/zrepl/zrepl/util/bandwidthlimit"
|
||||
"github.com/zrepl/zrepl/util/limitio"
|
||||
"github.com/zrepl/zrepl/util/nodefault"
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
@@ -42,6 +44,7 @@ type replicationInvocation struct {
|
||||
guarantee *pdu.ReplicationConfigProtection
|
||||
senderConfigHook func(*endpoint.SenderConfig)
|
||||
receiverConfigHook func(*endpoint.ReceiverConfig)
|
||||
plannerPolicyHook func(*logic.PlannerPolicy)
|
||||
}
|
||||
|
||||
func (i replicationInvocation) Do(ctx *platformtest.Context) *report.Report {
|
||||
@@ -66,6 +69,7 @@ func (i replicationInvocation) Do(ctx *platformtest.Context) *report.Report {
|
||||
FSF: i.sfilter.AsFilter(),
|
||||
Encrypt: &nodefault.Bool{B: false},
|
||||
JobID: i.sjid,
|
||||
BandwidthLimit: bandwidthlimit.NoLimitConfig(),
|
||||
}
|
||||
if i.senderConfigHook != nil {
|
||||
i.senderConfigHook(&senderConfig)
|
||||
@@ -75,6 +79,8 @@ func (i replicationInvocation) Do(ctx *platformtest.Context) *report.Report {
|
||||
JobID: i.rjid,
|
||||
AppendClientIdentity: false,
|
||||
RootWithoutClientComponent: mustDatasetPath(i.rfsRoot),
|
||||
BandwidthLimit: bandwidthlimit.NoLimitConfig(),
|
||||
PlaceholderEncryption: endpoint.PlaceholderCreationEncryptionPropertyUnspecified,
|
||||
}
|
||||
if i.receiverConfigHook != nil {
|
||||
i.receiverConfigHook(&receiverConfig)
|
||||
@@ -90,6 +96,13 @@ func (i replicationInvocation) Do(ctx *platformtest.Context) *report.Report {
|
||||
ReplicationConfig: &pdu.ReplicationConfig{
|
||||
Protection: i.guarantee,
|
||||
},
|
||||
ConflictResolution: &logic.ConflictResolution{
|
||||
InitialReplication: logic.InitialReplicationAutoResolutionMostRecent,
|
||||
},
|
||||
SizeEstimationConcurrency: 1,
|
||||
}
|
||||
if i.plannerPolicyHook != nil {
|
||||
i.plannerPolicyHook(&plannerPolicy)
|
||||
}
|
||||
|
||||
report, wait := replication.Do(
|
||||
@@ -804,13 +817,22 @@ type NeverEndingSender struct {
|
||||
*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) {
|
||||
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
|
||||
r = &pdu.SendRes{
|
||||
UsedResumeToken: false,
|
||||
ExpectedSize: 1 << 30,
|
||||
}
|
||||
if req.DryRun {
|
||||
if dry {
|
||||
return r, stream, nil
|
||||
}
|
||||
dz, err := os.Open("/dev/zero")
|
||||
@@ -889,13 +911,9 @@ func ReplicationFailingInitialParentProhibitsChildReplication(ctx *platformtest.
|
||||
fsAA := ctx.RootDataset + "/sender/aa"
|
||||
|
||||
sfilter := filters.NewDatasetMapFilter(3, true)
|
||||
mustAddToSFilter := func(fs string) {
|
||||
err := sfilter.Add(fs, "ok")
|
||||
require.NoError(ctx, err)
|
||||
}
|
||||
mustAddToSFilter(fsA)
|
||||
mustAddToSFilter(fsAChild)
|
||||
mustAddToSFilter(fsAA)
|
||||
mustAddToSFilter(ctx, sfilter, fsA)
|
||||
mustAddToSFilter(ctx, sfilter, fsAChild)
|
||||
mustAddToSFilter(ctx, sfilter, fsAA)
|
||||
rfsRoot := ctx.RootDataset + "/receiver"
|
||||
|
||||
mockRecvErr := fmt.Errorf("yifae4ohPhaquaes0hohghiep9oufie4roo7quoWooluaj2ee8")
|
||||
@@ -951,6 +969,7 @@ func ReplicationPropertyReplicationWorks(ctx *platformtest.Context) {
|
||||
+ "sender/a/child"
|
||||
+ "sender/a/child@1"
|
||||
+ "receiver"
|
||||
R zfs create -p "${ROOTDS}/receiver/${ROOTDS}/sender"
|
||||
`)
|
||||
|
||||
sjid := endpoint.MustMakeJobID("sender-job")
|
||||
@@ -960,12 +979,8 @@ func ReplicationPropertyReplicationWorks(ctx *platformtest.Context) {
|
||||
fsAChild := ctx.RootDataset + "/sender/a/child"
|
||||
|
||||
sfilter := filters.NewDatasetMapFilter(2, true)
|
||||
mustAddToSFilter := func(fs string) {
|
||||
err := sfilter.Add(fs, "ok")
|
||||
require.NoError(ctx, err)
|
||||
}
|
||||
mustAddToSFilter(fsA)
|
||||
mustAddToSFilter(fsAChild)
|
||||
mustAddToSFilter(ctx, sfilter, fsA)
|
||||
mustAddToSFilter(ctx, sfilter, fsAChild)
|
||||
rfsRoot := ctx.RootDataset + "/receiver"
|
||||
|
||||
type testPropExpectation struct {
|
||||
@@ -1054,6 +1069,9 @@ func ReplicationPropertyReplicationWorks(ctx *platformtest.Context) {
|
||||
rep := fsByName[fs]
|
||||
require.Len(ctx, rep.Steps, 1)
|
||||
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.Len(ctx, rep.Steps, 1)
|
||||
require.Equal(ctx, 1, rep.CurrentStep)
|
||||
@@ -1090,3 +1108,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")
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -57,6 +57,11 @@ func (k KeepLastN) KeepRule(snaps []Snapshot) (destroyList []Snapshot) {
|
||||
// then lexicographically descending (e.g. b, a)
|
||||
return strings.Compare(matching[i].Name(), matching[j].Name()) == 1
|
||||
})
|
||||
destroyList = append(destroyList, matching[k.n:]...)
|
||||
|
||||
n := k.n
|
||||
if n > len(matching) {
|
||||
n = len(matching)
|
||||
}
|
||||
destroyList = append(destroyList, matching[n:]...)
|
||||
return destroyList
|
||||
}
|
||||
|
||||
@@ -83,6 +83,19 @@ func TestKeepLastN(t *testing.T) {
|
||||
"b1": true,
|
||||
},
|
||||
},
|
||||
"keep_more_than_matching": {
|
||||
inputs: []Snapshot{
|
||||
stubSnap{"a1", false, o(10)},
|
||||
stubSnap{"b1", false, o(11)},
|
||||
stubSnap{"a2", false, o(12)},
|
||||
},
|
||||
rules: []KeepRule{
|
||||
MustKeepLastN(3, "a"),
|
||||
},
|
||||
expDestroy: map[string]bool{
|
||||
"b1": true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
testTable(tcs, t)
|
||||
|
||||
@@ -151,6 +151,8 @@ type fs struct {
|
||||
|
||||
l *chainlock.L
|
||||
|
||||
blockedOn report.FsBlockedOn
|
||||
|
||||
// ordering relationship that must be maintained for initial replication
|
||||
initialRepOrd struct {
|
||||
parents, children []*fs
|
||||
@@ -158,6 +160,7 @@ type fs struct {
|
||||
}
|
||||
|
||||
planning struct {
|
||||
waitingForStepQueue bool
|
||||
done bool
|
||||
err *timedError
|
||||
}
|
||||
@@ -339,6 +342,7 @@ func (a *attempt) doGlobalPlanning(ctx context.Context, prev *attempt) map[*fs]*
|
||||
fs := &fs{
|
||||
fs: pfs,
|
||||
l: a.l,
|
||||
blockedOn: report.FsBlockedOnNothing,
|
||||
}
|
||||
fs.initialRepOrd.parentDidUpdate = make(chan struct{}, 1)
|
||||
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)
|
||||
defer endTask()
|
||||
f.do(ctx, stepQueue, prevs[f])
|
||||
f.l.HoldWhile(func() {
|
||||
// every return from f means it's unblocked...
|
||||
f.blockedOn = report.FsBlockedOnNothing
|
||||
})
|
||||
}(f)
|
||||
}
|
||||
a.l.DropWhile(func() {
|
||||
@@ -486,11 +494,16 @@ func (f *fs) do(ctx context.Context, pq *stepQueue, prev *fs) {
|
||||
var psteps []Step
|
||||
var errTime time.Time
|
||||
var err error
|
||||
f.blockedOn = report.FsBlockedOnPlanningStepQueue
|
||||
f.l.DropWhile(func() {
|
||||
// TODO hacky
|
||||
// choose target time that is earlier than any snapshot, so fs planning is always prioritized
|
||||
targetDate := time.Unix(0, 0)
|
||||
defer pq.WaitReady(ctx, f, targetDate)()
|
||||
f.l.HoldWhile(func() {
|
||||
// transition before we call PlanFS
|
||||
f.blockedOn = report.FsBlockedOnNothing
|
||||
})
|
||||
psteps, err = f.fs.PlanFS(ctx) // 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
|
||||
|
||||
// wait for parents' initial replication
|
||||
f.blockedOn = report.FsBlockedOnParentInitialRepl
|
||||
var parents []string
|
||||
for _, p := range f.initialRepOrd.parents {
|
||||
parents = append(parents, p.fs.ReportInfo().Name)
|
||||
@@ -613,7 +627,6 @@ func (f *fs) do(ctx context.Context, pq *stepQueue, prev *fs) {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
f.planned.stepErr = newTimedError(ctx.Err(), time.Now())
|
||||
return
|
||||
case <-f.initialRepOrd.parentDidUpdate:
|
||||
// loop
|
||||
}
|
||||
@@ -631,7 +644,9 @@ func (f *fs) do(ctx context.Context, pq *stepQueue, prev *fs) {
|
||||
f.l.DropWhile(func() {
|
||||
// wait for parallel replication
|
||||
targetDate := s.step.TargetDate()
|
||||
f.l.HoldWhile(func() { f.blockedOn = report.FsBlockedOnReplStepQueue })
|
||||
defer pq.WaitReady(ctx, f, targetDate)()
|
||||
f.l.HoldWhile(func() { f.blockedOn = report.FsBlockedOnNothing })
|
||||
// do the step
|
||||
ctx, endSpan := trace.WithSpan(ctx, fmt.Sprintf("%#v", s.step.ReportInfo()))
|
||||
defer endSpan()
|
||||
@@ -725,6 +740,7 @@ func (f *fs) report() *report.FilesystemReport {
|
||||
r := &report.FilesystemReport{
|
||||
Info: f.fs.ReportInfo(),
|
||||
State: state,
|
||||
BlockedOn: f.blockedOn,
|
||||
PlanError: f.planning.err.IntoReportError(),
|
||||
StepError: f.planned.stepErr.IntoReportError(),
|
||||
Steps: make([]*report.StepReport, len(f.planned.steps)),
|
||||
|
||||
@@ -14,11 +14,12 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"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) {
|
||||
zreplcircleci.SkipOnCircleCI(t, "because it relies on scheduler responsiveness < 500ms")
|
||||
|
||||
ctx, end := trace.WithTaskFromStack(context.Background())
|
||||
defer end()
|
||||
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
|
||||
// times for each step are close together.
|
||||
func TestPqConcurrent(t *testing.T) {
|
||||
zreplcircleci.SkipOnCircleCI(t, "because it relies on scheduler responsiveness < 500ms")
|
||||
|
||||
ctx, end := trace.WithTaskFromStack(context.Background())
|
||||
defer end()
|
||||
|
||||
|
||||
@@ -49,6 +49,23 @@ func (c *ConflictDiverged) Error() 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 {
|
||||
lesser := func(s []*FilesystemVersion) func(i, j int) bool {
|
||||
return func(i, j int) bool {
|
||||
@@ -71,7 +88,6 @@ func SortVersionListByCreateTXGThenBookmarkLTSnapshot(fsvslice []*FilesystemVers
|
||||
return sorted
|
||||
}
|
||||
|
||||
// conflict may be a *ConflictDiverged or a *ConflictNoCommonAncestor
|
||||
func IncrementalPath(receiver, sender []*FilesystemVersion) (incPath []*FilesystemVersion, conflict error) {
|
||||
|
||||
receiver = SortVersionListByCreateTXGThenBookmarkLTSnapshot(receiver)
|
||||
@@ -98,10 +114,14 @@ findCandidate:
|
||||
|
||||
// handle failure cases
|
||||
if !mrcaCandidate.found {
|
||||
if len(sender) == 0 {
|
||||
return nil, &ConflictNoSenderSnapshots{}
|
||||
} else {
|
||||
return nil, &ConflictNoCommonAncestor{
|
||||
SortedSenderVersions: sender,
|
||||
SortedReceiverVersions: receiver,
|
||||
}
|
||||
}
|
||||
} else if mrcaCandidate.r != len(receiver)-1 {
|
||||
return nil, &ConflictDiverged{
|
||||
SortedSenderVersions: sender,
|
||||
@@ -125,7 +145,11 @@ findCandidate:
|
||||
}
|
||||
if len(incPath) == 1 {
|
||||
// nothing to do
|
||||
incPath = incPath[1:]
|
||||
return nil, &ConflictMostRecentSnapshotAlreadyPresent{
|
||||
SortedSenderVersions: sender,
|
||||
SortedReceiverVersions: receiver,
|
||||
CommonAncestor: sender[mrcaCandidate.s],
|
||||
}
|
||||
}
|
||||
return incPath, nil
|
||||
}
|
||||
|
||||
@@ -96,12 +96,37 @@ func TestIncrementalPath_SnapshotsOnly(t *testing.T) {
|
||||
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) {
|
||||
t.Logf("path: %#v", path)
|
||||
t.Logf("conflict: %#v", conflict)
|
||||
assert.Empty(t, path)
|
||||
assert.Nil(t, conflict)
|
||||
assert.Nil(t, path)
|
||||
_, ok := conflict.(*ConflictMostRecentSnapshotAlreadyPresent)
|
||||
assert.True(t, ok)
|
||||
})
|
||||
|
||||
// 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)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
+134
-152
@@ -518,8 +518,7 @@ type SendReq struct {
|
||||
// encoded in the ResumeToken. Otherwise, the Sender MUST return an error.
|
||||
ResumeToken string `protobuf:"bytes,4,opt,name=ResumeToken,proto3" json:"ResumeToken,omitempty"`
|
||||
Encrypted Tri `protobuf:"varint,5,opt,name=Encrypted,proto3,enum=Tri" json:"Encrypted,omitempty"`
|
||||
DryRun bool `protobuf:"varint,6,opt,name=DryRun,proto3" json:"DryRun,omitempty"`
|
||||
ReplicationConfig *ReplicationConfig `protobuf:"bytes,7,opt,name=ReplicationConfig,proto3" json:"ReplicationConfig,omitempty"`
|
||||
ReplicationConfig *ReplicationConfig `protobuf:"bytes,6,opt,name=ReplicationConfig,proto3" json:"ReplicationConfig,omitempty"`
|
||||
}
|
||||
|
||||
func (x *SendReq) Reset() {
|
||||
@@ -589,13 +588,6 @@ func (x *SendReq) GetEncrypted() Tri {
|
||||
return Tri_DontCare
|
||||
}
|
||||
|
||||
func (x *SendReq) GetDryRun() bool {
|
||||
if x != nil {
|
||||
return x.DryRun
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *SendReq) GetReplicationConfig() *ReplicationConfig {
|
||||
if x != nil {
|
||||
return x.ReplicationConfig
|
||||
@@ -766,12 +758,11 @@ type SendRes struct {
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
// Whether the resume token provided in the request has been used or not.
|
||||
// If the SendReq.ResumeToken == "", this field has no meaning.
|
||||
UsedResumeToken bool `protobuf:"varint,2,opt,name=UsedResumeToken,proto3" json:"UsedResumeToken,omitempty"`
|
||||
// If the SendReq.ResumeToken == "", this field MUST be false.
|
||||
UsedResumeToken bool `protobuf:"varint,1,opt,name=UsedResumeToken,proto3" json:"UsedResumeToken,omitempty"`
|
||||
// Expected stream size determined by dry run, not exact.
|
||||
// 0 indicates that for the given SendReq, no size estimate could be made.
|
||||
ExpectedSize int64 `protobuf:"varint,3,opt,name=ExpectedSize,proto3" json:"ExpectedSize,omitempty"`
|
||||
Properties []*Property `protobuf:"bytes,4,rep,name=Properties,proto3" json:"Properties,omitempty"`
|
||||
ExpectedSize uint64 `protobuf:"varint,2,opt,name=ExpectedSize,proto3" json:"ExpectedSize,omitempty"`
|
||||
}
|
||||
|
||||
func (x *SendRes) Reset() {
|
||||
@@ -813,20 +804,13 @@ func (x *SendRes) GetUsedResumeToken() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *SendRes) GetExpectedSize() int64 {
|
||||
func (x *SendRes) GetExpectedSize() uint64 {
|
||||
if x != nil {
|
||||
return x.ExpectedSize
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *SendRes) GetProperties() []*Property {
|
||||
if x != nil {
|
||||
return x.Properties
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type SendCompletedReq struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
@@ -1443,7 +1427,7 @@ var file_pdu_proto_rawDesc = []byte{
|
||||
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,
|
||||
0x10, 0x01, 0x22, 0xfd, 0x01, 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,
|
||||
@@ -1455,122 +1439,119 @@ var file_pdu_proto_rawDesc = []byte{
|
||||
0x0b, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x22, 0x0a, 0x09,
|
||||
0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0e, 0x32,
|
||||
0x04, 0x2e, 0x54, 0x72, 0x69, 0x52, 0x09, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64,
|
||||
0x12, 0x16, 0x0a, 0x06, 0x44, 0x72, 0x79, 0x52, 0x75, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08,
|
||||
0x52, 0x06, 0x44, 0x72, 0x79, 0x52, 0x75, 0x6e, 0x12, 0x40, 0x0a, 0x11, 0x52, 0x65, 0x70, 0x6c,
|
||||
0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x07, 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, 0x51, 0x0a, 0x11, 0x52, 0x65,
|
||||
0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12,
|
||||
0x3c, 0x0a, 0x0a, 0x70, 0x72, 0x6f, 0x74, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20,
|
||||
0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f,
|
||||
0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x50, 0x72, 0x6f, 0x74, 0x65, 0x63, 0x74, 0x69, 0x6f,
|
||||
0x6e, 0x52, 0x0a, 0x70, 0x72, 0x6f, 0x74, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x8f, 0x01,
|
||||
0x0a, 0x1b, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 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, 0x82, 0x01, 0x0a, 0x07, 0x53, 0x65, 0x6e, 0x64, 0x52, 0x65,
|
||||
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,
|
||||
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,
|
||||
0x03, 0x52, 0x0c, 0x45, 0x78, 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, 0x53, 0x69, 0x7a, 0x65, 0x12,
|
||||
0x29, 0x0a, 0x0a, 0x50, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x69, 0x65, 0x73, 0x18, 0x04, 0x20,
|
||||
0x03, 0x28, 0x0b, 0x32, 0x09, 0x2e, 0x50, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x79, 0x52, 0x0a,
|
||||
0x50, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x69, 0x65, 0x73, 0x22, 0x3e, 0x0a, 0x10, 0x53, 0x65,
|
||||
0x6e, 0x64, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x52, 0x65, 0x71, 0x12, 0x2a,
|
||||
0x0a, 0x0b, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x61, 0x6c, 0x52, 0x65, 0x71, 0x18, 0x02, 0x20,
|
||||
0x01, 0x28, 0x0b, 0x32, 0x08, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x52, 0x65, 0x71, 0x52, 0x0b, 0x4f,
|
||||
0x72, 0x69, 0x67, 0x69, 0x6e, 0x61, 0x6c, 0x52, 0x65, 0x71, 0x22, 0x12, 0x0a, 0x10, 0x53, 0x65,
|
||||
0x6e, 0x64, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x52, 0x65, 0x73, 0x22, 0xbe,
|
||||
0x01, 0x0a, 0x0a, 0x52, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 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, 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,
|
||||
0x12, 0x40, 0x0a, 0x11, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43,
|
||||
0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x06, 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, 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,
|
||||
0x6f, 0x6e, 0x52, 0x08, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x12, 0x14, 0x0a, 0x05,
|
||||
0x45, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x45, 0x72, 0x72,
|
||||
0x6f, 0x72, 0x22, 0x44, 0x0a, 0x13, 0x44, 0x65, 0x73, 0x74, 0x72, 0x6f, 0x79, 0x53, 0x6e, 0x61,
|
||||
0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x52, 0x65, 0x73, 0x12, 0x2d, 0x0a, 0x07, 0x52, 0x65, 0x73,
|
||||
0x75, 0x6c, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x44, 0x65, 0x73,
|
||||
0x74, 0x72, 0x6f, 0x79, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x52, 0x65, 0x73, 0x52,
|
||||
0x07, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x22, 0x36, 0x0a, 0x14, 0x52, 0x65, 0x70, 0x6c,
|
||||
0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x75, 0x72, 0x73, 0x6f, 0x72, 0x52, 0x65, 0x71,
|
||||
0x69, 0x67, 0x22, 0x51, 0x0a, 0x11, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f,
|
||||
0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x3c, 0x0a, 0x0a, 0x70, 0x72, 0x6f, 0x74, 0x65,
|
||||
0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x52, 0x65,
|
||||
0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x50,
|
||||
0x72, 0x6f, 0x74, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0a, 0x70, 0x72, 0x6f, 0x74, 0x65,
|
||||
0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x8f, 0x01, 0x0a, 0x1b, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63,
|
||||
0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 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, 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, 0x78, 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, 0x53, 0x69,
|
||||
0x7a, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0c, 0x45, 0x78, 0x70, 0x65, 0x63, 0x74,
|
||||
0x65, 0x64, 0x53, 0x69, 0x7a, 0x65, 0x22, 0x3e, 0x0a, 0x10, 0x53, 0x65, 0x6e, 0x64, 0x43, 0x6f,
|
||||
0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x52, 0x65, 0x71, 0x12, 0x2a, 0x0a, 0x0b, 0x4f, 0x72,
|
||||
0x69, 0x67, 0x69, 0x6e, 0x61, 0x6c, 0x52, 0x65, 0x71, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32,
|
||||
0x08, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x52, 0x65, 0x71, 0x52, 0x0b, 0x4f, 0x72, 0x69, 0x67, 0x69,
|
||||
0x6e, 0x61, 0x6c, 0x52, 0x65, 0x71, 0x22, 0x12, 0x0a, 0x10, 0x53, 0x65, 0x6e, 0x64, 0x43, 0x6f,
|
||||
0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x52, 0x65, 0x73, 0x22, 0xbe, 0x01, 0x0a, 0x0a, 0x52,
|
||||
0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 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, 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,
|
||||
0x22, 0x54, 0x0a, 0x14, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43,
|
||||
0x75, 0x72, 0x73, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x12, 0x14, 0x0a, 0x04, 0x47, 0x75, 0x69, 0x64,
|
||||
0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x48, 0x00, 0x52, 0x04, 0x47, 0x75, 0x69, 0x64, 0x12, 0x1c,
|
||||
0x0a, 0x08, 0x4e, 0x6f, 0x74, 0x65, 0x78, 0x69, 0x73, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08,
|
||||
0x48, 0x00, 0x52, 0x08, 0x4e, 0x6f, 0x74, 0x65, 0x78, 0x69, 0x73, 0x74, 0x42, 0x08, 0x0a, 0x06,
|
||||
0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x22, 0x23, 0x0a, 0x07, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65,
|
||||
0x71, 0x12, 0x18, 0x0a, 0x07, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01,
|
||||
0x28, 0x09, 0x52, 0x07, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x1d, 0x0a, 0x07, 0x50,
|
||||
0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x45, 0x63, 0x68, 0x6f, 0x18, 0x01,
|
||||
0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x45, 0x63, 0x68, 0x6f, 0x2a, 0x28, 0x0a, 0x03, 0x54, 0x72,
|
||||
0x69, 0x12, 0x0c, 0x0a, 0x08, 0x44, 0x6f, 0x6e, 0x74, 0x43, 0x61, 0x72, 0x65, 0x10, 0x00, 0x12,
|
||||
0x09, 0x0a, 0x05, 0x46, 0x61, 0x6c, 0x73, 0x65, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x54, 0x72,
|
||||
0x75, 0x65, 0x10, 0x02, 0x2a, 0x86, 0x01, 0x0a, 0x18, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61,
|
||||
0x74, 0x69, 0x6f, 0x6e, 0x47, 0x75, 0x61, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x65, 0x4b, 0x69, 0x6e,
|
||||
0x64, 0x12, 0x14, 0x0a, 0x10, 0x47, 0x75, 0x61, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x65, 0x49, 0x6e,
|
||||
0x76, 0x61, 0x6c, 0x69, 0x64, 0x10, 0x00, 0x12, 0x19, 0x0a, 0x15, 0x47, 0x75, 0x61, 0x72, 0x61,
|
||||
0x6e, 0x74, 0x65, 0x65, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79,
|
||||
0x10, 0x01, 0x12, 0x23, 0x0a, 0x1f, 0x47, 0x75, 0x61, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x65, 0x49,
|
||||
0x6e, 0x63, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x6c, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63,
|
||||
0x61, 0x74, 0x69, 0x6f, 0x6e, 0x10, 0x02, 0x12, 0x14, 0x0a, 0x10, 0x47, 0x75, 0x61, 0x72, 0x61,
|
||||
0x6e, 0x74, 0x65, 0x65, 0x4e, 0x6f, 0x74, 0x68, 0x69, 0x6e, 0x67, 0x10, 0x03, 0x32, 0xf0, 0x02,
|
||||
0x0a, 0x0b, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1a, 0x0a,
|
||||
0x04, 0x50, 0x69, 0x6e, 0x67, 0x12, 0x08, 0x2e, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x1a,
|
||||
0x08, 0x2e, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x12, 0x39, 0x0a, 0x0f, 0x4c, 0x69, 0x73,
|
||||
0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x73, 0x12, 0x12, 0x2e, 0x4c,
|
||||
0x69, 0x73, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x52, 0x65, 0x71,
|
||||
0x1a, 0x12, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65,
|
||||
0x6d, 0x52, 0x65, 0x73, 0x12, 0x50, 0x0a, 0x16, 0x4c, 0x69, 0x73, 0x74, 0x46, 0x69, 0x6c, 0x65,
|
||||
0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 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, 0x71, 0x1a, 0x1a, 0x2e, 0x4c, 0x69, 0x73,
|
||||
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, 0x6f, 0x6e, 0x52, 0x08,
|
||||
0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x45, 0x72, 0x72, 0x6f,
|
||||
0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x44,
|
||||
0x0a, 0x13, 0x44, 0x65, 0x73, 0x74, 0x72, 0x6f, 0x79, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f,
|
||||
0x74, 0x73, 0x52, 0x65, 0x73, 0x12, 0x2d, 0x0a, 0x07, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73,
|
||||
0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x44, 0x65, 0x73, 0x74, 0x72, 0x6f, 0x79,
|
||||
0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x52, 0x65, 0x73, 0x52, 0x07, 0x52, 0x65, 0x73,
|
||||
0x75, 0x6c, 0x74, 0x73, 0x22, 0x36, 0x0a, 0x14, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74,
|
||||
0x69, 0x6f, 0x6e, 0x43, 0x75, 0x72, 0x73, 0x6f, 0x72, 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, 0x54, 0x0a, 0x14,
|
||||
0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x75, 0x72, 0x73, 0x6f,
|
||||
0x72, 0x52, 0x65, 0x73, 0x12, 0x14, 0x0a, 0x04, 0x47, 0x75, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01,
|
||||
0x28, 0x04, 0x48, 0x00, 0x52, 0x04, 0x47, 0x75, 0x69, 0x64, 0x12, 0x1c, 0x0a, 0x08, 0x4e, 0x6f,
|
||||
0x74, 0x65, 0x78, 0x69, 0x73, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x08,
|
||||
0x4e, 0x6f, 0x74, 0x65, 0x78, 0x69, 0x73, 0x74, 0x42, 0x08, 0x0a, 0x06, 0x52, 0x65, 0x73, 0x75,
|
||||
0x6c, 0x74, 0x22, 0x23, 0x0a, 0x07, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x12, 0x18, 0x0a,
|
||||
0x07, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07,
|
||||
0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x1d, 0x0a, 0x07, 0x50, 0x69, 0x6e, 0x67, 0x52,
|
||||
0x65, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x45, 0x63, 0x68, 0x6f, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
|
||||
0x52, 0x04, 0x45, 0x63, 0x68, 0x6f, 0x2a, 0x28, 0x0a, 0x03, 0x54, 0x72, 0x69, 0x12, 0x0c, 0x0a,
|
||||
0x08, 0x44, 0x6f, 0x6e, 0x74, 0x43, 0x61, 0x72, 0x65, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x46,
|
||||
0x61, 0x6c, 0x73, 0x65, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x54, 0x72, 0x75, 0x65, 0x10, 0x02,
|
||||
0x2a, 0x86, 0x01, 0x0a, 0x18, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e,
|
||||
0x47, 0x75, 0x61, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x65, 0x4b, 0x69, 0x6e, 0x64, 0x12, 0x14, 0x0a,
|
||||
0x10, 0x47, 0x75, 0x61, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x65, 0x49, 0x6e, 0x76, 0x61, 0x6c, 0x69,
|
||||
0x64, 0x10, 0x00, 0x12, 0x19, 0x0a, 0x15, 0x47, 0x75, 0x61, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x65,
|
||||
0x52, 0x65, 0x73, 0x75, 0x6d, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x10, 0x01, 0x12, 0x23,
|
||||
0x0a, 0x1f, 0x47, 0x75, 0x61, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x65, 0x49, 0x6e, 0x63, 0x72, 0x65,
|
||||
0x6d, 0x65, 0x6e, 0x74, 0x61, 0x6c, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f,
|
||||
0x6e, 0x10, 0x02, 0x12, 0x14, 0x0a, 0x10, 0x47, 0x75, 0x61, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x65,
|
||||
0x4e, 0x6f, 0x74, 0x68, 0x69, 0x6e, 0x67, 0x10, 0x03, 0x32, 0x8f, 0x03, 0x0a, 0x0b, 0x52, 0x65,
|
||||
0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1a, 0x0a, 0x04, 0x50, 0x69, 0x6e,
|
||||
0x67, 0x12, 0x08, 0x2e, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x1a, 0x08, 0x2e, 0x50, 0x69,
|
||||
0x6e, 0x67, 0x52, 0x65, 0x73, 0x12, 0x39, 0x0a, 0x0f, 0x4c, 0x69, 0x73, 0x74, 0x46, 0x69, 0x6c,
|
||||
0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x73, 0x12, 0x12, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x46,
|
||||
0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x52, 0x65, 0x71, 0x1a, 0x12, 0x2e, 0x4c,
|
||||
0x69, 0x73, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x52, 0x65, 0x73,
|
||||
0x12, 0x50, 0x0a, 0x16, 0x4c, 0x69, 0x73, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74,
|
||||
0x65, 0x6d, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 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, 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,
|
||||
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 (
|
||||
@@ -1625,30 +1606,31 @@ var file_pdu_proto_depIdxs = []int32{
|
||||
11, // 7: ReplicationConfig.protection:type_name -> ReplicationConfigProtection
|
||||
1, // 8: ReplicationConfigProtection.Initial:type_name -> ReplicationGuaranteeKind
|
||||
1, // 9: ReplicationConfigProtection.Incremental:type_name -> ReplicationGuaranteeKind
|
||||
12, // 10: SendRes.Properties:type_name -> Property
|
||||
9, // 11: SendCompletedReq.OriginalReq:type_name -> SendReq
|
||||
8, // 12: ReceiveReq.To:type_name -> FilesystemVersion
|
||||
10, // 13: ReceiveReq.ReplicationConfig:type_name -> ReplicationConfig
|
||||
8, // 14: DestroySnapshotsReq.Snapshots:type_name -> FilesystemVersion
|
||||
8, // 15: DestroySnapshotRes.Snapshot:type_name -> FilesystemVersion
|
||||
19, // 16: DestroySnapshotsRes.Results:type_name -> DestroySnapshotRes
|
||||
23, // 17: Replication.Ping:input_type -> PingReq
|
||||
3, // 18: Replication.ListFilesystems:input_type -> ListFilesystemReq
|
||||
6, // 19: Replication.ListFilesystemVersions:input_type -> ListFilesystemVersionsReq
|
||||
18, // 20: Replication.DestroySnapshots:input_type -> DestroySnapshotsReq
|
||||
21, // 21: Replication.ReplicationCursor:input_type -> ReplicationCursorReq
|
||||
9, // 10: SendCompletedReq.OriginalReq:type_name -> SendReq
|
||||
8, // 11: ReceiveReq.To:type_name -> FilesystemVersion
|
||||
10, // 12: ReceiveReq.ReplicationConfig:type_name -> ReplicationConfig
|
||||
8, // 13: DestroySnapshotsReq.Snapshots:type_name -> FilesystemVersion
|
||||
8, // 14: DestroySnapshotRes.Snapshot:type_name -> FilesystemVersion
|
||||
19, // 15: DestroySnapshotsRes.Results:type_name -> DestroySnapshotRes
|
||||
23, // 16: Replication.Ping:input_type -> PingReq
|
||||
3, // 17: Replication.ListFilesystems:input_type -> ListFilesystemReq
|
||||
6, // 18: Replication.ListFilesystemVersions:input_type -> ListFilesystemVersionsReq
|
||||
18, // 19: Replication.DestroySnapshots:input_type -> DestroySnapshotsReq
|
||||
21, // 20: Replication.ReplicationCursor:input_type -> ReplicationCursorReq
|
||||
9, // 21: Replication.SendDry:input_type -> SendReq
|
||||
14, // 22: Replication.SendCompleted:input_type -> SendCompletedReq
|
||||
24, // 23: Replication.Ping:output_type -> PingRes
|
||||
4, // 24: Replication.ListFilesystems:output_type -> ListFilesystemRes
|
||||
7, // 25: Replication.ListFilesystemVersions:output_type -> ListFilesystemVersionsRes
|
||||
20, // 26: Replication.DestroySnapshots:output_type -> DestroySnapshotsRes
|
||||
22, // 27: Replication.ReplicationCursor:output_type -> ReplicationCursorRes
|
||||
15, // 28: Replication.SendCompleted:output_type -> SendCompletedRes
|
||||
23, // [23:29] is the sub-list for method output_type
|
||||
17, // [17:23] is the sub-list for method input_type
|
||||
17, // [17:17] is the sub-list for extension type_name
|
||||
17, // [17:17] is the sub-list for extension extendee
|
||||
0, // [0:17] is the sub-list for field type_name
|
||||
13, // 28: Replication.SendDry:output_type -> SendRes
|
||||
15, // 29: Replication.SendCompleted:output_type -> SendCompletedRes
|
||||
23, // [23:30] is the sub-list for method output_type
|
||||
16, // [16:23] is the sub-list for method input_type
|
||||
16, // [16:16] is the sub-list for extension type_name
|
||||
16, // [16:16] is the sub-list for extension extendee
|
||||
0, // [0:16] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_pdu_proto_init() }
|
||||
|
||||
@@ -8,6 +8,7 @@ service Replication {
|
||||
returns (ListFilesystemVersionsRes);
|
||||
rpc DestroySnapshots(DestroySnapshotsReq) returns (DestroySnapshotsRes);
|
||||
rpc ReplicationCursor(ReplicationCursorReq) returns (ReplicationCursorRes);
|
||||
rpc SendDry(SendReq) returns (SendRes);
|
||||
rpc SendCompleted(SendCompletedReq) returns (SendCompletedRes);
|
||||
// for Send and Recv, see package rpc
|
||||
}
|
||||
@@ -60,9 +61,7 @@ message SendReq {
|
||||
string ResumeToken = 4;
|
||||
Tri Encrypted = 5;
|
||||
|
||||
bool DryRun = 6;
|
||||
|
||||
ReplicationConfig ReplicationConfig = 7;
|
||||
ReplicationConfig ReplicationConfig = 6;
|
||||
}
|
||||
|
||||
message ReplicationConfig {
|
||||
@@ -89,14 +88,12 @@ message Property {
|
||||
|
||||
message SendRes {
|
||||
// Whether the resume token provided in the request has been used or not.
|
||||
// If the SendReq.ResumeToken == "", this field has no meaning.
|
||||
bool UsedResumeToken = 2;
|
||||
// If the SendReq.ResumeToken == "", this field MUST be false.
|
||||
bool UsedResumeToken = 1;
|
||||
|
||||
// Expected stream size determined by dry run, not exact.
|
||||
// 0 indicates that for the given SendReq, no size estimate could be made.
|
||||
int64 ExpectedSize = 3;
|
||||
|
||||
repeated Property Properties = 4;
|
||||
uint64 ExpectedSize = 2;
|
||||
}
|
||||
|
||||
message SendCompletedReq {
|
||||
|
||||
@@ -23,6 +23,7 @@ type ReplicationClient interface {
|
||||
ListFilesystemVersions(ctx context.Context, in *ListFilesystemVersionsReq, opts ...grpc.CallOption) (*ListFilesystemVersionsRes, error)
|
||||
DestroySnapshots(ctx context.Context, in *DestroySnapshotsReq, opts ...grpc.CallOption) (*DestroySnapshotsRes, error)
|
||||
ReplicationCursor(ctx context.Context, in *ReplicationCursorReq, opts ...grpc.CallOption) (*ReplicationCursorRes, error)
|
||||
SendDry(ctx context.Context, in *SendReq, opts ...grpc.CallOption) (*SendRes, 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
|
||||
}
|
||||
|
||||
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) {
|
||||
out := new(SendCompletedRes)
|
||||
err := c.cc.Invoke(ctx, "/Replication/SendCompleted", in, out, opts...)
|
||||
@@ -97,6 +107,7 @@ type ReplicationServer interface {
|
||||
ListFilesystemVersions(context.Context, *ListFilesystemVersionsReq) (*ListFilesystemVersionsRes, error)
|
||||
DestroySnapshots(context.Context, *DestroySnapshotsReq) (*DestroySnapshotsRes, error)
|
||||
ReplicationCursor(context.Context, *ReplicationCursorReq) (*ReplicationCursorRes, error)
|
||||
SendDry(context.Context, *SendReq) (*SendRes, error)
|
||||
SendCompleted(context.Context, *SendCompletedReq) (*SendCompletedRes, error)
|
||||
mustEmbedUnimplementedReplicationServer()
|
||||
}
|
||||
@@ -120,6 +131,9 @@ func (UnimplementedReplicationServer) DestroySnapshots(context.Context, *Destroy
|
||||
func (UnimplementedReplicationServer) ReplicationCursor(context.Context, *ReplicationCursorReq) (*ReplicationCursorRes, error) {
|
||||
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) {
|
||||
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)
|
||||
}
|
||||
|
||||
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) {
|
||||
in := new(SendCompletedReq)
|
||||
if err := dec(in); err != nil {
|
||||
@@ -271,6 +303,10 @@ var Replication_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "ReplicationCursor",
|
||||
Handler: _Replication_ReplicationCursor_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "SendDry",
|
||||
Handler: _Replication_SendDry_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "SendCompleted",
|
||||
Handler: _Replication_SendCompleted_Handler,
|
||||
|
||||
@@ -41,6 +41,7 @@ type Sender interface {
|
||||
// any next call to the parent github.com/zrepl/zrepl/replication.Endpoint.
|
||||
// If the send request is for dry run the io.ReadCloser will be nil
|
||||
Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, io.ReadCloser, error)
|
||||
SendDry(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, error)
|
||||
SendCompleted(ctx context.Context, r *pdu.SendCompletedReq) (*pdu.SendCompletedRes, error)
|
||||
ReplicationCursor(ctx context.Context, req *pdu.ReplicationCursorReq) (*pdu.ReplicationCursorRes, error)
|
||||
}
|
||||
@@ -158,7 +159,7 @@ type Step struct {
|
||||
encrypt tri
|
||||
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
|
||||
// => concurrent read of that pointer from Step.ReportInfo must be protected
|
||||
@@ -189,7 +190,7 @@ func (s *Step) Step(ctx context.Context) error {
|
||||
func (s *Step) ReportInfo() *report.StepInfo {
|
||||
|
||||
// get current byteCounter value
|
||||
var byteCounter int64
|
||||
var byteCounter uint64
|
||||
s.byteCounterMtx.Lock()
|
||||
if s.byteCounter != nil {
|
||||
byteCounter = s.byteCounter.Count()
|
||||
@@ -234,11 +235,25 @@ func NewPlanner(secsPerState *prometheus.HistogramVec, bytesReplicated *promethe
|
||||
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 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 {
|
||||
return nil, fmt.Errorf("no snapshots available on sender side")
|
||||
}
|
||||
|
||||
switch policy.InitialReplication {
|
||||
|
||||
case InitialReplicationAutoResolutionMostRecent:
|
||||
|
||||
var mostRecentSnap *pdu.FilesystemVersion
|
||||
for n := len(noCommonAncestor.SortedSenderVersions) - 1; n >= 0; n-- {
|
||||
if noCommonAncestor.SortedSenderVersions[n].Type == pdu.FilesystemVersion_Snapshot {
|
||||
@@ -246,13 +261,29 @@ func resolveConflict(conflict error) (path []*pdu.FilesystemVersion, msg string)
|
||||
break
|
||||
}
|
||||
}
|
||||
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 []*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 nil, "no automated way to handle conflict type"
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, conflict
|
||||
}
|
||||
|
||||
func (p *Planner) doPlanning(ctx context.Context) ([]*Filesystem, error) {
|
||||
@@ -455,39 +486,31 @@ func (fs *Filesystem) doPlanning(ctx context.Context) ([]*Step, error) {
|
||||
} else { // resumeToken == nil
|
||||
path, conflict := IncrementalPath(rfsvs, sfsvs)
|
||||
if conflict != nil {
|
||||
var msg string
|
||||
path, msg = resolveConflict(conflict) // no shadowing allowed!
|
||||
if path != nil {
|
||||
log(ctx).WithField("conflict", conflict).Info("conflict")
|
||||
log(ctx).WithField("resolution", msg).Info("automatically resolved")
|
||||
updPath, updConflict := tryAutoresolveConflict(conflict, *fs.policy.ConflictResolution)
|
||||
if updConflict == nil {
|
||||
log(ctx).WithField("conflict", conflict).Info("conflict automatically resolved")
|
||||
|
||||
} else {
|
||||
log(ctx).WithField("conflict", conflict).Error("conflict")
|
||||
log(ctx).WithField("problem", msg).Error("cannot resolve conflict")
|
||||
log(ctx).WithField("conflict", conflict).Error("cannot resolve conflict")
|
||||
}
|
||||
path, conflict = updPath, updConflict
|
||||
}
|
||||
if len(path) == 0 {
|
||||
if conflict != nil {
|
||||
return nil, conflict
|
||||
}
|
||||
|
||||
steps = make([]*Step, 0, len(path)) // shadow
|
||||
if len(path) == 1 {
|
||||
steps = append(steps, &Step{
|
||||
parent: fs,
|
||||
sender: fs.sender,
|
||||
receiver: fs.receiver,
|
||||
|
||||
from: nil,
|
||||
to: path[0],
|
||||
encrypt: fs.policy.EncryptedSend,
|
||||
})
|
||||
if len(path) == 0 {
|
||||
steps = nil
|
||||
} else if len(path) == 1 {
|
||||
panic(fmt.Sprintf("len(path) must be two for incremental repl, and initial repl must start with nil, got path[0]=%#v", path[0]))
|
||||
} else {
|
||||
steps = make([]*Step, 0, len(path)) // shadow
|
||||
for i := 0; i < len(path)-1; i++ {
|
||||
steps = append(steps, &Step{
|
||||
parent: fs,
|
||||
sender: fs.sender,
|
||||
receiver: fs.receiver,
|
||||
|
||||
from: path[i],
|
||||
from: path[i], // nil in case of initial repl
|
||||
to: path[i+1],
|
||||
encrypt: fs.policy.EncryptedSend,
|
||||
})
|
||||
@@ -546,10 +569,10 @@ func (s *Step) updateSizeEstimate(ctx context.Context) error {
|
||||
|
||||
log := getLogger(ctx)
|
||||
|
||||
sr := s.buildSendRequest(true)
|
||||
sr := s.buildSendRequest()
|
||||
|
||||
log.Debug("initiate dry run send request")
|
||||
sres, _, err := s.sender.Send(ctx, sr)
|
||||
sres, err := s.sender.SendDry(ctx, sr)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("dry run send request failed")
|
||||
return err
|
||||
@@ -563,7 +586,7 @@ func (s *Step) updateSizeEstimate(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Step) buildSendRequest(dryRun bool) (sr *pdu.SendReq) {
|
||||
func (s *Step) buildSendRequest() (sr *pdu.SendReq) {
|
||||
fs := s.parent.Path
|
||||
sr = &pdu.SendReq{
|
||||
Filesystem: fs,
|
||||
@@ -571,7 +594,6 @@ func (s *Step) buildSendRequest(dryRun bool) (sr *pdu.SendReq) {
|
||||
To: s.to,
|
||||
Encrypted: s.encrypt.ToPDU(),
|
||||
ResumeToken: s.resumeToken,
|
||||
DryRun: dryRun,
|
||||
ReplicationConfig: s.parent.policy.ReplicationConfig,
|
||||
}
|
||||
return sr
|
||||
@@ -582,7 +604,7 @@ func (s *Step) doReplication(ctx context.Context) error {
|
||||
fs := s.parent.Path
|
||||
|
||||
log := getLogger(ctx).WithField("filesystem", fs)
|
||||
sr := s.buildSendRequest(false)
|
||||
sr := s.buildSendRequest()
|
||||
|
||||
log.Debug("initiate send request")
|
||||
sres, stream, err := s.sender.Send(ctx, sr)
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package logic
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/go-playground/validator"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
@@ -8,16 +11,74 @@ import (
|
||||
"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 {
|
||||
EncryptedSend tri // all sends must be encrypted (send -w, and encryption!=off)
|
||||
ReplicationConfig *pdu.ReplicationConfig
|
||||
ConflictResolution *ConflictResolution `validate:"ne=nil"`
|
||||
ReplicationConfig *pdu.ReplicationConfig `validate:"ne=nil"`
|
||||
SizeEstimationConcurrency int `validate:"gte=1"`
|
||||
}
|
||||
|
||||
var validate = validator.New()
|
||||
|
||||
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) {
|
||||
|
||||
@@ -62,11 +62,23 @@ const (
|
||||
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 {
|
||||
Info *FilesystemInfo
|
||||
|
||||
State FilesystemState
|
||||
|
||||
// Always valid.
|
||||
BlockedOn FsBlockedOn
|
||||
|
||||
// Valid in State = FilesystemPlanningErrored
|
||||
PlanError *TimedError
|
||||
// Valid in State = FilesystemSteppingErrored
|
||||
@@ -97,11 +109,11 @@ type StepInfo struct {
|
||||
From, To string
|
||||
Resumed bool
|
||||
Encrypted EncryptedEnum
|
||||
BytesExpected int64
|
||||
BytesReplicated int64
|
||||
BytesExpected uint64
|
||||
BytesReplicated uint64
|
||||
}
|
||||
|
||||
func (a *AttemptReport) BytesSum() (expected, replicated int64, containsInvalidSizeEstimates bool) {
|
||||
func (a *AttemptReport) BytesSum() (expected, replicated uint64, containsInvalidSizeEstimates bool) {
|
||||
for _, fs := range a.Filesystems {
|
||||
e, r, fsContainsInvalidEstimate := fs.BytesSum()
|
||||
containsInvalidSizeEstimates = containsInvalidSizeEstimates || fsContainsInvalidEstimate
|
||||
@@ -111,7 +123,7 @@ func (a *AttemptReport) BytesSum() (expected, replicated int64, containsInvalidS
|
||||
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 {
|
||||
expected += step.Info.BytesExpected
|
||||
replicated += step.Info.BytesReplicated
|
||||
|
||||
@@ -114,12 +114,6 @@ func (c *Client) ReqSend(ctx context.Context, req *pdu.SendReq) (*pdu.SendRes, i
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
putWireOnReturn := true
|
||||
defer func() {
|
||||
if putWireOnReturn {
|
||||
c.putWire(conn)
|
||||
}
|
||||
}()
|
||||
|
||||
if err := c.send(ctx, conn, EndpointSend, req, nil); err != nil {
|
||||
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
|
||||
if !req.DryRun {
|
||||
putWireOnReturn = false
|
||||
stream, err = conn.ReadStream(ZFSStream, true) // no shadow
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &res, stream, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -172,6 +172,15 @@ func (s *Server) serveConnRequest(ctx context.Context, endpoint string, c *strea
|
||||
return
|
||||
}
|
||||
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:
|
||||
var req pdu.ReceiveReq
|
||||
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 {
|
||||
err := c.SendStream(ctx, sendStream, ZFSStream)
|
||||
closeErr := sendStream.Close()
|
||||
if closeErr != nil {
|
||||
s.log.WithError(err).Error("cannot close send stream")
|
||||
}
|
||||
if err != nil {
|
||||
s.log.WithError(err).Error("cannot write send stream")
|
||||
}
|
||||
// sendStream.Close() done via defer above
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ func main() {
|
||||
case "connect":
|
||||
tc, err := getTestCase(args.testCase)
|
||||
noerror(err)
|
||||
connecter, err := transportconfig.ConnecterFromConfig(global, conf.Connect)
|
||||
connecter, err := transportconfig.ConnecterFromConfig(global, conf.Connect, config.ParseFlagsNone)
|
||||
noerror(err)
|
||||
wire, err := connecter.Connect(ctx)
|
||||
noerror(err)
|
||||
@@ -75,7 +75,7 @@ func main() {
|
||||
case "serve":
|
||||
tc, err := getTestCase(args.testCase)
|
||||
noerror(err)
|
||||
lf, err := transportconfig.ListenerFactoryFromConfig(global, conf.Serve)
|
||||
lf, err := transportconfig.ListenerFactoryFromConfig(global, conf.Serve, config.ParseFlagsNone)
|
||||
noerror(err)
|
||||
l, err := lf()
|
||||
noerror(err)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
//go:build illumos || solaris
|
||||
// +build illumos solaris
|
||||
|
||||
package timeoutconn
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/zrepl/zrepl/util/socketpair"
|
||||
"github.com/zrepl/zrepl/util/zreplcircleci"
|
||||
)
|
||||
|
||||
func TestReadTimeout(t *testing.T) {
|
||||
@@ -81,6 +82,8 @@ func TestWriteTimeout(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNoPartialReadsDueToDeadline(t *testing.T) {
|
||||
zreplcircleci.SkipOnCircleCI(t, "needs predictable low scheduling latency")
|
||||
|
||||
a, b, err := socketpair.SocketPair()
|
||||
require.NoError(t, err)
|
||||
defer a.Close()
|
||||
@@ -151,6 +154,7 @@ func (c *partialWriteMockConn) Write(p []byte) (int, error) {
|
||||
}
|
||||
|
||||
func TestPartialWriteMockConn(t *testing.T) {
|
||||
zreplcircleci.SkipOnCircleCI(t, "because it relies on scheduler responsiveness < 50ms")
|
||||
mc := newPartialWriteMockConn(100*time.Millisecond, 5)
|
||||
buf := []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
|
||||
begin := time.Now()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// +build !illumos
|
||||
// +build !solaris
|
||||
//go:build !illumos && !solaris
|
||||
// +build !illumos,!solaris
|
||||
|
||||
package timeoutconn
|
||||
|
||||
|
||||
@@ -118,7 +118,11 @@ func NewInterceptors(logger Logger, clientIdentityKey interface{}, interceptor I
|
||||
if !ok {
|
||||
panic("peer.FromContext expected to return a peer in grpc.UnaryServerInterceptor")
|
||||
}
|
||||
logger.WithField("peer_addr", p.Addr.String()).Debug("peer addr")
|
||||
peerAddr := ""
|
||||
if p.Addr != nil { // https://github.com/zrepl/zrepl/issues/598
|
||||
peerAddr = p.Addr.String()
|
||||
}
|
||||
logger.WithField("peer_addr", peerAddr).Debug("peer addr")
|
||||
a, ok := p.AuthInfo.(*authConnAuthType)
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("NewInterceptors must be used in combination with grpc.NewTransportCredentials, but got auth type %T", p.AuthInfo))
|
||||
|
||||
@@ -108,6 +108,13 @@ func (c *Client) Receive(ctx context.Context, req *pdu.ReceiveReq, stream io.Rea
|
||||
return c.dataClient.ReqRecv(ctx, req, stream)
|
||||
}
|
||||
|
||||
func (c *Client) SendDry(ctx context.Context, in *pdu.SendReq) (*pdu.SendRes, error) {
|
||||
ctx, endSpan := trace.WithSpan(ctx, "rpc.client.SendDry")
|
||||
defer endSpan()
|
||||
|
||||
return c.controlClient.SendDry(ctx, in)
|
||||
}
|
||||
|
||||
func (c *Client) ListFilesystems(ctx context.Context, in *pdu.ListFilesystemReq) (*pdu.ListFilesystemRes, error) {
|
||||
ctx, endSpan := trace.WithSpan(ctx, "rpc.client.ListFilesystems")
|
||||
defer endSpan()
|
||||
|
||||
@@ -152,7 +152,7 @@ func (m *HandshakeMessage) DecodeReader(r io.Reader, maxLen int) error {
|
||||
|
||||
func DoHandshakeCurrentVersion(conn net.Conn, deadline time.Time) *HandshakeError {
|
||||
// current protocol version is hardcoded here
|
||||
return DoHandshakeVersion(conn, deadline, 5)
|
||||
return DoHandshakeVersion(conn, deadline, 6)
|
||||
}
|
||||
|
||||
const HandshakeMessageMaxLen = 16 * 4096
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
"github.com/zrepl/zrepl/transport/tls"
|
||||
)
|
||||
|
||||
func ListenerFactoryFromConfig(g *config.Global, in config.ServeEnum) (transport.AuthenticatedListenerFactory, error) {
|
||||
func ListenerFactoryFromConfig(g *config.Global, in config.ServeEnum, parseFlags config.ParseFlags) (transport.AuthenticatedListenerFactory, error) {
|
||||
|
||||
var (
|
||||
l transport.AuthenticatedListenerFactory
|
||||
@@ -25,7 +25,7 @@ func ListenerFactoryFromConfig(g *config.Global, in config.ServeEnum) (transport
|
||||
case *config.TCPServe:
|
||||
l, err = tcp.TCPListenerFactoryFromConfig(g, v)
|
||||
case *config.TLSServe:
|
||||
l, err = tls.TLSListenerFactoryFromConfig(g, v)
|
||||
l, err = tls.TLSListenerFactoryFromConfig(g, v, parseFlags)
|
||||
case *config.StdinserverServer:
|
||||
l, err = ssh.MultiStdinserverListenerFactoryFromConfig(g, v)
|
||||
case *config.LocalServe:
|
||||
@@ -37,7 +37,7 @@ func ListenerFactoryFromConfig(g *config.Global, in config.ServeEnum) (transport
|
||||
return l, err
|
||||
}
|
||||
|
||||
func ConnecterFromConfig(g *config.Global, in config.ConnectEnum) (transport.Connecter, error) {
|
||||
func ConnecterFromConfig(g *config.Global, in config.ConnectEnum, parseFlags config.ParseFlags) (transport.Connecter, error) {
|
||||
var (
|
||||
connecter transport.Connecter
|
||||
err error
|
||||
@@ -48,7 +48,7 @@ func ConnecterFromConfig(g *config.Global, in config.ConnectEnum) (transport.Con
|
||||
case *config.TCPConnect:
|
||||
connecter, err = tcp.TCPConnecterFromConfig(v)
|
||||
case *config.TLSConnect:
|
||||
connecter, err = tls.TLSConnecterFromConfig(v)
|
||||
connecter, err = tls.TLSConnecterFromConfig(v, parseFlags)
|
||||
case *config.LocalConnect:
|
||||
connecter, err = local.LocalConnecterFromConfig(v)
|
||||
default:
|
||||
|
||||
@@ -93,13 +93,10 @@ func TestIPMap(t *testing.T) {
|
||||
expect: map[string]testCaseExpect{
|
||||
"10.1.2.3": {expectNoMapping: true},
|
||||
"192.168.23.1": {expectIdent: "db-192.168.23.1"},
|
||||
"192.168.42.1": {expectIdent: "web-192.168.42.1"},
|
||||
"192.168.23.23": {expectIdent: "db-twentythree"},
|
||||
"192.168.023.001": {expectIdent: "db-192.168.23.1"},
|
||||
"10.1.4.5": {expectIdent: "my-10.1.4.5-server"},
|
||||
|
||||
// normalization
|
||||
"192.168.42.1": {expectIdent: "web-192.168.42.1"},
|
||||
"192.168.042.001": {expectIdent: "web-192.168.42.1"},
|
||||
// v6 matching
|
||||
"fe80::23:42%eth1": {expectIdent: "san-fe80::23:42-eth1"},
|
||||
"fe80::23:42%eth2": {expectNoMapping: true},
|
||||
@@ -179,7 +176,8 @@ func TestIPMap(t *testing.T) {
|
||||
for input, expect := range c.expect {
|
||||
// reuse newIPMapEntry to parse test case input
|
||||
// "test" is not used during testing but must not be empty.
|
||||
ipMapEntry, _ := newIPMapEntry(input, "test")
|
||||
ipMapEntry, err := newIPMapEntry(input, "test")
|
||||
require.NoError(t, err)
|
||||
ones, bits := ipMapEntry.subnet.Mask.Size()
|
||||
require.Equal(t, bits, net.IPv6len*8, "and we know ipMapEntry always expands its IPs to 16bytes")
|
||||
require.Equal(t, ones, net.IPv6len*8, "test case addresses must be fully specified")
|
||||
|
||||
@@ -18,12 +18,12 @@ type TLSConnecter struct {
|
||||
tlsConfig *tls.Config
|
||||
}
|
||||
|
||||
func TLSConnecterFromConfig(in *config.TLSConnect) (*TLSConnecter, error) {
|
||||
func TLSConnecterFromConfig(in *config.TLSConnect, parseFlags config.ParseFlags) (*TLSConnecter, error) {
|
||||
dialer := net.Dialer{
|
||||
Timeout: in.DialTimeout,
|
||||
}
|
||||
|
||||
if fakeCertificateLoading {
|
||||
if parseFlags&config.ParseFlagsNoCertCheck != 0 {
|
||||
return &TLSConnecter{in.Address, dialer, nil}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
|
||||
type TLSListenerFactory struct{}
|
||||
|
||||
func TLSListenerFactoryFromConfig(c *config.Global, in *config.TLSServe) (transport.AuthenticatedListenerFactory, error) {
|
||||
func TLSListenerFactoryFromConfig(c *config.Global, in *config.TLSServe, parseFlags config.ParseFlags) (transport.AuthenticatedListenerFactory, error) {
|
||||
|
||||
address := in.Listen
|
||||
handshakeTimeout := in.HandshakeTimeout
|
||||
@@ -25,7 +25,7 @@ func TLSListenerFactoryFromConfig(c *config.Global, in *config.TLSServe) (transp
|
||||
return nil, errors.New("fields 'ca', 'cert' and 'key'must be specified")
|
||||
}
|
||||
|
||||
if fakeCertificateLoading {
|
||||
if parseFlags&config.ParseFlagsNoCertCheck != 0 {
|
||||
return func() (transport.AuthenticatedListener, error) { return nil, nil }, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
package tls
|
||||
|
||||
import "testing"
|
||||
|
||||
var fakeCertificateLoading bool
|
||||
|
||||
func FakeCertificateLoading(t *testing.T) {
|
||||
t.Logf("faking certificate loading")
|
||||
fakeCertificateLoading = true
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package bandwidthlimit
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
|
||||
"github.com/juju/ratelimit"
|
||||
)
|
||||
|
||||
type Wrapper interface {
|
||||
WrapReadCloser(io.ReadCloser) io.ReadCloser
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
// Units in this struct are in _bytes_.
|
||||
|
||||
Max int64 // < 0 means no limit, BucketCapacity is irrelevant then
|
||||
BucketCapacity int64
|
||||
}
|
||||
|
||||
func NoLimitConfig() Config {
|
||||
return Config{
|
||||
Max: -1,
|
||||
BucketCapacity: -1,
|
||||
}
|
||||
}
|
||||
|
||||
func ValidateConfig(conf Config) error {
|
||||
if conf.BucketCapacity == 0 {
|
||||
return errors.New("BucketCapacity must not be zero")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func WrapperFromConfig(conf Config) Wrapper {
|
||||
if err := ValidateConfig(conf); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if conf.Max < 0 {
|
||||
return noLimit{}
|
||||
}
|
||||
|
||||
return &withLimit{
|
||||
bucket: ratelimit.NewBucketWithRate(float64(conf.Max), conf.BucketCapacity),
|
||||
}
|
||||
}
|
||||
|
||||
type noLimit struct{}
|
||||
|
||||
func (_ noLimit) WrapReadCloser(rc io.ReadCloser) io.ReadCloser { return rc }
|
||||
|
||||
type withLimit struct {
|
||||
bucket *ratelimit.Bucket
|
||||
}
|
||||
|
||||
func (l *withLimit) WrapReadCloser(rc io.ReadCloser) io.ReadCloser {
|
||||
return WrapReadCloser(rc, l.bucket)
|
||||
}
|
||||
|
||||
type withLimitReadCloser struct {
|
||||
orig io.Closer
|
||||
limited io.Reader
|
||||
}
|
||||
|
||||
func (r *withLimitReadCloser) Read(buf []byte) (int, error) {
|
||||
return r.limited.Read(buf)
|
||||
}
|
||||
|
||||
func (r *withLimitReadCloser) Close() error {
|
||||
return r.orig.Close()
|
||||
}
|
||||
|
||||
func WrapReadCloser(rc io.ReadCloser, bucket *ratelimit.Bucket) io.ReadCloser {
|
||||
return &withLimitReadCloser{
|
||||
limited: ratelimit.Reader(rc, bucket),
|
||||
orig: rc,
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user