Compare commits
59 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
@@ -150,7 +150,7 @@ parameters:
|
|||||||
|
|
||||||
release_docker_baseimage_tag:
|
release_docker_baseimage_tag:
|
||||||
type: string
|
type: string
|
||||||
default: "1.16"
|
default: "1.17"
|
||||||
|
|
||||||
workflows:
|
workflows:
|
||||||
version: 2
|
version: 2
|
||||||
@@ -160,15 +160,15 @@ workflows:
|
|||||||
jobs:
|
jobs:
|
||||||
- quickcheck-docs
|
- quickcheck-docs
|
||||||
- quickcheck-go: &quickcheck-go-smoketest
|
- quickcheck-go: &quickcheck-go-smoketest
|
||||||
name: quickcheck-go-amd64-linux-1.16
|
name: quickcheck-go-amd64-linux-1.17
|
||||||
goversion: &latest-go-release "1.16"
|
goversion: &latest-go-release "1.17"
|
||||||
goos: linux
|
goos: linux
|
||||||
goarch: amd64
|
goarch: amd64
|
||||||
- test-go-on-latest-go-release:
|
- test-go-on-latest-go-release:
|
||||||
goversion: *latest-go-release
|
goversion: *latest-go-release
|
||||||
- quickcheck-go:
|
- quickcheck-go:
|
||||||
requires:
|
requires:
|
||||||
- quickcheck-go-amd64-linux-1.16 #quickcheck-go-smoketest.name
|
- quickcheck-go-amd64-linux-1.17 #quickcheck-go-smoketest.name
|
||||||
matrix: &quickcheck-go-matrix
|
matrix: &quickcheck-go-matrix
|
||||||
alias: quickcheck-go-matrix
|
alias: quickcheck-go-matrix
|
||||||
parameters:
|
parameters:
|
||||||
@@ -231,8 +231,6 @@ jobs:
|
|||||||
- install-docdep
|
- install-docdep
|
||||||
- run: make docs
|
- run: make docs
|
||||||
|
|
||||||
- store_artifacts:
|
|
||||||
path: artifacts
|
|
||||||
- download-and-install-minio-client
|
- download-and-install-minio-client
|
||||||
- upload-minio:
|
- upload-minio:
|
||||||
src: artifacts
|
src: artifacts
|
||||||
@@ -358,7 +356,7 @@ jobs:
|
|||||||
subcommand: docdep
|
subcommand: docdep
|
||||||
- run:
|
- run:
|
||||||
command: |
|
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"
|
git config --global user.name "zrepl-github-io-ci"
|
||||||
|
|
||||||
# https://circleci.com/docs/2.0/add-ssh-key/#adding-multiple-keys-with-blank-hostnames
|
# 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)
|
GO_BUILD := $(GO_ENV_VARS) $(GO) build $(GO_BUILDFLAGS) -ldflags $(GO_LDFLAGS)
|
||||||
GOLANGCI_LINT := golangci-lint
|
GOLANGCI_LINT := golangci-lint
|
||||||
GOCOVMERGE := gocovmerge
|
GOCOVMERGE := gocovmerge
|
||||||
RELEASE_DOCKER_BASEIMAGE_TAG ?= 1.16
|
RELEASE_DOCKER_BASEIMAGE_TAG ?= 1.17
|
||||||
RELEASE_DOCKER_BASEIMAGE ?= golang:$(RELEASE_DOCKER_BASEIMAGE_TAG)
|
RELEASE_DOCKER_BASEIMAGE ?= golang:$(RELEASE_DOCKER_BASEIMAGE_TAG)
|
||||||
|
|
||||||
ifneq ($(GOARM),)
|
ifneq ($(GOARM),)
|
||||||
@@ -189,6 +189,8 @@ GO_SUPPORTS_ILLUMOS := $(shell $(GO) version | gawk -F '.' '/^go version /{split
|
|||||||
bins-all:
|
bins-all:
|
||||||
$(MAKE) $(BINS_ALL_TARGETS) GOOS=freebsd GOARCH=amd64
|
$(MAKE) $(BINS_ALL_TARGETS) GOOS=freebsd GOARCH=amd64
|
||||||
$(MAKE) $(BINS_ALL_TARGETS) GOOS=freebsd GOARCH=386
|
$(MAKE) $(BINS_ALL_TARGETS) GOOS=freebsd GOARCH=386
|
||||||
|
$(MAKE) $(BINS_ALL_TARGETS) GOOS=freebsd GOARCH=arm GOARM=7
|
||||||
|
$(MAKE) $(BINS_ALL_TARGETS) GOOS=freebsd GOARCH=arm64
|
||||||
$(MAKE) $(BINS_ALL_TARGETS) GOOS=linux GOARCH=amd64
|
$(MAKE) $(BINS_ALL_TARGETS) GOOS=linux GOARCH=amd64
|
||||||
$(MAKE) $(BINS_ALL_TARGETS) GOOS=linux GOARCH=arm64
|
$(MAKE) $(BINS_ALL_TARGETS) GOOS=linux GOARCH=arm64
|
||||||
$(MAKE) $(BINS_ALL_TARGETS) GOOS=linux GOARCH=arm GOARM=7
|
$(MAKE) $(BINS_ALL_TARGETS) GOOS=linux GOARCH=arm GOARM=7
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ Downstream packagers can read the changelog to determine whether they want to pu
|
|||||||
|
|
||||||
### Additional Notes to Distro Package Maintainers
|
### Additional Notes to Distro Package Maintainers
|
||||||
|
|
||||||
* Use `sudo make test-platform-bin && sudo make test-platform` **on a test system** to validate that zrepl's abstractions on top of ZFS work with the system ZFS.
|
* Run the platform tests (Docs -> Usage -> Platform Tests) **on a test system** to validate that zrepl's abstractions on top of ZFS work with the system ZFS.
|
||||||
* Ship a default config that adheres to your distro's `hier` and logging system.
|
* Ship a default config that adheres to your distro's `hier` and logging system.
|
||||||
* Ship a service manager file and _please_ try to upstream it to this repository.
|
* Ship a service manager file and _please_ try to upstream it to this repository.
|
||||||
* `dist/systemd` contains a Systemd unit template.
|
* `dist/systemd` contains a Systemd unit template.
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
//go:build tools
|
||||||
// +build tools
|
// +build tools
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|||||||
@@ -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,101 @@
|
|||||||
|
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 := controlHttpClient(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 controlHttpClient(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
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := c.Post("http://unix"+endpoint, "application/json", &buf)
|
||||||
|
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,619 @@
|
|||||||
|
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)
|
||||||
|
if len(rep.Steps) > 0 {
|
||||||
|
userVisisbleCurrentStep = rep.CurrentStep + 1 // CurrentStep is an index that starts at 0
|
||||||
|
}
|
||||||
|
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 {
|
func runTestPlaceholder(ctx context.Context, subcommand *cli.Subcommand, args []string) error {
|
||||||
|
|
||||||
var checkDPs []*zfs.DatasetPath
|
var checkDPs []*zfs.DatasetPath
|
||||||
|
var datasetWasExplicitArgument bool
|
||||||
|
|
||||||
// all actions first
|
// all actions first
|
||||||
if testPlaceholderArgs.all {
|
if testPlaceholderArgs.all {
|
||||||
|
datasetWasExplicitArgument = false
|
||||||
out, err := zfs.ZFSList(ctx, []string{"name"})
|
out, err := zfs.ZFSList(ctx, []string{"name"})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "could not list ZFS filesystems")
|
return errors.Wrap(err, "could not list ZFS filesystems")
|
||||||
@@ -154,6 +156,7 @@ func runTestPlaceholder(ctx context.Context, subcommand *cli.Subcommand, args []
|
|||||||
checkDPs = append(checkDPs, dp)
|
checkDPs = append(checkDPs, dp)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
datasetWasExplicitArgument = true
|
||||||
dp, err := zfs.NewDatasetPath(testPlaceholderArgs.ds)
|
dp, err := zfs.NewDatasetPath(testPlaceholderArgs.ds)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -171,7 +174,12 @@ func runTestPlaceholder(ctx context.Context, subcommand *cli.Subcommand, args []
|
|||||||
return errors.Wrap(err, "cannot get placeholder state")
|
return errors.Wrap(err, "cannot get placeholder state")
|
||||||
}
|
}
|
||||||
if !ph.FSExists {
|
if !ph.FSExists {
|
||||||
panic("placeholder state inconsistent: filesystem " + ph.FS + " must exist in this context")
|
if datasetWasExplicitArgument {
|
||||||
|
return errors.Errorf("filesystem %q does not exist", ph.FS)
|
||||||
|
} else {
|
||||||
|
// got deleted between ZFSList and ZFSGetFilesystemPlaceholderState
|
||||||
|
continue
|
||||||
|
}
|
||||||
}
|
}
|
||||||
is := "yes"
|
is := "yes"
|
||||||
if !ph.IsPlaceholder {
|
if !ph.IsPlaceholder {
|
||||||
|
|||||||
+19
-5
@@ -13,6 +13,7 @@ import (
|
|||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/zrepl/yaml-config"
|
"github.com/zrepl/yaml-config"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/util/datasizeunit"
|
||||||
zfsprop "github.com/zrepl/zrepl/zfs/property"
|
zfsprop "github.com/zrepl/zrepl/zfs/property"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -85,8 +86,10 @@ type SendOptions struct {
|
|||||||
BackupProperties bool `yaml:"backup_properties,optional,default=false"`
|
BackupProperties bool `yaml:"backup_properties,optional,default=false"`
|
||||||
LargeBlocks bool `yaml:"large_blocks,optional,default=false"`
|
LargeBlocks bool `yaml:"large_blocks,optional,default=false"`
|
||||||
Compressed bool `yaml:"compressed,optional,default=false"`
|
Compressed bool `yaml:"compressed,optional,default=false"`
|
||||||
EmbeddedData bool `yaml:"embbeded_data,optional,default=false"`
|
EmbeddedData bool `yaml:"embedded_data,optional,default=false"`
|
||||||
Saved bool `yaml:"saved,optional,default=false"`
|
Saved bool `yaml:"saved,optional,default=false"`
|
||||||
|
|
||||||
|
BandwidthLimit *BandwidthLimit `yaml:"bandwidth_limit,optional,fromdefaults"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type RecvOptions struct {
|
type RecvOptions struct {
|
||||||
@@ -96,6 +99,17 @@ type RecvOptions struct {
|
|||||||
// Reencrypt bool `yaml:"reencrypt"`
|
// Reencrypt bool `yaml:"reencrypt"`
|
||||||
|
|
||||||
Properties *PropertyRecvOptions `yaml:"properties,fromdefaults"`
|
Properties *PropertyRecvOptions `yaml:"properties,fromdefaults"`
|
||||||
|
|
||||||
|
BandwidthLimit *BandwidthLimit `yaml:"bandwidth_limit,optional,fromdefaults"`
|
||||||
|
|
||||||
|
Placeholder *PlaceholderRecvOptions `yaml:"placeholder,fromdefaults"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ yaml.Unmarshaler = &datasizeunit.Bits{}
|
||||||
|
|
||||||
|
type BandwidthLimit struct {
|
||||||
|
Max datasizeunit.Bits `yaml:"max,default=-1 B"`
|
||||||
|
BucketCapacity datasizeunit.Bits `yaml:"bucket_capacity,default=128 KiB"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Replication struct {
|
type Replication struct {
|
||||||
@@ -113,15 +127,15 @@ type ReplicationOptionsConcurrency struct {
|
|||||||
SizeEstimates int `yaml:"size_estimates,optional,default=4"`
|
SizeEstimates int `yaml:"size_estimates,optional,default=4"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *RecvOptions) SetDefault() {
|
|
||||||
*l = RecvOptions{Properties: &PropertyRecvOptions{}}
|
|
||||||
}
|
|
||||||
|
|
||||||
type PropertyRecvOptions struct {
|
type PropertyRecvOptions struct {
|
||||||
Inherit []zfsprop.Property `yaml:"inherit,optional"`
|
Inherit []zfsprop.Property `yaml:"inherit,optional"`
|
||||||
Override map[zfsprop.Property]string `yaml:"override,optional"`
|
Override map[zfsprop.Property]string `yaml:"override,optional"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PlaceholderRecvOptions struct {
|
||||||
|
Encryption string `yaml:"encryption,default=unspecified"`
|
||||||
|
}
|
||||||
|
|
||||||
type PushJob struct {
|
type PushJob struct {
|
||||||
ActiveJob `yaml:",inline"`
|
ActiveJob `yaml:",inline"`
|
||||||
Snapshotting SnapshottingEnum `yaml:"snapshotting"`
|
Snapshotting SnapshottingEnum `yaml:"snapshotting"`
|
||||||
|
|||||||
@@ -70,9 +70,9 @@ func TestPrometheusMonitoring(t *testing.T) {
|
|||||||
global:
|
global:
|
||||||
monitoring:
|
monitoring:
|
||||||
- type: prometheus
|
- 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) {
|
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.
|
# quick start section which inlines this example.
|
||||||
#
|
#
|
||||||
# CUSTOMIZATIONS YOU WILL LIKELY WANT TO APPLY:
|
# 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 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)
|
# - 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)
|
# - make sure the `zrepl_` prefix is not being used by any other zfs tools you might have installed (it likely isn't)
|
||||||
|
|||||||
@@ -9,4 +9,7 @@ jobs:
|
|||||||
key: "/etc/zrepl/backups.key"
|
key: "/etc/zrepl/backups.key"
|
||||||
client_cns:
|
client_cns:
|
||||||
- "prod"
|
- "prod"
|
||||||
|
recv:
|
||||||
|
placeholder:
|
||||||
|
encryption: inherit # use 'off' if sender uses send.encrypted
|
||||||
root_fs: "storage/zrepl/sink"
|
root_fs: "storage/zrepl/sink"
|
||||||
|
|||||||
+4
-2
@@ -8,6 +8,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
@@ -126,6 +127,7 @@ func (j *controlJob) Run(ctx context.Context) {
|
|||||||
Global: GlobalStatus{
|
Global: GlobalStatus{
|
||||||
ZFSCmds: globalZFS,
|
ZFSCmds: globalZFS,
|
||||||
Envconst: envconstReport,
|
Envconst: envconstReport,
|
||||||
|
OsEnviron: os.Environ(),
|
||||||
}}
|
}}
|
||||||
return s, nil
|
return s, nil
|
||||||
}})
|
}})
|
||||||
@@ -156,8 +158,8 @@ func (j *controlJob) Run(ctx context.Context) {
|
|||||||
server := http.Server{
|
server := http.Server{
|
||||||
Handler: mux,
|
Handler: mux,
|
||||||
// control socket is local, 1s timeout should be more than sufficient, even on a loaded system
|
// control socket is local, 1s timeout should be more than sufficient, even on a loaded system
|
||||||
WriteTimeout: 1 * time.Second,
|
WriteTimeout: envconst.Duration("ZREPL_DAEMON_CONTROL_SERVER_WRITE_TIMEOUT", 1*time.Second),
|
||||||
ReadTimeout: 1 * time.Second,
|
ReadTimeout: envconst.Duration("ZREPL_DAEMON_CONTROL_SERVER_READ_TIMEOUT", 1*time.Second),
|
||||||
}
|
}
|
||||||
|
|
||||||
outer:
|
outer:
|
||||||
|
|||||||
@@ -162,6 +162,7 @@ type Status struct {
|
|||||||
type GlobalStatus struct {
|
type GlobalStatus struct {
|
||||||
ZFSCmds *zfscmd.Report
|
ZFSCmds *zfscmd.Report
|
||||||
Envconst *envconst.Report
|
Envconst *envconst.Report
|
||||||
|
OsEnviron []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *jobs) status() map[string]*job.Status {
|
func (s *jobs) status() map[string]*job.Status {
|
||||||
|
|||||||
+14
-2
@@ -42,6 +42,7 @@ type ActiveSide struct {
|
|||||||
promPruneSecs *prometheus.HistogramVec // labels: prune_side
|
promPruneSecs *prometheus.HistogramVec // labels: prune_side
|
||||||
promBytesReplicated *prometheus.CounterVec // labels: filesystem
|
promBytesReplicated *prometheus.CounterVec // labels: filesystem
|
||||||
promReplicationErrors prometheus.Gauge
|
promReplicationErrors prometheus.Gauge
|
||||||
|
promLastSuccessful prometheus.Gauge
|
||||||
|
|
||||||
tasksMtx sync.Mutex
|
tasksMtx sync.Mutex
|
||||||
tasks activeSideTasks
|
tasks activeSideTasks
|
||||||
@@ -321,7 +322,6 @@ func activeSide(g *config.Global, in *config.ActiveJob, configJob interface{}) (
|
|||||||
Help: "number of bytes replicated from sender to receiver per filesystem",
|
Help: "number of bytes replicated from sender to receiver per filesystem",
|
||||||
ConstLabels: prometheus.Labels{"zrepl_job": j.name.String()},
|
ConstLabels: prometheus.Labels{"zrepl_job": j.name.String()},
|
||||||
}, []string{"filesystem"})
|
}, []string{"filesystem"})
|
||||||
|
|
||||||
j.promReplicationErrors = prometheus.NewGauge(prometheus.GaugeOpts{
|
j.promReplicationErrors = prometheus.NewGauge(prometheus.GaugeOpts{
|
||||||
Namespace: "zrepl",
|
Namespace: "zrepl",
|
||||||
Subsystem: "replication",
|
Subsystem: "replication",
|
||||||
@@ -329,6 +329,13 @@ func activeSide(g *config.Global, in *config.ActiveJob, configJob interface{}) (
|
|||||||
Help: "number of filesystems that failed replication in the latest replication attempt, or -1 if the job failed before enumerating the filesystems",
|
Help: "number of filesystems that failed replication in the latest replication attempt, or -1 if the job failed before enumerating the filesystems",
|
||||||
ConstLabels: prometheus.Labels{"zrepl_job": j.name.String()},
|
ConstLabels: prometheus.Labels{"zrepl_job": j.name.String()},
|
||||||
})
|
})
|
||||||
|
j.promLastSuccessful = prometheus.NewGauge(prometheus.GaugeOpts{
|
||||||
|
Namespace: "zrepl",
|
||||||
|
Subsystem: "replication",
|
||||||
|
Name: "last_successful",
|
||||||
|
Help: "timestamp of last successful replication",
|
||||||
|
ConstLabels: prometheus.Labels{"zrepl_job": j.name.String()},
|
||||||
|
})
|
||||||
|
|
||||||
j.connecter, err = fromconfig.ConnecterFromConfig(g, in.Connect)
|
j.connecter, err = fromconfig.ConnecterFromConfig(g, in.Connect)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -360,6 +367,7 @@ func (j *ActiveSide) RegisterMetrics(registerer prometheus.Registerer) {
|
|||||||
registerer.MustRegister(j.promPruneSecs)
|
registerer.MustRegister(j.promPruneSecs)
|
||||||
registerer.MustRegister(j.promBytesReplicated)
|
registerer.MustRegister(j.promBytesReplicated)
|
||||||
registerer.MustRegister(j.promReplicationErrors)
|
registerer.MustRegister(j.promReplicationErrors)
|
||||||
|
registerer.MustRegister(j.promLastSuccessful)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *ActiveSide) Name() string { return j.name.String() }
|
func (j *ActiveSide) Name() string { return j.name.String() }
|
||||||
@@ -494,7 +502,11 @@ func (j *ActiveSide) do(ctx context.Context) {
|
|||||||
repCancel() // always cancel to free up context resources
|
repCancel() // always cancel to free up context resources
|
||||||
|
|
||||||
replicationReport := j.tasks.replicationReport()
|
replicationReport := j.tasks.replicationReport()
|
||||||
j.promReplicationErrors.Set(float64(replicationReport.GetFailedFilesystemsCountInLatestAttempt()))
|
var numErrors = replicationReport.GetFailedFilesystemsCountInLatestAttempt()
|
||||||
|
j.promReplicationErrors.Set(float64(numErrors))
|
||||||
|
if numErrors == 0 {
|
||||||
|
j.promLastSuccessful.SetToCurrentTime()
|
||||||
|
}
|
||||||
|
|
||||||
endSpan()
|
endSpan()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/config"
|
"github.com/zrepl/zrepl/config"
|
||||||
|
"github.com/zrepl/zrepl/util/bandwidthlimit"
|
||||||
)
|
)
|
||||||
|
|
||||||
func JobsFromConfig(c *config.Config) ([]Job, error) {
|
func JobsFromConfig(c *config.Config) ([]Job, error) {
|
||||||
@@ -107,3 +108,13 @@ func validateReceivingSidesDoNotOverlap(receivingRootFSs []string) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func buildBandwidthLimitConfig(in *config.BandwidthLimit) (c bandwidthlimit.Config, _ error) {
|
||||||
|
if in.Max.ToBytes() > 0 && int64(in.Max.ToBytes()) == 0 {
|
||||||
|
return c, fmt.Errorf("bandwidth limit `max` is too small, must at least specify one byte")
|
||||||
|
}
|
||||||
|
return bandwidthlimit.Config{
|
||||||
|
Max: int64(in.Max.ToBytes()),
|
||||||
|
BucketCapacity: int64(in.BucketCapacity.ToBytes()),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -22,7 +22,12 @@ func buildSenderConfig(in SendingJobConfig, jobID endpoint.JobID) (*endpoint.Sen
|
|||||||
return nil, errors.Wrap(err, "cannot build filesystem filter")
|
return nil, errors.Wrap(err, "cannot build filesystem filter")
|
||||||
}
|
}
|
||||||
sendOpts := in.GetSendOptions()
|
sendOpts := in.GetSendOptions()
|
||||||
return &endpoint.SenderConfig{
|
bwlim, err := buildBandwidthLimitConfig(sendOpts.BandwidthLimit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "cannot build bandwith limit config")
|
||||||
|
}
|
||||||
|
|
||||||
|
sc := &endpoint.SenderConfig{
|
||||||
FSF: fsf,
|
FSF: fsf,
|
||||||
JobID: jobID,
|
JobID: jobID,
|
||||||
|
|
||||||
@@ -34,7 +39,15 @@ func buildSenderConfig(in SendingJobConfig, jobID endpoint.JobID) (*endpoint.Sen
|
|||||||
SendCompressed: sendOpts.Compressed,
|
SendCompressed: sendOpts.Compressed,
|
||||||
SendEmbeddedData: sendOpts.EmbeddedData,
|
SendEmbeddedData: sendOpts.EmbeddedData,
|
||||||
SendSaved: sendOpts.Saved,
|
SendSaved: sendOpts.Saved,
|
||||||
}, nil
|
|
||||||
|
BandwidthLimit: bwlim,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := sc.Validate(); err != nil {
|
||||||
|
return nil, errors.Wrap(err, "cannot build sender config")
|
||||||
|
}
|
||||||
|
|
||||||
|
return sc, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type ReceivingJobConfig interface {
|
type ReceivingJobConfig interface {
|
||||||
@@ -53,6 +66,22 @@ func buildReceiverConfig(in ReceivingJobConfig, jobID endpoint.JobID) (rc endpoi
|
|||||||
}
|
}
|
||||||
|
|
||||||
recvOpts := in.GetRecvOptions()
|
recvOpts := in.GetRecvOptions()
|
||||||
|
|
||||||
|
bwlim, err := buildBandwidthLimitConfig(recvOpts.BandwidthLimit)
|
||||||
|
if err != nil {
|
||||||
|
return rc, errors.Wrap(err, "cannot build bandwith limit config")
|
||||||
|
}
|
||||||
|
|
||||||
|
placeholderEncryption, err := endpoint.PlaceholderCreationEncryptionPropertyString(recvOpts.Placeholder.Encryption)
|
||||||
|
if err != nil {
|
||||||
|
options := []string{}
|
||||||
|
for _, v := range endpoint.PlaceholderCreationEncryptionPropertyValues() {
|
||||||
|
options = append(options, endpoint.PlaceholderCreationEncryptionProperty(v).String())
|
||||||
|
}
|
||||||
|
return rc, errors.Errorf("placeholder encryption value %q is invalid, must be one of %s",
|
||||||
|
recvOpts.Placeholder.Encryption, options)
|
||||||
|
}
|
||||||
|
|
||||||
rc = endpoint.ReceiverConfig{
|
rc = endpoint.ReceiverConfig{
|
||||||
JobID: jobID,
|
JobID: jobID,
|
||||||
RootWithoutClientComponent: rootFs,
|
RootWithoutClientComponent: rootFs,
|
||||||
@@ -60,6 +89,10 @@ func buildReceiverConfig(in ReceivingJobConfig, jobID endpoint.JobID) (rc endpoi
|
|||||||
|
|
||||||
InheritProperties: recvOpts.Properties.Inherit,
|
InheritProperties: recvOpts.Properties.Inherit,
|
||||||
OverrideProperties: recvOpts.Properties.Override,
|
OverrideProperties: recvOpts.Properties.Override,
|
||||||
|
|
||||||
|
BandwidthLimit: bwlim,
|
||||||
|
|
||||||
|
PlaceholderEncryption: placeholderEncryption,
|
||||||
}
|
}
|
||||||
if err := rc.Validate(); err != nil {
|
if err := rc.Validate(); err != nil {
|
||||||
return rc, errors.Wrap(err, "cannot build receiver config")
|
return rc, errors.Wrap(err, "cannot build receiver config")
|
||||||
|
|||||||
@@ -119,6 +119,14 @@ func TestSampleConfigsAreBuiltWithoutErrors(t *testing.T) {
|
|||||||
t.Errorf("glob failed: %+v", err)
|
t.Errorf("glob failed: %+v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type additionalCheck struct {
|
||||||
|
state int
|
||||||
|
test func(t *testing.T, jobs []Job)
|
||||||
|
}
|
||||||
|
additionalChecks := map[string]*additionalCheck{
|
||||||
|
"bandwidth_limit.yml": {test: testSampleConfig_BandwidthLimit},
|
||||||
|
}
|
||||||
|
|
||||||
for _, p := range paths {
|
for _, p := range paths {
|
||||||
|
|
||||||
if path.Ext(p) != ".yml" {
|
if path.Ext(p) != ".yml" {
|
||||||
@@ -126,10 +134,20 @@ func TestSampleConfigsAreBuiltWithoutErrors(t *testing.T) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
filename := path.Base(p)
|
||||||
|
t.Logf("checking for presence additonal checks for file %q", filename)
|
||||||
|
additionalCheck := additionalChecks[filename]
|
||||||
|
if additionalCheck == nil {
|
||||||
|
t.Logf("no additional checks")
|
||||||
|
} else {
|
||||||
|
t.Logf("additional check present")
|
||||||
|
additionalCheck.state = 1
|
||||||
|
}
|
||||||
|
|
||||||
t.Run(p, func(t *testing.T) {
|
t.Run(p, func(t *testing.T) {
|
||||||
c, err := config.ParseConfig(p)
|
c, err := config.ParseConfig(p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("error parsing %s:\n%+v", p, err)
|
t.Fatalf("error parsing %s:\n%+v", p, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Logf("file: %s", p)
|
t.Logf("file: %s", p)
|
||||||
@@ -138,11 +156,57 @@ func TestSampleConfigsAreBuiltWithoutErrors(t *testing.T) {
|
|||||||
tls.FakeCertificateLoading(t)
|
tls.FakeCertificateLoading(t)
|
||||||
jobs, err := JobsFromConfig(c)
|
jobs, err := JobsFromConfig(c)
|
||||||
t.Logf("jobs: %#v", jobs)
|
t.Logf("jobs: %#v", jobs)
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
if additionalCheck != nil {
|
||||||
|
additionalCheck.test(t, jobs)
|
||||||
|
additionalCheck.state = 2
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for basename, c := range additionalChecks {
|
||||||
|
if c.state == 0 {
|
||||||
|
panic("univisited additional check " + basename)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func testSampleConfig_BandwidthLimit(t *testing.T, jobs []Job) {
|
||||||
|
require.Len(t, jobs, 3)
|
||||||
|
|
||||||
|
{
|
||||||
|
limitedSink, ok := jobs[0].(*PassiveSide)
|
||||||
|
require.True(t, ok, "%T", jobs[0])
|
||||||
|
limitedSinkMode, ok := limitedSink.mode.(*modeSink)
|
||||||
|
require.True(t, ok, "%T", limitedSink)
|
||||||
|
|
||||||
|
assert.Equal(t, int64(12345), limitedSinkMode.receiverConfig.BandwidthLimit.Max)
|
||||||
|
assert.Equal(t, int64(1<<17), limitedSinkMode.receiverConfig.BandwidthLimit.BucketCapacity)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
limitedPush, ok := jobs[1].(*ActiveSide)
|
||||||
|
require.True(t, ok, "%T", jobs[1])
|
||||||
|
limitedPushMode, ok := limitedPush.mode.(*modePush)
|
||||||
|
require.True(t, ok, "%T", limitedPush)
|
||||||
|
|
||||||
|
assert.Equal(t, int64(54321), limitedPushMode.senderConfig.BandwidthLimit.Max)
|
||||||
|
assert.Equal(t, int64(1024), limitedPushMode.senderConfig.BandwidthLimit.BucketCapacity)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
unlimitedSink, ok := jobs[2].(*PassiveSide)
|
||||||
|
require.True(t, ok, "%T", jobs[2])
|
||||||
|
unlimitedSinkMode, ok := unlimitedSink.mode.(*modeSink)
|
||||||
|
require.True(t, ok, "%T", unlimitedSink)
|
||||||
|
|
||||||
|
max := unlimitedSinkMode.receiverConfig.BandwidthLimit.Max
|
||||||
|
assert.Less(t, max, int64(0), max, "unlimited mode <=> negative value for .Max, see bandwidthlimit.Config")
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestReplicationOptions(t *testing.T) {
|
func TestReplicationOptions(t *testing.T) {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/daemon/logging/trace"
|
"github.com/zrepl/zrepl/daemon/logging/trace"
|
||||||
|
"github.com/zrepl/zrepl/util/bandwidthlimit"
|
||||||
"github.com/zrepl/zrepl/util/nodefault"
|
"github.com/zrepl/zrepl/util/nodefault"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/config"
|
"github.com/zrepl/zrepl/config"
|
||||||
@@ -146,7 +147,7 @@ type alwaysUpToDateReplicationCursorHistory struct {
|
|||||||
target pruner.Target
|
target pruner.Target
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ pruner.History = (*alwaysUpToDateReplicationCursorHistory)(nil)
|
var _ pruner.Sender = (*alwaysUpToDateReplicationCursorHistory)(nil)
|
||||||
|
|
||||||
func (h alwaysUpToDateReplicationCursorHistory) ReplicationCursor(ctx context.Context, req *pdu.ReplicationCursorReq) (*pdu.ReplicationCursorRes, error) {
|
func (h alwaysUpToDateReplicationCursorHistory) ReplicationCursor(ctx context.Context, req *pdu.ReplicationCursorReq) (*pdu.ReplicationCursorRes, error) {
|
||||||
fsvReq := &pdu.ListFilesystemVersionsReq{
|
fsvReq := &pdu.ListFilesystemVersionsReq{
|
||||||
@@ -179,8 +180,11 @@ func (j *SnapJob) doPrune(ctx context.Context) {
|
|||||||
sender := endpoint.NewSender(endpoint.SenderConfig{
|
sender := endpoint.NewSender(endpoint.SenderConfig{
|
||||||
JobID: j.name,
|
JobID: j.name,
|
||||||
FSF: j.fsfilter,
|
FSF: j.fsfilter,
|
||||||
// FIXME encryption setting is irrelevant for SnapJob because the endpoint is only used as pruner.Target
|
// FIXME the following config fields are irrelevant for SnapJob
|
||||||
|
// because the endpoint is only used as pruner.Target.
|
||||||
|
// However, the implementation requires them to be set.
|
||||||
Encrypt: &nodefault.Bool{B: true},
|
Encrypt: &nodefault.Bool{B: true},
|
||||||
|
BandwidthLimit: bandwidthlimit.NoLimitConfig(),
|
||||||
})
|
})
|
||||||
j.prunerMtx.Lock()
|
j.prunerMtx.Lock()
|
||||||
j.pruner = j.prunerFactory.BuildLocalPruner(ctx, sender, alwaysUpToDateReplicationCursorHistory{sender})
|
j.pruner = j.prunerFactory.BuildLocalPruner(ctx, sender, alwaysUpToDateReplicationCursorHistory{sender})
|
||||||
|
|||||||
@@ -246,7 +246,7 @@ func WithTask(ctx context.Context, taskName string) (context.Context, DoneFunc)
|
|||||||
// the debugString can be quite long and panic won't print it completely
|
// the debugString can be quite long and panic won't print it completely
|
||||||
fmt.Fprintf(os.Stderr, "going to panic due to activeChildTasks:\n%s\n", this.debugString())
|
fmt.Fprintf(os.Stderr, "going to panic due to activeChildTasks:\n%s\n", this.debugString())
|
||||||
}
|
}
|
||||||
panic(errors.WithMessagef(ErrTaskStillHasActiveChildTasks, "end task: %v active child tasks\n", this.activeChildTasks))
|
panic(errors.WithMessagef(ErrTaskStillHasActiveChildTasks, "end task: %v active child tasks (run daemon with env var %s=1 for more details)\n", this.activeChildTasks, debugEnabledEnvVar))
|
||||||
}
|
}
|
||||||
|
|
||||||
// support idempotent task ends
|
// support idempotent task ends
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ import (
|
|||||||
"github.com/zrepl/zrepl/util/envconst"
|
"github.com/zrepl/zrepl/util/envconst"
|
||||||
)
|
)
|
||||||
|
|
||||||
var debugEnabled = envconst.Bool("ZREPL_TRACE_DEBUG_ENABLED", false)
|
const debugEnabledEnvVar = "ZREPL_TRACE_DEBUG_ENABLED"
|
||||||
|
|
||||||
|
var debugEnabled = envconst.Bool(debugEnabledEnvVar, false)
|
||||||
|
|
||||||
func debug(format string, args ...interface{}) {
|
func debug(format string, args ...interface{}) {
|
||||||
if !debugEnabled {
|
if !debugEnabled {
|
||||||
|
|||||||
+20
-12
@@ -19,12 +19,20 @@ import (
|
|||||||
"github.com/zrepl/zrepl/util/envconst"
|
"github.com/zrepl/zrepl/util/envconst"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// The sender in the replication setup.
|
||||||
|
// The pruner uses the Sender to determine which of the Target's filesystems need to be pruned.
|
||||||
|
// Also, it asks the Sender about the replication cursor of each filesystem
|
||||||
|
// to enable the 'not_replicated' pruning rule.
|
||||||
|
//
|
||||||
// Try to keep it compatible with github.com/zrepl/zrepl/endpoint.Endpoint
|
// Try to keep it compatible with github.com/zrepl/zrepl/endpoint.Endpoint
|
||||||
type History interface {
|
type Sender interface {
|
||||||
ReplicationCursor(ctx context.Context, req *pdu.ReplicationCursorReq) (*pdu.ReplicationCursorRes, error)
|
ReplicationCursor(ctx context.Context, req *pdu.ReplicationCursorReq) (*pdu.ReplicationCursorRes, error)
|
||||||
ListFilesystems(ctx context.Context, req *pdu.ListFilesystemReq) (*pdu.ListFilesystemRes, error)
|
ListFilesystems(ctx context.Context, req *pdu.ListFilesystemReq) (*pdu.ListFilesystemRes, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The pruning target, i.e., on which snapshots are destroyed.
|
||||||
|
// This can be a replication sender or receiver.
|
||||||
|
//
|
||||||
// Try to keep it compatible with github.com/zrepl/zrepl/endpoint.Endpoint
|
// Try to keep it compatible with github.com/zrepl/zrepl/endpoint.Endpoint
|
||||||
type Target interface {
|
type Target interface {
|
||||||
ListFilesystems(ctx context.Context, req *pdu.ListFilesystemReq) (*pdu.ListFilesystemRes, error)
|
ListFilesystems(ctx context.Context, req *pdu.ListFilesystemReq) (*pdu.ListFilesystemRes, error)
|
||||||
@@ -48,7 +56,7 @@ func GetLogger(ctx context.Context) Logger {
|
|||||||
type args struct {
|
type args struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
target Target
|
target Target
|
||||||
receiver History
|
sender Sender
|
||||||
rules []pruning.KeepRule
|
rules []pruning.KeepRule
|
||||||
retryWait time.Duration
|
retryWait time.Duration
|
||||||
considerSnapAtCursorReplicated bool
|
considerSnapAtCursorReplicated bool
|
||||||
@@ -132,12 +140,12 @@ func NewPrunerFactory(in config.PruningSenderReceiver, promPruneSecs *prometheus
|
|||||||
return f, nil
|
return f, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *PrunerFactory) BuildSenderPruner(ctx context.Context, target Target, receiver History) *Pruner {
|
func (f *PrunerFactory) BuildSenderPruner(ctx context.Context, target Target, sender Sender) *Pruner {
|
||||||
p := &Pruner{
|
p := &Pruner{
|
||||||
args: args{
|
args: args{
|
||||||
context.WithValue(ctx, contextKeyPruneSide, "sender"),
|
context.WithValue(ctx, contextKeyPruneSide, "sender"),
|
||||||
target,
|
target,
|
||||||
receiver,
|
sender,
|
||||||
f.senderRules,
|
f.senderRules,
|
||||||
f.retryWait,
|
f.retryWait,
|
||||||
f.considerSnapAtCursorReplicated,
|
f.considerSnapAtCursorReplicated,
|
||||||
@@ -148,12 +156,12 @@ func (f *PrunerFactory) BuildSenderPruner(ctx context.Context, target Target, re
|
|||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *PrunerFactory) BuildReceiverPruner(ctx context.Context, target Target, receiver History) *Pruner {
|
func (f *PrunerFactory) BuildReceiverPruner(ctx context.Context, target Target, sender Sender) *Pruner {
|
||||||
p := &Pruner{
|
p := &Pruner{
|
||||||
args: args{
|
args: args{
|
||||||
context.WithValue(ctx, contextKeyPruneSide, "receiver"),
|
context.WithValue(ctx, contextKeyPruneSide, "receiver"),
|
||||||
target,
|
target,
|
||||||
receiver,
|
sender,
|
||||||
f.receiverRules,
|
f.receiverRules,
|
||||||
f.retryWait,
|
f.retryWait,
|
||||||
false, // senseless here anyways
|
false, // senseless here anyways
|
||||||
@@ -164,12 +172,12 @@ func (f *PrunerFactory) BuildReceiverPruner(ctx context.Context, target Target,
|
|||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *LocalPrunerFactory) BuildLocalPruner(ctx context.Context, target Target, receiver History) *Pruner {
|
func (f *LocalPrunerFactory) BuildLocalPruner(ctx context.Context, target Target, history Sender) *Pruner {
|
||||||
p := &Pruner{
|
p := &Pruner{
|
||||||
args: args{
|
args: args{
|
||||||
context.WithValue(ctx, contextKeyPruneSide, "local"),
|
context.WithValue(ctx, contextKeyPruneSide, "local"),
|
||||||
target,
|
target,
|
||||||
receiver,
|
history,
|
||||||
f.keepRules,
|
f.keepRules,
|
||||||
f.retryWait,
|
f.retryWait,
|
||||||
false, // considerSnapAtCursorReplicated is not relevant for local pruning
|
false, // considerSnapAtCursorReplicated is not relevant for local pruning
|
||||||
@@ -343,9 +351,9 @@ func (s snapshot) Date() time.Time { return s.date }
|
|||||||
|
|
||||||
func doOneAttempt(a *args, u updater) {
|
func doOneAttempt(a *args, u updater) {
|
||||||
|
|
||||||
ctx, target, receiver := a.ctx, a.target, a.receiver
|
ctx, target, sender := a.ctx, a.target, a.sender
|
||||||
|
|
||||||
sfssres, err := receiver.ListFilesystems(ctx, &pdu.ListFilesystemReq{})
|
sfssres, err := sender.ListFilesystems(ctx, &pdu.ListFilesystemReq{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
u(func(p *Pruner) {
|
u(func(p *Pruner) {
|
||||||
p.state = PlanErr
|
p.state = PlanErr
|
||||||
@@ -410,7 +418,7 @@ tfss_loop:
|
|||||||
rcReq := &pdu.ReplicationCursorReq{
|
rcReq := &pdu.ReplicationCursorReq{
|
||||||
Filesystem: tfs.Path,
|
Filesystem: tfs.Path,
|
||||||
}
|
}
|
||||||
rc, err := receiver.ReplicationCursor(ctx, rcReq)
|
rc, err := sender.ReplicationCursor(ctx, rcReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
pfsPlanErrAndLog(err, "cannot get replication cursor bookmark")
|
pfsPlanErrAndLog(err, "cannot get replication cursor bookmark")
|
||||||
continue tfss_loop
|
continue tfss_loop
|
||||||
@@ -456,7 +464,7 @@ tfss_loop:
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
if preCursor {
|
if preCursor {
|
||||||
pfsPlanErrAndLog(fmt.Errorf("replication cursor not found in prune target filesystem versions"), "")
|
pfsPlanErrAndLog(fmt.Errorf("prune target has no snapshot that corresponds to sender replication cursor bookmark"), "")
|
||||||
continue tfss_loop
|
continue tfss_loop
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+62
-17
@@ -6,6 +6,7 @@
|
|||||||
.. |docs| replace:: [DOCS]
|
.. |docs| replace:: [DOCS]
|
||||||
.. |feature| replace:: [FEATURE]
|
.. |feature| replace:: [FEATURE]
|
||||||
.. |mig| replace:: **[MIGRATION]**
|
.. |mig| replace:: **[MIGRATION]**
|
||||||
|
.. |maint| replace:: [MAINT]
|
||||||
|
|
||||||
.. _changelog:
|
.. _changelog:
|
||||||
|
|
||||||
@@ -15,17 +16,68 @@ Changelog
|
|||||||
The changelog summarizes bugfixes that are deemed relevant for users and package maintainers.
|
The changelog summarizes bugfixes that are deemed relevant for users and package maintainers.
|
||||||
Developers should consult the git commit log or GitHub issue tracker.
|
Developers should consult the git commit log or GitHub issue tracker.
|
||||||
|
|
||||||
We use the following annotations for classifying changes:
|
0.5
|
||||||
|
---
|
||||||
|
|
||||||
* |break_config| Change that breaks the config.
|
* |feature| :ref:`Bandwidth limiting <job-send-recv-options--bandwidth-limit>` (Thanks, Prominic.NET, Inc.)
|
||||||
As a package maintainer, make sure to warn your users about config breakage somehow.
|
* |feature| zrepl status: use a ``*`` to indicate which filesystem is currently replicating
|
||||||
* |break| Change that breaks interoperability or persistent state representation with previous releases.
|
* |feature| include daemon environment variables in zrepl status (currently only in ``--raw``)
|
||||||
As a package maintainer, make sure to warn your users about config breakage somehow.
|
* |bugfix| **fix encrypt-on-receive + placeholders use case** (:issue:`504`)
|
||||||
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.
|
* 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.
|
||||||
* |feature| Change that introduces new functionality.
|
* 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.
|
||||||
* |bugfix| Change that fixes a bug, no regressions or incompatibilities expected.
|
* Thanks to `@mologie <https://github.com/mologie>`_ and `@razielgn <https://github.com/razielgn>`_ for reporting and testing!
|
||||||
* |docs| Change to the documentation.
|
|
||||||
|
* |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
|
0.3.1
|
||||||
-----
|
-----
|
||||||
@@ -45,13 +97,6 @@ Mostly a bugfix release for :ref:`zrepl 0.3 <release-0.3>`.
|
|||||||
* CI infrastructure rework
|
* CI infrastructure rework
|
||||||
* Continuous deployment of that new `stable` branch to zrepl.github.io.
|
* 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:
|
.. _release-0.3:
|
||||||
|
|
||||||
0.3
|
0.3
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ Prometheus & Grafana
|
|||||||
--------------------
|
--------------------
|
||||||
|
|
||||||
zrepl can expose `Prometheus metrics <https://prometheus.io/docs/instrumenting/exposition_formats/>`_ via HTTP.
|
zrepl can expose `Prometheus metrics <https://prometheus.io/docs/instrumenting/exposition_formats/>`_ via HTTP.
|
||||||
The ``listen`` attribute is a `net.Listen <https://golang.org/pkg/net/#Listen>`_ string for tcp, e.g. ``:9091`` or ``127.0.0.1:9091``.
|
The ``listen`` attribute is a `net.Listen <https://golang.org/pkg/net/#Listen>`_ string for tcp, e.g. ``: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 ``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**.
|
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:
|
global:
|
||||||
monitoring:
|
monitoring:
|
||||||
- type: prometheus
|
- type: prometheus
|
||||||
listen: ':9091'
|
listen: ':9811'
|
||||||
listen_freebind: true # optional, default false
|
listen_freebind: true # optional, default false
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -67,8 +67,9 @@ Policy ``not_replicated``
|
|||||||
...
|
...
|
||||||
|
|
||||||
``not_replicated`` keeps all snapshots that have not been replicated to the receiving side.
|
``not_replicated`` keeps all snapshots that have not been replicated to the receiving side.
|
||||||
It only makes sense to specify this rule on a sender (source or push job).
|
It only makes sense to specify this rule for the ``keep_sender``.
|
||||||
The state required to evaluate this rule is stored in the :ref:`replication cursor bookmark <replication-cursor-and-last-received-hold>` on the sending side.
|
The reason is that, by definition, all snapshots on the receiver have already been replicated to there from the sender.
|
||||||
|
To determine whether a sender-side snapshot has already been replicated, zrepl uses the :ref:`replication cursor bookmark <replication-cursor-and-last-received-hold>` which corresponds to the most recent successfully replicated snapshot.
|
||||||
|
|
||||||
.. _prune-keep-retention-grid:
|
.. _prune-keep-retention-grid:
|
||||||
|
|
||||||
@@ -106,7 +107,7 @@ The following procedure happens during pruning:
|
|||||||
#. All subsequent buckets are placed adjacent to their predecessor bucket.
|
#. All subsequent buckets are placed adjacent to their predecessor bucket.
|
||||||
#. Now each snapshot on the axis either falls into one bucket or it is older than our rightmost bucket.
|
#. Now each snapshot on the axis either falls into one bucket or it is older than our rightmost bucket.
|
||||||
Buckets are left-inclusive and right-exclusive which means that a snapshot on the edge of bucket will always 'fall into the right one'.
|
Buckets are left-inclusive and right-exclusive which means that a snapshot on the edge of bucket will always 'fall into the right one'.
|
||||||
#. Snapshots older than the rightmost bucket **not kept** by this gridspec.
|
#. Snapshots older than the rightmost bucket are **not kept** by the grid specification.
|
||||||
#. For each bucket, we only keep the ``keep`` oldest snapshots.
|
#. For each bucket, we only keep the ``keep`` oldest snapshots.
|
||||||
|
|
||||||
The syntax to describe the bucket list is as follows:
|
The syntax to describe the bucket list is as follows:
|
||||||
@@ -124,43 +125,51 @@ The syntax to describe the bucket list is as follows:
|
|||||||
|
|
||||||
::
|
::
|
||||||
|
|
||||||
This grid spec produces the following list of adjacent buckets. For the sake of simplicity,
|
Assume the following grid specification:
|
||||||
we subject all snapshots to the grid pruning policy by settings `regex: .*`.
|
|
||||||
|
|
||||||
`
|
|
||||||
grid: 1x1h(keep=all) | 2x2h | 1x3h
|
grid: 1x1h(keep=all) | 2x2h | 1x3h
|
||||||
regex: .*
|
|
||||||
`
|
This grid specification produces the following constellation of buckets:
|
||||||
|
|
||||||
0h 1h 2h 3h 4h 5h 6h 7h 8h 9h
|
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 |
|
| 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:
|
||||||
|
|
||||||
Bucket1: a,b,c
|
Bucket1: a,b,c
|
||||||
Bucket 2: d,e,f,g,h,i,j
|
Bucket2: d,e,f,g,h,i
|
||||||
Bucket 3: k,l,m,n,o,p
|
Bucket3: j,k,l,m,n,o,p
|
||||||
Bucket 4: q,r, q,r,s,t,u,v,w,x,y,z
|
Bucket4: q,r,s,t,u,v,w,x,y,z
|
||||||
None: A,B,C,D
|
No bucket: A,B,C,D
|
||||||
|
|
||||||
It then applies the per-bucket pruning logic described above which resulting in the
|
For each bucket, we now prune snapshots until it only contains `keep` snapshots.
|
||||||
following list of remaining snapshots.
|
Newer snapshots are destroyed first.
|
||||||
|
Snapshots that do not fall into a bucket are always destroyed.
|
||||||
|
|
||||||
| a b c j p z |
|
Result after pruning:
|
||||||
|
|
||||||
Note that it only makes sense to grow (not shorten) the interval duration for buckets
|
|
||||||
further in the past since each bucket acts like a low-pass filter for incoming snapshots
|
|
||||||
and adding a less-low-pass-filter after a low-pass one has no effect.
|
|
||||||
|
|
||||||
|
0h 1h 2h 3h 4h 5h 6h 7h 8h 9h
|
||||||
|
| | | | | | | | | |
|
||||||
|
|-Bucket1-|-----Bucket2-------|------Bucket3------|-----------Bucket4-----------|
|
||||||
|
| | | | |
|
||||||
|
| a b c | i | p | z |
|
||||||
|
|
||||||
.. _prune-keep-last-n:
|
.. _prune-keep-last-n:
|
||||||
|
|
||||||
|
|||||||
@@ -36,9 +36,12 @@ See the `upstream man page <https://openzfs.github.io/openzfs-docs/man/8/zfs-sen
|
|||||||
* - ``encrypted``
|
* - ``encrypted``
|
||||||
-
|
-
|
||||||
- Specific to zrepl, :ref:`see below <job-send-options-encrypted>`.
|
- Specific to zrepl, :ref:`see below <job-send-options-encrypted>`.
|
||||||
|
* - ``bandwidth_limit``
|
||||||
|
-
|
||||||
|
- Specific to zrepl, :ref:`see below <job-send-recv-options--bandwidth-limit>`.
|
||||||
* - ``raw``
|
* - ``raw``
|
||||||
- ``-w``
|
- ``-w``
|
||||||
- Use ``encrypted`` to only allow encrypted sends.
|
- Use ``encrypted`` to only allow encrypted sends. Mixed sends are not supported.
|
||||||
* - ``send_properties``
|
* - ``send_properties``
|
||||||
- ``-p``
|
- ``-p``
|
||||||
- **Be careful**, read the :ref:`note on property replication below <job-note-property-replication>`.
|
- **Be careful**, read the :ref:`note on property replication below <job-note-property-replication>`.
|
||||||
@@ -51,7 +54,7 @@ See the `upstream man page <https://openzfs.github.io/openzfs-docs/man/8/zfs-sen
|
|||||||
* - ``compressed``
|
* - ``compressed``
|
||||||
- ``-c``
|
- ``-c``
|
||||||
-
|
-
|
||||||
* - ``embbeded_data``
|
* - ``embedded_data``
|
||||||
- ``-e``
|
- ``-e``
|
||||||
-
|
-
|
||||||
* - ``saved``
|
* - ``saved``
|
||||||
@@ -138,8 +141,16 @@ Recv Options
|
|||||||
override: {
|
override: {
|
||||||
"org.openzfs.systemd:ignore": "on"
|
"org.openzfs.systemd:ignore": "on"
|
||||||
}
|
}
|
||||||
|
bandwidth_limit: ...
|
||||||
|
placeholder:
|
||||||
|
encryption: unspecified | off | inherit
|
||||||
...
|
...
|
||||||
|
|
||||||
|
Jump to
|
||||||
|
:ref:`properties <job-recv-options--inherit-and-override>` ,
|
||||||
|
:ref:`bandwidth_limit <job-send-recv-options--bandwidth-limit>` , and
|
||||||
|
:ref:`bandwidth_limit <job-recv-options--placeholder>`.
|
||||||
|
|
||||||
.. _job-recv-options--inherit-and-override:
|
.. _job-recv-options--inherit-and-override:
|
||||||
|
|
||||||
``properties``
|
``properties``
|
||||||
@@ -186,6 +197,12 @@ Mount behaviour
|
|||||||
* ``canmount``
|
* ``canmount``
|
||||||
* ``overlay``
|
* ``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
|
Systemd
|
||||||
-------
|
-------
|
||||||
|
|
||||||
@@ -206,3 +223,55 @@ and property replication is enabled, the receiver must :ref:`inherit the followi
|
|||||||
* ``keylocation``
|
* ``keylocation``
|
||||||
* ``keyformat``
|
* ``keyformat``
|
||||||
* ``encryption``
|
* ``encryption``
|
||||||
|
|
||||||
|
.. _job-recv-options--placeholder:
|
||||||
|
|
||||||
|
Placeholders
|
||||||
|
------------
|
||||||
|
|
||||||
|
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.
|
||||||
|
|||||||
+3
-2
@@ -25,10 +25,10 @@ zrepl - ZFS replication
|
|||||||
Progress: [=========================\----] 246.7 MiB / 264.7 MiB @ 11.5 MiB/s
|
Progress: [=========================\----] 246.7 MiB / 264.7 MiB @ 11.5 MiB/s
|
||||||
zroot STEPPING (step 1/2, 624 B/1.2 KiB) next: @a => @b
|
zroot STEPPING (step 1/2, 624 B/1.2 KiB) next: @a => @b
|
||||||
zroot/ROOT DONE (step 2/2, 1.2 KiB/1.2 KiB)
|
zroot/ROOT DONE (step 2/2, 1.2 KiB/1.2 KiB)
|
||||||
zroot/ROOT/default STEPPING (step 1/2, 123.4 MiB/129.3 MiB) next: @a => @b
|
* zroot/ROOT/default STEPPING (step 1/2, 123.4 MiB/129.3 MiB) next: @a => @b
|
||||||
zroot/tmp STEPPING (step 1/2, 29.9 KiB/44.2 KiB) next: @a => @b
|
zroot/tmp STEPPING (step 1/2, 29.9 KiB/44.2 KiB) next: @a => @b
|
||||||
zroot/usr STEPPING (step 1/2, 624 B/1.2 KiB) next: @a => @b
|
zroot/usr STEPPING (step 1/2, 624 B/1.2 KiB) next: @a => @b
|
||||||
zroot/usr/home STEPPING (step 1/2, 123.3 MiB/135.3 MiB) next: @a => @b
|
* zroot/usr/home STEPPING (step 1/2, 123.3 MiB/135.3 MiB) next: @a => @b
|
||||||
zroot/var STEPPING (step 1/2, 624 B/1.2 KiB) next: @a => @b
|
zroot/var STEPPING (step 1/2, 624 B/1.2 KiB) next: @a => @b
|
||||||
zroot/var/audit DONE (step 2/2, 1.2 KiB/1.2 KiB)
|
zroot/var/audit DONE (step 2/2, 1.2 KiB/1.2 KiB)
|
||||||
zroot/var/crash DONE (step 2/2, 1.2 KiB/1.2 KiB)
|
zroot/var/crash DONE (step 2/2, 1.2 KiB/1.2 KiB)
|
||||||
@@ -66,6 +66,7 @@ Main Features
|
|||||||
* [x] Large blocks send & receive
|
* [x] Large blocks send & receive
|
||||||
* [x] Embedded data send & receive
|
* [x] Embedded data send & receive
|
||||||
* [x] Resume state send & receive
|
* [x] Resume state send & receive
|
||||||
|
* [x] Bandwidth limiting
|
||||||
|
|
||||||
* **Automatic snapshot management**
|
* **Automatic snapshot management**
|
||||||
|
|
||||||
|
|||||||
@@ -9,18 +9,29 @@ The fingerprint of the signing key is ``E101 418F D3D6 FBCB 9D65 A62D 7086 99FC
|
|||||||
It is available at `<https://zrepl.cschwarz.com/apt/apt-key.asc>`_ .
|
It is available at `<https://zrepl.cschwarz.com/apt/apt-key.asc>`_ .
|
||||||
Please open an issue in on GitHub if you encounter any issues with the repository.
|
Please open an issue in on GitHub if you encounter any issues with the repository.
|
||||||
|
|
||||||
The following snippet configure the repository for your Debian or Ubuntu release:
|
|
||||||
|
|
||||||
::
|
::
|
||||||
|
|
||||||
apt update && apt install curl gnupg lsb-release; \
|
(
|
||||||
ARCH="$(dpkg --print-architecture)"; \
|
set -ex
|
||||||
CODENAME="$(lsb_release -i -s | tr '[:upper:]' '[:lower:]') $(lsb_release -c -s | tr '[:upper:]' '[:lower:]')"; \
|
zrepl_apt_key_url=https://zrepl.cschwarz.com/apt/apt-key.asc
|
||||||
echo "Using Distro and Codename: $CODENAME"; \
|
zrepl_apt_key_dst=/usr/share/keyrings/zrepl.gpg
|
||||||
(curl https://zrepl.cschwarz.com/apt/apt-key.asc | apt-key add -) && \
|
zrepl_apt_repo_file=/etc/apt/sources.list.d/zrepl.list
|
||||||
(echo "deb [arch=$ARCH] https://zrepl.cschwarz.com/apt/$CODENAME main" > /etc/apt/sources.list.d/zrepl.list) && \
|
|
||||||
apt update
|
|
||||||
|
|
||||||
|
# Install dependencies for subsequent commands
|
||||||
|
sudo apt update && sudo apt install curl gnupg lsb-release
|
||||||
|
|
||||||
|
# Deploy the zrepl apt key.
|
||||||
|
curl -fsSL "$zrepl_apt_key_url" | tee | gpg --dearmor | sudo tee "$zrepl_apt_key_dst" > /dev/null
|
||||||
|
|
||||||
|
# Add the zrepl apt repo.
|
||||||
|
ARCH="$(dpkg --print-architecture)"
|
||||||
|
CODENAME="$(lsb_release -i -s | tr '[:upper:]' '[:lower:]') $(lsb_release -c -s | tr '[:upper:]' '[:lower:]')"
|
||||||
|
echo "Using Distro and Codename: $CODENAME"
|
||||||
|
echo "deb [arch=$ARCH signed-by=$zrepl_apt_key_dst] https://zrepl.cschwarz.com/apt/$CODENAME main" | sudo tee /etc/apt/sources.list.d/zrepl.list
|
||||||
|
|
||||||
|
# Update apt repos.
|
||||||
|
sudo apt update
|
||||||
|
)
|
||||||
|
|
||||||
.. NOTE::
|
.. NOTE::
|
||||||
|
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ Enable the zrepl daemon to start automatically at boot:
|
|||||||
|
|
||||||
sysrc zrepl_enable="YES"
|
sysrc zrepl_enable="YES"
|
||||||
|
|
||||||
|
Now jump to :ref:`the summary <installation-freebsd-jail-summary>` below.
|
||||||
|
|
||||||
Plugin
|
Plugin
|
||||||
######
|
######
|
||||||
@@ -134,7 +135,18 @@ Now ``zrepl`` can be started.
|
|||||||
|
|
||||||
service zrepl start
|
service zrepl start
|
||||||
|
|
||||||
|
Now jump to :ref:`the summary <installation-freebsd-jail-summary>` below.
|
||||||
|
|
||||||
|
.. _installation-freebsd-jail-summary:
|
||||||
|
|
||||||
Summary
|
Summary
|
||||||
-------
|
-------
|
||||||
|
|
||||||
Congratulations, you have a working jail!
|
Congratulations, you have a working jail!
|
||||||
|
|
||||||
|
.. NOTE::
|
||||||
|
|
||||||
|
With FreeBSD 13's transition to OpenZFS 2.0, please ensure that your jail's FreeBSD version matches the one in the kernel module.
|
||||||
|
If you are getting cryptic errors such as
|
||||||
|
``cannot receive new filesystem stream: invalid backup stream``
|
||||||
|
the instructions posted `here <https://github.com/zrepl/zrepl/issues/500#issuecomment-966215205>`_ might help.
|
||||||
|
|||||||
+11
-8
@@ -1,6 +1,5 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -eo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
|
|
||||||
NON_INTERACTIVE=false
|
NON_INTERACTIVE=false
|
||||||
DO_CLONE=false
|
DO_CLONE=false
|
||||||
@@ -13,7 +12,7 @@ while getopts "ca" arg; do
|
|||||||
DO_CLONE=true
|
DO_CLONE=true
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
echo invalid option
|
echo "invalid option '-$arg'"
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
@@ -27,11 +26,6 @@ checkout_repo_msg() {
|
|||||||
echo "clone ${GHPAGESREPO} to ${PUBLICDIR}:"
|
echo "clone ${GHPAGESREPO} to ${PUBLICDIR}:"
|
||||||
}
|
}
|
||||||
|
|
||||||
exit_msg() {
|
|
||||||
echo "error, exiting..."
|
|
||||||
}
|
|
||||||
trap exit_msg EXIT
|
|
||||||
|
|
||||||
if ! type sphinx-versioning >/dev/null; then
|
if ! type sphinx-versioning >/dev/null; then
|
||||||
echo "install sphinx-versioning and come back"
|
echo "install sphinx-versioning and come back"
|
||||||
exit 1
|
exit 1
|
||||||
@@ -71,6 +65,9 @@ git reset --hard origin/master
|
|||||||
|
|
||||||
echo "cleaning GitHub pages repo"
|
echo "cleaning GitHub pages repo"
|
||||||
git rm -rf .
|
git rm -rf .
|
||||||
|
cat > .gitignore <<EOF
|
||||||
|
**/.doctrees
|
||||||
|
EOF
|
||||||
|
|
||||||
popd
|
popd
|
||||||
|
|
||||||
@@ -96,7 +93,13 @@ COMMIT_MSG="sphinx-versioning render from publish.sh - $(date -u) - ${CURRENT_CO
|
|||||||
pushd "$PUBLICDIR"
|
pushd "$PUBLICDIR"
|
||||||
|
|
||||||
echo "adding and commiting all changes in GitHub pages repo"
|
echo "adding and commiting all changes in GitHub pages repo"
|
||||||
|
git add .gitignore
|
||||||
git add -A
|
git add -A
|
||||||
|
if [ "$(git status --porcelain)" != "" ]; then
|
||||||
git commit -m "$COMMIT_MSG"
|
git commit -m "$COMMIT_MSG"
|
||||||
|
else
|
||||||
|
echo "nothing to commit"
|
||||||
|
fi
|
||||||
|
echo "pushing to GitHub pages repo"
|
||||||
git push origin master
|
git push origin master
|
||||||
|
|
||||||
|
|||||||
@@ -51,6 +51,19 @@ We hope that you have found a configuration that fits your use case.
|
|||||||
Use ``zrepl configcheck`` once again to make sure the config is correct (output indicates that everything is fine).
|
Use ``zrepl configcheck`` once again to make sure the config is correct (output indicates that everything is fine).
|
||||||
Then restart the zrepl daemon on all systems involved in the replication, likely using ``service zrepl restart`` or ``systemctl restart zrepl``.
|
Then restart the zrepl daemon on all systems involved in the replication, likely using ``service zrepl restart`` or ``systemctl restart zrepl``.
|
||||||
|
|
||||||
|
.. WARNING::
|
||||||
|
|
||||||
|
Please :ref:`read up carefully <prune>` on the pruning rules before applying the config.
|
||||||
|
In particular, note that most example configs apply to all snapshots, not just zrepl-created snapshots.
|
||||||
|
Use the following keep rule on sender and receiver to prevent this:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
- type: regex
|
||||||
|
negate: true
|
||||||
|
regex: "^zrepl_.*" # <- the 'prefix' specified in snapshotting.prefix
|
||||||
|
|
||||||
|
|
||||||
Watch it Work
|
Watch it Work
|
||||||
=============
|
=============
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ We would like to thank the following people and organizations for supporting zre
|
|||||||
|
|
||||||
<div class="fa fa-code" style="width: 1em;"></div>
|
<div class="fa fa-code" style="width: 1em;"></div>
|
||||||
|
|
||||||
|
* |supporter-gold| Prominic.NET, Inc.
|
||||||
|
* |supporter-std| Torsten Blum
|
||||||
* |supporter-gold| Cyberiada GmbH
|
* |supporter-gold| Cyberiada GmbH
|
||||||
* |supporter-std| `Gordon Schulz <https://github.com/azmodude>`_
|
* |supporter-std| `Gordon Schulz <https://github.com/azmodude>`_
|
||||||
* |supporter-std| `@jwittlincohen <https://github.com/jwittlincohen>`_
|
* |supporter-std| `@jwittlincohen <https://github.com/jwittlincohen>`_
|
||||||
|
|||||||
@@ -78,3 +78,73 @@ Systemd Unit File
|
|||||||
A systemd service definition template is available in :repomasterlink:`dist/systemd`.
|
A systemd service definition template is available in :repomasterlink:`dist/systemd`.
|
||||||
Note that some of the options only work on recent versions of systemd.
|
Note that some of the options only work on recent versions of systemd.
|
||||||
Any help & improvements are very welcome, see :issue:`145`.
|
Any help & improvements are very welcome, see :issue:`145`.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
============
|
||||||
|
Ops Runbooks
|
||||||
|
============
|
||||||
|
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
|
||||||
|
usage/runbooks/migrating_sending_side_to_new_zpool.rst
|
||||||
|
|
||||||
|
|
||||||
|
.. _usage-platform-tests:
|
||||||
|
|
||||||
|
==============
|
||||||
|
Platform Tests
|
||||||
|
==============
|
||||||
|
|
||||||
|
Along with the main ``zrepl`` binary, we release the ``platformtest`` binaries.
|
||||||
|
The zrepl platform tests are an integration test suite that is complementary to the pure Go unit tests.
|
||||||
|
Any test that needs to interact with ZFS is a platform test.
|
||||||
|
|
||||||
|
The platform need to run as root.
|
||||||
|
For each test, we create a fresh dummy zpool backed by a file-based vdev.
|
||||||
|
The file path, and a root mountpoint for the dummy zpool, must be specified on the command line:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
mkdir -p /tmp/zreplplatformtest
|
||||||
|
./platformtest \
|
||||||
|
-poolname 'zreplplatformtest' \ # <- name must contain zreplplatformtest
|
||||||
|
-imagepath /tmp/zreplplatformtest.img \ # <- zrepl will create the file
|
||||||
|
-mountpoint /tmp/zreplplatformtest # <- must exist
|
||||||
|
|
||||||
|
|
||||||
|
.. WARNING::
|
||||||
|
|
||||||
|
``platformtest`` will unconditionally overwrite the file at `imagepath`
|
||||||
|
and unconditionally ``zpool destroy $poolname``.
|
||||||
|
So, don't use a production poolname, and consider running the test in a VM.
|
||||||
|
It'll be a lot faster as well because the underlying operations, ``zfs list`` in particular, will be faster.
|
||||||
|
|
||||||
|
|
||||||
|
While the platformtests are running, there will be a log of log output.
|
||||||
|
After all tests have run, it prints a summary with a list of tests, grouped by result type (success, failure, skipped):
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
PASSING TESTS:
|
||||||
|
github.com/zrepl/zrepl/platformtest/tests.BatchDestroy
|
||||||
|
github.com/zrepl/zrepl/platformtest/tests.CreateReplicationCursor
|
||||||
|
github.com/zrepl/zrepl/platformtest/tests.GetNonexistent
|
||||||
|
github.com/zrepl/zrepl/platformtest/tests.HoldsWork
|
||||||
|
...
|
||||||
|
github.com/zrepl/zrepl/platformtest/tests.SendStreamNonEOFReadErrorHandling
|
||||||
|
github.com/zrepl/zrepl/platformtest/tests.UndestroyableSnapshotParsing
|
||||||
|
SKIPPED TESTS:
|
||||||
|
github.com/zrepl/zrepl/platformtest/tests.SendArgsValidationEncryptedSendOfUnencryptedDatasetForbidden__EncryptionSupported_false
|
||||||
|
FAILED TESTS: []
|
||||||
|
|
||||||
|
|
||||||
|
If there is a failure, or a skipped test that you believe should be passing, re-run the test suite, capture stderr & stdout to a text file, and create an issue on GitHub.
|
||||||
|
|
||||||
|
To run a specific test case, or a subset of tests matched by regex, use the ``-run REGEX`` command line flag.
|
||||||
|
|
||||||
|
To stop test execution at the first failing test, and prevent cleanup of the dummy zpool, use the ``-failure.stop-and-keep-pool`` flag.
|
||||||
|
|
||||||
|
To build the platformtests yourself, use ``make test-platform-bin``.
|
||||||
|
There's also the ``make test-platform`` target to run the platform tests with a default command line.
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
|
||||||
|
Migrating Sending Side
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
**Objective**:
|
||||||
|
Move sending-side zpool to new hardware.
|
||||||
|
Make the move fully transparent to the sending-side jobs.
|
||||||
|
After the move is done, all sending-side zrepl jobs should continue to work as if the move had not happened.
|
||||||
|
In particular, incremental replication should be able to pick up where it left before the move.
|
||||||
|
|
||||||
|
Suppose we want to migrate all data from one zpool ``oldpool`` to another zpool ``newpool``.
|
||||||
|
A possible reason might be that we want to change RAID levels, ``ashift``, or just migrate over to next-gen hardware.
|
||||||
|
|
||||||
|
If the pool names are different, zrepl's matching between sender and receiver dataset will break becase the receive-side dataset names contain ``oldpool``.
|
||||||
|
To avoid this, we will need the name of the new pool to match that of the old pool.
|
||||||
|
The following steps will accomplish this:
|
||||||
|
|
||||||
|
1. Stop zrepl.
|
||||||
|
2. Create the new pool: ``zpool create newpool ...``
|
||||||
|
3. Take a snapshot of the old pool so that you have something that you can ``zfs send``.
|
||||||
|
For example, run ``zfs snapshot -r oldpool@migration_oldpool_newpool``.
|
||||||
|
4. Send all of the oldpool's datasets to the new pool:
|
||||||
|
``zfs send -R oldpool@migration_oldpool_newpool | zfs recv -F newpool``
|
||||||
|
5. Export the old pool: ``zpool export oldpool``
|
||||||
|
6. Export the new pool: ``zpool export newpool``
|
||||||
|
7. (Optional) Change the name of the old pool to something that does not conflict with the new pool.
|
||||||
|
We are going to use the name ``oldoldpool`` in this example.
|
||||||
|
Use ``zpool import`` with no arguments to see the pool id.
|
||||||
|
Then ``zpool import <id> oldoldpool && zpool export oldoldpool``.
|
||||||
|
8. Import the new pool, while changing the name to match the old pool: ``zpool import newpool oldpool``
|
||||||
|
9. Start zrepl again and wake up the relevant jobs.
|
||||||
|
10. Use ``zrepl status`` or you monitoring to ensure that replication works.
|
||||||
|
The best test is an end-to-end test where you write some junk data on a sender dataset and wait until a snapshot with that data appears on the receiving side.
|
||||||
|
11. Once you are confident that replication is working, you may dispose of the old pool.
|
||||||
|
|
||||||
|
Note that, depending on pruning rules, it will not be possible to switch back to the old pool seamlessly, i.e., without a full re-replication.
|
||||||
+159
-42
@@ -14,6 +14,7 @@ import (
|
|||||||
"github.com/zrepl/zrepl/daemon/logging/trace"
|
"github.com/zrepl/zrepl/daemon/logging/trace"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/replication/logic/pdu"
|
"github.com/zrepl/zrepl/replication/logic/pdu"
|
||||||
|
"github.com/zrepl/zrepl/util/bandwidthlimit"
|
||||||
"github.com/zrepl/zrepl/util/chainedio"
|
"github.com/zrepl/zrepl/util/chainedio"
|
||||||
"github.com/zrepl/zrepl/util/chainlock"
|
"github.com/zrepl/zrepl/util/chainlock"
|
||||||
"github.com/zrepl/zrepl/util/envconst"
|
"github.com/zrepl/zrepl/util/envconst"
|
||||||
@@ -34,6 +35,8 @@ type SenderConfig struct {
|
|||||||
SendCompressed bool
|
SendCompressed bool
|
||||||
SendEmbeddedData bool
|
SendEmbeddedData bool
|
||||||
SendSaved bool
|
SendSaved bool
|
||||||
|
|
||||||
|
BandwidthLimit bandwidthlimit.Config
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *SenderConfig) Validate() error {
|
func (c *SenderConfig) Validate() error {
|
||||||
@@ -44,6 +47,9 @@ func (c *SenderConfig) Validate() error {
|
|||||||
if _, err := StepHoldTag(c.JobID); err != nil {
|
if _, err := StepHoldTag(c.JobID); err != nil {
|
||||||
return fmt.Errorf("JobID cannot be used for hold tag: %s", err)
|
return fmt.Errorf("JobID cannot be used for hold tag: %s", err)
|
||||||
}
|
}
|
||||||
|
if err := bandwidthlimit.ValidateConfig(c.BandwidthLimit); err != nil {
|
||||||
|
return errors.Wrap(err, "`BandwidthLimit` field invalid")
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,16 +60,21 @@ type Sender struct {
|
|||||||
FSFilter zfs.DatasetFilter
|
FSFilter zfs.DatasetFilter
|
||||||
jobId JobID
|
jobId JobID
|
||||||
config SenderConfig
|
config SenderConfig
|
||||||
|
bwLimit bandwidthlimit.Wrapper
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSender(conf SenderConfig) *Sender {
|
func NewSender(conf SenderConfig) *Sender {
|
||||||
if err := conf.Validate(); err != nil {
|
if err := conf.Validate(); err != nil {
|
||||||
panic("invalid config" + err.Error())
|
panic("invalid config" + err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ratelimiter := bandwidthlimit.WrapperFromConfig(conf.BandwidthLimit)
|
||||||
|
|
||||||
return &Sender{
|
return &Sender{
|
||||||
FSFilter: conf.FSF,
|
FSFilter: conf.FSF,
|
||||||
jobId: conf.JobID,
|
jobId: conf.JobID,
|
||||||
config: conf,
|
config: conf,
|
||||||
|
bwLimit: ratelimiter,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,12 +159,11 @@ func sendArgsFromPDUAndValidateExistsAndGetVersion(ctx context.Context, fs strin
|
|||||||
return version, nil
|
return version, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Sender) Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, io.ReadCloser, error) {
|
func (s *Sender) sendMakeArgs(ctx context.Context, r *pdu.SendReq) (sendArgs zfs.ZFSSendArgsValidated, _ error) {
|
||||||
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
|
|
||||||
|
|
||||||
_, err := s.filterCheckFS(r.Filesystem)
|
_, err := s.filterCheckFS(r.Filesystem)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return sendArgs, err
|
||||||
}
|
}
|
||||||
switch r.Encrypted {
|
switch r.Encrypted {
|
||||||
case pdu.Tri_DontCare:
|
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
|
// ok, fallthrough outer
|
||||||
case pdu.Tri_False:
|
case pdu.Tri_False:
|
||||||
if s.config.Encrypt.B {
|
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
|
// fallthrough outer
|
||||||
case pdu.Tri_True:
|
case pdu.Tri_True:
|
||||||
if !s.config.Encrypt.B {
|
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
|
// fallthrough outer
|
||||||
default:
|
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{
|
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 {
|
if err != nil {
|
||||||
return nil, nil, errors.Wrap(err, "validate send arguments")
|
return sendArgs, errors.Wrap(err, "validate send arguments")
|
||||||
|
}
|
||||||
|
return sendArgs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
si, err := zfs.ZFSSendDry(ctx, sendArgs)
|
func (s *Sender) Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, io.ReadCloser, error) {
|
||||||
|
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
|
||||||
|
|
||||||
|
sendArgs, err := s.sendMakeArgs(ctx, r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, errors.Wrap(err, "zfs send dry failed")
|
return nil, nil, err
|
||||||
}
|
|
||||||
|
|
||||||
// From now on, assume that sendArgs has been validated by ZFSSendDry
|
|
||||||
// (because validation involves shelling out, it's actually a little expensive)
|
|
||||||
|
|
||||||
var expSize int64 = 0 // protocol says 0 means no estimate
|
|
||||||
if si.SizeEstimate != -1 { // but si returns -1 for no size estimate
|
|
||||||
expSize = si.SizeEstimate
|
|
||||||
}
|
|
||||||
res := &pdu.SendRes{
|
|
||||||
ExpectedSize: expSize,
|
|
||||||
UsedResumeToken: r.ResumeToken != "",
|
|
||||||
}
|
|
||||||
|
|
||||||
if r.DryRun {
|
|
||||||
return res, nil, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// create holds or bookmarks of `From` and `To` to guarantee one of the following:
|
// create holds or bookmarks of `From` and `To` to guarantee one of the following:
|
||||||
@@ -301,15 +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)
|
abstractionsCacheSingleton.TryBatchDestroy(ctx, s.jobId, sendArgs.FS, destroyTypes, keep, check)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
sendStream, err := zfs.ZFSSend(ctx, sendArgs)
|
var sendStream io.ReadCloser
|
||||||
|
sendStream, err = zfs.ZFSSend(ctx, sendArgs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// it's ok to not destroy the abstractions we just created here, a new send attempt will take care of it
|
// it's ok to not destroy the abstractions we just created here, a new send attempt will take care of it
|
||||||
return nil, nil, errors.Wrap(err, "zfs send failed")
|
return nil, nil, errors.Wrap(err, "zfs send failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// apply rate limit
|
||||||
|
sendStream = s.bwLimit.WrapReadCloser(sendStream)
|
||||||
|
|
||||||
|
res := &pdu.SendRes{
|
||||||
|
ExpectedSize: 0,
|
||||||
|
UsedResumeToken: r.ResumeToken != "",
|
||||||
|
}
|
||||||
|
|
||||||
return res, sendStream, nil
|
return res, sendStream, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Sender) SendDry(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, error) {
|
||||||
|
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
|
||||||
|
|
||||||
|
sendArgs, err := s.sendMakeArgs(ctx, r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
si, err := zfs.ZFSSendDry(ctx, sendArgs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "zfs send dry failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// From now on, assume that sendArgs has been validated by ZFSSendDry
|
||||||
|
// (because validation involves shelling out, it's actually a little expensive)
|
||||||
|
|
||||||
|
res := &pdu.SendRes{
|
||||||
|
ExpectedSize: si.SizeEstimate,
|
||||||
|
UsedResumeToken: r.ResumeToken != "",
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (p *Sender) SendCompleted(ctx context.Context, r *pdu.SendCompletedReq) (*pdu.SendCompletedRes, error) {
|
func (p *Sender) SendCompleted(ctx context.Context, r *pdu.SendCompletedReq) (*pdu.SendCompletedRes, error) {
|
||||||
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
|
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
|
||||||
|
|
||||||
@@ -439,8 +470,22 @@ type ReceiverConfig struct {
|
|||||||
|
|
||||||
InheritProperties []zfsprop.Property
|
InheritProperties []zfsprop.Property
|
||||||
OverrideProperties map[zfsprop.Property]string
|
OverrideProperties map[zfsprop.Property]string
|
||||||
|
|
||||||
|
BandwidthLimit bandwidthlimit.Config
|
||||||
|
|
||||||
|
PlaceholderEncryption PlaceholderCreationEncryptionProperty
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//go:generate enumer -type=PlaceholderCreationEncryptionProperty -transform=kebab -trimprefix=PlaceholderCreationEncryptionProperty
|
||||||
|
type PlaceholderCreationEncryptionProperty int
|
||||||
|
|
||||||
|
// Note: the constant names, transformed through enumer, are part of the config format!
|
||||||
|
const (
|
||||||
|
PlaceholderCreationEncryptionPropertyUnspecified PlaceholderCreationEncryptionProperty = 1 << iota
|
||||||
|
PlaceholderCreationEncryptionPropertyInherit
|
||||||
|
PlaceholderCreationEncryptionPropertyOff
|
||||||
|
)
|
||||||
|
|
||||||
func (c *ReceiverConfig) copyIn() {
|
func (c *ReceiverConfig) copyIn() {
|
||||||
c.RootWithoutClientComponent = c.RootWithoutClientComponent.Copy()
|
c.RootWithoutClientComponent = c.RootWithoutClientComponent.Copy()
|
||||||
|
|
||||||
@@ -475,6 +520,15 @@ func (c *ReceiverConfig) Validate() error {
|
|||||||
if c.RootWithoutClientComponent.Length() <= 0 {
|
if c.RootWithoutClientComponent.Length() <= 0 {
|
||||||
return errors.New("RootWithoutClientComponent must not be an empty dataset path")
|
return errors.New("RootWithoutClientComponent must not be an empty dataset path")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := bandwidthlimit.ValidateConfig(c.BandwidthLimit); err != nil {
|
||||||
|
return errors.Wrap(err, "`BandwidthLimit` field invalid")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !c.PlaceholderEncryption.IsAPlaceholderCreationEncryptionProperty() {
|
||||||
|
return errors.Errorf("`PlaceholderEncryption` field is invalid")
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -484,7 +538,11 @@ type Receiver struct {
|
|||||||
|
|
||||||
conf ReceiverConfig // validated
|
conf ReceiverConfig // validated
|
||||||
|
|
||||||
|
bwLimit bandwidthlimit.Wrapper
|
||||||
|
|
||||||
recvParentCreationMtx *chainlock.L
|
recvParentCreationMtx *chainlock.L
|
||||||
|
|
||||||
|
Test_OverrideClientIdentityFunc func() string // for use by platformtest
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewReceiver(config ReceiverConfig) *Receiver {
|
func NewReceiver(config ReceiverConfig) *Receiver {
|
||||||
@@ -495,6 +553,7 @@ func NewReceiver(config ReceiverConfig) *Receiver {
|
|||||||
return &Receiver{
|
return &Receiver{
|
||||||
conf: config,
|
conf: config,
|
||||||
recvParentCreationMtx: chainlock.New(),
|
recvParentCreationMtx: chainlock.New(),
|
||||||
|
bwLimit: bandwidthlimit.WrapperFromConfig(config.BandwidthLimit),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -521,10 +580,16 @@ func (s *Receiver) clientRootFromCtx(ctx context.Context) *zfs.DatasetPath {
|
|||||||
return s.conf.RootWithoutClientComponent.Copy()
|
return s.conf.RootWithoutClientComponent.Copy()
|
||||||
}
|
}
|
||||||
|
|
||||||
clientIdentity, ok := ctx.Value(ClientIdentityKey).(string)
|
var clientIdentity string
|
||||||
|
if s.Test_OverrideClientIdentityFunc != nil {
|
||||||
|
clientIdentity = s.Test_OverrideClientIdentityFunc()
|
||||||
|
} else {
|
||||||
|
var ok bool
|
||||||
|
clientIdentity, ok = ctx.Value(ClientIdentityKey).(string) // no shadow
|
||||||
if !ok {
|
if !ok {
|
||||||
panic("ClientIdentityKey context value must be set")
|
panic("ClientIdentityKey context value must be set")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
clientRoot, err := clientRoot(s.conf.RootWithoutClientComponent, clientIdentity)
|
clientRoot, err := clientRoot(s.conf.RootWithoutClientComponent, clientIdentity)
|
||||||
if err != nil {
|
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 {
|
if rphs, err := zfs.ZFSGetFilesystemPlaceholderState(ctx, s.conf.RootWithoutClientComponent); err != nil {
|
||||||
return nil, errors.Wrap(err, "cannot determine whether root_fs exists")
|
return nil, errors.Wrap(err, "cannot determine whether root_fs exists")
|
||||||
} else if !rphs.FSExists {
|
} else if !rphs.FSExists {
|
||||||
return nil, errors.New("root_fs does not exist")
|
getLogger(ctx).WithField("root_fs", s.conf.RootWithoutClientComponent).Error("root_fs does not exist")
|
||||||
|
return nil, errors.Errorf("root_fs does not exist")
|
||||||
}
|
}
|
||||||
|
|
||||||
root := s.clientRootFromCtx(ctx)
|
root := s.clientRootFromCtx(ctx)
|
||||||
@@ -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()")
|
return nil, nil, fmt.Errorf("receiver does not implement Send()")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Receiver) SendDry(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, error) {
|
||||||
|
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
|
||||||
|
return nil, fmt.Errorf("receiver does not implement SendDry()")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Receiver) receive_GetPlaceholderCreationEncryptionValue(client_root, path *zfs.DatasetPath) (zfs.FilesystemPlaceholderCreateEncryptionValue, error) {
|
||||||
|
if !s.conf.PlaceholderEncryption.IsAPlaceholderCreationEncryptionProperty() {
|
||||||
|
panic(s.conf.PlaceholderEncryption)
|
||||||
|
}
|
||||||
|
|
||||||
|
if client_root.Equal(path) && s.conf.PlaceholderEncryption == PlaceholderCreationEncryptionPropertyUnspecified {
|
||||||
|
// If our Receiver is configured to append a client component to s.conf.RootWithoutClientComponent
|
||||||
|
// then that dataset is always going to be a placeholder.
|
||||||
|
// We don't want to burden users with the concept of placeholders if their `filesystems` filter on the sender
|
||||||
|
// doesn't introduce any gaps.
|
||||||
|
// Since the dataset hierarchy up to and including that client component dataset is still fully controlled by us,
|
||||||
|
// using `inherit` is going to make it work in all expected use cases.
|
||||||
|
return zfs.FilesystemPlaceholderCreateEncryptionInherit, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch s.conf.PlaceholderEncryption {
|
||||||
|
case PlaceholderCreationEncryptionPropertyUnspecified:
|
||||||
|
return 0, fmt.Errorf("placeholder filesystem encryption handling is unspecified in receiver config")
|
||||||
|
case PlaceholderCreationEncryptionPropertyInherit:
|
||||||
|
return zfs.FilesystemPlaceholderCreateEncryptionInherit, nil
|
||||||
|
case PlaceholderCreationEncryptionPropertyOff:
|
||||||
|
return zfs.FilesystemPlaceholderCreateEncryptionOff, nil
|
||||||
|
default:
|
||||||
|
panic(s.conf.PlaceholderEncryption)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Receiver) Receive(ctx context.Context, req *pdu.ReceiveReq, receive io.ReadCloser) (*pdu.ReceiveRes, error) {
|
func (s *Receiver) Receive(ctx context.Context, req *pdu.ReceiveReq, receive io.ReadCloser) (*pdu.ReceiveRes, error) {
|
||||||
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
|
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
|
||||||
|
|
||||||
@@ -708,15 +806,18 @@ func (s *Receiver) Receive(ctx context.Context, req *pdu.ReceiveReq, receive io.
|
|||||||
if v.Path.Equal(lp) {
|
if v.Path.Equal(lp) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
l := getLogger(ctx).
|
||||||
|
WithField("placeholder_fs", v.Path.ToString()).
|
||||||
|
WithField("receive_fs", lp.ToString())
|
||||||
|
|
||||||
ph, err := zfs.ZFSGetFilesystemPlaceholderState(ctx, v.Path)
|
ph, err := zfs.ZFSGetFilesystemPlaceholderState(ctx, v.Path)
|
||||||
getLogger(ctx).
|
l.WithField("placeholder_state", fmt.Sprintf("%#v", ph)).
|
||||||
WithField("fs", v.Path.ToString()).
|
|
||||||
WithField("placeholder_state", fmt.Sprintf("%#v", ph)).
|
|
||||||
WithField("err", fmt.Sprintf("%s", err)).
|
WithField("err", fmt.Sprintf("%s", err)).
|
||||||
WithField("errType", fmt.Sprintf("%T", err)).
|
WithField("errType", fmt.Sprintf("%T", err)).
|
||||||
Debug("placeholder state for filesystem")
|
Debug("get placeholder state for filesystem")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
visitErr = err
|
visitErr = errors.Wrapf(err, "cannot get placeholder state of %s", v.Path.ToString())
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -727,21 +828,34 @@ func (s *Receiver) Receive(ctx context.Context, req *pdu.ReceiveReq, receive io.
|
|||||||
} else {
|
} else {
|
||||||
visitErr = fmt.Errorf("root_fs %q does not exist", s.conf.RootWithoutClientComponent.ToString())
|
visitErr = fmt.Errorf("root_fs %q does not exist", s.conf.RootWithoutClientComponent.ToString())
|
||||||
}
|
}
|
||||||
getLogger(ctx).WithError(visitErr).Error("placeholders are only created automatically below root_fs")
|
l.WithError(visitErr).Error("placeholders are only created automatically below root_fs")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
l := getLogger(ctx).WithField("placeholder_fs", v.Path)
|
|
||||||
l.Debug("create placeholder filesystem")
|
// compute the value lazily so that users who don't rely on
|
||||||
err := zfs.ZFSCreatePlaceholderFilesystem(ctx, v.Path, v.Parent.Path)
|
// placeholders can use the default value PlaceholderCreationEncryptionPropertyUnspecified
|
||||||
|
placeholderEncryption, err := s.receive_GetPlaceholderCreationEncryptionValue(root, v.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.WithError(err).Error("cannot create placeholder filesystem")
|
l.WithError(err).Error("cannot create placeholder filesystem") // logger already contains path
|
||||||
visitErr = err
|
visitErr = errors.Wrapf(err, "cannot create placeholder filesystem %s", v.Path.ToString())
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
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
|
return true // leave this fs as is
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}()
|
}()
|
||||||
getLogger(ctx).WithField("visitErr", visitErr).Debug("complete tree-walk")
|
getLogger(ctx).WithField("visitErr", visitErr).Debug("complete tree-walk")
|
||||||
@@ -787,6 +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")
|
return nil, errors.Wrap(err, "cannot determine whether we can use resumable send & recv")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// apply rate limit
|
||||||
|
receive = s.bwLimit.WrapReadCloser(receive)
|
||||||
|
|
||||||
var peek bytes.Buffer
|
var peek bytes.Buffer
|
||||||
var MaxPeek = envconst.Int64("ZREPL_ENDPOINT_RECV_PEEK_SIZE", 1<<20)
|
var MaxPeek = envconst.Int64("ZREPL_ENDPOINT_RECV_PEEK_SIZE", 1<<20)
|
||||||
log.WithField("max_peek_bytes", MaxPeek).Info("peeking incoming stream")
|
log.WithField("max_peek_bytes", MaxPeek).Info("peeking incoming stream")
|
||||||
|
|||||||
@@ -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,14 +5,16 @@ go 1.12
|
|||||||
require (
|
require (
|
||||||
github.com/fatih/color v1.7.0
|
github.com/fatih/color v1.7.0
|
||||||
github.com/gdamore/tcell v1.2.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/gitchander/permutation v0.0.0-20181107151852-9e56b92e9909
|
||||||
github.com/go-logfmt/logfmt v0.4.0
|
github.com/go-logfmt/logfmt v0.4.0
|
||||||
github.com/go-playground/universal-translator v0.17.0 // indirect
|
|
||||||
github.com/go-playground/validator v9.31.0+incompatible
|
github.com/go-playground/validator v9.31.0+incompatible
|
||||||
|
github.com/go-playground/validator/v10 v10.4.1
|
||||||
github.com/go-sql-driver/mysql v1.4.1-0.20190907122137-b2c03bcae3d4
|
github.com/go-sql-driver/mysql v1.4.1-0.20190907122137-b2c03bcae3d4
|
||||||
github.com/golang/protobuf v1.4.3
|
github.com/golang/protobuf v1.4.3
|
||||||
github.com/google/uuid v1.1.2
|
github.com/google/uuid v1.1.2
|
||||||
github.com/jinzhu/copier v0.0.0-20170922082739-db4671f3a9b8
|
github.com/jinzhu/copier v0.0.0-20170922082739-db4671f3a9b8
|
||||||
|
github.com/juju/ratelimit v1.0.1
|
||||||
github.com/kisielk/gotool v1.0.0 // indirect
|
github.com/kisielk/gotool v1.0.0 // indirect
|
||||||
github.com/kr/pretty v0.1.0
|
github.com/kr/pretty v0.1.0
|
||||||
github.com/leodido/go-urn v1.2.1 // indirect
|
github.com/leodido/go-urn v1.2.1 // indirect
|
||||||
@@ -37,10 +39,10 @@ require (
|
|||||||
github.com/yudai/gojsondiff v0.0.0-20170107030110-7b1b7adf999d
|
github.com/yudai/gojsondiff v0.0.0-20170107030110-7b1b7adf999d
|
||||||
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // go1.12 thinks it needs this
|
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // go1.12 thinks it needs this
|
||||||
github.com/zrepl/yaml-config v0.0.0-20191220194647-cbb6b0cf4bdd
|
github.com/zrepl/yaml-config v0.0.0-20191220194647-cbb6b0cf4bdd
|
||||||
|
gitlab.com/tslocum/cview v1.5.3
|
||||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777
|
golang.org/x/net v0.0.0-20210119194325-5f4716e94777
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58
|
||||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c
|
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c
|
||||||
golang.org/x/text v0.3.5 // indirect
|
|
||||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135
|
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135
|
||||||
gonum.org/v1/gonum v0.7.0 // indirect
|
gonum.org/v1/gonum v0.7.0 // indirect
|
||||||
google.golang.org/genproto v0.0.0-20210122163508-8081c04a3579 // indirect
|
google.golang.org/genproto v0.0.0-20210122163508-8081c04a3579 // indirect
|
||||||
|
|||||||
@@ -46,6 +46,11 @@ github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdk
|
|||||||
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
|
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
|
||||||
github.com/gdamore/tcell v1.2.0 h1:ikixzsxc8K8o3V2/CEmyoEW8mJZaNYQQ3NP3VIQdUe4=
|
github.com/gdamore/tcell v1.2.0 h1:ikixzsxc8K8o3V2/CEmyoEW8mJZaNYQQ3NP3VIQdUe4=
|
||||||
github.com/gdamore/tcell v1.2.0/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebKS4zMM=
|
github.com/gdamore/tcell v1.2.0/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebKS4zMM=
|
||||||
|
github.com/gdamore/tcell v1.4.0 h1:vUnHwJRvcPQa3tzi+0QI4U9JINXYJlOz9yiaiPQ2wMU=
|
||||||
|
github.com/gdamore/tcell/v2 v2.0.0-dev/go.mod h1:vSVL/GV5mCSlPC6thFP5kfOFdM9MGZcalipmpTxTgQA=
|
||||||
|
github.com/gdamore/tcell/v2 v2.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 h1:9NC8seTx6/zRmMTAdsHj/uOMi0EGHGQtjyLafBjk77Q=
|
||||||
github.com/gitchander/permutation v0.0.0-20181107151852-9e56b92e9909/go.mod h1:lP+DW8LR6Rw3ru9Vo2/y/3iiLaLWmofYql/va+7zJOk=
|
github.com/gitchander/permutation v0.0.0-20181107151852-9e56b92e9909/go.mod h1:lP+DW8LR6Rw3ru9Vo2/y/3iiLaLWmofYql/va+7zJOk=
|
||||||
github.com/go-critic/go-critic v0.3.4/go.mod h1:AHR42Lk/E/aOznsrYdMYeIQS5RH10HZHSqP+rD6AJrc=
|
github.com/go-critic/go-critic v0.3.4/go.mod h1:AHR42Lk/E/aOznsrYdMYeIQS5RH10HZHSqP+rD6AJrc=
|
||||||
@@ -59,12 +64,15 @@ github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9
|
|||||||
github.com/go-logfmt/logfmt v0.4.0 h1:MP4Eh7ZCb31lleYCFuwm0oe4/YGak+5l1vA2NOE80nA=
|
github.com/go-logfmt/logfmt v0.4.0 h1:MP4Eh7ZCb31lleYCFuwm0oe4/YGak+5l1vA2NOE80nA=
|
||||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||||
github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8=
|
github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8=
|
||||||
|
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
|
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
|
||||||
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||||
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
|
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/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 h1:UA72EPEogEnq76ehGdEDp4Mit+3FDh548oRqwVgNsHA=
|
||||||
github.com/go-playground/validator v9.31.0+incompatible/go.mod h1:yrEkQXlcI+PugkyDjY2bRrL/UBU4f3rvrgkN3V8JEig=
|
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 h1:0suja/iKSDbEIYLbrS/8C7iArJiWpgCNcR+zwAHu7Ig=
|
||||||
github.com/go-sql-driver/mysql v1.4.1-0.20190907122137-b2c03bcae3d4/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
github.com/go-sql-driver/mysql v1.4.1-0.20190907122137-b2c03bcae3d4/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||||
@@ -156,6 +164,8 @@ github.com/jinzhu/copier v0.0.0-20170922082739-db4671f3a9b8 h1:+dKzeuiDYbD/Cfi/s
|
|||||||
github.com/jinzhu/copier v0.0.0-20170922082739-db4671f3a9b8/go.mod h1:yL958EeXv8Ylng6IfnvG4oflryUi3vgA3xPs9hmII1s=
|
github.com/jinzhu/copier v0.0.0-20170922082739-db4671f3a9b8/go.mod h1:yL958EeXv8Ylng6IfnvG4oflryUi3vgA3xPs9hmII1s=
|
||||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||||
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||||
|
github.com/juju/ratelimit v1.0.1 h1:+7AIFJVQ0EQgq/K9+0Krm7m530Du7tIz0METWzN0RgY=
|
||||||
|
github.com/juju/ratelimit v1.0.1/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk=
|
||||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||||
github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
|
github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
|
||||||
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88 h1:uC1QfSlInpQF+M0ao65imhwqKnz3Q2z/d8PWZRMQvDM=
|
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88 h1:uC1QfSlInpQF+M0ao65imhwqKnz3Q2z/d8PWZRMQvDM=
|
||||||
@@ -177,6 +187,7 @@ 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/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 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
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 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
|
||||||
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
||||||
github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0=
|
github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0=
|
||||||
@@ -184,6 +195,8 @@ 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/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 h1:mCMFu6PgSozg9tDNMMK3g18oJBX7oYGrC09mS6CXfO4=
|
||||||
github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s=
|
github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s=
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac=
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
github.com/magiconair/properties v1.7.6/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
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/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 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4=
|
||||||
@@ -196,6 +209,10 @@ github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE
|
|||||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||||
github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y=
|
github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y=
|
||||||
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||||
|
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||||
|
github.com/mattn/go-runewidth v0.0.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/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw=
|
github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw=
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.0/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
github.com/matttproud/golang_protobuf_extensions v1.0.0/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
|
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
|
||||||
@@ -264,6 +281,9 @@ github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsT
|
|||||||
github.com/prometheus/procfs v0.0.5 h1:3+auTFlqw+ZaQYJARz6ArODtkaIwtvBTx3N2NehQlL8=
|
github.com/prometheus/procfs v0.0.5 h1:3+auTFlqw+ZaQYJARz6ArODtkaIwtvBTx3N2NehQlL8=
|
||||||
github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
|
github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
|
||||||
github.com/quasilyte/go-consistent v0.0.0-20190521200055-c6f3937de18c/go.mod h1:5STLWrekHfjyYwxBRVRXNOSewLJ3PWfDJd1VyTS21fI=
|
github.com/quasilyte/go-consistent v0.0.0-20190521200055-c6f3937de18c/go.mod h1:5STLWrekHfjyYwxBRVRXNOSewLJ3PWfDJd1VyTS21fI=
|
||||||
|
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
|
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||||
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
github.com/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/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/ryanuber/go-glob v0.0.0-20170128012129-256dc444b735/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
|
||||||
@@ -332,11 +352,17 @@ github.com/zrepl/yaml-config v0.0.0-20190928121844-af7ca3f8448f/go.mod h1:JmNwis
|
|||||||
github.com/zrepl/yaml-config v0.0.0-20191220194647-cbb6b0cf4bdd h1:SSo67WLS+99QESvbW8Meibz7zCrxshP71U9dH5KOCXM=
|
github.com/zrepl/yaml-config v0.0.0-20191220194647-cbb6b0cf4bdd h1:SSo67WLS+99QESvbW8Meibz7zCrxshP71U9dH5KOCXM=
|
||||||
github.com/zrepl/yaml-config v0.0.0-20191220194647-cbb6b0cf4bdd/go.mod h1:JmNwisZzOvW4GfpfLvhZ+gtyKLsIiA+WC+wNKJGJaFg=
|
github.com/zrepl/yaml-config v0.0.0-20191220194647-cbb6b0cf4bdd/go.mod h1:JmNwisZzOvW4GfpfLvhZ+gtyKLsIiA+WC+wNKJGJaFg=
|
||||||
github.com/zrepl/zrepl v0.2.0/go.mod h1:M3Zv2IGSO8iYpUjsZD6ayZ2LHy7zyMfzet9XatKOrZ8=
|
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-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-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/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-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
@@ -357,6 +383,7 @@ golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73r
|
|||||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980 h1:dfGZHvZk057jK2MCeWus/TowKpJ8y4AmooUzdBSR9GU=
|
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980 h1:dfGZHvZk057jK2MCeWus/TowKpJ8y4AmooUzdBSR9GU=
|
||||||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777 h1:003p0dJM77cxMSyCPFphvZf/Y5/NXf5fzg6ufd1/Oew=
|
golang.org/x/net v0.0.0-20210119194325-5f4716e94777 h1:003p0dJM77cxMSyCPFphvZf/Y5/NXf5fzg6ufd1/Oew=
|
||||||
@@ -376,6 +403,7 @@ golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5h
|
|||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-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-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 h1:9nuHUbU8dRnRRfj9KjWUVrJeoexdbeMjttk6Oh1rD10=
|
||||||
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
@@ -383,10 +411,14 @@ golang.org/x/sys v0.0.0-20191010194322-b09406accb47 h1:/XfQ9z7ib8eEJX2hdgFTZJ/nt
|
|||||||
golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
|
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
|
||||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
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-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 h1:VwygUrnw9jn88c4u8GD3rZQbqrP/tgas88tPUbBxQrk=
|
||||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
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/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf h1:MZ2shdL+ZM/XzY3ZGOnh4Nlpnxz5GSOhOmtHo3iPU6M=
|
||||||
|
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/text v0.0.0-20170915090833-1cbadb444a80/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.0.0-20170915090833-1cbadb444a80/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
|||||||
@@ -4,12 +4,13 @@ package main
|
|||||||
import (
|
import (
|
||||||
"github.com/zrepl/zrepl/cli"
|
"github.com/zrepl/zrepl/cli"
|
||||||
"github.com/zrepl/zrepl/client"
|
"github.com/zrepl/zrepl/client"
|
||||||
|
"github.com/zrepl/zrepl/client/status"
|
||||||
"github.com/zrepl/zrepl/daemon"
|
"github.com/zrepl/zrepl/daemon"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
cli.AddSubcommand(daemon.DaemonCmd)
|
cli.AddSubcommand(daemon.DaemonCmd)
|
||||||
cli.AddSubcommand(client.StatusCmd)
|
cli.AddSubcommand(status.Subcommand)
|
||||||
cli.AddSubcommand(client.SignalCmd)
|
cli.AddSubcommand(client.SignalCmd)
|
||||||
cli.AddSubcommand(client.StdinserverCmd)
|
cli.AddSubcommand(client.StdinserverCmd)
|
||||||
cli.AddSubcommand(client.ConfigcheckCmd)
|
cli.AddSubcommand(client.ConfigcheckCmd)
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
FROM debian:latest
|
FROM debian:latest
|
||||||
|
|
||||||
|
# binutils are for cross-compilation to work in bullseye
|
||||||
RUN apt-get update && apt-get install -y \
|
RUN apt-get update && apt-get install -y \
|
||||||
build-essential \
|
build-essential \
|
||||||
devscripts \
|
devscripts \
|
||||||
dh-exec
|
dh-exec \
|
||||||
|
binutils-aarch64-linux-gnu \
|
||||||
|
binutils-arm-linux-gnueabihf \
|
||||||
|
binutils-i686-linux-gnu
|
||||||
|
|
||||||
RUN mkdir -p /build/src && chmod -R 0777 /build
|
RUN mkdir -p /build/src && chmod -R 0777 /build
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ func main() {
|
|||||||
flag.StringVar(&args.CreateArgs.Mountpoint, "mountpoint", "", "")
|
flag.StringVar(&args.CreateArgs.Mountpoint, "mountpoint", "", "")
|
||||||
flag.BoolVar(&args.StopAndKeepPoolOnFail, "failure.stop-and-keep-pool", false, "if a test case fails, stop test execution and keep pool as it was when the test failed")
|
flag.BoolVar(&args.StopAndKeepPoolOnFail, "failure.stop-and-keep-pool", false, "if a test case fails, stop test execution and keep pool as it was when the test failed")
|
||||||
flag.StringVar(&args.Run, "run", "", "")
|
flag.StringVar(&args.Run, "run", "", "")
|
||||||
|
flag.DurationVar(&platformtest.ZpoolExportTimeout, "zfs.zpool-export-timeout", platformtest.ZpoolExportTimeout, "")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
if err := HarnessRun(args); err != nil {
|
if err := HarnessRun(args); err != nil {
|
||||||
|
|||||||
@@ -5,12 +5,17 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/zfs"
|
"github.com/zrepl/zrepl/zfs"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var ZpoolExportTimeout time.Duration = 500 * time.Millisecond
|
||||||
|
|
||||||
type Zpool struct {
|
type Zpool struct {
|
||||||
args ZpoolCreateArgs
|
args ZpoolCreateArgs
|
||||||
}
|
}
|
||||||
@@ -93,9 +98,24 @@ func (p *Zpool) Name() string { return p.args.PoolName }
|
|||||||
|
|
||||||
func (p *Zpool) Destroy(ctx context.Context, e Execer) error {
|
func (p *Zpool) Destroy(ctx context.Context, e Execer) error {
|
||||||
|
|
||||||
if err := e.RunExpectSuccessNoOutput(ctx, "zpool", "export", p.args.PoolName); err != nil {
|
exportDeadline := time.Now().Add(ZpoolExportTimeout)
|
||||||
|
|
||||||
|
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)
|
return errors.Wrapf(err, "export pool %q", p.args.PoolName)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if err := os.Remove(p.args.ImagePath); err != nil {
|
if err := os.Remove(p.args.ImagePath); err != nil {
|
||||||
return errors.Wrapf(err, "remove pool image")
|
return errors.Wrapf(err, "remove pool image")
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ var Cases = []Case{BatchDestroy,
|
|||||||
ReplicationIsResumableFullSend__both_GuaranteeResumability,
|
ReplicationIsResumableFullSend__both_GuaranteeResumability,
|
||||||
ReplicationIsResumableFullSend__initial_GuaranteeIncrementalReplication_incremental_GuaranteeIncrementalReplication,
|
ReplicationIsResumableFullSend__initial_GuaranteeIncrementalReplication_incremental_GuaranteeIncrementalReplication,
|
||||||
ReplicationIsResumableFullSend__initial_GuaranteeResumability_incremental_GuaranteeIncrementalReplication,
|
ReplicationIsResumableFullSend__initial_GuaranteeResumability_incremental_GuaranteeIncrementalReplication,
|
||||||
|
ReplicationPlaceholderEncryption__EncryptOnReceiverUseCase__WorksIfConfiguredWithInherit,
|
||||||
|
ReplicationPlaceholderEncryption__UnspecifiedIsOkForClientIdentityPlaceholder,
|
||||||
|
ReplicationPlaceholderEncryption__UnspecifiedLeadsToFailureAtRuntimeWhenCreatingPlaceholders,
|
||||||
ReplicationPropertyReplicationWorks,
|
ReplicationPropertyReplicationWorks,
|
||||||
ReplicationReceiverErrorWhileStillSending,
|
ReplicationReceiverErrorWhileStillSending,
|
||||||
ReplicationStepCompletedLostBehavior__GuaranteeIncrementalReplication,
|
ReplicationStepCompletedLostBehavior__GuaranteeIncrementalReplication,
|
||||||
@@ -34,5 +37,10 @@ var Cases = []Case{BatchDestroy,
|
|||||||
SendArgsValidationEncryptedSendOfUnencryptedDatasetForbidden__EncryptionSupported_true,
|
SendArgsValidationEncryptedSendOfUnencryptedDatasetForbidden__EncryptionSupported_true,
|
||||||
SendArgsValidationResumeTokenDifferentFilesystemForbidden,
|
SendArgsValidationResumeTokenDifferentFilesystemForbidden,
|
||||||
SendArgsValidationResumeTokenEncryptionMismatchForbidden,
|
SendArgsValidationResumeTokenEncryptionMismatchForbidden,
|
||||||
|
SendStreamCloseAfterBlockedOnPipeWrite,
|
||||||
|
SendStreamCloseAfterEOFRead,
|
||||||
|
SendStreamMultipleCloseAfterEOF,
|
||||||
|
SendStreamMultipleCloseBeforeEOF,
|
||||||
|
SendStreamNonEOFReadErrorHandling,
|
||||||
UndestroyableSnapshotParsing,
|
UndestroyableSnapshotParsing,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/daemon/filters"
|
||||||
"github.com/zrepl/zrepl/platformtest"
|
"github.com/zrepl/zrepl/platformtest"
|
||||||
"github.com/zrepl/zrepl/util/limitio"
|
"github.com/zrepl/zrepl/util/limitio"
|
||||||
"github.com/zrepl/zrepl/zfs"
|
"github.com/zrepl/zrepl/zfs"
|
||||||
@@ -174,3 +175,8 @@ func datasetToStringSortedTrimPrefix(prefix *zfs.DatasetPath, paths []*zfs.Datas
|
|||||||
sort.Strings(pstrs)
|
sort.Strings(pstrs)
|
||||||
return pstrs
|
return pstrs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func mustAddToSFilter(ctx *platformtest.Context, f *filters.DatasetMapFilter, fs string) {
|
||||||
|
err := f.Add(fs, "ok")
|
||||||
|
require.NoError(ctx, err)
|
||||||
|
}
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ func ReceiveForceRollbackWorksUnencrypted(ctx *platformtest.Context) {
|
|||||||
|
|
||||||
sendStream, err := zfs.ZFSSend(ctx, sendArgs)
|
sendStream, err := zfs.ZFSSend(ctx, sendArgs)
|
||||||
require.NoError(ctx, err)
|
require.NoError(ctx, err)
|
||||||
|
defer sendStream.Close()
|
||||||
|
|
||||||
recvOpts := zfs.RecvOptions{
|
recvOpts := zfs.RecvOptions{
|
||||||
RollbackAndForceRecv: true,
|
RollbackAndForceRecv: true,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/kr/pretty"
|
"github.com/kr/pretty"
|
||||||
@@ -20,6 +21,7 @@ import (
|
|||||||
"github.com/zrepl/zrepl/replication/logic"
|
"github.com/zrepl/zrepl/replication/logic"
|
||||||
"github.com/zrepl/zrepl/replication/logic/pdu"
|
"github.com/zrepl/zrepl/replication/logic/pdu"
|
||||||
"github.com/zrepl/zrepl/replication/report"
|
"github.com/zrepl/zrepl/replication/report"
|
||||||
|
"github.com/zrepl/zrepl/util/bandwidthlimit"
|
||||||
"github.com/zrepl/zrepl/util/limitio"
|
"github.com/zrepl/zrepl/util/limitio"
|
||||||
"github.com/zrepl/zrepl/util/nodefault"
|
"github.com/zrepl/zrepl/util/nodefault"
|
||||||
"github.com/zrepl/zrepl/zfs"
|
"github.com/zrepl/zrepl/zfs"
|
||||||
@@ -66,6 +68,7 @@ func (i replicationInvocation) Do(ctx *platformtest.Context) *report.Report {
|
|||||||
FSF: i.sfilter.AsFilter(),
|
FSF: i.sfilter.AsFilter(),
|
||||||
Encrypt: &nodefault.Bool{B: false},
|
Encrypt: &nodefault.Bool{B: false},
|
||||||
JobID: i.sjid,
|
JobID: i.sjid,
|
||||||
|
BandwidthLimit: bandwidthlimit.NoLimitConfig(),
|
||||||
}
|
}
|
||||||
if i.senderConfigHook != nil {
|
if i.senderConfigHook != nil {
|
||||||
i.senderConfigHook(&senderConfig)
|
i.senderConfigHook(&senderConfig)
|
||||||
@@ -75,6 +78,8 @@ func (i replicationInvocation) Do(ctx *platformtest.Context) *report.Report {
|
|||||||
JobID: i.rjid,
|
JobID: i.rjid,
|
||||||
AppendClientIdentity: false,
|
AppendClientIdentity: false,
|
||||||
RootWithoutClientComponent: mustDatasetPath(i.rfsRoot),
|
RootWithoutClientComponent: mustDatasetPath(i.rfsRoot),
|
||||||
|
BandwidthLimit: bandwidthlimit.NoLimitConfig(),
|
||||||
|
PlaceholderEncryption: endpoint.PlaceholderCreationEncryptionPropertyUnspecified,
|
||||||
}
|
}
|
||||||
if i.receiverConfigHook != nil {
|
if i.receiverConfigHook != nil {
|
||||||
i.receiverConfigHook(&receiverConfig)
|
i.receiverConfigHook(&receiverConfig)
|
||||||
@@ -90,6 +95,7 @@ func (i replicationInvocation) Do(ctx *platformtest.Context) *report.Report {
|
|||||||
ReplicationConfig: &pdu.ReplicationConfig{
|
ReplicationConfig: &pdu.ReplicationConfig{
|
||||||
Protection: i.guarantee,
|
Protection: i.guarantee,
|
||||||
},
|
},
|
||||||
|
SizeEstimationConcurrency: 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
report, wait := replication.Do(
|
report, wait := replication.Do(
|
||||||
@@ -804,13 +810,22 @@ type NeverEndingSender struct {
|
|||||||
*endpoint.Sender
|
*endpoint.Sender
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *NeverEndingSender) SendDry(ctx context.Context, req *pdu.SendReq) (r *pdu.SendRes, err error) {
|
||||||
|
r, _, err = s.sendImpl(ctx, req, true)
|
||||||
|
return r, err
|
||||||
|
}
|
||||||
|
|
||||||
func (s *NeverEndingSender) Send(ctx context.Context, req *pdu.SendReq) (r *pdu.SendRes, stream io.ReadCloser, _ error) {
|
func (s *NeverEndingSender) Send(ctx context.Context, req *pdu.SendReq) (r *pdu.SendRes, stream io.ReadCloser, _ error) {
|
||||||
|
return s.sendImpl(ctx, req, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *NeverEndingSender) sendImpl(ctx context.Context, req *pdu.SendReq, dry bool) (r *pdu.SendRes, stream io.ReadCloser, _ error) {
|
||||||
stream = nil
|
stream = nil
|
||||||
r = &pdu.SendRes{
|
r = &pdu.SendRes{
|
||||||
UsedResumeToken: false,
|
UsedResumeToken: false,
|
||||||
ExpectedSize: 1 << 30,
|
ExpectedSize: 1 << 30,
|
||||||
}
|
}
|
||||||
if req.DryRun {
|
if dry {
|
||||||
return r, stream, nil
|
return r, stream, nil
|
||||||
}
|
}
|
||||||
dz, err := os.Open("/dev/zero")
|
dz, err := os.Open("/dev/zero")
|
||||||
@@ -889,13 +904,9 @@ func ReplicationFailingInitialParentProhibitsChildReplication(ctx *platformtest.
|
|||||||
fsAA := ctx.RootDataset + "/sender/aa"
|
fsAA := ctx.RootDataset + "/sender/aa"
|
||||||
|
|
||||||
sfilter := filters.NewDatasetMapFilter(3, true)
|
sfilter := filters.NewDatasetMapFilter(3, true)
|
||||||
mustAddToSFilter := func(fs string) {
|
mustAddToSFilter(ctx, sfilter, fsA)
|
||||||
err := sfilter.Add(fs, "ok")
|
mustAddToSFilter(ctx, sfilter, fsAChild)
|
||||||
require.NoError(ctx, err)
|
mustAddToSFilter(ctx, sfilter, fsAA)
|
||||||
}
|
|
||||||
mustAddToSFilter(fsA)
|
|
||||||
mustAddToSFilter(fsAChild)
|
|
||||||
mustAddToSFilter(fsAA)
|
|
||||||
rfsRoot := ctx.RootDataset + "/receiver"
|
rfsRoot := ctx.RootDataset + "/receiver"
|
||||||
|
|
||||||
mockRecvErr := fmt.Errorf("yifae4ohPhaquaes0hohghiep9oufie4roo7quoWooluaj2ee8")
|
mockRecvErr := fmt.Errorf("yifae4ohPhaquaes0hohghiep9oufie4roo7quoWooluaj2ee8")
|
||||||
@@ -951,6 +962,7 @@ func ReplicationPropertyReplicationWorks(ctx *platformtest.Context) {
|
|||||||
+ "sender/a/child"
|
+ "sender/a/child"
|
||||||
+ "sender/a/child@1"
|
+ "sender/a/child@1"
|
||||||
+ "receiver"
|
+ "receiver"
|
||||||
|
R zfs create -p "${ROOTDS}/receiver/${ROOTDS}/sender"
|
||||||
`)
|
`)
|
||||||
|
|
||||||
sjid := endpoint.MustMakeJobID("sender-job")
|
sjid := endpoint.MustMakeJobID("sender-job")
|
||||||
@@ -960,12 +972,8 @@ func ReplicationPropertyReplicationWorks(ctx *platformtest.Context) {
|
|||||||
fsAChild := ctx.RootDataset + "/sender/a/child"
|
fsAChild := ctx.RootDataset + "/sender/a/child"
|
||||||
|
|
||||||
sfilter := filters.NewDatasetMapFilter(2, true)
|
sfilter := filters.NewDatasetMapFilter(2, true)
|
||||||
mustAddToSFilter := func(fs string) {
|
mustAddToSFilter(ctx, sfilter, fsA)
|
||||||
err := sfilter.Add(fs, "ok")
|
mustAddToSFilter(ctx, sfilter, fsAChild)
|
||||||
require.NoError(ctx, err)
|
|
||||||
}
|
|
||||||
mustAddToSFilter(fsA)
|
|
||||||
mustAddToSFilter(fsAChild)
|
|
||||||
rfsRoot := ctx.RootDataset + "/receiver"
|
rfsRoot := ctx.RootDataset + "/receiver"
|
||||||
|
|
||||||
type testPropExpectation struct {
|
type testPropExpectation struct {
|
||||||
@@ -1054,6 +1062,9 @@ func ReplicationPropertyReplicationWorks(ctx *platformtest.Context) {
|
|||||||
rep := fsByName[fs]
|
rep := fsByName[fs]
|
||||||
require.Len(ctx, rep.Steps, 1)
|
require.Len(ctx, rep.Steps, 1)
|
||||||
require.Nil(ctx, rep.PlanError)
|
require.Nil(ctx, rep.PlanError)
|
||||||
|
if rep.StepError != nil && strings.Contains(rep.StepError.Error(), "invalid option 'x'") {
|
||||||
|
ctx.SkipNow() // XXX feature detection
|
||||||
|
}
|
||||||
require.Nil(ctx, rep.StepError)
|
require.Nil(ctx, rep.StepError)
|
||||||
require.Len(ctx, rep.Steps, 1)
|
require.Len(ctx, rep.Steps, 1)
|
||||||
require.Equal(ctx, 1, rep.CurrentStep)
|
require.Equal(ctx, 1, rep.CurrentStep)
|
||||||
@@ -1090,3 +1101,208 @@ 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"))
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)
|
// then lexicographically descending (e.g. b, a)
|
||||||
return strings.Compare(matching[i].Name(), matching[j].Name()) == 1
|
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
|
return destroyList
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,6 +83,19 @@ func TestKeepLastN(t *testing.T) {
|
|||||||
"b1": true,
|
"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)
|
testTable(tcs, t)
|
||||||
|
|||||||
@@ -151,6 +151,8 @@ type fs struct {
|
|||||||
|
|
||||||
l *chainlock.L
|
l *chainlock.L
|
||||||
|
|
||||||
|
blockedOn report.FsBlockedOn
|
||||||
|
|
||||||
// ordering relationship that must be maintained for initial replication
|
// ordering relationship that must be maintained for initial replication
|
||||||
initialRepOrd struct {
|
initialRepOrd struct {
|
||||||
parents, children []*fs
|
parents, children []*fs
|
||||||
@@ -158,6 +160,7 @@ type fs struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
planning struct {
|
planning struct {
|
||||||
|
waitingForStepQueue bool
|
||||||
done bool
|
done bool
|
||||||
err *timedError
|
err *timedError
|
||||||
}
|
}
|
||||||
@@ -339,6 +342,7 @@ func (a *attempt) doGlobalPlanning(ctx context.Context, prev *attempt) map[*fs]*
|
|||||||
fs := &fs{
|
fs := &fs{
|
||||||
fs: pfs,
|
fs: pfs,
|
||||||
l: a.l,
|
l: a.l,
|
||||||
|
blockedOn: report.FsBlockedOnNothing,
|
||||||
}
|
}
|
||||||
fs.initialRepOrd.parentDidUpdate = make(chan struct{}, 1)
|
fs.initialRepOrd.parentDidUpdate = make(chan struct{}, 1)
|
||||||
a.fss = append(a.fss, fs)
|
a.fss = append(a.fss, fs)
|
||||||
@@ -448,6 +452,10 @@ func (a *attempt) doFilesystems(ctx context.Context, prevs map[*fs]*fs) {
|
|||||||
ctx, endTask := trace.WithTaskAndSpan(ctx, "repl-fs", f.report().Info.Name)
|
ctx, endTask := trace.WithTaskAndSpan(ctx, "repl-fs", f.report().Info.Name)
|
||||||
defer endTask()
|
defer endTask()
|
||||||
f.do(ctx, stepQueue, prevs[f])
|
f.do(ctx, stepQueue, prevs[f])
|
||||||
|
f.l.HoldWhile(func() {
|
||||||
|
// every return from f means it's unblocked...
|
||||||
|
f.blockedOn = report.FsBlockedOnNothing
|
||||||
|
})
|
||||||
}(f)
|
}(f)
|
||||||
}
|
}
|
||||||
a.l.DropWhile(func() {
|
a.l.DropWhile(func() {
|
||||||
@@ -486,11 +494,16 @@ func (f *fs) do(ctx context.Context, pq *stepQueue, prev *fs) {
|
|||||||
var psteps []Step
|
var psteps []Step
|
||||||
var errTime time.Time
|
var errTime time.Time
|
||||||
var err error
|
var err error
|
||||||
|
f.blockedOn = report.FsBlockedOnPlanningStepQueue
|
||||||
f.l.DropWhile(func() {
|
f.l.DropWhile(func() {
|
||||||
// TODO hacky
|
// TODO hacky
|
||||||
// choose target time that is earlier than any snapshot, so fs planning is always prioritized
|
// choose target time that is earlier than any snapshot, so fs planning is always prioritized
|
||||||
targetDate := time.Unix(0, 0)
|
targetDate := time.Unix(0, 0)
|
||||||
defer pq.WaitReady(ctx, f, targetDate)()
|
defer pq.WaitReady(ctx, f, targetDate)()
|
||||||
|
f.l.HoldWhile(func() {
|
||||||
|
// transition before we call PlanFS
|
||||||
|
f.blockedOn = report.FsBlockedOnNothing
|
||||||
|
})
|
||||||
psteps, err = f.fs.PlanFS(ctx) // no shadow
|
psteps, err = f.fs.PlanFS(ctx) // no shadow
|
||||||
errTime = time.Now() // no shadow
|
errTime = time.Now() // no shadow
|
||||||
})
|
})
|
||||||
@@ -545,6 +558,7 @@ func (f *fs) do(ctx context.Context, pq *stepQueue, prev *fs) {
|
|||||||
f.planning.done = true
|
f.planning.done = true
|
||||||
|
|
||||||
// wait for parents' initial replication
|
// wait for parents' initial replication
|
||||||
|
f.blockedOn = report.FsBlockedOnParentInitialRepl
|
||||||
var parents []string
|
var parents []string
|
||||||
for _, p := range f.initialRepOrd.parents {
|
for _, p := range f.initialRepOrd.parents {
|
||||||
parents = append(parents, p.fs.ReportInfo().Name)
|
parents = append(parents, p.fs.ReportInfo().Name)
|
||||||
@@ -613,7 +627,6 @@ func (f *fs) do(ctx context.Context, pq *stepQueue, prev *fs) {
|
|||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
f.planned.stepErr = newTimedError(ctx.Err(), time.Now())
|
f.planned.stepErr = newTimedError(ctx.Err(), time.Now())
|
||||||
return
|
|
||||||
case <-f.initialRepOrd.parentDidUpdate:
|
case <-f.initialRepOrd.parentDidUpdate:
|
||||||
// loop
|
// loop
|
||||||
}
|
}
|
||||||
@@ -631,7 +644,9 @@ func (f *fs) do(ctx context.Context, pq *stepQueue, prev *fs) {
|
|||||||
f.l.DropWhile(func() {
|
f.l.DropWhile(func() {
|
||||||
// wait for parallel replication
|
// wait for parallel replication
|
||||||
targetDate := s.step.TargetDate()
|
targetDate := s.step.TargetDate()
|
||||||
|
f.l.HoldWhile(func() { f.blockedOn = report.FsBlockedOnReplStepQueue })
|
||||||
defer pq.WaitReady(ctx, f, targetDate)()
|
defer pq.WaitReady(ctx, f, targetDate)()
|
||||||
|
f.l.HoldWhile(func() { f.blockedOn = report.FsBlockedOnNothing })
|
||||||
// do the step
|
// do the step
|
||||||
ctx, endSpan := trace.WithSpan(ctx, fmt.Sprintf("%#v", s.step.ReportInfo()))
|
ctx, endSpan := trace.WithSpan(ctx, fmt.Sprintf("%#v", s.step.ReportInfo()))
|
||||||
defer endSpan()
|
defer endSpan()
|
||||||
@@ -725,6 +740,7 @@ func (f *fs) report() *report.FilesystemReport {
|
|||||||
r := &report.FilesystemReport{
|
r := &report.FilesystemReport{
|
||||||
Info: f.fs.ReportInfo(),
|
Info: f.fs.ReportInfo(),
|
||||||
State: state,
|
State: state,
|
||||||
|
BlockedOn: f.blockedOn,
|
||||||
PlanError: f.planning.err.IntoReportError(),
|
PlanError: f.planning.err.IntoReportError(),
|
||||||
StepError: f.planned.stepErr.IntoReportError(),
|
StepError: f.planned.stepErr.IntoReportError(),
|
||||||
Steps: make([]*report.StepReport, len(f.planned.steps)),
|
Steps: make([]*report.StepReport, len(f.planned.steps)),
|
||||||
|
|||||||
@@ -14,11 +14,12 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/daemon/logging/trace"
|
"github.com/zrepl/zrepl/daemon/logging/trace"
|
||||||
|
"github.com/zrepl/zrepl/util/zreplcircleci"
|
||||||
)
|
)
|
||||||
|
|
||||||
// FIXME: this test relies on timing and is thus rather flaky
|
|
||||||
// (relies on scheduler responsiveness of < 500ms)
|
|
||||||
func TestPqNotconcurrent(t *testing.T) {
|
func TestPqNotconcurrent(t *testing.T) {
|
||||||
|
zreplcircleci.SkipOnCircleCI(t, "because it relies on scheduler responsiveness < 500ms")
|
||||||
|
|
||||||
ctx, end := trace.WithTaskFromStack(context.Background())
|
ctx, end := trace.WithTaskFromStack(context.Background())
|
||||||
defer end()
|
defer end()
|
||||||
var ctr uint32
|
var ctr uint32
|
||||||
@@ -90,6 +91,8 @@ func (r record) String() string {
|
|||||||
// Hence, perform some statistics on the wakeup times and assert that the mean wakeup
|
// Hence, perform some statistics on the wakeup times and assert that the mean wakeup
|
||||||
// times for each step are close together.
|
// times for each step are close together.
|
||||||
func TestPqConcurrent(t *testing.T) {
|
func TestPqConcurrent(t *testing.T) {
|
||||||
|
zreplcircleci.SkipOnCircleCI(t, "because it relies on scheduler responsiveness < 500ms")
|
||||||
|
|
||||||
ctx, end := trace.WithTaskFromStack(context.Background())
|
ctx, end := trace.WithTaskFromStack(context.Background())
|
||||||
defer end()
|
defer end()
|
||||||
|
|
||||||
|
|||||||
+134
-152
@@ -518,8 +518,7 @@ type SendReq struct {
|
|||||||
// encoded in the ResumeToken. Otherwise, the Sender MUST return an error.
|
// encoded in the ResumeToken. Otherwise, the Sender MUST return an error.
|
||||||
ResumeToken string `protobuf:"bytes,4,opt,name=ResumeToken,proto3" json:"ResumeToken,omitempty"`
|
ResumeToken string `protobuf:"bytes,4,opt,name=ResumeToken,proto3" json:"ResumeToken,omitempty"`
|
||||||
Encrypted Tri `protobuf:"varint,5,opt,name=Encrypted,proto3,enum=Tri" json:"Encrypted,omitempty"`
|
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,6,opt,name=ReplicationConfig,proto3" json:"ReplicationConfig,omitempty"`
|
||||||
ReplicationConfig *ReplicationConfig `protobuf:"bytes,7,opt,name=ReplicationConfig,proto3" json:"ReplicationConfig,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *SendReq) Reset() {
|
func (x *SendReq) Reset() {
|
||||||
@@ -589,13 +588,6 @@ func (x *SendReq) GetEncrypted() Tri {
|
|||||||
return Tri_DontCare
|
return Tri_DontCare
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *SendReq) GetDryRun() bool {
|
|
||||||
if x != nil {
|
|
||||||
return x.DryRun
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *SendReq) GetReplicationConfig() *ReplicationConfig {
|
func (x *SendReq) GetReplicationConfig() *ReplicationConfig {
|
||||||
if x != nil {
|
if x != nil {
|
||||||
return x.ReplicationConfig
|
return x.ReplicationConfig
|
||||||
@@ -766,12 +758,11 @@ type SendRes struct {
|
|||||||
unknownFields protoimpl.UnknownFields
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
// Whether the resume token provided in the request has been used or not.
|
// Whether the resume token provided in the request has been used or not.
|
||||||
// If the SendReq.ResumeToken == "", this field has no meaning.
|
// If the SendReq.ResumeToken == "", this field MUST be false.
|
||||||
UsedResumeToken bool `protobuf:"varint,2,opt,name=UsedResumeToken,proto3" json:"UsedResumeToken,omitempty"`
|
UsedResumeToken bool `protobuf:"varint,1,opt,name=UsedResumeToken,proto3" json:"UsedResumeToken,omitempty"`
|
||||||
// Expected stream size determined by dry run, not exact.
|
// Expected stream size determined by dry run, not exact.
|
||||||
// 0 indicates that for the given SendReq, no size estimate could be made.
|
// 0 indicates that for the given SendReq, no size estimate could be made.
|
||||||
ExpectedSize int64 `protobuf:"varint,3,opt,name=ExpectedSize,proto3" json:"ExpectedSize,omitempty"`
|
ExpectedSize uint64 `protobuf:"varint,2,opt,name=ExpectedSize,proto3" json:"ExpectedSize,omitempty"`
|
||||||
Properties []*Property `protobuf:"bytes,4,rep,name=Properties,proto3" json:"Properties,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *SendRes) Reset() {
|
func (x *SendRes) Reset() {
|
||||||
@@ -813,20 +804,13 @@ func (x *SendRes) GetUsedResumeToken() bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *SendRes) GetExpectedSize() int64 {
|
func (x *SendRes) GetExpectedSize() uint64 {
|
||||||
if x != nil {
|
if x != nil {
|
||||||
return x.ExpectedSize
|
return x.ExpectedSize
|
||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *SendRes) GetProperties() []*Property {
|
|
||||||
if x != nil {
|
|
||||||
return x.Properties
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type SendCompletedReq struct {
|
type SendCompletedReq struct {
|
||||||
state protoimpl.MessageState
|
state protoimpl.MessageState
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
@@ -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,
|
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, 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,
|
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,
|
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,
|
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,
|
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,
|
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,
|
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,
|
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,
|
0x12, 0x40, 0x0a, 0x11, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43,
|
||||||
0x52, 0x06, 0x44, 0x72, 0x79, 0x52, 0x75, 0x6e, 0x12, 0x40, 0x0a, 0x11, 0x52, 0x65, 0x70, 0x6c,
|
0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x52, 0x65,
|
||||||
0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x07, 0x20,
|
0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52,
|
||||||
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,
|
|
||||||
0x11, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66,
|
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,
|
0x69, 0x67, 0x22, 0x51, 0x0a, 0x11, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f,
|
||||||
0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x11, 0x52, 0x65,
|
0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x3c, 0x0a, 0x0a, 0x70, 0x72, 0x6f, 0x74, 0x65,
|
||||||
0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22,
|
0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x52, 0x65,
|
||||||
0x0c, 0x0a, 0x0a, 0x52, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x52, 0x65, 0x73, 0x22, 0x67, 0x0a,
|
0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x50,
|
||||||
0x13, 0x44, 0x65, 0x73, 0x74, 0x72, 0x6f, 0x79, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74,
|
0x72, 0x6f, 0x74, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0a, 0x70, 0x72, 0x6f, 0x74, 0x65,
|
||||||
0x73, 0x52, 0x65, 0x71, 0x12, 0x1e, 0x0a, 0x0a, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74,
|
0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x8f, 0x01, 0x0a, 0x1b, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63,
|
||||||
0x65, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79,
|
0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x50, 0x72, 0x6f, 0x74, 0x65,
|
||||||
0x73, 0x74, 0x65, 0x6d, 0x12, 0x30, 0x0a, 0x09, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74,
|
0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x33, 0x0a, 0x07, 0x49, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x6c,
|
||||||
0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79,
|
0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x19, 0x2e, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61,
|
||||||
0x73, 0x74, 0x65, 0x6d, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x53, 0x6e, 0x61,
|
0x74, 0x69, 0x6f, 0x6e, 0x47, 0x75, 0x61, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x65, 0x4b, 0x69, 0x6e,
|
||||||
0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x22, 0x5a, 0x0a, 0x12, 0x44, 0x65, 0x73, 0x74, 0x72, 0x6f,
|
0x64, 0x52, 0x07, 0x49, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x6c, 0x12, 0x3b, 0x0a, 0x0b, 0x49, 0x6e,
|
||||||
0x79, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x52, 0x65, 0x73, 0x12, 0x2e, 0x0a, 0x08,
|
0x63, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32,
|
||||||
0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12,
|
0x19, 0x2e, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x47, 0x75, 0x61,
|
||||||
0x2e, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x56, 0x65, 0x72, 0x73, 0x69,
|
0x72, 0x61, 0x6e, 0x74, 0x65, 0x65, 0x4b, 0x69, 0x6e, 0x64, 0x52, 0x0b, 0x49, 0x6e, 0x63, 0x72,
|
||||||
0x6f, 0x6e, 0x52, 0x08, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x12, 0x14, 0x0a, 0x05,
|
0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x6c, 0x22, 0x34, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x70, 0x65,
|
||||||
0x45, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x45, 0x72, 0x72,
|
0x72, 0x74, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28,
|
||||||
0x6f, 0x72, 0x22, 0x44, 0x0a, 0x13, 0x44, 0x65, 0x73, 0x74, 0x72, 0x6f, 0x79, 0x53, 0x6e, 0x61,
|
0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x56, 0x61, 0x6c, 0x75, 0x65,
|
||||||
0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x52, 0x65, 0x73, 0x12, 0x2d, 0x0a, 0x07, 0x52, 0x65, 0x73,
|
0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x57, 0x0a,
|
||||||
0x75, 0x6c, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x44, 0x65, 0x73,
|
0x07, 0x53, 0x65, 0x6e, 0x64, 0x52, 0x65, 0x73, 0x12, 0x28, 0x0a, 0x0f, 0x55, 0x73, 0x65, 0x64,
|
||||||
0x74, 0x72, 0x6f, 0x79, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x52, 0x65, 0x73, 0x52,
|
0x52, 0x65, 0x73, 0x75, 0x6d, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28,
|
||||||
0x07, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x22, 0x36, 0x0a, 0x14, 0x52, 0x65, 0x70, 0x6c,
|
0x08, 0x52, 0x0f, 0x55, 0x73, 0x65, 0x64, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x65, 0x54, 0x6f, 0x6b,
|
||||||
0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x75, 0x72, 0x73, 0x6f, 0x72, 0x52, 0x65, 0x71,
|
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,
|
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,
|
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,
|
0x12, 0x30, 0x0a, 0x09, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x18, 0x02, 0x20,
|
||||||
0x75, 0x72, 0x73, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x12, 0x14, 0x0a, 0x04, 0x47, 0x75, 0x69, 0x64,
|
0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d,
|
||||||
0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x48, 0x00, 0x52, 0x04, 0x47, 0x75, 0x69, 0x64, 0x12, 0x1c,
|
0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f,
|
||||||
0x0a, 0x08, 0x4e, 0x6f, 0x74, 0x65, 0x78, 0x69, 0x73, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08,
|
0x74, 0x73, 0x22, 0x5a, 0x0a, 0x12, 0x44, 0x65, 0x73, 0x74, 0x72, 0x6f, 0x79, 0x53, 0x6e, 0x61,
|
||||||
0x48, 0x00, 0x52, 0x08, 0x4e, 0x6f, 0x74, 0x65, 0x78, 0x69, 0x73, 0x74, 0x42, 0x08, 0x0a, 0x06,
|
0x70, 0x73, 0x68, 0x6f, 0x74, 0x52, 0x65, 0x73, 0x12, 0x2e, 0x0a, 0x08, 0x53, 0x6e, 0x61, 0x70,
|
||||||
0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x22, 0x23, 0x0a, 0x07, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65,
|
0x73, 0x68, 0x6f, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x46, 0x69, 0x6c,
|
||||||
0x71, 0x12, 0x18, 0x0a, 0x07, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01,
|
0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x08,
|
||||||
0x28, 0x09, 0x52, 0x07, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x1d, 0x0a, 0x07, 0x50,
|
0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x45, 0x72, 0x72, 0x6f,
|
||||||
0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x45, 0x63, 0x68, 0x6f, 0x18, 0x01,
|
0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x44,
|
||||||
0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x45, 0x63, 0x68, 0x6f, 0x2a, 0x28, 0x0a, 0x03, 0x54, 0x72,
|
0x0a, 0x13, 0x44, 0x65, 0x73, 0x74, 0x72, 0x6f, 0x79, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f,
|
||||||
0x69, 0x12, 0x0c, 0x0a, 0x08, 0x44, 0x6f, 0x6e, 0x74, 0x43, 0x61, 0x72, 0x65, 0x10, 0x00, 0x12,
|
0x74, 0x73, 0x52, 0x65, 0x73, 0x12, 0x2d, 0x0a, 0x07, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73,
|
||||||
0x09, 0x0a, 0x05, 0x46, 0x61, 0x6c, 0x73, 0x65, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x54, 0x72,
|
0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x44, 0x65, 0x73, 0x74, 0x72, 0x6f, 0x79,
|
||||||
0x75, 0x65, 0x10, 0x02, 0x2a, 0x86, 0x01, 0x0a, 0x18, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61,
|
0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x52, 0x65, 0x73, 0x52, 0x07, 0x52, 0x65, 0x73,
|
||||||
0x74, 0x69, 0x6f, 0x6e, 0x47, 0x75, 0x61, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x65, 0x4b, 0x69, 0x6e,
|
0x75, 0x6c, 0x74, 0x73, 0x22, 0x36, 0x0a, 0x14, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74,
|
||||||
0x64, 0x12, 0x14, 0x0a, 0x10, 0x47, 0x75, 0x61, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x65, 0x49, 0x6e,
|
0x69, 0x6f, 0x6e, 0x43, 0x75, 0x72, 0x73, 0x6f, 0x72, 0x52, 0x65, 0x71, 0x12, 0x1e, 0x0a, 0x0a,
|
||||||
0x76, 0x61, 0x6c, 0x69, 0x64, 0x10, 0x00, 0x12, 0x19, 0x0a, 0x15, 0x47, 0x75, 0x61, 0x72, 0x61,
|
0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
|
||||||
0x6e, 0x74, 0x65, 0x65, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79,
|
0x52, 0x0a, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x22, 0x54, 0x0a, 0x14,
|
||||||
0x10, 0x01, 0x12, 0x23, 0x0a, 0x1f, 0x47, 0x75, 0x61, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x65, 0x49,
|
0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x75, 0x72, 0x73, 0x6f,
|
||||||
0x6e, 0x63, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x6c, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63,
|
0x72, 0x52, 0x65, 0x73, 0x12, 0x14, 0x0a, 0x04, 0x47, 0x75, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01,
|
||||||
0x61, 0x74, 0x69, 0x6f, 0x6e, 0x10, 0x02, 0x12, 0x14, 0x0a, 0x10, 0x47, 0x75, 0x61, 0x72, 0x61,
|
0x28, 0x04, 0x48, 0x00, 0x52, 0x04, 0x47, 0x75, 0x69, 0x64, 0x12, 0x1c, 0x0a, 0x08, 0x4e, 0x6f,
|
||||||
0x6e, 0x74, 0x65, 0x65, 0x4e, 0x6f, 0x74, 0x68, 0x69, 0x6e, 0x67, 0x10, 0x03, 0x32, 0xf0, 0x02,
|
0x74, 0x65, 0x78, 0x69, 0x73, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x08,
|
||||||
0x0a, 0x0b, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1a, 0x0a,
|
0x4e, 0x6f, 0x74, 0x65, 0x78, 0x69, 0x73, 0x74, 0x42, 0x08, 0x0a, 0x06, 0x52, 0x65, 0x73, 0x75,
|
||||||
0x04, 0x50, 0x69, 0x6e, 0x67, 0x12, 0x08, 0x2e, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x1a,
|
0x6c, 0x74, 0x22, 0x23, 0x0a, 0x07, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x12, 0x18, 0x0a,
|
||||||
0x08, 0x2e, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x12, 0x39, 0x0a, 0x0f, 0x4c, 0x69, 0x73,
|
0x07, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07,
|
||||||
0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x73, 0x12, 0x12, 0x2e, 0x4c,
|
0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x1d, 0x0a, 0x07, 0x50, 0x69, 0x6e, 0x67, 0x52,
|
||||||
0x69, 0x73, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x52, 0x65, 0x71,
|
0x65, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x45, 0x63, 0x68, 0x6f, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
|
||||||
0x1a, 0x12, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65,
|
0x52, 0x04, 0x45, 0x63, 0x68, 0x6f, 0x2a, 0x28, 0x0a, 0x03, 0x54, 0x72, 0x69, 0x12, 0x0c, 0x0a,
|
||||||
0x6d, 0x52, 0x65, 0x73, 0x12, 0x50, 0x0a, 0x16, 0x4c, 0x69, 0x73, 0x74, 0x46, 0x69, 0x6c, 0x65,
|
0x08, 0x44, 0x6f, 0x6e, 0x74, 0x43, 0x61, 0x72, 0x65, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x46,
|
||||||
0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x1a,
|
0x61, 0x6c, 0x73, 0x65, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x54, 0x72, 0x75, 0x65, 0x10, 0x02,
|
||||||
0x2e, 0x4c, 0x69, 0x73, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x56,
|
0x2a, 0x86, 0x01, 0x0a, 0x18, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e,
|
||||||
0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x1a, 0x1a, 0x2e, 0x4c, 0x69, 0x73,
|
0x47, 0x75, 0x61, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x65, 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,
|
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,
|
0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x1a, 0x1a, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x46, 0x69, 0x6c,
|
||||||
0x79, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x12, 0x14, 0x2e, 0x44, 0x65, 0x73,
|
0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52,
|
||||||
0x74, 0x72, 0x6f, 0x79, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x52, 0x65, 0x71,
|
0x65, 0x73, 0x12, 0x3e, 0x0a, 0x10, 0x44, 0x65, 0x73, 0x74, 0x72, 0x6f, 0x79, 0x53, 0x6e, 0x61,
|
||||||
0x1a, 0x14, 0x2e, 0x44, 0x65, 0x73, 0x74, 0x72, 0x6f, 0x79, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68,
|
0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x12, 0x14, 0x2e, 0x44, 0x65, 0x73, 0x74, 0x72, 0x6f, 0x79,
|
||||||
0x6f, 0x74, 0x73, 0x52, 0x65, 0x73, 0x12, 0x41, 0x0a, 0x11, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63,
|
0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x52, 0x65, 0x71, 0x1a, 0x14, 0x2e, 0x44,
|
||||||
0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x75, 0x72, 0x73, 0x6f, 0x72, 0x12, 0x15, 0x2e, 0x52, 0x65,
|
0x65, 0x73, 0x74, 0x72, 0x6f, 0x79, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x52,
|
||||||
0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x75, 0x72, 0x73, 0x6f, 0x72, 0x52,
|
0x65, 0x73, 0x12, 0x41, 0x0a, 0x11, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f,
|
||||||
0x65, 0x71, 0x1a, 0x15, 0x2e, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e,
|
0x6e, 0x43, 0x75, 0x72, 0x73, 0x6f, 0x72, 0x12, 0x15, 0x2e, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63,
|
||||||
0x43, 0x75, 0x72, 0x73, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x12, 0x35, 0x0a, 0x0d, 0x53, 0x65, 0x6e,
|
0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x75, 0x72, 0x73, 0x6f, 0x72, 0x52, 0x65, 0x71, 0x1a, 0x15,
|
||||||
0x64, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x12, 0x11, 0x2e, 0x53, 0x65, 0x6e,
|
0x2e, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x75, 0x72, 0x73,
|
||||||
0x64, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x52, 0x65, 0x71, 0x1a, 0x11, 0x2e,
|
0x6f, 0x72, 0x52, 0x65, 0x73, 0x12, 0x1d, 0x0a, 0x07, 0x53, 0x65, 0x6e, 0x64, 0x44, 0x72, 0x79,
|
||||||
0x53, 0x65, 0x6e, 0x64, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x52, 0x65, 0x73,
|
0x12, 0x08, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x52, 0x65, 0x71, 0x1a, 0x08, 0x2e, 0x53, 0x65, 0x6e,
|
||||||
0x42, 0x07, 0x5a, 0x05, 0x2e, 0x3b, 0x70, 0x64, 0x75, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f,
|
0x64, 0x52, 0x65, 0x73, 0x12, 0x35, 0x0a, 0x0d, 0x53, 0x65, 0x6e, 0x64, 0x43, 0x6f, 0x6d, 0x70,
|
||||||
0x33,
|
0x6c, 0x65, 0x74, 0x65, 0x64, 0x12, 0x11, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x43, 0x6f, 0x6d, 0x70,
|
||||||
|
0x6c, 0x65, 0x74, 0x65, 0x64, 0x52, 0x65, 0x71, 0x1a, 0x11, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x43,
|
||||||
|
0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x52, 0x65, 0x73, 0x42, 0x07, 0x5a, 0x05, 0x2e,
|
||||||
|
0x3b, 0x70, 0x64, 0x75, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -1625,30 +1606,31 @@ var file_pdu_proto_depIdxs = []int32{
|
|||||||
11, // 7: ReplicationConfig.protection:type_name -> ReplicationConfigProtection
|
11, // 7: ReplicationConfig.protection:type_name -> ReplicationConfigProtection
|
||||||
1, // 8: ReplicationConfigProtection.Initial:type_name -> ReplicationGuaranteeKind
|
1, // 8: ReplicationConfigProtection.Initial:type_name -> ReplicationGuaranteeKind
|
||||||
1, // 9: ReplicationConfigProtection.Incremental:type_name -> ReplicationGuaranteeKind
|
1, // 9: ReplicationConfigProtection.Incremental:type_name -> ReplicationGuaranteeKind
|
||||||
12, // 10: SendRes.Properties:type_name -> Property
|
9, // 10: SendCompletedReq.OriginalReq:type_name -> SendReq
|
||||||
9, // 11: SendCompletedReq.OriginalReq:type_name -> SendReq
|
8, // 11: ReceiveReq.To:type_name -> FilesystemVersion
|
||||||
8, // 12: ReceiveReq.To:type_name -> FilesystemVersion
|
10, // 12: ReceiveReq.ReplicationConfig:type_name -> ReplicationConfig
|
||||||
10, // 13: ReceiveReq.ReplicationConfig:type_name -> ReplicationConfig
|
8, // 13: DestroySnapshotsReq.Snapshots:type_name -> FilesystemVersion
|
||||||
8, // 14: DestroySnapshotsReq.Snapshots:type_name -> FilesystemVersion
|
8, // 14: DestroySnapshotRes.Snapshot:type_name -> FilesystemVersion
|
||||||
8, // 15: DestroySnapshotRes.Snapshot:type_name -> FilesystemVersion
|
19, // 15: DestroySnapshotsRes.Results:type_name -> DestroySnapshotRes
|
||||||
19, // 16: DestroySnapshotsRes.Results:type_name -> DestroySnapshotRes
|
23, // 16: Replication.Ping:input_type -> PingReq
|
||||||
23, // 17: Replication.Ping:input_type -> PingReq
|
3, // 17: Replication.ListFilesystems:input_type -> ListFilesystemReq
|
||||||
3, // 18: Replication.ListFilesystems:input_type -> ListFilesystemReq
|
6, // 18: Replication.ListFilesystemVersions:input_type -> ListFilesystemVersionsReq
|
||||||
6, // 19: Replication.ListFilesystemVersions:input_type -> ListFilesystemVersionsReq
|
18, // 19: Replication.DestroySnapshots:input_type -> DestroySnapshotsReq
|
||||||
18, // 20: Replication.DestroySnapshots:input_type -> DestroySnapshotsReq
|
21, // 20: Replication.ReplicationCursor:input_type -> ReplicationCursorReq
|
||||||
21, // 21: Replication.ReplicationCursor:input_type -> ReplicationCursorReq
|
9, // 21: Replication.SendDry:input_type -> SendReq
|
||||||
14, // 22: Replication.SendCompleted:input_type -> SendCompletedReq
|
14, // 22: Replication.SendCompleted:input_type -> SendCompletedReq
|
||||||
24, // 23: Replication.Ping:output_type -> PingRes
|
24, // 23: Replication.Ping:output_type -> PingRes
|
||||||
4, // 24: Replication.ListFilesystems:output_type -> ListFilesystemRes
|
4, // 24: Replication.ListFilesystems:output_type -> ListFilesystemRes
|
||||||
7, // 25: Replication.ListFilesystemVersions:output_type -> ListFilesystemVersionsRes
|
7, // 25: Replication.ListFilesystemVersions:output_type -> ListFilesystemVersionsRes
|
||||||
20, // 26: Replication.DestroySnapshots:output_type -> DestroySnapshotsRes
|
20, // 26: Replication.DestroySnapshots:output_type -> DestroySnapshotsRes
|
||||||
22, // 27: Replication.ReplicationCursor:output_type -> ReplicationCursorRes
|
22, // 27: Replication.ReplicationCursor:output_type -> ReplicationCursorRes
|
||||||
15, // 28: Replication.SendCompleted:output_type -> SendCompletedRes
|
13, // 28: Replication.SendDry:output_type -> SendRes
|
||||||
23, // [23:29] is the sub-list for method output_type
|
15, // 29: Replication.SendCompleted:output_type -> SendCompletedRes
|
||||||
17, // [17:23] is the sub-list for method input_type
|
23, // [23:30] is the sub-list for method output_type
|
||||||
17, // [17:17] is the sub-list for extension type_name
|
16, // [16:23] is the sub-list for method input_type
|
||||||
17, // [17:17] is the sub-list for extension extendee
|
16, // [16:16] is the sub-list for extension type_name
|
||||||
0, // [0:17] is the sub-list for field 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() }
|
func init() { file_pdu_proto_init() }
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ service Replication {
|
|||||||
returns (ListFilesystemVersionsRes);
|
returns (ListFilesystemVersionsRes);
|
||||||
rpc DestroySnapshots(DestroySnapshotsReq) returns (DestroySnapshotsRes);
|
rpc DestroySnapshots(DestroySnapshotsReq) returns (DestroySnapshotsRes);
|
||||||
rpc ReplicationCursor(ReplicationCursorReq) returns (ReplicationCursorRes);
|
rpc ReplicationCursor(ReplicationCursorReq) returns (ReplicationCursorRes);
|
||||||
|
rpc SendDry(SendReq) returns (SendRes);
|
||||||
rpc SendCompleted(SendCompletedReq) returns (SendCompletedRes);
|
rpc SendCompleted(SendCompletedReq) returns (SendCompletedRes);
|
||||||
// for Send and Recv, see package rpc
|
// for Send and Recv, see package rpc
|
||||||
}
|
}
|
||||||
@@ -60,9 +61,7 @@ message SendReq {
|
|||||||
string ResumeToken = 4;
|
string ResumeToken = 4;
|
||||||
Tri Encrypted = 5;
|
Tri Encrypted = 5;
|
||||||
|
|
||||||
bool DryRun = 6;
|
ReplicationConfig ReplicationConfig = 6;
|
||||||
|
|
||||||
ReplicationConfig ReplicationConfig = 7;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message ReplicationConfig {
|
message ReplicationConfig {
|
||||||
@@ -89,14 +88,12 @@ message Property {
|
|||||||
|
|
||||||
message SendRes {
|
message SendRes {
|
||||||
// Whether the resume token provided in the request has been used or not.
|
// Whether the resume token provided in the request has been used or not.
|
||||||
// If the SendReq.ResumeToken == "", this field has no meaning.
|
// If the SendReq.ResumeToken == "", this field MUST be false.
|
||||||
bool UsedResumeToken = 2;
|
bool UsedResumeToken = 1;
|
||||||
|
|
||||||
// Expected stream size determined by dry run, not exact.
|
// Expected stream size determined by dry run, not exact.
|
||||||
// 0 indicates that for the given SendReq, no size estimate could be made.
|
// 0 indicates that for the given SendReq, no size estimate could be made.
|
||||||
int64 ExpectedSize = 3;
|
uint64 ExpectedSize = 2;
|
||||||
|
|
||||||
repeated Property Properties = 4;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message SendCompletedReq {
|
message SendCompletedReq {
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ type ReplicationClient interface {
|
|||||||
ListFilesystemVersions(ctx context.Context, in *ListFilesystemVersionsReq, opts ...grpc.CallOption) (*ListFilesystemVersionsRes, error)
|
ListFilesystemVersions(ctx context.Context, in *ListFilesystemVersionsReq, opts ...grpc.CallOption) (*ListFilesystemVersionsRes, error)
|
||||||
DestroySnapshots(ctx context.Context, in *DestroySnapshotsReq, opts ...grpc.CallOption) (*DestroySnapshotsRes, error)
|
DestroySnapshots(ctx context.Context, in *DestroySnapshotsReq, opts ...grpc.CallOption) (*DestroySnapshotsRes, error)
|
||||||
ReplicationCursor(ctx context.Context, in *ReplicationCursorReq, opts ...grpc.CallOption) (*ReplicationCursorRes, error)
|
ReplicationCursor(ctx context.Context, in *ReplicationCursorReq, opts ...grpc.CallOption) (*ReplicationCursorRes, error)
|
||||||
|
SendDry(ctx context.Context, in *SendReq, opts ...grpc.CallOption) (*SendRes, error)
|
||||||
SendCompleted(ctx context.Context, in *SendCompletedReq, opts ...grpc.CallOption) (*SendCompletedRes, error)
|
SendCompleted(ctx context.Context, in *SendCompletedReq, opts ...grpc.CallOption) (*SendCompletedRes, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,6 +80,15 @@ func (c *replicationClient) ReplicationCursor(ctx context.Context, in *Replicati
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *replicationClient) SendDry(ctx context.Context, in *SendReq, opts ...grpc.CallOption) (*SendRes, error) {
|
||||||
|
out := new(SendRes)
|
||||||
|
err := c.cc.Invoke(ctx, "/Replication/SendDry", in, out, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *replicationClient) SendCompleted(ctx context.Context, in *SendCompletedReq, opts ...grpc.CallOption) (*SendCompletedRes, error) {
|
func (c *replicationClient) SendCompleted(ctx context.Context, in *SendCompletedReq, opts ...grpc.CallOption) (*SendCompletedRes, error) {
|
||||||
out := new(SendCompletedRes)
|
out := new(SendCompletedRes)
|
||||||
err := c.cc.Invoke(ctx, "/Replication/SendCompleted", in, out, opts...)
|
err := c.cc.Invoke(ctx, "/Replication/SendCompleted", in, out, opts...)
|
||||||
@@ -97,6 +107,7 @@ type ReplicationServer interface {
|
|||||||
ListFilesystemVersions(context.Context, *ListFilesystemVersionsReq) (*ListFilesystemVersionsRes, error)
|
ListFilesystemVersions(context.Context, *ListFilesystemVersionsReq) (*ListFilesystemVersionsRes, error)
|
||||||
DestroySnapshots(context.Context, *DestroySnapshotsReq) (*DestroySnapshotsRes, error)
|
DestroySnapshots(context.Context, *DestroySnapshotsReq) (*DestroySnapshotsRes, error)
|
||||||
ReplicationCursor(context.Context, *ReplicationCursorReq) (*ReplicationCursorRes, error)
|
ReplicationCursor(context.Context, *ReplicationCursorReq) (*ReplicationCursorRes, error)
|
||||||
|
SendDry(context.Context, *SendReq) (*SendRes, error)
|
||||||
SendCompleted(context.Context, *SendCompletedReq) (*SendCompletedRes, error)
|
SendCompleted(context.Context, *SendCompletedReq) (*SendCompletedRes, error)
|
||||||
mustEmbedUnimplementedReplicationServer()
|
mustEmbedUnimplementedReplicationServer()
|
||||||
}
|
}
|
||||||
@@ -120,6 +131,9 @@ func (UnimplementedReplicationServer) DestroySnapshots(context.Context, *Destroy
|
|||||||
func (UnimplementedReplicationServer) ReplicationCursor(context.Context, *ReplicationCursorReq) (*ReplicationCursorRes, error) {
|
func (UnimplementedReplicationServer) ReplicationCursor(context.Context, *ReplicationCursorReq) (*ReplicationCursorRes, error) {
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method ReplicationCursor not implemented")
|
return nil, status.Errorf(codes.Unimplemented, "method ReplicationCursor not implemented")
|
||||||
}
|
}
|
||||||
|
func (UnimplementedReplicationServer) SendDry(context.Context, *SendReq) (*SendRes, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method SendDry not implemented")
|
||||||
|
}
|
||||||
func (UnimplementedReplicationServer) SendCompleted(context.Context, *SendCompletedReq) (*SendCompletedRes, error) {
|
func (UnimplementedReplicationServer) SendCompleted(context.Context, *SendCompletedReq) (*SendCompletedRes, error) {
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method SendCompleted not implemented")
|
return nil, status.Errorf(codes.Unimplemented, "method SendCompleted not implemented")
|
||||||
}
|
}
|
||||||
@@ -226,6 +240,24 @@ func _Replication_ReplicationCursor_Handler(srv interface{}, ctx context.Context
|
|||||||
return interceptor(ctx, in, info, handler)
|
return interceptor(ctx, in, info, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func _Replication_SendDry_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(SendReq)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(ReplicationServer).SendDry(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: "/Replication/SendDry",
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(ReplicationServer).SendDry(ctx, req.(*SendReq))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
func _Replication_SendCompleted_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
func _Replication_SendCompleted_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
in := new(SendCompletedReq)
|
in := new(SendCompletedReq)
|
||||||
if err := dec(in); err != nil {
|
if err := dec(in); err != nil {
|
||||||
@@ -271,6 +303,10 @@ var Replication_ServiceDesc = grpc.ServiceDesc{
|
|||||||
MethodName: "ReplicationCursor",
|
MethodName: "ReplicationCursor",
|
||||||
Handler: _Replication_ReplicationCursor_Handler,
|
Handler: _Replication_ReplicationCursor_Handler,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
MethodName: "SendDry",
|
||||||
|
Handler: _Replication_SendDry_Handler,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
MethodName: "SendCompleted",
|
MethodName: "SendCompleted",
|
||||||
Handler: _Replication_SendCompleted_Handler,
|
Handler: _Replication_SendCompleted_Handler,
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ type Sender interface {
|
|||||||
// any next call to the parent github.com/zrepl/zrepl/replication.Endpoint.
|
// any next call to the parent github.com/zrepl/zrepl/replication.Endpoint.
|
||||||
// If the send request is for dry run the io.ReadCloser will be nil
|
// If the send request is for dry run the io.ReadCloser will be nil
|
||||||
Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, io.ReadCloser, error)
|
Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, io.ReadCloser, error)
|
||||||
|
SendDry(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, error)
|
||||||
SendCompleted(ctx context.Context, r *pdu.SendCompletedReq) (*pdu.SendCompletedRes, error)
|
SendCompleted(ctx context.Context, r *pdu.SendCompletedReq) (*pdu.SendCompletedRes, error)
|
||||||
ReplicationCursor(ctx context.Context, req *pdu.ReplicationCursorReq) (*pdu.ReplicationCursorRes, error)
|
ReplicationCursor(ctx context.Context, req *pdu.ReplicationCursorReq) (*pdu.ReplicationCursorRes, error)
|
||||||
}
|
}
|
||||||
@@ -158,7 +159,7 @@ type Step struct {
|
|||||||
encrypt tri
|
encrypt tri
|
||||||
resumeToken string // empty means no resume token shall be used
|
resumeToken string // empty means no resume token shall be used
|
||||||
|
|
||||||
expectedSize int64 // 0 means no size estimate present / possible
|
expectedSize uint64 // 0 means no size estimate present / possible
|
||||||
|
|
||||||
// byteCounter is nil initially, and set later in Step.doReplication
|
// byteCounter is nil initially, and set later in Step.doReplication
|
||||||
// => concurrent read of that pointer from Step.ReportInfo must be protected
|
// => concurrent read of that pointer from Step.ReportInfo must be protected
|
||||||
@@ -189,7 +190,7 @@ func (s *Step) Step(ctx context.Context) error {
|
|||||||
func (s *Step) ReportInfo() *report.StepInfo {
|
func (s *Step) ReportInfo() *report.StepInfo {
|
||||||
|
|
||||||
// get current byteCounter value
|
// get current byteCounter value
|
||||||
var byteCounter int64
|
var byteCounter uint64
|
||||||
s.byteCounterMtx.Lock()
|
s.byteCounterMtx.Lock()
|
||||||
if s.byteCounter != nil {
|
if s.byteCounter != nil {
|
||||||
byteCounter = s.byteCounter.Count()
|
byteCounter = s.byteCounter.Count()
|
||||||
@@ -546,10 +547,10 @@ func (s *Step) updateSizeEstimate(ctx context.Context) error {
|
|||||||
|
|
||||||
log := getLogger(ctx)
|
log := getLogger(ctx)
|
||||||
|
|
||||||
sr := s.buildSendRequest(true)
|
sr := s.buildSendRequest()
|
||||||
|
|
||||||
log.Debug("initiate dry run send request")
|
log.Debug("initiate dry run send request")
|
||||||
sres, _, err := s.sender.Send(ctx, sr)
|
sres, err := s.sender.SendDry(ctx, sr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithError(err).Error("dry run send request failed")
|
log.WithError(err).Error("dry run send request failed")
|
||||||
return err
|
return err
|
||||||
@@ -563,7 +564,7 @@ func (s *Step) updateSizeEstimate(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Step) buildSendRequest(dryRun bool) (sr *pdu.SendReq) {
|
func (s *Step) buildSendRequest() (sr *pdu.SendReq) {
|
||||||
fs := s.parent.Path
|
fs := s.parent.Path
|
||||||
sr = &pdu.SendReq{
|
sr = &pdu.SendReq{
|
||||||
Filesystem: fs,
|
Filesystem: fs,
|
||||||
@@ -571,7 +572,6 @@ func (s *Step) buildSendRequest(dryRun bool) (sr *pdu.SendReq) {
|
|||||||
To: s.to,
|
To: s.to,
|
||||||
Encrypted: s.encrypt.ToPDU(),
|
Encrypted: s.encrypt.ToPDU(),
|
||||||
ResumeToken: s.resumeToken,
|
ResumeToken: s.resumeToken,
|
||||||
DryRun: dryRun,
|
|
||||||
ReplicationConfig: s.parent.policy.ReplicationConfig,
|
ReplicationConfig: s.parent.policy.ReplicationConfig,
|
||||||
}
|
}
|
||||||
return sr
|
return sr
|
||||||
@@ -582,7 +582,7 @@ func (s *Step) doReplication(ctx context.Context) error {
|
|||||||
fs := s.parent.Path
|
fs := s.parent.Path
|
||||||
|
|
||||||
log := getLogger(ctx).WithField("filesystem", fs)
|
log := getLogger(ctx).WithField("filesystem", fs)
|
||||||
sr := s.buildSendRequest(false)
|
sr := s.buildSendRequest()
|
||||||
|
|
||||||
log.Debug("initiate send request")
|
log.Debug("initiate send request")
|
||||||
sres, stream, err := s.sender.Send(ctx, sr)
|
sres, stream, err := s.sender.Send(ctx, sr)
|
||||||
|
|||||||
@@ -62,11 +62,23 @@ const (
|
|||||||
FilesystemDone FilesystemState = "done"
|
FilesystemDone FilesystemState = "done"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type FsBlockedOn string
|
||||||
|
|
||||||
|
const (
|
||||||
|
FsBlockedOnNothing FsBlockedOn = "nothing"
|
||||||
|
FsBlockedOnPlanningStepQueue FsBlockedOn = "plan-queue"
|
||||||
|
FsBlockedOnParentInitialRepl FsBlockedOn = "parent-initial-repl"
|
||||||
|
FsBlockedOnReplStepQueue FsBlockedOn = "repl-queue"
|
||||||
|
)
|
||||||
|
|
||||||
type FilesystemReport struct {
|
type FilesystemReport struct {
|
||||||
Info *FilesystemInfo
|
Info *FilesystemInfo
|
||||||
|
|
||||||
State FilesystemState
|
State FilesystemState
|
||||||
|
|
||||||
|
// Always valid.
|
||||||
|
BlockedOn FsBlockedOn
|
||||||
|
|
||||||
// Valid in State = FilesystemPlanningErrored
|
// Valid in State = FilesystemPlanningErrored
|
||||||
PlanError *TimedError
|
PlanError *TimedError
|
||||||
// Valid in State = FilesystemSteppingErrored
|
// Valid in State = FilesystemSteppingErrored
|
||||||
@@ -97,11 +109,11 @@ type StepInfo struct {
|
|||||||
From, To string
|
From, To string
|
||||||
Resumed bool
|
Resumed bool
|
||||||
Encrypted EncryptedEnum
|
Encrypted EncryptedEnum
|
||||||
BytesExpected int64
|
BytesExpected uint64
|
||||||
BytesReplicated int64
|
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 {
|
for _, fs := range a.Filesystems {
|
||||||
e, r, fsContainsInvalidEstimate := fs.BytesSum()
|
e, r, fsContainsInvalidEstimate := fs.BytesSum()
|
||||||
containsInvalidSizeEstimates = containsInvalidSizeEstimates || fsContainsInvalidEstimate
|
containsInvalidSizeEstimates = containsInvalidSizeEstimates || fsContainsInvalidEstimate
|
||||||
@@ -111,7 +123,7 @@ func (a *AttemptReport) BytesSum() (expected, replicated int64, containsInvalidS
|
|||||||
return expected, replicated, containsInvalidSizeEstimates
|
return expected, replicated, containsInvalidSizeEstimates
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *FilesystemReport) BytesSum() (expected, replicated int64, containsInvalidSizeEstimates bool) {
|
func (f *FilesystemReport) BytesSum() (expected, replicated uint64, containsInvalidSizeEstimates bool) {
|
||||||
for _, step := range f.Steps {
|
for _, step := range f.Steps {
|
||||||
expected += step.Info.BytesExpected
|
expected += step.Info.BytesExpected
|
||||||
replicated += step.Info.BytesReplicated
|
replicated += step.Info.BytesReplicated
|
||||||
|
|||||||
@@ -114,12 +114,6 @@ func (c *Client) ReqSend(ctx context.Context, req *pdu.SendReq) (*pdu.SendRes, i
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
putWireOnReturn := true
|
|
||||||
defer func() {
|
|
||||||
if putWireOnReturn {
|
|
||||||
c.putWire(conn)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
if err := c.send(ctx, conn, EndpointSend, req, nil); err != nil {
|
if err := c.send(ctx, conn, EndpointSend, req, nil); err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
@@ -131,14 +125,10 @@ func (c *Client) ReqSend(ctx context.Context, req *pdu.SendReq) (*pdu.SendRes, i
|
|||||||
}
|
}
|
||||||
|
|
||||||
var stream io.ReadCloser
|
var stream io.ReadCloser
|
||||||
if !req.DryRun {
|
|
||||||
putWireOnReturn = false
|
|
||||||
stream, err = conn.ReadStream(ZFSStream, true) // no shadow
|
stream, err = conn.ReadStream(ZFSStream, true) // no shadow
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return &res, stream, nil
|
return &res, stream, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -172,6 +172,15 @@ func (s *Server) serveConnRequest(ctx context.Context, endpoint string, c *strea
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
res, sendStream, handlerErr = s.h.Send(ctx, &req) // SHADOWING
|
res, sendStream, handlerErr = s.h.Send(ctx, &req) // SHADOWING
|
||||||
|
// ensure that we always close the sendStream
|
||||||
|
if sendStream != nil {
|
||||||
|
defer func() {
|
||||||
|
err := sendStream.Close()
|
||||||
|
if err != nil {
|
||||||
|
s.log.WithError(err).Error("cannot close send stream")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
case EndpointRecv:
|
case EndpointRecv:
|
||||||
var req pdu.ReceiveReq
|
var req pdu.ReceiveReq
|
||||||
if err := proto.Unmarshal(reqStructured, &req); err != nil {
|
if err := proto.Unmarshal(reqStructured, &req); err != nil {
|
||||||
@@ -239,12 +248,9 @@ func (s *Server) serveConnRequest(ctx context.Context, endpoint string, c *strea
|
|||||||
|
|
||||||
if sendStream != nil {
|
if sendStream != nil {
|
||||||
err := c.SendStream(ctx, sendStream, ZFSStream)
|
err := c.SendStream(ctx, sendStream, ZFSStream)
|
||||||
closeErr := sendStream.Close()
|
|
||||||
if closeErr != nil {
|
|
||||||
s.log.WithError(err).Error("cannot close send stream")
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.log.WithError(err).Error("cannot write send stream")
|
s.log.WithError(err).Error("cannot write send stream")
|
||||||
}
|
}
|
||||||
|
// sendStream.Close() done via defer above
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
//go:build illumos || solaris
|
||||||
// +build illumos solaris
|
// +build illumos solaris
|
||||||
|
|
||||||
package timeoutconn
|
package timeoutconn
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/util/socketpair"
|
"github.com/zrepl/zrepl/util/socketpair"
|
||||||
|
"github.com/zrepl/zrepl/util/zreplcircleci"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestReadTimeout(t *testing.T) {
|
func TestReadTimeout(t *testing.T) {
|
||||||
@@ -81,6 +82,8 @@ func TestWriteTimeout(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestNoPartialReadsDueToDeadline(t *testing.T) {
|
func TestNoPartialReadsDueToDeadline(t *testing.T) {
|
||||||
|
zreplcircleci.SkipOnCircleCI(t, "needs predictable low scheduling latency")
|
||||||
|
|
||||||
a, b, err := socketpair.SocketPair()
|
a, b, err := socketpair.SocketPair()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer a.Close()
|
defer a.Close()
|
||||||
@@ -151,6 +154,7 @@ func (c *partialWriteMockConn) Write(p []byte) (int, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestPartialWriteMockConn(t *testing.T) {
|
func TestPartialWriteMockConn(t *testing.T) {
|
||||||
|
zreplcircleci.SkipOnCircleCI(t, "because it relies on scheduler responsiveness < 50ms")
|
||||||
mc := newPartialWriteMockConn(100*time.Millisecond, 5)
|
mc := newPartialWriteMockConn(100*time.Millisecond, 5)
|
||||||
buf := []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
|
buf := []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
|
||||||
begin := time.Now()
|
begin := time.Now()
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// +build !illumos
|
//go:build !illumos && !solaris
|
||||||
// +build !solaris
|
// +build !illumos,!solaris
|
||||||
|
|
||||||
package timeoutconn
|
package timeoutconn
|
||||||
|
|
||||||
|
|||||||
@@ -108,6 +108,13 @@ func (c *Client) Receive(ctx context.Context, req *pdu.ReceiveReq, stream io.Rea
|
|||||||
return c.dataClient.ReqRecv(ctx, req, stream)
|
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) {
|
func (c *Client) ListFilesystems(ctx context.Context, in *pdu.ListFilesystemReq) (*pdu.ListFilesystemRes, error) {
|
||||||
ctx, endSpan := trace.WithSpan(ctx, "rpc.client.ListFilesystems")
|
ctx, endSpan := trace.WithSpan(ctx, "rpc.client.ListFilesystems")
|
||||||
defer endSpan()
|
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 {
|
func DoHandshakeCurrentVersion(conn net.Conn, deadline time.Time) *HandshakeError {
|
||||||
// current protocol version is hardcoded here
|
// current protocol version is hardcoded here
|
||||||
return DoHandshakeVersion(conn, deadline, 5)
|
return DoHandshakeVersion(conn, deadline, 6)
|
||||||
}
|
}
|
||||||
|
|
||||||
const HandshakeMessageMaxLen = 16 * 4096
|
const HandshakeMessageMaxLen = 16 * 4096
|
||||||
|
|||||||
@@ -93,13 +93,10 @@ func TestIPMap(t *testing.T) {
|
|||||||
expect: map[string]testCaseExpect{
|
expect: map[string]testCaseExpect{
|
||||||
"10.1.2.3": {expectNoMapping: true},
|
"10.1.2.3": {expectNoMapping: true},
|
||||||
"192.168.23.1": {expectIdent: "db-192.168.23.1"},
|
"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.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"},
|
"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
|
// v6 matching
|
||||||
"fe80::23:42%eth1": {expectIdent: "san-fe80::23:42-eth1"},
|
"fe80::23:42%eth1": {expectIdent: "san-fe80::23:42-eth1"},
|
||||||
"fe80::23:42%eth2": {expectNoMapping: true},
|
"fe80::23:42%eth2": {expectNoMapping: true},
|
||||||
@@ -179,7 +176,8 @@ func TestIPMap(t *testing.T) {
|
|||||||
for input, expect := range c.expect {
|
for input, expect := range c.expect {
|
||||||
// reuse newIPMapEntry to parse test case input
|
// reuse newIPMapEntry to parse test case input
|
||||||
// "test" is not used during testing but must not be empty.
|
// "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()
|
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, bits, net.IPv6len*8, "and we know ipMapEntry always expands its IPs to 16bytes")
|
||||||
require.Equal(t, ones, net.IPv6len*8, "test case addresses must be fully specified")
|
require.Equal(t, ones, net.IPv6len*8, "test case addresses must be fully specified")
|
||||||
|
|||||||
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package bandwidthlimit
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNoLimitConfig(t *testing.T) {
|
||||||
|
|
||||||
|
conf := NoLimitConfig()
|
||||||
|
|
||||||
|
err := ValidateConfig(conf)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.NotPanics(t, func() {
|
||||||
|
_ = WrapperFromConfig(conf)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
// its interface and counting the bytes written to during copying.
|
// its interface and counting the bytes written to during copying.
|
||||||
type ReadCloser interface {
|
type ReadCloser interface {
|
||||||
io.ReadCloser
|
io.ReadCloser
|
||||||
Count() int64
|
Count() uint64
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewReadCloser wraps rc.
|
// NewReadCloser wraps rc.
|
||||||
@@ -19,11 +19,11 @@ func NewReadCloser(rc io.ReadCloser) ReadCloser {
|
|||||||
|
|
||||||
type readCloser struct {
|
type readCloser struct {
|
||||||
rc io.ReadCloser
|
rc io.ReadCloser
|
||||||
count int64
|
count uint64
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *readCloser) Count() int64 {
|
func (r *readCloser) Count() uint64 {
|
||||||
return atomic.LoadInt64(&r.count)
|
return atomic.LoadUint64(&r.count)
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ io.ReadCloser = &readCloser{}
|
var _ io.ReadCloser = &readCloser{}
|
||||||
@@ -34,6 +34,9 @@ func (r *readCloser) Close() error {
|
|||||||
|
|
||||||
func (r *readCloser) Read(p []byte) (int, error) {
|
func (r *readCloser) Read(p []byte) (int, error) {
|
||||||
n, err := r.rc.Read(p)
|
n, err := r.rc.Read(p)
|
||||||
atomic.AddInt64(&r.count, int64(n))
|
if n < 0 {
|
||||||
|
panic("expecting n >= 0")
|
||||||
|
}
|
||||||
|
atomic.AddUint64(&r.count, uint64(n))
|
||||||
return n, err
|
return n, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
// Package choice implements a flag.Value type that accepts a set of choices.
|
||||||
|
//
|
||||||
|
// See test cases or grep the code base for usage hints.
|
||||||
|
package choices
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Choices struct {
|
||||||
|
choices map[string]interface{}
|
||||||
|
typeString string
|
||||||
|
value interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ flag.Value = (*Choices)(nil)
|
||||||
|
|
||||||
|
func new(pairs ...interface{}) Choices {
|
||||||
|
if (len(pairs) % 2) != 0 {
|
||||||
|
panic("must provide a sequence of key value pairs")
|
||||||
|
}
|
||||||
|
c := Choices{
|
||||||
|
choices: make(map[string]interface{}, len(pairs)/2),
|
||||||
|
value: nil,
|
||||||
|
}
|
||||||
|
for i := 0; i < len(pairs); {
|
||||||
|
key, ok := pairs[i].(string)
|
||||||
|
if !ok {
|
||||||
|
panic(fmt.Sprintf("argument %d is %T but should be a string, value: %#v", i, pairs[i], pairs[i]))
|
||||||
|
}
|
||||||
|
c.choices[key] = pairs[i+1]
|
||||||
|
i += 2
|
||||||
|
}
|
||||||
|
c.typeString = strings.Join(c.choicesList(true), ",") // overrideable by setter
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Choices) Init(pairs ...interface{}) {
|
||||||
|
*c = new(pairs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Choices) choicesList(escaped bool) []string {
|
||||||
|
keys := make([]string, len(c.choices))
|
||||||
|
i := 0
|
||||||
|
for k := range c.choices {
|
||||||
|
e := k
|
||||||
|
if escaped {
|
||||||
|
e = fmt.Sprintf("%q", k)
|
||||||
|
}
|
||||||
|
keys[i] = e
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
return keys
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Choices) Usage() string {
|
||||||
|
return fmt.Sprintf("one of %s", strings.Join(c.choicesList(true), ","))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Choices) InputForChoice(v interface{}) (string, error) {
|
||||||
|
for input, choice := range c.choices {
|
||||||
|
if choice == v {
|
||||||
|
return input, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("choice not registered at .Init(): %v", v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Choices) SetDefaultValue(v interface{}) {
|
||||||
|
c.value = v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Choices) Value() interface{} {
|
||||||
|
return c.value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Choices) Set(input string) error {
|
||||||
|
v, ok := c.choices[input]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("invalid value %q: must be one of %s", input, c.Usage())
|
||||||
|
}
|
||||||
|
c.value = v
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Choices) String() string {
|
||||||
|
return "" // c.value.(fmt.Stringer).String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Choices) SetTypeString(ts string) {
|
||||||
|
c.typeString = ts
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Choices) Type() string {
|
||||||
|
return c.typeString
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package choices_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"flag"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/util/choices"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestChoices(t *testing.T) {
|
||||||
|
|
||||||
|
var c choices.Choices
|
||||||
|
|
||||||
|
fs := flag.NewFlagSet("testset", flag.ContinueOnError)
|
||||||
|
c.Init("append", os.O_APPEND, "overwrite", os.O_TRUNC|os.O_CREATE)
|
||||||
|
fs.Var(&c, "mode", c.Usage())
|
||||||
|
var o bytes.Buffer
|
||||||
|
fs.SetOutput(&o)
|
||||||
|
|
||||||
|
fs.Usage()
|
||||||
|
usage := o.String()
|
||||||
|
o.Reset()
|
||||||
|
|
||||||
|
t.Logf("usage:\n%s", usage)
|
||||||
|
require.Contains(t, usage, "\"append\"")
|
||||||
|
require.Contains(t, usage, "\"overwrite\"")
|
||||||
|
|
||||||
|
err := fs.Parse([]string{"-mode", "append"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
o.Reset()
|
||||||
|
require.Equal(t, os.O_APPEND, c.Value())
|
||||||
|
|
||||||
|
c.SetDefaultValue(nil)
|
||||||
|
err = fs.Parse([]string{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
o.Reset()
|
||||||
|
require.Nil(t, c.Value())
|
||||||
|
|
||||||
|
// a little whitebox testing: this is allowed ATM, we don't check that the default value was specified as a choice in init
|
||||||
|
c.SetDefaultValue(os.O_RDWR)
|
||||||
|
err = fs.Parse([]string{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
o.Reset()
|
||||||
|
require.Equal(t, os.O_RDWR, c.Value())
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
package datasizeunit
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Bits struct {
|
||||||
|
bits float64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b Bits) ToBits() float64 { return b.bits }
|
||||||
|
func (b Bits) ToBytes() float64 { return b.bits / 8 }
|
||||||
|
func FromBytesInt64(i int64) Bits { return Bits{float64(i) * 8} }
|
||||||
|
|
||||||
|
var datarateRegex = regexp.MustCompile(`^([-0-9\.]*)\s*(bit|(|K|Ki|M|Mi|G|Gi|T|Ti)([bB]))$`)
|
||||||
|
|
||||||
|
func (r *Bits) UnmarshalYAML(u func(interface{}, bool) error) (_ error) {
|
||||||
|
|
||||||
|
var s string
|
||||||
|
err := u(&s, false)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
genericErr := func(err error) error {
|
||||||
|
var buf strings.Builder
|
||||||
|
fmt.Fprintf(&buf, "cannot parse %q using regex %s", s, datarateRegex)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(&buf, ": %s", err)
|
||||||
|
}
|
||||||
|
return errors.New(buf.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
match := datarateRegex.FindStringSubmatch(s)
|
||||||
|
if match == nil {
|
||||||
|
return genericErr(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
bps, err := strconv.ParseFloat(match[1], 64)
|
||||||
|
if err != nil {
|
||||||
|
return genericErr(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if match[2] == "bit" {
|
||||||
|
if math.Round(bps) != bps {
|
||||||
|
return genericErr(fmt.Errorf("unit bit must be an integer value"))
|
||||||
|
}
|
||||||
|
r.bits = bps
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
factorMap := map[string]uint64{
|
||||||
|
"": 1,
|
||||||
|
|
||||||
|
"K": 1e3,
|
||||||
|
"M": 1e6,
|
||||||
|
"G": 1e9,
|
||||||
|
"T": 1e12,
|
||||||
|
|
||||||
|
"Ki": 1 << 10,
|
||||||
|
"Mi": 1 << 20,
|
||||||
|
"Gi": 1 << 30,
|
||||||
|
"Ti": 1 << 40,
|
||||||
|
}
|
||||||
|
factor, ok := factorMap[match[3]]
|
||||||
|
if !ok {
|
||||||
|
panic(match)
|
||||||
|
}
|
||||||
|
|
||||||
|
baseUnitFactorMap := map[string]uint64{
|
||||||
|
"b": 1,
|
||||||
|
"B": 8,
|
||||||
|
}
|
||||||
|
baseUnitFactor, ok := baseUnitFactorMap[match[4]]
|
||||||
|
if !ok {
|
||||||
|
panic(match)
|
||||||
|
}
|
||||||
|
|
||||||
|
r.bits = bps * float64(factor) * float64(baseUnitFactor)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package datasizeunit
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/zrepl/yaml-config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBits(t *testing.T) {
|
||||||
|
|
||||||
|
tcs := []struct {
|
||||||
|
input string
|
||||||
|
expectRate float64
|
||||||
|
expectErr string
|
||||||
|
}{
|
||||||
|
{`23 bit`, 23, ""}, // bit special case works
|
||||||
|
{`23bit`, 23, ""}, // also without space
|
||||||
|
|
||||||
|
{`10MiB`, 10 * (1 << 20) * 8, ""}, // integer unit without space
|
||||||
|
{`10 MiB`, 8 * 10 * (1 << 20), ""}, // integer unit with space
|
||||||
|
|
||||||
|
{`10.5 Kib`, 10.5 * (1 << 10), ""}, // floating point with bit unit works with space
|
||||||
|
{`10.5Kib`, 10.5 * (1 << 10), ""}, // floating point with bit unit works without space
|
||||||
|
|
||||||
|
// unit checks
|
||||||
|
{`1 bit`, 1, ""},
|
||||||
|
{`1 B`, 1 * 8, ""},
|
||||||
|
{`1 Kb`, 1e3, ""},
|
||||||
|
{`1 Kib`, 1 << 10, ""},
|
||||||
|
{`1 Mb`, 1e6, ""},
|
||||||
|
{`1 Mib`, 1 << 20, ""},
|
||||||
|
{`1 Gb`, 1e9, ""},
|
||||||
|
{`1 Gib`, 1 << 30, ""},
|
||||||
|
{`1 Tb`, 1e12, ""},
|
||||||
|
{`1 Tib`, 1 << 40, ""},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tcs {
|
||||||
|
t.Run(tc.input, func(t *testing.T) {
|
||||||
|
|
||||||
|
var bits Bits
|
||||||
|
err := yaml.Unmarshal([]byte(tc.input), &bits)
|
||||||
|
if tc.expectErr != "" {
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Regexp(t, tc.expectErr, err.Error())
|
||||||
|
assert.Zero(t, bits.bits)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, tc.expectRate, bits.bits)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -73,6 +73,24 @@ func Int64(varname string, def int64) (d int64) {
|
|||||||
return d
|
return d
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Uint64(varname string, def uint64) (d uint64) {
|
||||||
|
var err error
|
||||||
|
if v, ok := cache.Load(varname); ok {
|
||||||
|
return v.(uint64)
|
||||||
|
}
|
||||||
|
e := os.Getenv(varname)
|
||||||
|
if e == "" {
|
||||||
|
d = def
|
||||||
|
} else {
|
||||||
|
d, err = strconv.ParseUint(e, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cache.Store(varname, d)
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
func Bool(varname string, def bool) (d bool) {
|
func Bool(varname string, def bool) (d bool) {
|
||||||
var err error
|
var err error
|
||||||
if v, ok := cache.Load(varname); ok {
|
if v, ok := cache.Load(varname); ok {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
//go:build freebsd
|
||||||
// +build freebsd
|
// +build freebsd
|
||||||
|
|
||||||
package tcpsock
|
package tcpsock
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
//go:build linux
|
||||||
// +build linux
|
// +build linux
|
||||||
|
|
||||||
package tcpsock
|
package tcpsock
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
//go:build !linux && !freebsd
|
||||||
// +build !linux,!freebsd
|
// +build !linux,!freebsd
|
||||||
|
|
||||||
package tcpsock
|
package tcpsock
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package zreplcircleci
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SkipOnCircleCI(t *testing.T, reasonFmt string, args ...interface{}) {
|
||||||
|
if os.Getenv("CIRCLECI") != "" {
|
||||||
|
t.Skipf("This test is skipped in CircleCI. Reason: %s", fmt.Sprintf(reasonFmt, args...))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
// Code generated by "enumer -type=FilesystemPlaceholderCreateEncryptionValue -trimprefix=FilesystemPlaceholderCreateEncryption"; DO NOT EDIT.
|
||||||
|
|
||||||
|
//
|
||||||
|
package zfs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
const _FilesystemPlaceholderCreateEncryptionValueName = "InheritOff"
|
||||||
|
|
||||||
|
var _FilesystemPlaceholderCreateEncryptionValueIndex = [...]uint8{0, 7, 10}
|
||||||
|
|
||||||
|
func (i FilesystemPlaceholderCreateEncryptionValue) String() string {
|
||||||
|
i -= 1
|
||||||
|
if i < 0 || i >= FilesystemPlaceholderCreateEncryptionValue(len(_FilesystemPlaceholderCreateEncryptionValueIndex)-1) {
|
||||||
|
return fmt.Sprintf("FilesystemPlaceholderCreateEncryptionValue(%d)", i+1)
|
||||||
|
}
|
||||||
|
return _FilesystemPlaceholderCreateEncryptionValueName[_FilesystemPlaceholderCreateEncryptionValueIndex[i]:_FilesystemPlaceholderCreateEncryptionValueIndex[i+1]]
|
||||||
|
}
|
||||||
|
|
||||||
|
var _FilesystemPlaceholderCreateEncryptionValueValues = []FilesystemPlaceholderCreateEncryptionValue{1, 2}
|
||||||
|
|
||||||
|
var _FilesystemPlaceholderCreateEncryptionValueNameToValueMap = map[string]FilesystemPlaceholderCreateEncryptionValue{
|
||||||
|
_FilesystemPlaceholderCreateEncryptionValueName[0:7]: 1,
|
||||||
|
_FilesystemPlaceholderCreateEncryptionValueName[7:10]: 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
// FilesystemPlaceholderCreateEncryptionValueString retrieves an enum value from the enum constants string name.
|
||||||
|
// Throws an error if the param is not part of the enum.
|
||||||
|
func FilesystemPlaceholderCreateEncryptionValueString(s string) (FilesystemPlaceholderCreateEncryptionValue, error) {
|
||||||
|
if val, ok := _FilesystemPlaceholderCreateEncryptionValueNameToValueMap[s]; ok {
|
||||||
|
return val, nil
|
||||||
|
}
|
||||||
|
return 0, fmt.Errorf("%s does not belong to FilesystemPlaceholderCreateEncryptionValue values", s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FilesystemPlaceholderCreateEncryptionValueValues returns all values of the enum
|
||||||
|
func FilesystemPlaceholderCreateEncryptionValueValues() []FilesystemPlaceholderCreateEncryptionValue {
|
||||||
|
return _FilesystemPlaceholderCreateEncryptionValueValues
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsAFilesystemPlaceholderCreateEncryptionValue returns "true" if the value is listed in the enum definition. "false" otherwise
|
||||||
|
func (i FilesystemPlaceholderCreateEncryptionValue) IsAFilesystemPlaceholderCreateEncryptionValue() bool {
|
||||||
|
for _, v := range _FilesystemPlaceholderCreateEncryptionValueValues {
|
||||||
|
if i == v {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
+52
-5
@@ -80,7 +80,15 @@ func ZFSGetFilesystemPlaceholderState(ctx context.Context, p *DatasetPath) (stat
|
|||||||
return state, nil
|
return state, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ZFSCreatePlaceholderFilesystem(ctx context.Context, fs *DatasetPath, parent *DatasetPath) (err error) {
|
//go:generate enumer -type=FilesystemPlaceholderCreateEncryptionValue -trimprefix=FilesystemPlaceholderCreateEncryption
|
||||||
|
type FilesystemPlaceholderCreateEncryptionValue int
|
||||||
|
|
||||||
|
const (
|
||||||
|
FilesystemPlaceholderCreateEncryptionInherit FilesystemPlaceholderCreateEncryptionValue = 1 << iota
|
||||||
|
FilesystemPlaceholderCreateEncryptionOff
|
||||||
|
)
|
||||||
|
|
||||||
|
func ZFSCreatePlaceholderFilesystem(ctx context.Context, fs *DatasetPath, parent *DatasetPath, encryption FilesystemPlaceholderCreateEncryptionValue) (err error) {
|
||||||
if fs.Length() == 1 {
|
if fs.Length() == 1 {
|
||||||
return fmt.Errorf("cannot create %q: pools cannot be created with zfs create", fs.ToString())
|
return fmt.Errorf("cannot create %q: pools cannot be created with zfs create", fs.ToString())
|
||||||
}
|
}
|
||||||
@@ -90,11 +98,19 @@ func ZFSCreatePlaceholderFilesystem(ctx context.Context, fs *DatasetPath, parent
|
|||||||
"-o", fmt.Sprintf("%s=%s", PlaceholderPropertyName, placeholderPropertyOn),
|
"-o", fmt.Sprintf("%s=%s", PlaceholderPropertyName, placeholderPropertyOn),
|
||||||
"-o", "mountpoint=none",
|
"-o", "mountpoint=none",
|
||||||
}
|
}
|
||||||
if parentEncrypted, err := ZFSGetEncryptionEnabled(ctx, parent.ToString()); err != nil {
|
|
||||||
return errors.Wrap(err, "cannot determine encryption support")
|
if !encryption.IsAFilesystemPlaceholderCreateEncryptionValue() {
|
||||||
} else if parentEncrypted {
|
panic(encryption)
|
||||||
cmdline = append(cmdline, "-o", "encryption=off")
|
|
||||||
}
|
}
|
||||||
|
switch encryption {
|
||||||
|
case FilesystemPlaceholderCreateEncryptionInherit:
|
||||||
|
// no-op
|
||||||
|
case FilesystemPlaceholderCreateEncryptionOff:
|
||||||
|
cmdline = append(cmdline, "-o", "encryption=off")
|
||||||
|
default:
|
||||||
|
panic(encryption)
|
||||||
|
}
|
||||||
|
|
||||||
cmdline = append(cmdline, fs.ToString())
|
cmdline = append(cmdline, fs.ToString())
|
||||||
cmd := zfscmd.CommandContext(ctx, ZFS_BINARY, cmdline...)
|
cmd := zfscmd.CommandContext(ctx, ZFS_BINARY, cmdline...)
|
||||||
|
|
||||||
@@ -148,3 +164,34 @@ func ZFSMigrateHashBasedPlaceholderToCurrent(ctx context.Context, fs *DatasetPat
|
|||||||
}
|
}
|
||||||
return &report, nil
|
return &report, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ZFSListPlaceholderFilesystemsWithAdditionalProps(ctx context.Context, root string, additionalProps []string) (map[string]*ZFSProperties, error) {
|
||||||
|
|
||||||
|
props := []string{PlaceholderPropertyName}
|
||||||
|
if len(additionalProps) > 0 {
|
||||||
|
props = append(props, additionalProps...)
|
||||||
|
}
|
||||||
|
|
||||||
|
propsByFS, err := zfsGetRecursive(ctx, root, -1, []string{"filesystem", "volume"}, props, SourceAny)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "cannot get placeholder filesystems under %q", root)
|
||||||
|
}
|
||||||
|
|
||||||
|
filtered := make(map[string]*ZFSProperties)
|
||||||
|
for fs, props := range propsByFS {
|
||||||
|
details := props.GetDetails(PlaceholderPropertyName)
|
||||||
|
if details.Source != SourceLocal {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fsp, err := NewDatasetPath(fs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "zfs get returned invalid dataset path %q", fs)
|
||||||
|
}
|
||||||
|
if !isLocalPlaceholderPropertyValuePlaceholder(fsp, details.Value) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
filtered[fs] = props
|
||||||
|
}
|
||||||
|
|
||||||
|
return filtered, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -38,14 +38,14 @@ func (i PropertySource) String() string {
|
|||||||
return _PropertySourceName_3
|
return _PropertySourceName_3
|
||||||
case i == 32:
|
case i == 32:
|
||||||
return _PropertySourceName_4
|
return _PropertySourceName_4
|
||||||
case i == 18446744073709551615:
|
case i == 4294967295:
|
||||||
return _PropertySourceName_5
|
return _PropertySourceName_5
|
||||||
default:
|
default:
|
||||||
return fmt.Sprintf("PropertySource(%d)", i)
|
return fmt.Sprintf("PropertySource(%d)", i)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var _PropertySourceValues = []PropertySource{1, 2, 4, 8, 16, 32, 18446744073709551615}
|
var _PropertySourceValues = []PropertySource{1, 2, 4, 8, 16, 32, 4294967295}
|
||||||
|
|
||||||
var _PropertySourceNameToValueMap = map[string]PropertySource{
|
var _PropertySourceNameToValueMap = map[string]PropertySource{
|
||||||
_PropertySourceName_0[0:5]: 1,
|
_PropertySourceName_0[0:5]: 1,
|
||||||
@@ -54,7 +54,7 @@ var _PropertySourceNameToValueMap = map[string]PropertySource{
|
|||||||
_PropertySourceName_2[0:4]: 8,
|
_PropertySourceName_2[0:4]: 8,
|
||||||
_PropertySourceName_3[0:9]: 16,
|
_PropertySourceName_3[0:9]: 16,
|
||||||
_PropertySourceName_4[0:8]: 32,
|
_PropertySourceName_4[0:8]: 32,
|
||||||
_PropertySourceName_5[0:3]: 18446744073709551615,
|
_PropertySourceName_5[0:3]: 4294967295,
|
||||||
}
|
}
|
||||||
|
|
||||||
// PropertySourceString retrieves an enum value from the enum constants string name.
|
// PropertySourceString retrieves an enum value from the enum constants string name.
|
||||||
|
|||||||
+189
-87
@@ -7,6 +7,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"math"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"regexp"
|
"regexp"
|
||||||
@@ -14,7 +15,6 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
@@ -26,11 +26,6 @@ import (
|
|||||||
"github.com/zrepl/zrepl/zfs/zfscmd"
|
"github.com/zrepl/zrepl/zfs/zfscmd"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
|
||||||
ZFSSendPipeCapacityHint = int(envconst.Int64("ZFS_SEND_PIPE_CAPACITY_HINT", 1<<25))
|
|
||||||
ZFSRecvPipeCapacityHint = int(envconst.Int64("ZFS_RECV_PIPE_CAPACITY_HINT", 1<<25))
|
|
||||||
)
|
|
||||||
|
|
||||||
type DatasetPath struct {
|
type DatasetPath struct {
|
||||||
comps []string
|
comps []string
|
||||||
}
|
}
|
||||||
@@ -328,8 +323,8 @@ func absVersion(fs string, v *ZFSSendArgVersion) (full string, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func pipeWithCapacityHint(capacity int) (r, w *os.File, err error) {
|
func pipeWithCapacityHint(capacity int) (r, w *os.File, err error) {
|
||||||
if capacity <= 0 {
|
if capacity < 0 {
|
||||||
panic(fmt.Sprintf("capacity must be positive %v", capacity))
|
panic(fmt.Sprintf("capacity must be non-negative, got %v", capacity))
|
||||||
}
|
}
|
||||||
stdoutReader, stdoutWriter, err := os.Pipe()
|
stdoutReader, stdoutWriter, err := os.Pipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -339,60 +334,122 @@ func pipeWithCapacityHint(capacity int) (r, w *os.File, err error) {
|
|||||||
return stdoutReader, stdoutWriter, nil
|
return stdoutReader, stdoutWriter, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type sendStreamState int
|
||||||
|
|
||||||
|
const (
|
||||||
|
sendStreamOpen sendStreamState = iota
|
||||||
|
sendStreamClosed
|
||||||
|
)
|
||||||
|
|
||||||
type SendStream struct {
|
type SendStream struct {
|
||||||
cmd *zfscmd.Cmd
|
cmd *zfscmd.Cmd
|
||||||
kill context.CancelFunc
|
kill context.CancelFunc
|
||||||
|
stdoutReader io.ReadCloser // not *os.File for mocking during platformtest
|
||||||
closeMtx sync.Mutex
|
|
||||||
stdoutReader *os.File
|
|
||||||
stderrBuf *circlog.CircularLog
|
stderrBuf *circlog.CircularLog
|
||||||
opErr error
|
|
||||||
|
mtx sync.Mutex
|
||||||
|
state sendStreamState
|
||||||
|
exitErr *ZFSError
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SendStream) Read(p []byte) (n int, err error) {
|
func (s *SendStream) Read(p []byte) (n int, _ error) {
|
||||||
s.closeMtx.Lock()
|
s.mtx.Lock()
|
||||||
opErr := s.opErr
|
defer s.mtx.Unlock()
|
||||||
s.closeMtx.Unlock()
|
|
||||||
if opErr != nil {
|
|
||||||
return 0, opErr
|
|
||||||
}
|
|
||||||
|
|
||||||
n, err = s.stdoutReader.Read(p)
|
switch s.state {
|
||||||
if err != nil {
|
case sendStreamClosed:
|
||||||
debug("sendStream: read err: %T %s", err, err)
|
return 0, os.ErrClosed
|
||||||
// TODO we assume here that any read error is permanent
|
|
||||||
// which is most likely the case for a local zfs send
|
case sendStreamOpen:
|
||||||
kwerr := s.killAndWait(err)
|
n, readErr := s.stdoutReader.Read(p)
|
||||||
debug("sendStream: killAndWait n=%v err= %T %s", n, kwerr, kwerr)
|
if readErr != nil {
|
||||||
// TODO we assume here that any read error is permanent
|
debug("sendStream: read: readErr=%T %s", readErr, readErr)
|
||||||
return n, kwerr
|
if readErr == io.EOF {
|
||||||
|
// io.EOF must be bubbled up as is so that consumers can handle it properly.
|
||||||
|
return n, readErr
|
||||||
|
}
|
||||||
|
// Assume that the error is not retryable.
|
||||||
|
// Try to kill now so that we can return a nice *ZFSError with captured stderr.
|
||||||
|
// If the kill doesn't work, it doesn't matter because the caller must by contract call Close() anyways.
|
||||||
|
killErr := s.killAndWait()
|
||||||
|
debug("sendStream: read: killErr=%T %s", killErr, killErr)
|
||||||
|
if killErr == nil {
|
||||||
|
s.state = sendStreamClosed
|
||||||
|
return n, s.exitErr // return the nice error
|
||||||
|
} else {
|
||||||
|
// we remain open so that we retry
|
||||||
|
return n, readErr // return the normal error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return n, readErr
|
||||||
|
|
||||||
|
default:
|
||||||
|
panic("unreachable")
|
||||||
}
|
}
|
||||||
return n, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SendStream) Close() error {
|
func (s *SendStream) Close() error {
|
||||||
debug("sendStream: close called")
|
debug("sendStream: close called")
|
||||||
return s.killAndWait(nil)
|
s.mtx.Lock()
|
||||||
|
defer s.mtx.Unlock()
|
||||||
|
|
||||||
|
switch s.state {
|
||||||
|
case sendStreamOpen:
|
||||||
|
err := s.killAndWait()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
s.state = sendStreamClosed
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
case sendStreamClosed:
|
||||||
|
return os.ErrClosed
|
||||||
|
default:
|
||||||
|
panic("unreachable")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SendStream) killAndWait(precedingReadErr error) error {
|
// returns nil iff the child process is gone (has been successfully waited upon)
|
||||||
|
// in that case, s.exitErr is set
|
||||||
|
func (s *SendStream) killAndWait() error {
|
||||||
|
|
||||||
debug("sendStream: killAndWait enter")
|
debug("sendStream: killAndWait enter")
|
||||||
defer debug("sendStream: killAndWait leave")
|
defer debug("sendStream: killAndWait leave")
|
||||||
if precedingReadErr == io.EOF {
|
|
||||||
// give the zfs process a little bit of time to terminate itself
|
// send SIGKILL
|
||||||
// if it holds this deadline, exitErr will be nil
|
|
||||||
time.AfterFunc(200*time.Millisecond, s.kill)
|
|
||||||
} else {
|
|
||||||
s.kill()
|
s.kill()
|
||||||
|
|
||||||
|
// Close our read-end of the pipe.
|
||||||
|
//
|
||||||
|
// We must do this before .Wait() because in some (not all) versions/build configs of ZFS,
|
||||||
|
// `zfs send` uses a separate kernel thread (taskq) to write the send stream (function `dump_bytes`).
|
||||||
|
// The `zfs send` thread then waits uinterruptably for the taskq thread to finish the write.
|
||||||
|
// And signalling the `zfs send` thread doesn't propagate to the taskq thread.
|
||||||
|
// So we end up in a state where we .Wait() forever.
|
||||||
|
// (See https://github.com/openzfs/zfs/issues/12500 and
|
||||||
|
// https://github.com/zrepl/zrepl/issues/495#issuecomment-902530043)
|
||||||
|
//
|
||||||
|
// By closing our read end of the pipe before .Wait(), we unblock the taskq thread if there is any.
|
||||||
|
// If there is no separate taskq thread, the SIGKILL to `zfs end` would suffice and be most precise,
|
||||||
|
// but due to the circumstances above, there is no other portable & robust way.
|
||||||
|
//
|
||||||
|
// However, the fallout from closing the pipe is that (in non-taskq builds) `zfs sends` will get a SIGPIPE.
|
||||||
|
// And on Linux, that SIGPIPE appears to win over the previously issued SIGKILL.
|
||||||
|
// And thus, on Linux, the `zfs send` will be killed by the default SIGPIPE handler.
|
||||||
|
// We can observe this in the WaitStatus below.
|
||||||
|
// This behavior is slightly annoying because the *exec.ExitError's message ("signal: broken pipe")
|
||||||
|
// isn't as clear as ("signal: killed").
|
||||||
|
// However, it seems like we just have to live with that. (covered by platformtest)
|
||||||
|
var closePipeErr error
|
||||||
|
if s.stdoutReader != nil {
|
||||||
|
closePipeErr = s.stdoutReader.Close()
|
||||||
|
if closePipeErr == nil {
|
||||||
|
// avoid double-closes in case waiting below doesn't work
|
||||||
|
// and someone attempts Close again
|
||||||
|
s.stdoutReader = nil
|
||||||
|
} else {
|
||||||
|
return closePipeErr
|
||||||
}
|
}
|
||||||
|
|
||||||
// allow async kills from Close(), that's why we only take the mutex here
|
|
||||||
s.closeMtx.Lock()
|
|
||||||
defer s.closeMtx.Unlock()
|
|
||||||
|
|
||||||
if s.opErr != nil {
|
|
||||||
return s.opErr
|
|
||||||
}
|
}
|
||||||
|
|
||||||
waitErr := s.cmd.Wait()
|
waitErr := s.cmd.Wait()
|
||||||
@@ -407,39 +464,30 @@ func (s *SendStream) killAndWait(precedingReadErr error) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// now, after we know the program exited do we close the pipe
|
// invariant: at this point, the child is gone and we cleaned up everything related to the SendStream
|
||||||
var closePipeErr error
|
|
||||||
if s.stdoutReader != nil {
|
|
||||||
closePipeErr = s.stdoutReader.Close()
|
|
||||||
if closePipeErr == nil {
|
|
||||||
// avoid double-closes in case anything below doesn't work
|
|
||||||
// and someone calls Close again
|
|
||||||
s.stdoutReader = nil
|
|
||||||
} else {
|
|
||||||
return closePipeErr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// we managed to tear things down, no let's give the user some pretty *ZFSError
|
|
||||||
if exitErr != nil {
|
if exitErr != nil {
|
||||||
s.opErr = &ZFSError{
|
// zfs send exited with an error or was killed by a signal.
|
||||||
|
s.exitErr = &ZFSError{
|
||||||
Stderr: []byte(s.stderrBuf.String()),
|
Stderr: []byte(s.stderrBuf.String()),
|
||||||
WaitErr: exitErr,
|
WaitErr: exitErr,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
s.opErr = precedingReadErr
|
// zfs send exited successfully (we know that since waitErr was either nil or wasn't an *exec.ExitError)
|
||||||
|
s.exitErr = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// detect the edge where we're called from s.Read
|
return nil
|
||||||
// after the pipe EOFed and zfs send exited without errors
|
|
||||||
// this is actually the "hot" / nice path
|
|
||||||
if exitErr == nil && precedingReadErr == io.EOF {
|
|
||||||
return precedingReadErr
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return s.opErr
|
func (s *SendStream) TestOnly_ReplaceStdoutReader(f io.ReadCloser) (prev io.ReadCloser) {
|
||||||
|
prev = s.stdoutReader
|
||||||
|
s.stdoutReader = f
|
||||||
|
return prev
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SendStream) TestOnly_ExitErr() *ZFSError { return s.exitErr }
|
||||||
|
|
||||||
// NOTE: When updating this struct, make sure to update funcs Validate ValidateCorrespondsToResumeToken
|
// NOTE: When updating this struct, make sure to update funcs Validate ValidateCorrespondsToResumeToken
|
||||||
type ZFSSendArgVersion struct {
|
type ZFSSendArgVersion struct {
|
||||||
RelName string
|
RelName string
|
||||||
@@ -858,7 +906,7 @@ func ZFSSend(ctx context.Context, sendArgs ZFSSendArgsValidated) (*SendStream, e
|
|||||||
ctx, cancel := context.WithCancel(ctx)
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
|
|
||||||
// setup stdout with an os.Pipe to control pipe buffer size
|
// setup stdout with an os.Pipe to control pipe buffer size
|
||||||
stdoutReader, stdoutWriter, err := pipeWithCapacityHint(ZFSSendPipeCapacityHint)
|
stdoutReader, stdoutWriter, err := pipeWithCapacityHint(getPipeCapacityHint("ZFS_SEND_PIPE_CAPACITY_HINT"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cancel()
|
cancel()
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -913,7 +961,7 @@ type DrySendInfo struct {
|
|||||||
Type DrySendType
|
Type DrySendType
|
||||||
Filesystem string // parsed from To field
|
Filesystem string // parsed from To field
|
||||||
From, To string // direct copy from ZFS output
|
From, To string // direct copy from ZFS output
|
||||||
SizeEstimate int64 // -1 if size estimate is not possible
|
SizeEstimate uint64 // 0 if size estimate is not possible
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -986,11 +1034,10 @@ func (s *DrySendInfo) unmarshalInfoLine(l string) (regexMatched bool, err error)
|
|||||||
// see https://github.com/zrepl/zrepl/issues/289
|
// see https://github.com/zrepl/zrepl/issues/289
|
||||||
fields["size"] = "0"
|
fields["size"] = "0"
|
||||||
}
|
}
|
||||||
s.SizeEstimate, err = strconv.ParseInt(fields["size"], 10, 64)
|
s.SizeEstimate, err = strconv.ParseUint(fields["size"], 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return true, fmt.Errorf("cannot not parse size: %s", err)
|
return true, fmt.Errorf("cannot not parse size: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1000,6 +1047,7 @@ func ZFSSendDry(ctx context.Context, sendArgs ZFSSendArgsValidated) (_ *DrySendI
|
|||||||
|
|
||||||
if sendArgs.From != nil && strings.Contains(sendArgs.From.RelName, "#") {
|
if sendArgs.From != nil && strings.Contains(sendArgs.From.RelName, "#") {
|
||||||
/* TODO:
|
/* TODO:
|
||||||
|
* XXX feature check & support this as well
|
||||||
* ZFS at the time of writing does not support dry-run send because size-estimation
|
* ZFS at the time of writing does not support dry-run send because size-estimation
|
||||||
* uses fromSnap's deadlist. However, for a bookmark, that deadlist no longer exists.
|
* uses fromSnap's deadlist. However, for a bookmark, that deadlist no longer exists.
|
||||||
* Redacted send & recv will bring this functionality, see
|
* Redacted send & recv will bring this functionality, see
|
||||||
@@ -1018,7 +1066,7 @@ func ZFSSendDry(ctx context.Context, sendArgs ZFSSendArgsValidated) (_ *DrySendI
|
|||||||
Filesystem: sendArgs.FS,
|
Filesystem: sendArgs.FS,
|
||||||
From: fromAbs,
|
From: fromAbs,
|
||||||
To: toAbs,
|
To: toAbs,
|
||||||
SizeEstimate: -1}, nil
|
SizeEstimate: 0}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
args := make([]string, 0)
|
args := make([]string, 0)
|
||||||
@@ -1038,6 +1086,19 @@ func ZFSSendDry(ctx context.Context, sendArgs ZFSSendArgsValidated) (_ *DrySendI
|
|||||||
if err := si.unmarshalZFSOutput(output); err != nil {
|
if err := si.unmarshalZFSOutput(output); err != nil {
|
||||||
return nil, fmt.Errorf("could not parse zfs send -n output: %s", err)
|
return nil, fmt.Errorf("could not parse zfs send -n output: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// There is a bug in OpenZFS where it estimates the size incorrectly.
|
||||||
|
// - zrepl: https://github.com/zrepl/zrepl/issues/463
|
||||||
|
// - resulting upstream bug: https://github.com/openzfs/zfs/issues/12265
|
||||||
|
//
|
||||||
|
// The wrong estimates are easy to detect because they are absurdly large.
|
||||||
|
// NB: we're doing the workaround for this late so that the test cases are not affected.
|
||||||
|
sizeEstimateThreshold := envconst.Uint64("ZREPL_ZFS_SEND_SIZE_ESTIMATE_INCORRECT_THRESHOLD", math.MaxInt64)
|
||||||
|
if sizeEstimateThreshold != 0 && si.SizeEstimate >= sizeEstimateThreshold {
|
||||||
|
debug("size estimate exceeds threshold %v, working around it: %#v %q", sizeEstimateThreshold, si, args)
|
||||||
|
si.SizeEstimate = 0
|
||||||
|
}
|
||||||
|
|
||||||
return &si, nil
|
return &si, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1160,7 +1221,7 @@ func ZFSRecv(ctx context.Context, fs string, v *ZFSSendArgVersion, stream io.Rea
|
|||||||
|
|
||||||
stderr := bytes.NewBuffer(make([]byte, 0, RecvStderrBufSiz))
|
stderr := bytes.NewBuffer(make([]byte, 0, RecvStderrBufSiz))
|
||||||
|
|
||||||
stdin, stdinWriter, err := pipeWithCapacityHint(ZFSRecvPipeCapacityHint)
|
stdin, stdinWriter, err := pipeWithCapacityHint(getPipeCapacityHint("ZFS_RECV_PIPE_CAPACITY_HINT"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -1446,7 +1507,7 @@ func tryDatasetDoesNotExist(expectPath string, stderr []byte) *DatasetDoesNotExi
|
|||||||
}
|
}
|
||||||
|
|
||||||
//go:generate enumer -type=PropertySource -trimprefix=Source
|
//go:generate enumer -type=PropertySource -trimprefix=Source
|
||||||
type PropertySource uint
|
type PropertySource uint32
|
||||||
|
|
||||||
const (
|
const (
|
||||||
SourceLocal PropertySource = 1 << iota
|
SourceLocal PropertySource = 1 << iota
|
||||||
@@ -1506,8 +1567,18 @@ func (s PropertySource) zfsGetSourceFieldPrefixes() []string {
|
|||||||
return prefixes
|
return prefixes
|
||||||
}
|
}
|
||||||
|
|
||||||
func zfsGet(ctx context.Context, path string, props []string, allowedSources PropertySource) (*ZFSProperties, error) {
|
func zfsGetRecursive(ctx context.Context, path string, depth int, dstypes []string, props []string, allowedSources PropertySource) (map[string]*ZFSProperties, error) {
|
||||||
args := []string{"get", "-Hp", "-o", "property,value,source", strings.Join(props, ","), path}
|
args := []string{"get", "-Hp", "-o", "name,property,value,source"}
|
||||||
|
if depth != 0 {
|
||||||
|
args = append(args, "-r")
|
||||||
|
if depth != -1 {
|
||||||
|
args = append(args, "-d", fmt.Sprintf("%d", depth))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(dstypes) > 0 {
|
||||||
|
args = append(args, "-t", strings.Join(dstypes, ","))
|
||||||
|
}
|
||||||
|
args = append(args, strings.Join(props, ","), path)
|
||||||
cmd := zfscmd.CommandContext(ctx, ZFS_BINARY, args...)
|
cmd := zfscmd.CommandContext(ctx, ZFS_BINARY, args...)
|
||||||
stdout, err := cmd.Output()
|
stdout, err := cmd.Output()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1527,36 +1598,67 @@ func zfsGet(ctx context.Context, path string, props []string, allowedSources Pro
|
|||||||
}
|
}
|
||||||
o := string(stdout)
|
o := string(stdout)
|
||||||
lines := strings.Split(o, "\n")
|
lines := strings.Split(o, "\n")
|
||||||
if len(lines) < 1 || // account for newlines
|
propsByFS := make(map[string]*ZFSProperties)
|
||||||
len(lines)-1 != len(props) {
|
|
||||||
return nil, fmt.Errorf("zfs get did not return the number of expected property values")
|
|
||||||
}
|
|
||||||
res := &ZFSProperties{
|
|
||||||
make(map[string]PropertyValue, len(lines)),
|
|
||||||
}
|
|
||||||
allowedPrefixes := allowedSources.zfsGetSourceFieldPrefixes()
|
allowedPrefixes := allowedSources.zfsGetSourceFieldPrefixes()
|
||||||
for _, line := range lines[:len(lines)-1] {
|
for _, line := range lines[:len(lines)-1] { // last line is an empty line due to how strings.Split works
|
||||||
fields := strings.FieldsFunc(line, func(r rune) bool {
|
fields := strings.FieldsFunc(line, func(r rune) bool {
|
||||||
return r == '\t'
|
return r == '\t'
|
||||||
})
|
})
|
||||||
if len(fields) != 3 {
|
if len(fields) != 4 {
|
||||||
return nil, fmt.Errorf("zfs get did not return property,value,source tuples")
|
return nil, fmt.Errorf("zfs get did not return name,property,value,source tuples")
|
||||||
}
|
}
|
||||||
for _, p := range allowedPrefixes {
|
for _, p := range allowedPrefixes {
|
||||||
// prefix-match so that SourceAny (= "") works
|
// prefix-match so that SourceAny (= "") works
|
||||||
if strings.HasPrefix(fields[2], p) {
|
if strings.HasPrefix(fields[3], p) {
|
||||||
source, err := parsePropertySource(fields[2])
|
source, err := parsePropertySource(fields[3])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "parse property source")
|
return nil, errors.Wrap(err, "parse property source")
|
||||||
}
|
}
|
||||||
res.m[fields[0]] = PropertyValue{
|
fsProps, ok := propsByFS[fields[0]]
|
||||||
Value: fields[1],
|
if !ok {
|
||||||
|
fsProps = &ZFSProperties{
|
||||||
|
make(map[string]PropertyValue),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, ok := fsProps.m[fields[1]]; ok {
|
||||||
|
return nil, errors.Errorf("duplicate property %q for dataset %q", fields[1], fields[0])
|
||||||
|
}
|
||||||
|
fsProps.m[fields[1]] = PropertyValue{
|
||||||
|
Value: fields[2],
|
||||||
Source: source,
|
Source: source,
|
||||||
}
|
}
|
||||||
|
propsByFS[fields[0]] = fsProps
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// validate we got expected output
|
||||||
|
for fs, fsProps := range propsByFS {
|
||||||
|
if len(fsProps.m) != len(props) {
|
||||||
|
return nil, errors.Errorf("zfs get did not return all requested values for dataset %q\noutput was:\n%s", fs, o)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return propsByFS, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func zfsGet(ctx context.Context, path string, props []string, allowedSources PropertySource) (*ZFSProperties, error) {
|
||||||
|
propMap, err := zfsGetRecursive(ctx, path, 0, nil, props, allowedSources)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(propMap) == 0 {
|
||||||
|
// XXX callers expect to always get a result here
|
||||||
|
// They will observe props.Get("propname") == ""
|
||||||
|
// We should change .Get to return a tuple, or an error, or whatever.
|
||||||
|
return &ZFSProperties{make(map[string]PropertyValue)}, nil
|
||||||
|
}
|
||||||
|
if len(propMap) != 1 {
|
||||||
|
return nil, errors.Errorf("zfs get unexpectedly returned properties for multiple datasets")
|
||||||
|
}
|
||||||
|
res, ok := propMap[path]
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.Errorf("zfs get returned properties for a different dataset that requested")
|
||||||
|
}
|
||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+6
-1
@@ -1,3 +1,4 @@
|
|||||||
|
//go:build !linux
|
||||||
// +build !linux
|
// +build !linux
|
||||||
|
|
||||||
package zfs
|
package zfs
|
||||||
@@ -7,10 +8,14 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func getPipeCapacityHint(envvar string) int {
|
||||||
|
return 0 // not supported
|
||||||
|
}
|
||||||
|
|
||||||
var zfsPipeCapacityNotSupported sync.Once
|
var zfsPipeCapacityNotSupported sync.Once
|
||||||
|
|
||||||
func trySetPipeCapacity(p *os.File, capacity int) {
|
func trySetPipeCapacity(p *os.File, capacity int) {
|
||||||
if debugEnabled {
|
if debugEnabled && capacity != 0 {
|
||||||
zfsPipeCapacityNotSupported.Do(func() {
|
zfsPipeCapacityNotSupported.Do(func() {
|
||||||
debug("trySetPipeCapacity error: OS does not support setting pipe capacity")
|
debug("trySetPipeCapacity error: OS does not support setting pipe capacity")
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,11 +3,33 @@ package zfs
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"golang.org/x/sys/unix"
|
"golang.org/x/sys/unix"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/util/envconst"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func getPipeCapacityHint(envvar string) int {
|
||||||
|
var capacity int64 = 1 << 25
|
||||||
|
|
||||||
|
// Work around a race condition in Linux >= 5.8 related to pipe resizing.
|
||||||
|
// https://github.com/zrepl/zrepl/issues/424#issuecomment-800370928
|
||||||
|
// https://bugzilla.kernel.org/show_bug.cgi?id=212295
|
||||||
|
if _, err := os.Stat("/proc/sys/fs/pipe-max-size"); err == nil {
|
||||||
|
if dat, err := ioutil.ReadFile("/proc/sys/fs/pipe-max-size"); err == nil {
|
||||||
|
if capacity, err = strconv.ParseInt(strings.TrimSpace(string(dat)), 10, 64); err != nil {
|
||||||
|
capacity = 1 << 25
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return int(envconst.Int64(envvar, capacity))
|
||||||
|
}
|
||||||
|
|
||||||
func trySetPipeCapacity(p *os.File, capacity int) {
|
func trySetPipeCapacity(p *os.File, capacity int) {
|
||||||
res, err := unix.FcntlInt(p.Fd(), unix.F_SETPIPE_SZ, capacity)
|
res, err := unix.FcntlInt(p.Fd(), unix.F_SETPIPE_SZ, capacity)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -209,3 +209,7 @@ func (c *Cmd) Runtime() time.Duration {
|
|||||||
}
|
}
|
||||||
return c.waitReturnedAt.Sub(c.startedAt)
|
return c.waitReturnedAt.Sub(c.startedAt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Cmd) TestOnly_ExecCmd() *exec.Cmd {
|
||||||
|
return c.cmd
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user