Compare commits

..

1 Commits

Author SHA1 Message Date
Christian Schwarz 3cf48852e9 issue 611: add debug logging that periodically dumps remaining time, from daemon's perspective 2022-07-11 01:36:28 +02:00
81 changed files with 1506 additions and 3642 deletions
+13 -15
View File
@@ -24,12 +24,7 @@ commands:
apt-update-and-install-common-deps: apt-update-and-install-common-deps:
steps: steps:
- run: sudo apt-get update - run: sudo apt update && sudo apt install gawk make
- run: sudo apt-get install -y gawk make
# CircleCI doesn't update its cimg/go images.
# So, need to update manually to get up-to-date trust chains.
# The need for this was required for cimg/go:1.12, but let's future proof this here and now.
- run: sudo apt-get install -y git ca-certificates
restore-cache-gomod: restore-cache-gomod:
steps: steps:
@@ -155,7 +150,7 @@ parameters:
release_docker_baseimage_tag: release_docker_baseimage_tag:
type: string type: string
default: "1.19" default: "1.17"
workflows: workflows:
version: 2 version: 2
@@ -165,19 +160,19 @@ workflows:
jobs: jobs:
- quickcheck-docs - quickcheck-docs
- quickcheck-go: &quickcheck-go-smoketest - quickcheck-go: &quickcheck-go-smoketest
name: quickcheck-go-amd64-linux-1.19 name: quickcheck-go-amd64-linux-1.17
goversion: &latest-go-release "1.19" 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.19 #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:
goversion: [*latest-go-release, "1.18"] goversion: [*latest-go-release, "1.12"]
goos: ["linux", "freebsd"] goos: ["linux", "freebsd"]
goarch: ["amd64", "arm64"] goarch: ["amd64", "arm64"]
exclude: exclude:
@@ -185,6 +180,10 @@ workflows:
- goversion: *latest-go-release - goversion: *latest-go-release
goos: linux goos: linux
goarch: amd64 goarch: amd64
# not supported by Go 1.12
- goversion: "1.12"
goos: freebsd
goarch: arm64
release: release:
when: << pipeline.parameters.do_release >> when: << pipeline.parameters.do_release >>
@@ -250,7 +249,7 @@ jobs:
goarch: goarch:
type: string type: string
docker: docker:
- image: cimg/go:<<parameters.goversion>> - image: circleci/golang:<<parameters.goversion>>
environment: environment:
GOOS: <<parameters.goos>> GOOS: <<parameters.goos>>
GOARCH: <<parameters.goarch>> GOARCH: <<parameters.goarch>>
@@ -258,13 +257,12 @@ jobs:
steps: steps:
- checkout - checkout
- install-godep
- restore-cache-gomod - restore-cache-gomod
- run: go mod download - run: go mod download
- run: cd build && go mod download - run: cd build && go mod download
- save-cache-gomod - save-cache-gomod
- install-godep
- run: make formatcheck - run: make formatcheck
- run: make generate-platform-test-list - run: make generate-platform-test-list
- run: make zrepl-bin test-platform-bin - run: make zrepl-bin test-platform-bin
@@ -288,7 +286,7 @@ jobs:
goversion: goversion:
type: string type: string
docker: docker:
- image: cimg/go:<<parameters.goversion>> - image: circleci/golang:<<parameters.goversion>>
steps: steps:
- checkout - checkout
- restore-cache-gomod - restore-cache-gomod
-6
View File
@@ -7,10 +7,4 @@ issues:
- path: _test\.go - path: _test\.go
linters: linters:
- errcheck - errcheck
# Disable staticcheck 'Empty body in an if or else branch' as it's useful
# to put a comment into an empty else-clause that explains why whatever
# is done in the if-caluse is not necessary if the condition is false.
- linters:
- staticcheck
text: "SA9003:"
+4 -10
View File
@@ -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.19 RELEASE_DOCKER_BASEIMAGE_TAG ?= 1.17
RELEASE_DOCKER_BASEIMAGE ?= golang:$(RELEASE_DOCKER_BASEIMAGE_TAG) RELEASE_DOCKER_BASEIMAGE ?= golang:$(RELEASE_DOCKER_BASEIMAGE_TAG)
ifneq ($(GOARM),) ifneq ($(GOARM),)
@@ -55,7 +55,7 @@ release: clean
$(MAKE) wrapup-and-checksum $(MAKE) wrapup-and-checksum
$(MAKE) check-git-clean $(MAKE) check-git-clean
ifeq (SIGN, 1) ifeq (SIGN, 1)
$(MAKE) sign $(make) sign
endif endif
@echo "ZREPL RELEASE ARTIFACTS AVAILABLE IN artifacts/release" @echo "ZREPL RELEASE ARTIFACTS AVAILABLE IN artifacts/release"
@@ -134,13 +134,7 @@ endif
deb-docker: deb-docker:
docker build -t zrepl_debian_pkg --pull -f packaging/deb/Dockerfile . docker build -t zrepl_debian_pkg --pull -f packaging/deb/Dockerfile .
# Use a small open file limit to make fakeroot work. If we don't
# specify it, docker daemon will use its file limit. I don't know
# what changed (Docker, its systemd service, its Go version). But I
# observed fakeroot iterating close(i) up to i > 1000000, which costs
# a good amount of CPU time and makes the build slow.
docker run --rm -i -v $(CURDIR):/build/src -u $$(id -u):$$(id -g) \ docker run --rm -i -v $(CURDIR):/build/src -u $$(id -u):$$(id -g) \
--ulimit nofile=1024:1024 \
zrepl_debian_pkg \ zrepl_debian_pkg \
make deb GOOS=$(GOOS) GOARCH=$(GOARCH) GOARM=$(GOARM) make deb GOOS=$(GOOS) GOARCH=$(GOARCH) GOARM=$(GOARM)
@@ -339,12 +333,12 @@ $(ARTIFACTDIR)/go_env.txt:
docs: $(ARTIFACTDIR)/docs docs: $(ARTIFACTDIR)/docs
# https://www.sphinx-doc.org/en/master/man/sphinx-build.html # https://www.sphinx-doc.org/en/master/man/sphinx-build.html
$(MAKE) -C docs \ make -C docs \
html \ html \
BUILDDIR=../artifacts/docs \ BUILDDIR=../artifacts/docs \
SPHINXOPTS="-W --keep-going -n" SPHINXOPTS="-W --keep-going -n"
docs-clean: docs-clean:
$(MAKE) -C docs \ make -C docs \
clean \ clean \
BUILDDIR=../artifacts/docs BUILDDIR=../artifacts/docs
+6 -3
View File
@@ -4,10 +4,13 @@ go 1.12
require ( require (
github.com/alvaroloes/enumer v1.1.1 github.com/alvaroloes/enumer v1.1.1
github.com/golangci/golangci-lint v1.50.1 github.com/golangci/golangci-lint v1.35.2
github.com/golangci/misspell v0.3.4 // indirect
github.com/golangci/revgrep v0.0.0-20180812185044-276a5c0a1039 // indirect
github.com/spf13/afero v1.2.2 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/wadey/gocovmerge v0.0.0-20160331181800-b5bfa59ec0ad github.com/wadey/gocovmerge v0.0.0-20160331181800-b5bfa59ec0ad
golang.org/x/tools v0.2.0 golang.org/x/tools v0.0.0-20210105210202-9ed45478a130
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0 // indirect google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0 // indirect
google.golang.org/protobuf v1.28.0 google.golang.org/protobuf v1.25.0
) )
-1128
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -129,7 +129,7 @@ func interactive(c Client, flag statusFlags) error {
FSFilter: func(_ string) bool { return true }, FSFilter: func(_ string) bool { return true },
DetailViewWidth: 100, DetailViewWidth: 100,
DetailViewWrap: false, DetailViewWrap: false,
ShortKeybindingOverview: "[::b]Q[::-] quit [::b]<TAB>[::-] switch panes [::b]W[::-] wrap lines [::b]Shift+M[::-] toggle navbar [::b]Shift+S[::-] signal job [::b]</>[::-] filter filesystems", ShortKeybindingOverview: "[::b]Q[::-] quit [::b]<TAB>[::-] switch panes [::b]Shift+M[::-] toggle navbar [::b]Shift+S[::-] signal job [::b]</>[::-] filter filesystems",
} }
paramsMtx := &sync.Mutex{} paramsMtx := &sync.Mutex{}
var redraw func() var redraw func()
+10 -39
View File
@@ -273,9 +273,9 @@ func printFilesystemStatus(t *stringbuilder.B, rep *report.FilesystemReport, max
attribs = append(attribs, "resumed") attribs = append(attribs, "resumed")
} }
if len(attribs) > 0 { attribs = append(attribs, fmt.Sprintf("encrypted=%s", nextStep.Info.Encrypted))
next += fmt.Sprintf(" (%s)", strings.Join(attribs, ", "))
} next += fmt.Sprintf(" (%s)", strings.Join(attribs, ", "))
} else { } else {
next = "" // individual FSes may still be in planning state next = "" // individual FSes may still be in planning state
} }
@@ -547,20 +547,10 @@ func renderPrunerReport(t *stringbuilder.B, r *pruner.Report, fsfilter FilterFun
func renderSnapperReport(t *stringbuilder.B, r *snapper.Report, fsfilter FilterFunc) { func renderSnapperReport(t *stringbuilder.B, r *snapper.Report, fsfilter FilterFunc) {
if r == nil { if r == nil {
t.Printf("<no snapshotting report available>\n") t.Printf("<snapshot type does not have a report>\n")
return return
} }
t.Printf("Type: %s\n", r.Type)
if r.Periodic != nil {
renderSnapperReportPeriodic(t, r.Periodic, fsfilter)
} else if r.Cron != nil {
renderSnapperReportCron(t, r.Cron, fsfilter)
} else {
t.Printf("<no details available>")
}
}
func renderSnapperReportPeriodic(t *stringbuilder.B, r *snapper.PeriodicReport, fsfilter FilterFunc) {
t.Printf("Status: %s", r.State) t.Printf("Status: %s", r.State)
t.Newline() t.Newline()
@@ -571,25 +561,8 @@ func renderSnapperReportPeriodic(t *stringbuilder.B, r *snapper.PeriodicReport,
t.Printf("Sleep until: %s\n", r.SleepUntil) t.Printf("Sleep until: %s\n", r.SleepUntil)
} }
renderSnapperPlanReportFilesystem(t, r.Progress, fsfilter) sort.Slice(r.Progress, func(i, j int) bool {
} return strings.Compare(r.Progress[i].Path, r.Progress[j].Path) == -1
func renderSnapperReportCron(t *stringbuilder.B, r *snapper.CronReport, fsfilter FilterFunc) {
t.Printf("State: %s\n", r.State)
now := time.Now()
if r.WakeupTime.After(now) {
t.Printf("Sleep until: %s (%s remaining)\n", r.WakeupTime, r.WakeupTime.Sub(now).Round(time.Second))
} else {
t.Printf("Started: %s (lasting %s)\n", r.WakeupTime, now.Sub(r.WakeupTime).Round(time.Second))
}
renderSnapperPlanReportFilesystem(t, r.Progress, fsfilter)
}
func renderSnapperPlanReportFilesystem(t *stringbuilder.B, fss []*snapper.ReportFilesystem, fsfilter FilterFunc) {
sort.Slice(fss, func(i, j int) bool {
return strings.Compare(fss[i].Path, fss[j].Path) == -1
}) })
dur := func(d time.Duration) string { dur := func(d time.Duration) string {
@@ -602,8 +575,8 @@ func renderSnapperPlanReportFilesystem(t *stringbuilder.B, fss []*snapper.Report
var widths struct { var widths struct {
path, state, duration int path, state, duration int
} }
rows := make([]*row, 0, len(fss)) rows := make([]*row, 0, len(r.Progress))
for _, fs := range fss { for _, fs := range r.Progress {
if !fsfilter(fs.Path) { if !fsfilter(fs.Path) {
continue continue
} }
@@ -646,11 +619,9 @@ func renderSnapperPlanReportFilesystem(t *stringbuilder.B, fss []*snapper.Report
t.Printf("%s %s %s", path, state, duration) t.Printf("%s %s %s", path, state, duration)
t.PrintfDrawIndentedAndWrappedIfMultiline(" %s", r.remainder) t.PrintfDrawIndentedAndWrappedIfMultiline(" %s", r.remainder)
if r.hookReport != "" { if r.hookReport != "" {
t.AddIndent(1) t.PrintfDrawIndentedAndWrappedIfMultiline("%s", r.hookReport)
t.Newline()
t.Printf("%s", r.hookReport)
t.AddIndent(-1)
} }
t.Newline() t.Newline()
} }
} }
+1 -1
View File
@@ -57,7 +57,7 @@ func (f *zabsFilterFlags) registerZabsFilterFlags(s *pflag.FlagSet, verb string)
for v := range endpoint.AbstractionTypesAll { for v := range endpoint.AbstractionTypesAll {
variants = append(variants, string(v)) variants = append(variants, string(v))
} }
sort.Strings(variants) variants = sort.StringSlice(variants)
variantsJoined := strings.Join(variants, "|") variantsJoined := strings.Join(variants, "|")
s.Var(&f.Types, "type", fmt.Sprintf("only %s holds of the specified type [default: all] [comma-separated list of %s]", verb, variantsJoined)) s.Var(&f.Types, "type", fmt.Sprintf("only %s holds of the specified type [default: all] [comma-separated list of %s]", verb, variantsJoined))
+1 -2
View File
@@ -44,11 +44,10 @@ func doZabsList(ctx context.Context, sc *cli.Subcommand, args []string) error {
return errors.Wrap(err, "invalid filter specification on command line") return errors.Wrap(err, "invalid filter specification on command line")
} }
abstractions, errors, drainDone, err := endpoint.ListAbstractionsStreamed(ctx, q) abstractions, errors, err := endpoint.ListAbstractionsStreamed(ctx, q)
if err != nil { if err != nil {
return err // context clear by invocation of command return err // context clear by invocation of command
} }
defer drainDone()
var line chainlock.L var line chainlock.L
var wg sync.WaitGroup var wg sync.WaitGroup
+48 -41
View File
@@ -6,10 +6,11 @@ import (
"log/syslog" "log/syslog"
"os" "os"
"reflect" "reflect"
"regexp"
"strconv"
"time" "time"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/robfig/cron/v3"
"github.com/zrepl/yaml-config" "github.com/zrepl/yaml-config"
"github.com/zrepl/zrepl/util/datasizeunit" "github.com/zrepl/zrepl/util/datasizeunit"
@@ -188,10 +189,13 @@ func (i *PositiveDurationOrManual) UnmarshalYAML(u func(interface{}, bool) error
return fmt.Errorf("value must not be empty") return fmt.Errorf("value must not be empty")
default: default:
i.Manual = false i.Manual = false
i.Interval, err = parsePositiveDuration(s) i.Interval, err = time.ParseDuration(s)
if err != nil { if err != nil {
return err return err
} }
if i.Interval <= 0 {
return fmt.Errorf("value must be a positive duration, got %q", s)
}
} }
return nil return nil
} }
@@ -223,44 +227,10 @@ type SnapshottingEnum struct {
} }
type SnapshottingPeriodic struct { type SnapshottingPeriodic struct {
Type string `yaml:"type"` Type string `yaml:"type"`
Prefix string `yaml:"prefix"` Prefix string `yaml:"prefix"`
Interval *PositiveDuration `yaml:"interval"` Interval time.Duration `yaml:"interval,positive"`
Hooks HookList `yaml:"hooks,optional"` Hooks HookList `yaml:"hooks,optional"`
TimestampFormat string `yaml:"timestamp_format,optional,default=dense"`
}
type CronSpec struct {
Schedule cron.Schedule
}
var _ yaml.Unmarshaler = &CronSpec{}
func (s *CronSpec) UnmarshalYAML(unmarshal func(v interface{}, not_strict bool) error) error {
var specString string
if err := unmarshal(&specString, false); err != nil {
return err
}
// Use standard cron format.
// Disable the various "descriptors" (@daily, etc)
// They are just aliases to "top of hour", "midnight", etc.
parser := cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.SecondOptional)
sched, err := parser.Parse(specString)
if err != nil {
return errors.Wrap(err, "cron syntax invalid")
}
s.Schedule = sched
return nil
}
type SnapshottingCron struct {
Type string `yaml:"type"`
Prefix string `yaml:"prefix"`
Cron CronSpec `yaml:"cron"`
Hooks HookList `yaml:"hooks,optional"`
TimestampFormat string `yaml:"timestamp_format,optional,default=dense"`
} }
type SnapshottingManual struct { type SnapshottingManual struct {
@@ -586,7 +556,6 @@ func (t *SnapshottingEnum) UnmarshalYAML(u func(interface{}, bool) error) (err e
t.Ret, err = enumUnmarshal(u, map[string]interface{}{ t.Ret, err = enumUnmarshal(u, map[string]interface{}{
"periodic": &SnapshottingPeriodic{}, "periodic": &SnapshottingPeriodic{},
"manual": &SnapshottingManual{}, "manual": &SnapshottingManual{},
"cron": &SnapshottingCron{},
}) })
return return
} }
@@ -712,3 +681,41 @@ func ParseConfigBytes(bytes []byte) (*Config, error) {
} }
return c, nil return c, nil
} }
var durationStringRegex *regexp.Regexp = regexp.MustCompile(`^\s*(\d+)\s*(s|m|h|d|w)\s*$`)
func parsePositiveDuration(e string) (d time.Duration, err error) {
comps := durationStringRegex.FindStringSubmatch(e)
if len(comps) != 3 {
err = fmt.Errorf("does not match regex: %s %#v", e, comps)
return
}
durationFactor, err := strconv.ParseInt(comps[1], 10, 64)
if err != nil {
return 0, err
}
if durationFactor <= 0 {
return 0, errors.New("duration must be positive integer")
}
var durationUnit time.Duration
switch comps[2] {
case "s":
durationUnit = time.Second
case "m":
durationUnit = time.Minute
case "h":
durationUnit = time.Hour
case "d":
durationUnit = 24 * time.Hour
case "w":
durationUnit = 24 * 7 * time.Hour
default:
err = fmt.Errorf("contains unknown time unit '%s'", comps[2])
return
}
d = time.Duration(durationFactor) * durationUnit
return
}
-106
View File
@@ -1,106 +0,0 @@
package config
import (
"errors"
"fmt"
"regexp"
"strconv"
"time"
"github.com/kr/pretty"
"github.com/zrepl/yaml-config"
)
type Duration struct{ d time.Duration }
func (d Duration) Duration() time.Duration { return d.d }
var _ yaml.Unmarshaler = &Duration{}
func (d *Duration) UnmarshalYAML(unmarshal func(v interface{}, not_strict bool) error) error {
var s string
err := unmarshal(&s, false)
if err != nil {
return err
}
d.d, err = parseDuration(s)
if err != nil {
d.d = 0
return &yaml.TypeError{Errors: []string{fmt.Sprintf("cannot parse value %q: %s", s, err)}}
}
return nil
}
type PositiveDuration struct{ d Duration }
var _ yaml.Unmarshaler = &PositiveDuration{}
func (d PositiveDuration) Duration() time.Duration { return d.d.Duration() }
func (d *PositiveDuration) UnmarshalYAML(unmarshal func(v interface{}, not_strict bool) error) error {
err := d.d.UnmarshalYAML(unmarshal)
if err != nil {
return err
}
if d.d.Duration() <= 0 {
return fmt.Errorf("duration must be positive, got %s", d.d.Duration())
}
return nil
}
func parsePositiveDuration(e string) (time.Duration, error) {
d, err := parseDuration(e)
if err != nil {
return d, err
}
if d <= 0 {
return 0, errors.New("duration must be positive integer")
}
return d, err
}
var durationStringRegex *regexp.Regexp = regexp.MustCompile(`^\s*([\+-]?\d+)\s*(|s|m|h|d|w)\s*$`)
func parseDuration(e string) (d time.Duration, err error) {
comps := durationStringRegex.FindStringSubmatch(e)
if comps == nil {
err = fmt.Errorf("must match %s", durationStringRegex)
return
}
if len(comps) != 3 {
panic(pretty.Sprint(comps))
}
durationFactor, err := strconv.ParseInt(comps[1], 10, 64)
if err != nil {
return 0, err
}
var durationUnit time.Duration
switch comps[2] {
case "":
if durationFactor != 0 {
err = fmt.Errorf("missing time unit")
return
} else {
// It's the case where user specified '0'.
// We want to allow this, just like time.ParseDuration.
}
case "s":
durationUnit = time.Second
case "m":
durationUnit = time.Minute
case "h":
durationUnit = time.Hour
case "d":
durationUnit = 24 * time.Hour
case "w":
durationUnit = 24 * 7 * time.Hour
default:
err = fmt.Errorf("contains unknown time unit '%s'", comps[2])
return
}
d = time.Duration(durationFactor) * durationUnit
return
}
+1 -87
View File
@@ -35,23 +35,8 @@ jobs:
snapshotting: snapshotting:
type: periodic type: periodic
prefix: zrepl_ prefix: zrepl_
timestamp_format: dense
interval: 10m interval: 10m
` `
cron := `
snapshotting:
type: cron
prefix: zrepl_
timestamp_format: human
cron: "10 * * * *"
`
periodicDaily := `
snapshotting:
type: periodic
prefix: zrepl_
interval: 1d
`
hooks := ` hooks := `
snapshotting: snapshotting:
@@ -89,27 +74,10 @@ jobs:
c = testValidConfig(t, fillSnapshotting(periodic)) c = testValidConfig(t, fillSnapshotting(periodic))
snp := c.Jobs[0].Ret.(*PushJob).Snapshotting.Ret.(*SnapshottingPeriodic) snp := c.Jobs[0].Ret.(*PushJob).Snapshotting.Ret.(*SnapshottingPeriodic)
assert.Equal(t, "periodic", snp.Type) assert.Equal(t, "periodic", snp.Type)
assert.Equal(t, 10*time.Minute, snp.Interval.Duration()) assert.Equal(t, 10*time.Minute, snp.Interval)
assert.Equal(t, "zrepl_", snp.Prefix) assert.Equal(t, "zrepl_", snp.Prefix)
}) })
t.Run("periodicDaily", func(t *testing.T) {
c = testValidConfig(t, fillSnapshotting(periodicDaily))
snp := c.Jobs[0].Ret.(*PushJob).Snapshotting.Ret.(*SnapshottingPeriodic)
assert.Equal(t, "periodic", snp.Type)
assert.Equal(t, 24*time.Hour, snp.Interval.Duration())
assert.Equal(t, "zrepl_", snp.Prefix)
assert.Equal(t, "dense", snp.TimestampFormat)
})
t.Run("cron", func(t *testing.T) {
c = testValidConfig(t, fillSnapshotting(cron))
snp := c.Jobs[0].Ret.(*PushJob).Snapshotting.Ret.(*SnapshottingCron)
assert.Equal(t, "cron", snp.Type)
assert.Equal(t, "zrepl_", snp.Prefix)
assert.Equal(t, "human", snp.TimestampFormat)
})
t.Run("hooks", func(t *testing.T) { t.Run("hooks", func(t *testing.T) {
c = testValidConfig(t, fillSnapshotting(hooks)) c = testValidConfig(t, fillSnapshotting(hooks))
hs := c.Jobs[0].Ret.(*PushJob).Snapshotting.Ret.(*SnapshottingPeriodic).Hooks hs := c.Jobs[0].Ret.(*PushJob).Snapshotting.Ret.(*SnapshottingPeriodic).Hooks
@@ -120,57 +88,3 @@ jobs:
}) })
} }
func TestSnapshottingTimestampDefaults(t *testing.T) {
tmpl := `
jobs:
- name: foo
type: push
connect:
type: local
listener_name: foo
client_identity: bar
filesystems: {"<": true}
%s
pruning:
keep_sender:
- type: last_n
count: 10
keep_receiver:
- type: last_n
count: 10
`
periodic := `
snapshotting:
type: periodic
prefix: zrepl_
interval: 10m
`
cron := `
snapshotting:
type: cron
prefix: zrepl_
cron: "10 * * * *"
`
fillSnapshotting := func(s string) string { return fmt.Sprintf(tmpl, s) }
var c *Config
t.Run("periodic", func(t *testing.T) {
c = testValidConfig(t, fillSnapshotting(periodic))
snp := c.Jobs[0].Ret.(*PushJob).Snapshotting.Ret.(*SnapshottingPeriodic)
assert.Equal(t, "periodic", snp.Type)
assert.Equal(t, 10*time.Minute, snp.Interval.Duration())
assert.Equal(t, "zrepl_", snp.Prefix)
assert.Equal(t, "dense", snp.TimestampFormat) // default was set correctly
})
t.Run("cron", func(t *testing.T) {
c = testValidConfig(t, fillSnapshotting(cron))
snp := c.Jobs[0].Ret.(*PushJob).Snapshotting.Ret.(*SnapshottingCron)
assert.Equal(t, "cron", snp.Type)
assert.Equal(t, "zrepl_", snp.Prefix)
assert.Equal(t, "dense", snp.TimestampFormat) // default was set correctly
})
}
+1 -53
View File
@@ -13,7 +13,6 @@ import (
"github.com/kr/pretty" "github.com/kr/pretty"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/zrepl/yaml-config"
) )
func TestSampleConfigsAreParsedWithoutErrors(t *testing.T) { func TestSampleConfigsAreParsedWithoutErrors(t *testing.T) {
@@ -44,8 +43,7 @@ func TestSampleConfigsAreParsedWithoutErrors(t *testing.T) {
} }
// template must be a template/text template with a single '{{ . }}' as placeholder for val // template must be a template/text template with a single '{{ . }}' as placeholder for val
// //nolint[:deadcode,unused]
//nolint:deadcode,unused
func testValidConfigTemplate(t *testing.T, tmpl string, val string) *Config { func testValidConfigTemplate(t *testing.T, tmpl string, val string) *Config {
tmp, err := template.New("master").Parse(tmpl) tmp, err := template.New("master").Parse(tmpl)
if err != nil { if err != nil {
@@ -88,53 +86,3 @@ func TestTrimSpaceEachLineAndPad(t *testing.T) {
` `
assert.Equal(t, " \n foo\n bar baz\n \n", trimSpaceEachLineAndPad(foo, " ")) assert.Equal(t, " \n foo\n bar baz\n \n", trimSpaceEachLineAndPad(foo, " "))
} }
func TestCronSpec(t *testing.T) {
expectAccept := []string{
`"* * * * *"`,
`"0-10 * * * *"`,
`"* 0-5,8,12 * * *"`,
}
expectFail := []string{
`* * * *`,
``,
`23`,
`"@reboot"`,
`"@every 1h30m"`,
`"@daily"`,
`* * * * * *`,
}
for _, input := range expectAccept {
t.Run(input, func(t *testing.T) {
s := fmt.Sprintf("spec: %s\n", input)
var v struct {
Spec CronSpec
}
v.Spec.Schedule = nil
t.Logf("input:\n%s", s)
err := yaml.UnmarshalStrict([]byte(s), &v)
t.Logf("error: %T %s", err, err)
require.NoError(t, err)
require.NotNil(t, v.Spec.Schedule)
})
}
for _, input := range expectFail {
t.Run(input, func(t *testing.T) {
s := fmt.Sprintf("spec: %s\n", input)
var v struct {
Spec CronSpec
}
v.Spec.Schedule = nil
t.Logf("input: %q", s)
err := yaml.UnmarshalStrict([]byte(s), &v)
t.Logf("error: %T %s", err, err)
require.Error(t, err)
require.Nil(t, v.Spec.Schedule)
})
}
}
-14
View File
@@ -1,14 +0,0 @@
jobs:
- name: snapjob
type: snap
filesystems: {
"tank<": true,
}
snapshotting:
type: cron
prefix: zrepl_snapjob_
cron: "*/5 * * * *"
pruning:
keep:
- type: last_n
count: 60
+3 -2
View File
@@ -5,7 +5,7 @@
// //
// This package also provides all supported hook type implementations and abstractions around them. // This package also provides all supported hook type implementations and abstractions around them.
// //
// # Use For Other Kinds Of ExpectStepReports // Use For Other Kinds Of ExpectStepReports
// //
// This package REQUIRES REFACTORING before it can be used for other activities than snapshots, e.g. pre- and post-replication: // This package REQUIRES REFACTORING before it can be used for other activities than snapshots, e.g. pre- and post-replication:
// //
@@ -15,7 +15,7 @@
// The hook implementations should move out of this package. // The hook implementations should move out of this package.
// However, there is a lot of tight coupling which to untangle isn't worth it ATM. // However, there is a lot of tight coupling which to untangle isn't worth it ATM.
// //
// # How This Package Is Used By Package Snapper // How This Package Is Used By Package Snapper
// //
// Deserialize a config.List using ListFromConfig(). // Deserialize a config.List using ListFromConfig().
// Then it MUST filter the list to only contain hooks for a particular filesystem using // Then it MUST filter the list to only contain hooks for a particular filesystem using
@@ -30,4 +30,5 @@
// Command hooks make it available in the environment variable ZREPL_DRYRUN. // Command hooks make it available in the environment variable ZREPL_DRYRUN.
// //
// Plan.Report() can be called while Plan.Run() is executing to give an overview of plan execution progress (future use in "zrepl status"). // Plan.Report() can be called while Plan.Run() is executing to give an overview of plan execution progress (future use in "zrepl status").
//
package hooks package hooks
+1 -8
View File
@@ -93,14 +93,7 @@ func (r *CommandHookReport) String() string {
cmdLine.WriteString(fmt.Sprintf("%s'%s'", sep, a)) cmdLine.WriteString(fmt.Sprintf("%s'%s'", sep, a))
} }
var msg string return fmt.Sprintf("command hook invocation: \"%s\"", cmdLine.String()) // no %q to make copy-pastable
if r.Err == nil {
msg = "command hook"
} else {
msg = fmt.Sprintf("command hook failed with %q", r.Err)
}
return fmt.Sprintf("%s: \"%s\"", msg, cmdLine.String()) // no %q to make copy-pastable
} }
func (r *CommandHookReport) Error() string { func (r *CommandHookReport) Error() string {
if r.Err == nil { if r.Err == nil {
+8 -11
View File
@@ -17,22 +17,19 @@ import (
"github.com/zrepl/zrepl/zfs" "github.com/zrepl/zrepl/zfs"
) )
// Hook to implement the following recommmendation from MySQL docs
// https://dev.mysql.com/doc/mysql-backup-excerpt/5.7/en/backup-methods.html // https://dev.mysql.com/doc/mysql-backup-excerpt/5.7/en/backup-methods.html
// //
// Making Backups Using a File System Snapshot: // Making Backups Using a File System Snapshot:
// //
// If you are using a Veritas file system, you can make a backup like this: // If you are using a Veritas file system, you can make a backup like this:
// //
// From a client program, execute FLUSH TABLES WITH READ LOCK. // From a client program, execute FLUSH TABLES WITH READ LOCK.
// From another shell, execute mount vxfs snapshot. // From another shell, execute mount vxfs snapshot.
// From the first client, execute UNLOCK TABLES. // From the first client, execute UNLOCK TABLES.
// Copy files from the snapshot. // Copy files from the snapshot.
// Unmount the snapshot. // Unmount the snapshot.
// //
// Similar snapshot capabilities may be available in other file systems, such as LVM or ZFS. // Similar snapshot capabilities may be available in other file systems, such as LVM or ZFS.
//
type MySQLLockTables struct { type MySQLLockTables struct {
errIsFatal bool errIsFatal bool
connector sqldriver.Connector connector sqldriver.Connector
+5 -5
View File
@@ -161,7 +161,7 @@ jobs:
ExpectedEdge: hooks.Pre, ExpectedEdge: hooks.Pre,
ExpectStatus: hooks.StepErr, ExpectStatus: hooks.StepErr,
OutputTest: containsTest(fmt.Sprintf("TEST ERROR pre_testing %s@%s", testFSName, testSnapshotName)), OutputTest: containsTest(fmt.Sprintf("TEST ERROR pre_testing %s@%s", testFSName, testSnapshotName)),
ErrorTest: regexpTest("^command hook failed.*exit status 1$"), ErrorTest: regexpTest("^command hook invocation.*exit status 1$"),
}, },
expectStep{ExpectedEdge: hooks.Callback, ExpectStatus: hooks.StepOk}, expectStep{ExpectedEdge: hooks.Callback, ExpectStatus: hooks.StepOk},
expectStep{ expectStep{
@@ -185,7 +185,7 @@ jobs:
ExpectedEdge: hooks.Pre, ExpectedEdge: hooks.Pre,
ExpectStatus: hooks.StepErr, ExpectStatus: hooks.StepErr,
OutputTest: containsTest(fmt.Sprintf("TEST ERROR pre_testing %s@%s", testFSName, testSnapshotName)), OutputTest: containsTest(fmt.Sprintf("TEST ERROR pre_testing %s@%s", testFSName, testSnapshotName)),
ErrorTest: regexpTest("^command hook failed.*exit status 1$"), ErrorTest: regexpTest("^command hook invocation.*exit status 1$"),
}, },
expectStep{ expectStep{
ExpectedEdge: hooks.Pre, ExpectedEdge: hooks.Pre,
@@ -234,7 +234,7 @@ jobs:
ExpectedEdge: hooks.Post, ExpectedEdge: hooks.Post,
ExpectStatus: hooks.StepErr, ExpectStatus: hooks.StepErr,
OutputTest: containsTest(fmt.Sprintf("TEST ERROR post_testing %s@%s", testFSName, testSnapshotName)), OutputTest: containsTest(fmt.Sprintf("TEST ERROR post_testing %s@%s", testFSName, testSnapshotName)),
ErrorTest: regexpTest("^command hook failed.*exit status 1$"), ErrorTest: regexpTest("^command hook invocation.*exit status 1$"),
}, },
}, },
}, },
@@ -267,7 +267,7 @@ jobs:
ExpectedEdge: hooks.Pre, ExpectedEdge: hooks.Pre,
ExpectStatus: hooks.StepErr, ExpectStatus: hooks.StepErr,
OutputTest: containsTest(fmt.Sprintf("TEST ERROR pre_testing %s@%s", testFSName, testSnapshotName)), OutputTest: containsTest(fmt.Sprintf("TEST ERROR pre_testing %s@%s", testFSName, testSnapshotName)),
ErrorTest: regexpTest("^command hook failed.*exit status 1$"), ErrorTest: regexpTest("^command hook invocation.*exit status 1$"),
}, },
expectStep{ExpectedEdge: hooks.Callback, ExpectStatus: hooks.StepOk}, expectStep{ExpectedEdge: hooks.Callback, ExpectStatus: hooks.StepOk},
expectStep{ expectStep{
@@ -295,7 +295,7 @@ jobs:
ExpectedEdge: hooks.Pre, ExpectedEdge: hooks.Pre,
ExpectStatus: hooks.StepErr, ExpectStatus: hooks.StepErr,
OutputTest: containsTest(fmt.Sprintf("TEST ERROR pre_testing %s@%s", testFSName, testSnapshotName)), OutputTest: containsTest(fmt.Sprintf("TEST ERROR pre_testing %s@%s", testFSName, testSnapshotName)),
ErrorTest: regexpTest("^command hook failed.*exit status 1$"), ErrorTest: regexpTest("^command hook invocation.*exit status 1$"),
}, },
expectStep{ExpectedEdge: hooks.Callback, ExpectStatus: hooks.StepOk}, expectStep{ExpectedEdge: hooks.Callback, ExpectStatus: hooks.StepOk},
expectStep{ expectStep{
+4 -3
View File
@@ -101,7 +101,7 @@ type modePush struct {
receiver *rpc.Client receiver *rpc.Client
senderConfig *endpoint.SenderConfig senderConfig *endpoint.SenderConfig
plannerPolicy *logic.PlannerPolicy plannerPolicy *logic.PlannerPolicy
snapper snapper.Snapper snapper *snapper.PeriodicOrManual
} }
func (m *modePush) ConnectEndpoints(ctx context.Context, connecter transport.Connecter) { func (m *modePush) ConnectEndpoints(ctx context.Context, connecter transport.Connecter) {
@@ -137,8 +137,7 @@ func (m *modePush) RunPeriodic(ctx context.Context, wakeUpCommon chan<- struct{}
} }
func (m *modePush) SnapperReport() *snapper.Report { func (m *modePush) SnapperReport() *snapper.Report {
r := m.snapper.Report() return m.snapper.Report()
return &r
} }
func (m *modePush) ResetConnectBackoff() { func (m *modePush) ResetConnectBackoff() {
@@ -169,6 +168,7 @@ func modePushFromConfig(g *config.Global, in *config.PushJob, jobID endpoint.Job
} }
m.plannerPolicy = &logic.PlannerPolicy{ m.plannerPolicy = &logic.PlannerPolicy{
EncryptedSend: logic.TriFromBool(in.Send.Encrypted),
ConflictResolution: conflictResolution, ConflictResolution: conflictResolution,
ReplicationConfig: replicationConfig, ReplicationConfig: replicationConfig,
SizeEstimationConcurrency: in.Replication.Concurrency.SizeEstimates, SizeEstimationConcurrency: in.Replication.Concurrency.SizeEstimates,
@@ -273,6 +273,7 @@ func modePullFromConfig(g *config.Global, in *config.PullJob, jobID endpoint.Job
} }
m.plannerPolicy = &logic.PlannerPolicy{ m.plannerPolicy = &logic.PlannerPolicy{
EncryptedSend: logic.DontCare,
ConflictResolution: conflictResolution, ConflictResolution: conflictResolution,
ReplicationConfig: replicationConfig, ReplicationConfig: replicationConfig,
SizeEstimationConcurrency: in.Replication.Concurrency.SizeEstimates, SizeEstimationConcurrency: in.Replication.Concurrency.SizeEstimates,
+2 -3
View File
@@ -58,7 +58,7 @@ func modeSinkFromConfig(g *config.Global, in *config.SinkJob, jobID endpoint.Job
type modeSource struct { type modeSource struct {
senderConfig *endpoint.SenderConfig senderConfig *endpoint.SenderConfig
snapper snapper.Snapper snapper *snapper.PeriodicOrManual
} }
func modeSourceFromConfig(g *config.Global, in *config.SourceJob, jobID endpoint.JobID) (m *modeSource, err error) { func modeSourceFromConfig(g *config.Global, in *config.SourceJob, jobID endpoint.JobID) (m *modeSource, err error) {
@@ -88,8 +88,7 @@ func (m *modeSource) RunPeriodic(ctx context.Context) {
} }
func (m *modeSource) SnapperReport() *snapper.Report { func (m *modeSource) SnapperReport() *snapper.Report {
r := m.snapper.Report() return m.snapper.Report()
return &r
} }
func passiveSideFromConfig(g *config.Global, in *config.PassiveJob, configJob interface{}, parseFlags config.ParseFlags) (s *PassiveSide, err error) { func passiveSideFromConfig(g *config.Global, in *config.PassiveJob, configJob interface{}, parseFlags config.ParseFlags) (s *PassiveSide, err error) {
+3 -4
View File
@@ -26,7 +26,7 @@ import (
type SnapJob struct { type SnapJob struct {
name endpoint.JobID name endpoint.JobID
fsfilter zfs.DatasetFilter fsfilter zfs.DatasetFilter
snapper snapper.Snapper snapper *snapper.PeriodicOrManual
prunerFactory *pruner.LocalPrunerFactory prunerFactory *pruner.LocalPrunerFactory
@@ -86,8 +86,7 @@ func (j *SnapJob) Status() *Status {
s.Pruning = j.pruner.Report() s.Pruning = j.pruner.Report()
} }
j.prunerMtx.Unlock() j.prunerMtx.Unlock()
r := j.snapper.Report() s.Snapshotting = j.snapper.Report()
s.Snapshotting = &r
return &Status{Type: t, JobSpecific: s} return &Status{Type: t, JobSpecific: s}
} }
@@ -138,7 +137,7 @@ outer:
// TODO: // TODO:
// This is a work-around for the current package daemon/pruner // This is a work-around for the current package daemon/pruner
// and package pruning.Snapshot limitation: they require the // and package pruning.Snapshot limitation: they require the
// `Replicated` getter method be present, but obviously, // `Replicated` getter method be present, but obviously,
// a local job like SnapJob can't deliver on that. // a local job like SnapJob can't deliver on that.
// But the pruner.Pruner gives up on an FS if no replication // But the pruner.Pruner gives up on an FS if no replication
// cursor is present, which is why this pruner returns the // cursor is present, which is why this pruner returns the
+41 -38
View File
@@ -1,6 +1,6 @@
// package trace provides activity tracing via ctx through Tasks and Spans // package trace provides activity tracing via ctx through Tasks and Spans
// //
// # Basic Concepts // Basic Concepts
// //
// Tracing can be used to identify where a piece of code spends its time. // Tracing can be used to identify where a piece of code spends its time.
// //
@@ -10,50 +10,51 @@
// to tech-savvy users (albeit not developers). // to tech-savvy users (albeit not developers).
// //
// This package provides the concept of Tasks and Spans to express what activity is happening within an application: // This package provides the concept of Tasks and Spans to express what activity is happening within an application:
// - Neither task nor span is really tangible but instead contained within the context.Context tree //
// - Tasks represent concurrent activity (i.e. goroutines). // - Neither task nor span is really tangible but instead contained within the context.Context tree
// - Spans represent a semantic stack trace within a task. // - Tasks represent concurrent activity (i.e. goroutines).
// - Spans represent a semantic stack trace within a task.
//
// As a consequence, whenever a context is propagated across goroutine boundary, you need to create a child task: // As a consequence, whenever a context is propagated across goroutine boundary, you need to create a child task:
// //
// go func(ctx context.Context) { // go func(ctx context.Context) {
// ctx, endTask = WithTask(ctx, "what-happens-inside-the-child-task") // ctx, endTask = WithTask(ctx, "what-happens-inside-the-child-task")
// defer endTask() // defer endTask()
// // ... // // ...
// }(ctx) // }(ctx)
// //
// Within the task, you can open up a hierarchy of spans. // Within the task, you can open up a hierarchy of spans.
// In contrast to tasks, which have can multiple concurrently running child tasks, // In contrast to tasks, which have can multiple concurrently running child tasks,
// spans must nest and not cross the goroutine boundary. // spans must nest and not cross the goroutine boundary.
// //
// ctx, endSpan = WithSpan(ctx, "copy-dir") // ctx, endSpan = WithSpan(ctx, "copy-dir")
// defer endSpan() // defer endSpan()
// for _, f := range dir.Files() { // for _, f := range dir.Files() {
// func() { // func() {
// ctx, endSpan := WithSpan(ctx, fmt.Sprintf("copy-file %q", f)) // ctx, endSpan := WithSpan(ctx, fmt.Sprintf("copy-file %q", f))
// defer endspan() // defer endspan()
// b, _ := ioutil.ReadFile(f) // b, _ := ioutil.ReadFile(f)
// _ = ioutil.WriteFile(f + ".copy", b, 0600) // _ = ioutil.WriteFile(f + ".copy", b, 0600)
// }() // }()
// } // }
// //
// In combination: // In combination:
// // ctx, endTask = WithTask(ctx, "copy-dirs")
// ctx, endTask = WithTask(ctx, "copy-dirs") // defer endTask()
// defer endTask() // for i := range dirs {
// for i := range dirs { // go func(dir string) {
// go func(dir string) { // ctx, endTask := WithTask(ctx, "copy-dir")
// ctx, endTask := WithTask(ctx, "copy-dir") // defer endTask()
// defer endTask() // for _, f := range filesIn(dir) {
// for _, f := range filesIn(dir) { // func() {
// func() { // ctx, endSpan := WithSpan(ctx, fmt.Sprintf("copy-file %q", f))
// ctx, endSpan := WithSpan(ctx, fmt.Sprintf("copy-file %q", f)) // defer endspan()
// defer endspan() // b, _ := ioutil.ReadFile(f)
// b, _ := ioutil.ReadFile(f) // _ = ioutil.WriteFile(f + ".copy", b, 0600)
// _ = ioutil.WriteFile(f + ".copy", b, 0600) // }()
// }() // }
// } // }()
// }() // }
// }
// //
// Note that a span ends at the time you call endSpan - not before and not after that. // Note that a span ends at the time you call endSpan - not before and not after that.
// If you violate the stack-like nesting of spans by forgetting an endSpan() invocation, // If you violate the stack-like nesting of spans by forgetting an endSpan() invocation,
@@ -64,7 +65,8 @@
// //
// Recovering from endSpan() or endTask() panics will corrupt the trace stack and lead to corrupt tracefile output. // Recovering from endSpan() or endTask() panics will corrupt the trace stack and lead to corrupt tracefile output.
// //
// # Best Practices For Naming Tasks And Spans //
// Best Practices For Naming Tasks And Spans
// //
// Tasks should always have string constants as names, and must not contain the `#` character. WHy? // Tasks should always have string constants as names, and must not contain the `#` character. WHy?
// First, the visualization by chrome://tracing draws a horizontal bar for each task in the trace. // First, the visualization by chrome://tracing draws a horizontal bar for each task in the trace.
@@ -72,7 +74,8 @@
// Note that the `#NUM` suffix will be reused if a task has ended, in order to avoid an // Note that the `#NUM` suffix will be reused if a task has ended, in order to avoid an
// infinite number of horizontal bars in the visualization. // infinite number of horizontal bars in the visualization.
// //
// # Chrome-compatible Tracefile Support //
// Chrome-compatible Tracefile Support
// //
// The activity trace generated by usage of WithTask and WithSpan can be rendered to a JSON output file // The activity trace generated by usage of WithTask and WithSpan can be rendered to a JSON output file
// that can be loaded into chrome://tracing . // that can be loaded into chrome://tracing .
@@ -11,6 +11,8 @@ import (
// use like this: // use like this:
// //
// defer WithSpanFromStackUpdateCtx(&existingCtx)() // defer WithSpanFromStackUpdateCtx(&existingCtx)()
//
//
func WithSpanFromStackUpdateCtx(ctx *context.Context) DoneFunc { func WithSpanFromStackUpdateCtx(ctx *context.Context) DoneFunc {
childSpanCtx, end := WithSpan(*ctx, getMyCallerOrPanic()) childSpanCtx, end := WithSpan(*ctx, getMyCallerOrPanic())
*ctx = childSpanCtx *ctx = childSpanCtx
-152
View File
@@ -1,152 +0,0 @@
package snapper
import (
"context"
"fmt"
"sync"
"time"
"github.com/pkg/errors"
"github.com/zrepl/zrepl/config"
"github.com/zrepl/zrepl/daemon/hooks"
"github.com/zrepl/zrepl/util/suspendresumesafetimer"
"github.com/zrepl/zrepl/zfs"
)
func cronFromConfig(fsf zfs.DatasetFilter, in config.SnapshottingCron) (*Cron, error) {
hooksList, err := hooks.ListFromConfig(&in.Hooks)
if err != nil {
return nil, errors.Wrap(err, "hook config error")
}
planArgs := planArgs{
prefix: in.Prefix,
timestampFormat: in.TimestampFormat,
hooks: hooksList,
}
return &Cron{config: in, fsf: fsf, planArgs: planArgs}, nil
}
type Cron struct {
config config.SnapshottingCron
fsf zfs.DatasetFilter
planArgs planArgs
mtx sync.RWMutex
running bool
wakeupTime time.Time // zero value means uninit
lastError error
lastPlan *plan
wakeupWhileRunningCount int
}
func (s *Cron) Run(ctx context.Context, snapshotsTaken chan<- struct{}) {
for {
now := time.Now()
s.mtx.Lock()
s.wakeupTime = s.config.Cron.Schedule.Next(now)
s.mtx.Unlock()
ctxDone := suspendresumesafetimer.SleepUntil(ctx, s.wakeupTime)
if ctxDone != nil {
return
}
getLogger(ctx).Debug("cron timer fired")
s.mtx.Lock()
if s.running {
getLogger(ctx).Warn("snapshotting triggered according to cron rules but previous snapshotting is not done; not taking a snapshot this time")
s.wakeupWhileRunningCount++
s.mtx.Unlock()
continue
}
s.lastError = nil
s.lastPlan = nil
s.wakeupWhileRunningCount = 0
s.running = true
s.mtx.Unlock()
go func() {
err := s.do(ctx)
s.mtx.Lock()
s.lastError = err
s.running = false
s.mtx.Unlock()
select {
case snapshotsTaken <- struct{}{}:
default:
if snapshotsTaken != nil {
getLogger(ctx).Warn("callback channel is full, discarding snapshot update event")
}
}
}()
}
}
func (s *Cron) do(ctx context.Context) error {
fss, err := zfs.ZFSListMapping(ctx, s.fsf)
if err != nil {
return errors.Wrap(err, "cannot list filesystems")
}
p := makePlan(s.planArgs, fss)
s.mtx.Lock()
s.lastPlan = p
s.lastError = nil
s.mtx.Unlock()
ok := p.execute(ctx, false)
if !ok {
return errors.New("one or more snapshots could not be created, check logs for details")
} else {
return nil
}
}
type CronState string
const (
CronStateRunning CronState = "running"
CronStateWaiting CronState = "waiting"
)
type CronReport struct {
State CronState
WakeupTime time.Time
Errors []string
Progress []*ReportFilesystem
}
func (s *Cron) Report() Report {
s.mtx.Lock()
defer s.mtx.Unlock()
r := CronReport{}
r.WakeupTime = s.wakeupTime
if s.running {
r.State = CronStateRunning
} else {
r.State = CronStateWaiting
}
if s.lastError != nil {
r.Errors = append(r.Errors, s.lastError.Error())
}
if s.wakeupWhileRunningCount > 0 {
r.Errors = append(r.Errors, fmt.Sprintf("cron frequency is too high; snapshots were not taken %d times", s.wakeupWhileRunningCount))
}
r.Progress = nil
if s.lastPlan != nil {
r.Progress = s.lastPlan.report()
}
return Report{Type: TypeCron, Cron: &r}
}
-68
View File
@@ -1,68 +0,0 @@
package snapper
import (
"fmt"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zrepl/yaml-config"
"github.com/zrepl/zrepl/config"
)
func TestCronLibraryWorks(t *testing.T) {
type testCase struct {
spec string
in time.Time
expect time.Time
}
dhm := func(day, hour, minutes int) time.Time {
return time.Date(2022, 7, day, hour, minutes, 0, 0, time.UTC)
}
hm := func(hour, minutes int) time.Time {
return dhm(23, hour, minutes)
}
tcs := []testCase{
{"0-10 * * * *", dhm(17, 1, 10), dhm(17, 2, 0)},
{"0-10 * * * *", dhm(17, 23, 10), dhm(18, 0, 0)},
{"0-10 * * * *", hm(1, 9), hm(1, 10)},
{"0-10 * * * *", hm(1, 9), hm(1, 10)},
{"1,3,5 * * * *", hm(1, 1), hm(1, 3)},
{"1,3,5 * * * *", hm(1, 2), hm(1, 3)},
{"1,3,5 * * * *", hm(1, 3), hm(1, 5)},
{"1,3,5 * * * *", hm(1, 5), hm(2, 1)},
{"* 0-5,8,12 * * *", hm(0, 0), hm(0, 1)},
{"* 0-5,8,12 * * *", hm(4, 59), hm(5, 0)},
{"* 0-5,8,12 * * *", hm(5, 0), hm(5, 1)},
{"* 0-5,8,12 * * *", hm(5, 59), hm(8, 0)},
{"* 0-5,8,12 * * *", hm(8, 59), hm(12, 0)},
// https://github.com/zrepl/zrepl/pull/614#issuecomment-1188358989
{"53 17,18,19 * * *", dhm(23, 17, 52), dhm(23, 17, 53)},
{"53 17,18,19 * * *", dhm(23, 17, 53), dhm(23, 18, 53)},
{"53 17,18,19 * * *", dhm(23, 18, 53), dhm(23, 19, 53)},
{"53 17,18,19 * * *", dhm(23, 19, 53), dhm(24 /* ! */, 17, 53)},
}
for i, tc := range tcs {
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
var s struct {
Cron config.CronSpec `yaml:"cron"`
}
inp := fmt.Sprintf("cron: %q", tc.spec)
fmt.Println("spec is ", inp)
err := yaml.UnmarshalStrict([]byte(inp), &s)
require.NoError(t, err)
actual := s.Cron.Schedule.Next(tc.in)
assert.Equal(t, tc.expect, actual)
})
}
}
-268
View File
@@ -1,268 +0,0 @@
package snapper
import (
"context"
"fmt"
"sort"
"strconv"
"strings"
"time"
"github.com/zrepl/zrepl/daemon/hooks"
"github.com/zrepl/zrepl/daemon/logging"
"github.com/zrepl/zrepl/util/chainlock"
"github.com/zrepl/zrepl/zfs"
)
type planArgs struct {
prefix string
timestampFormat string
hooks *hooks.List
}
type plan struct {
mtx chainlock.L
args planArgs
snaps map[*zfs.DatasetPath]*snapProgress
}
func makePlan(args planArgs, fss []*zfs.DatasetPath) *plan {
snaps := make(map[*zfs.DatasetPath]*snapProgress, len(fss))
for _, fs := range fss {
snaps[fs] = &snapProgress{state: SnapPending}
}
return &plan{snaps: snaps, args: args}
}
//go:generate stringer -type=SnapState
type SnapState uint
const (
SnapPending SnapState = 1 << iota
SnapStarted
SnapDone
SnapError
)
// All fields protected by Snapper.mtx
type snapProgress struct {
state SnapState
// SnapStarted, SnapDone, SnapError
name string
startAt time.Time
hookPlan *hooks.Plan
// SnapDone
doneAt time.Time
// SnapErr TODO disambiguate state
runResults hooks.PlanReport
}
func (plan *plan) formatNow(format string) string {
now := time.Now().UTC()
switch strings.ToLower(format) {
case "dense":
format = "20060102_150405_000"
case "human":
format = "2006-01-02_15:04:05"
case "iso-8601":
format = "2006-01-02T15:04:05.000Z"
case "unix-seconds":
return strconv.FormatInt(now.Unix(), 10)
}
return now.Format(format)
}
func (plan *plan) execute(ctx context.Context, dryRun bool) (ok bool) {
hookMatchCount := make(map[hooks.Hook]int, len(*plan.args.hooks))
for _, h := range *plan.args.hooks {
hookMatchCount[h] = 0
}
anyFsHadErr := false
// TODO channel programs -> allow a little jitter?
for fs, progress := range plan.snaps {
suffix := plan.formatNow(plan.args.timestampFormat)
snapname := fmt.Sprintf("%s%s", plan.args.prefix, suffix)
ctx := logging.WithInjectedField(ctx, "fs", fs.ToString())
ctx = logging.WithInjectedField(ctx, "snap", snapname)
hookEnvExtra := hooks.Env{
hooks.EnvFS: fs.ToString(),
hooks.EnvSnapshot: snapname,
}
jobCallback := hooks.NewCallbackHookForFilesystem("snapshot", fs, func(ctx context.Context) (err error) {
l := getLogger(ctx)
l.Debug("create snapshot")
err = zfs.ZFSSnapshot(ctx, fs, snapname, false) // TODO propagate context to ZFSSnapshot
if err != nil {
l.WithError(err).Error("cannot create snapshot")
}
return
})
fsHadErr := false
var hookPlanReport hooks.PlanReport
var hookPlan *hooks.Plan
{
filteredHooks, err := plan.args.hooks.CopyFilteredForFilesystem(fs)
if err != nil {
getLogger(ctx).WithError(err).Error("unexpected filter error")
fsHadErr = true
goto updateFSState
}
// account for running hooks
for _, h := range filteredHooks {
hookMatchCount[h] = hookMatchCount[h] + 1
}
var planErr error
hookPlan, planErr = hooks.NewPlan(&filteredHooks, hooks.PhaseSnapshot, jobCallback, hookEnvExtra)
if planErr != nil {
fsHadErr = true
getLogger(ctx).WithError(planErr).Error("cannot create job hook plan")
goto updateFSState
}
}
plan.mtx.HoldWhile(func() {
progress.name = snapname
progress.startAt = time.Now()
progress.hookPlan = hookPlan
progress.state = SnapStarted
})
{
getLogger(ctx).WithField("report", hookPlan.Report().String()).Debug("begin run job plan")
hookPlan.Run(ctx, dryRun)
hookPlanReport = hookPlan.Report()
fsHadErr = hookPlanReport.HadError() // not just fatal errors
if fsHadErr {
getLogger(ctx).WithField("report", hookPlanReport.String()).Error("end run job plan with error")
} else {
getLogger(ctx).WithField("report", hookPlanReport.String()).Info("end run job plan successful")
}
}
updateFSState:
anyFsHadErr = anyFsHadErr || fsHadErr
plan.mtx.HoldWhile(func() {
progress.doneAt = time.Now()
progress.state = SnapDone
if fsHadErr {
progress.state = SnapError
}
progress.runResults = hookPlanReport
})
}
for h, mc := range hookMatchCount {
if mc == 0 {
hookIdx := -1
for idx, ah := range *plan.args.hooks {
if ah == h {
hookIdx = idx
break
}
}
getLogger(ctx).WithField("hook", h.String()).WithField("hook_number", hookIdx+1).Warn("hook did not match any snapshotted filesystems")
}
}
return !anyFsHadErr
}
type ReportFilesystem struct {
Path string
State SnapState
// Valid in SnapStarted and later
SnapName string
StartAt time.Time
Hooks string
HooksHadError bool
// Valid in SnapDone | SnapError
DoneAt time.Time
}
func (plan *plan) report() []*ReportFilesystem {
plan.mtx.Lock()
defer plan.mtx.Unlock()
pReps := make([]*ReportFilesystem, 0, len(plan.snaps))
for fs, p := range plan.snaps {
var hooksStr string
var hooksHadError bool
if p.hookPlan != nil {
hooksStr, hooksHadError = p.report()
}
pReps = append(pReps, &ReportFilesystem{
Path: fs.ToString(),
State: p.state,
SnapName: p.name,
StartAt: p.startAt,
DoneAt: p.doneAt,
Hooks: hooksStr,
HooksHadError: hooksHadError,
})
}
sort.Slice(pReps, func(i, j int) bool {
return strings.Compare(pReps[i].Path, pReps[j].Path) == -1
})
return pReps
}
func (p *snapProgress) report() (hooksStr string, hooksHadError bool) {
hr := p.hookPlan.Report()
// FIXME: technically this belongs into client
// but we can't serialize hooks.Step ATM
rightPad := func(str string, length int, pad string) string {
if len(str) > length {
return str[:length]
}
return str + strings.Repeat(pad, length-len(str))
}
hooksHadError = hr.HadError()
rows := make([][]string, len(hr))
const numCols = 4
lens := make([]int, numCols)
for i, e := range hr {
rows[i] = make([]string, numCols)
rows[i][0] = fmt.Sprintf("%d", i+1)
rows[i][1] = e.Status.String()
runTime := "..."
if e.Status != hooks.StepPending {
runTime = e.End.Sub(e.Begin).Round(time.Millisecond).String()
}
rows[i][2] = runTime
rows[i][3] = ""
if e.Report != nil {
rows[i][3] = e.Report.String()
}
for j, col := range lens {
if len(rows[i][j]) > col {
lens[j] = len(rows[i][j])
}
}
}
rowsFlat := make([]string, len(hr))
for i, r := range rows {
colsPadded := make([]string, len(r))
for j, c := range r[:len(r)-1] {
colsPadded[j] = rightPad(c, lens[j], " ")
}
colsPadded[len(r)-1] = r[len(r)-1]
rowsFlat[i] = strings.Join(colsPadded, " ")
}
hooksStr = strings.Join(rowsFlat, "\n")
return hooksStr, hooksHadError
}
-15
View File
@@ -1,15 +0,0 @@
package snapper
import (
"context"
)
type manual struct{}
func (s *manual) Run(ctx context.Context, wakeUpCommon chan<- struct{}) {
// nothing to do
}
func (s *manual) Report() Report {
return Report{Type: TypeManual, Manual: &struct{}{}}
}
-394
View File
@@ -1,394 +0,0 @@
package snapper
import (
"context"
"fmt"
"sort"
"sync"
"time"
"github.com/pkg/errors"
"github.com/zrepl/zrepl/daemon/logging/trace"
"github.com/zrepl/zrepl/config"
"github.com/zrepl/zrepl/daemon/hooks"
"github.com/zrepl/zrepl/daemon/logging"
"github.com/zrepl/zrepl/util/envconst"
"github.com/zrepl/zrepl/util/suspendresumesafetimer"
"github.com/zrepl/zrepl/zfs"
)
func periodicFromConfig(g *config.Global, fsf zfs.DatasetFilter, in *config.SnapshottingPeriodic) (*Periodic, error) {
if in.Prefix == "" {
return nil, errors.New("prefix must not be empty")
}
if in.Interval.Duration() <= 0 {
return nil, errors.New("interval must be positive")
}
hookList, err := hooks.ListFromConfig(&in.Hooks)
if err != nil {
return nil, errors.Wrap(err, "hook config error")
}
args := periodicArgs{
interval: in.Interval.Duration(),
fsf: fsf,
planArgs: planArgs{
prefix: in.Prefix,
timestampFormat: in.TimestampFormat,
hooks: hookList,
},
// ctx and log is set in Run()
}
return &Periodic{state: SyncUp, args: args}, nil
}
type periodicArgs struct {
ctx context.Context
interval time.Duration
fsf zfs.DatasetFilter
planArgs planArgs
snapshotsTaken chan<- struct{}
dryRun bool
}
type Periodic struct {
args periodicArgs
mtx sync.Mutex
state State
// set in state Plan, used in Waiting
lastInvocation time.Time
// valid for state Snapshotting
plan *plan
// valid for state SyncUp and Waiting
sleepUntil time.Time
// valid for state Err
err error
}
//go:generate stringer -type=State
type State uint
const (
SyncUp State = 1 << iota
SyncUpErrWait
Planning
Snapshotting
Waiting
ErrorWait
Stopped
)
func (s State) sf() state {
m := map[State]state{
SyncUp: periodicStateSyncUp,
SyncUpErrWait: periodicStateWait,
Planning: periodicStatePlan,
Snapshotting: periodicStateSnapshot,
Waiting: periodicStateWait,
ErrorWait: periodicStateWait,
Stopped: nil,
}
return m[s]
}
type updater func(u func(*Periodic)) State
type state func(a periodicArgs, u updater) state
func (s *Periodic) Run(ctx context.Context, snapshotsTaken chan<- struct{}) {
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
getLogger(ctx).Debug("start")
defer getLogger(ctx).Debug("stop")
s.args.snapshotsTaken = snapshotsTaken
s.args.ctx = ctx
s.args.dryRun = false // for future expansion
u := func(u func(*Periodic)) State {
s.mtx.Lock()
defer s.mtx.Unlock()
if u != nil {
u(s)
}
return s.state
}
var st state = periodicStateSyncUp
for st != nil {
pre := u(nil)
st = st(s.args, u)
post := u(nil)
getLogger(ctx).
WithField("transition", fmt.Sprintf("%s=>%s", pre, post)).
Debug("state transition")
}
}
func onErr(err error, u updater) state {
return u(func(s *Periodic) {
s.err = err
preState := s.state
switch s.state {
case SyncUp:
s.state = SyncUpErrWait
case Planning:
fallthrough
case Snapshotting:
s.state = ErrorWait
}
getLogger(s.args.ctx).WithError(err).WithField("pre_state", preState).WithField("post_state", s.state).Error("snapshotting error")
}).sf()
}
func onMainCtxDone(ctx context.Context, u updater) state {
return u(func(s *Periodic) {
s.err = ctx.Err()
s.state = Stopped
}).sf()
}
func periodicStateSyncUp(a periodicArgs, u updater) state {
u(func(snapper *Periodic) {
snapper.lastInvocation = time.Now()
})
fss, err := listFSes(a.ctx, a.fsf)
if err != nil {
return onErr(err, u)
}
syncPoint, err := findSyncPoint(a.ctx, fss, a.planArgs.prefix, a.interval)
if err != nil {
return onErr(err, u)
}
u(func(s *Periodic) {
s.sleepUntil = syncPoint
})
ctxDone := suspendresumesafetimer.SleepUntil(a.ctx, syncPoint)
if ctxDone != nil {
return onMainCtxDone(a.ctx, u)
}
return u(func(s *Periodic) {
s.state = Planning
}).sf()
}
func periodicStatePlan(a periodicArgs, u updater) state {
u(func(snapper *Periodic) {
snapper.lastInvocation = time.Now()
})
fss, err := listFSes(a.ctx, a.fsf)
if err != nil {
return onErr(err, u)
}
p := makePlan(a.planArgs, fss)
return u(func(s *Periodic) {
s.state = Snapshotting
s.plan = p
s.err = nil
}).sf()
}
func periodicStateSnapshot(a periodicArgs, u updater) state {
var plan *plan
u(func(snapper *Periodic) {
plan = snapper.plan
})
ok := plan.execute(a.ctx, false)
select {
case a.snapshotsTaken <- struct{}{}:
default:
if a.snapshotsTaken != nil {
getLogger(a.ctx).Warn("callback channel is full, discarding snapshot update event")
}
}
return u(func(snapper *Periodic) {
if !ok {
snapper.state = ErrorWait
snapper.err = errors.New("one or more snapshots could not be created, check logs for details")
} else {
snapper.state = Waiting
snapper.err = nil
}
}).sf()
}
func periodicStateWait(a periodicArgs, u updater) state {
var sleepUntil time.Time
u(func(snapper *Periodic) {
lastTick := snapper.lastInvocation
snapper.sleepUntil = lastTick.Add(a.interval)
sleepUntil = snapper.sleepUntil
log := getLogger(a.ctx).WithField("sleep_until", sleepUntil).WithField("duration", a.interval)
logFunc := log.Debug
if snapper.state == ErrorWait || snapper.state == SyncUpErrWait {
logFunc = log.Error
}
logFunc("enter wait-state after error")
})
ctxDone := suspendresumesafetimer.SleepUntil(a.ctx, sleepUntil)
if ctxDone != nil {
return onMainCtxDone(a.ctx, u)
}
return u(func(snapper *Periodic) {
snapper.state = Planning
}).sf()
}
func listFSes(ctx context.Context, mf zfs.DatasetFilter) (fss []*zfs.DatasetPath, err error) {
return zfs.ZFSListMapping(ctx, mf)
}
var syncUpWarnNoSnapshotUntilSyncupMinDuration = envconst.Duration("ZREPL_SNAPPER_SYNCUP_WARN_MIN_DURATION", 1*time.Second)
// see docs/snapshotting.rst
func findSyncPoint(ctx context.Context, fss []*zfs.DatasetPath, prefix string, interval time.Duration) (syncPoint time.Time, err error) {
const (
prioHasVersions int = iota
prioNoVersions
)
type snapTime struct {
ds *zfs.DatasetPath
prio int // lower is higher
time time.Time
}
if len(fss) == 0 {
return time.Now(), nil
}
snaptimes := make([]snapTime, 0, len(fss))
hardErrs := 0
now := time.Now()
getLogger(ctx).Debug("examine filesystem state to find sync point")
for _, d := range fss {
ctx := logging.WithInjectedField(ctx, "fs", d.ToString())
syncPoint, err := findSyncPointFSNextOptimalSnapshotTime(ctx, now, interval, prefix, d)
if err == findSyncPointFSNoFilesystemVersionsErr {
snaptimes = append(snaptimes, snapTime{
ds: d,
prio: prioNoVersions,
time: now,
})
} else if err != nil {
hardErrs++
getLogger(ctx).WithError(err).Error("cannot determine optimal sync point for this filesystem")
} else {
getLogger(ctx).WithField("syncPoint", syncPoint).Debug("found optimal sync point for this filesystem")
snaptimes = append(snaptimes, snapTime{
ds: d,
prio: prioHasVersions,
time: syncPoint,
})
}
}
if hardErrs == len(fss) {
return time.Time{}, fmt.Errorf("hard errors in determining sync point for every matching filesystem")
}
if len(snaptimes) == 0 {
panic("implementation error: loop must either inc hardErrs or add result to snaptimes")
}
// sort ascending by (prio,time)
// => those filesystems with versions win over those without any
sort.Slice(snaptimes, func(i, j int) bool {
if snaptimes[i].prio == snaptimes[j].prio {
return snaptimes[i].time.Before(snaptimes[j].time)
}
return snaptimes[i].prio < snaptimes[j].prio
})
winnerSyncPoint := snaptimes[0].time
l := getLogger(ctx).WithField("syncPoint", winnerSyncPoint.String())
l.Info("determined sync point")
if winnerSyncPoint.Sub(now) > syncUpWarnNoSnapshotUntilSyncupMinDuration {
for _, st := range snaptimes {
if st.prio == prioNoVersions {
l.WithField("fs", st.ds.ToString()).Warn("filesystem will not be snapshotted until sync point")
}
}
}
return snaptimes[0].time, nil
}
var findSyncPointFSNoFilesystemVersionsErr = fmt.Errorf("no filesystem versions")
func findSyncPointFSNextOptimalSnapshotTime(ctx context.Context, now time.Time, interval time.Duration, prefix string, d *zfs.DatasetPath) (time.Time, error) {
fsvs, err := zfs.ZFSListFilesystemVersions(ctx, d, zfs.ListFilesystemVersionsOptions{
Types: zfs.Snapshots,
ShortnamePrefix: prefix,
})
if err != nil {
return time.Time{}, errors.Wrap(err, "list filesystem versions")
}
if len(fsvs) <= 0 {
return time.Time{}, findSyncPointFSNoFilesystemVersionsErr
}
// Sort versions by creation
sort.SliceStable(fsvs, func(i, j int) bool {
return fsvs[i].CreateTXG < fsvs[j].CreateTXG
})
latest := fsvs[len(fsvs)-1]
getLogger(ctx).WithField("creation", latest.Creation).Debug("found latest snapshot")
since := now.Sub(latest.Creation)
if since < 0 {
return time.Time{}, fmt.Errorf("snapshot %q is from the future: creation=%q now=%q", latest.ToAbsPath(d), latest.Creation, now)
}
return latest.Creation.Add(interval), nil
}
type PeriodicReport struct {
State State
// valid in state SyncUp and Waiting
SleepUntil time.Time
// valid in state Err
Error string
// valid in state Snapshotting
Progress []*ReportFilesystem
}
func (s *Periodic) Report() Report {
s.mtx.Lock()
defer s.mtx.Unlock()
var progress []*ReportFilesystem = nil
if s.plan != nil {
progress = s.plan.report()
}
r := &PeriodicReport{
State: s.state,
SleepUntil: s.sleepUntil,
Error: errOrEmptyString(s.err),
Progress: progress,
}
return Report{Type: TypePeriodic, Periodic: r}
}
+486 -21
View File
@@ -3,40 +3,505 @@ package snapper
import ( import (
"context" "context"
"fmt" "fmt"
"sort"
"sync"
"time"
"github.com/pkg/errors"
"github.com/zrepl/zrepl/daemon/logging/trace"
"github.com/zrepl/zrepl/config" "github.com/zrepl/zrepl/config"
"github.com/zrepl/zrepl/daemon/hooks"
"github.com/zrepl/zrepl/daemon/logging"
"github.com/zrepl/zrepl/logger"
"github.com/zrepl/zrepl/util/envconst"
"github.com/zrepl/zrepl/zfs" "github.com/zrepl/zrepl/zfs"
) )
type Type string //go:generate stringer -type=SnapState
type SnapState uint
const ( const (
TypePeriodic Type = "periodic" SnapPending SnapState = 1 << iota
TypeCron Type = "cron" SnapStarted
TypeManual Type = "manual" SnapDone
SnapError
) )
type Snapper interface { // All fields protected by Snapper.mtx
Run(ctx context.Context, snapshotsTaken chan<- struct{}) type snapProgress struct {
Report() Report state SnapState
// SnapStarted, SnapDone, SnapError
name string
startAt time.Time
hookPlan *hooks.Plan
// SnapDone
doneAt time.Time
// SnapErr TODO disambiguate state
runResults hooks.PlanReport
} }
type Report struct { type args struct {
Type Type ctx context.Context
Periodic *PeriodicReport prefix string
Cron *CronReport interval time.Duration
Manual *struct{} fsf zfs.DatasetFilter
snapshotsTaken chan<- struct{}
hooks *hooks.List
dryRun bool
} }
func FromConfig(g *config.Global, fsf zfs.DatasetFilter, in config.SnapshottingEnum) (Snapper, error) { type Snapper struct {
switch v := in.Ret.(type) { args args
case *config.SnapshottingPeriodic:
return periodicFromConfig(g, fsf, v) mtx sync.Mutex
case *config.SnapshottingCron: state State
return cronFromConfig(fsf, *v)
case *config.SnapshottingManual: // set in state Plan, used in Waiting
return &manual{}, nil lastInvocation time.Time
// valid for state Snapshotting
plan map[*zfs.DatasetPath]*snapProgress
// valid for state SyncUp and Waiting
sleepUntil time.Time
// valid for state Err
err error
}
//go:generate stringer -type=State
type State uint
const (
SyncUp State = 1 << iota
SyncUpErrWait
Planning
Snapshotting
Waiting
ErrorWait
Stopped
)
func (s State) sf() state {
m := map[State]state{
SyncUp: syncUp,
SyncUpErrWait: wait,
Planning: plan,
Snapshotting: snapshot,
Waiting: wait,
ErrorWait: wait,
Stopped: nil,
}
return m[s]
}
type updater func(u func(*Snapper)) State
type state func(a args, u updater) state
type Logger = logger.Logger
func getLogger(ctx context.Context) Logger {
return logging.GetLogger(ctx, logging.SubsysSnapshot)
}
func PeriodicFromConfig(g *config.Global, fsf zfs.DatasetFilter, in *config.SnapshottingPeriodic) (*Snapper, error) {
if in.Prefix == "" {
return nil, errors.New("prefix must not be empty")
}
if in.Interval <= 0 {
return nil, errors.New("interval must be positive")
}
hookList, err := hooks.ListFromConfig(&in.Hooks)
if err != nil {
return nil, errors.Wrap(err, "hook config error")
}
args := args{
prefix: in.Prefix,
interval: in.Interval,
fsf: fsf,
hooks: hookList,
// ctx and log is set in Run()
}
return &Snapper{state: SyncUp, args: args}, nil
}
func (s *Snapper) Run(ctx context.Context, snapshotsTaken chan<- struct{}) {
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
getLogger(ctx).Debug("start")
defer getLogger(ctx).Debug("stop")
s.args.snapshotsTaken = snapshotsTaken
s.args.ctx = ctx
s.args.dryRun = false // for future expansion
u := func(u func(*Snapper)) State {
s.mtx.Lock()
defer s.mtx.Unlock()
if u != nil {
u(s)
}
return s.state
}
var st state = syncUp
for st != nil {
pre := u(nil)
st = st(s.args, u)
post := u(nil)
getLogger(ctx).
WithField("transition", fmt.Sprintf("%s=>%s", pre, post)).
Debug("state transition")
}
}
func onErr(err error, u updater) state {
return u(func(s *Snapper) {
s.err = err
preState := s.state
switch s.state {
case SyncUp:
s.state = SyncUpErrWait
case Planning:
fallthrough
case Snapshotting:
s.state = ErrorWait
}
getLogger(s.args.ctx).WithError(err).WithField("pre_state", preState).WithField("post_state", s.state).Error("snapshotting error")
}).sf()
}
func onMainCtxDone(ctx context.Context, u updater) state {
return u(func(s *Snapper) {
s.err = ctx.Err()
s.state = Stopped
}).sf()
}
func syncUp(a args, u updater) state {
u(func(snapper *Snapper) {
snapper.lastInvocation = time.Now()
})
fss, err := listFSes(a.ctx, a.fsf)
if err != nil {
return onErr(err, u)
}
syncPoint, err := findSyncPoint(a.ctx, fss, a.prefix, a.interval)
if err != nil {
return onErr(err, u)
}
u(func(s *Snapper) {
s.sleepUntil = syncPoint
})
return waitUntilThenTransitionToPlanning(a, u, syncPoint)
}
func plan(a args, u updater) state {
u(func(snapper *Snapper) {
snapper.lastInvocation = time.Now()
})
fss, err := listFSes(a.ctx, a.fsf)
if err != nil {
return onErr(err, u)
}
plan := make(map[*zfs.DatasetPath]*snapProgress, len(fss))
for _, fs := range fss {
plan[fs] = &snapProgress{state: SnapPending}
}
return u(func(s *Snapper) {
s.state = Snapshotting
s.plan = plan
s.err = nil
}).sf()
}
func snapshot(a args, u updater) state {
var plan map[*zfs.DatasetPath]*snapProgress
u(func(snapper *Snapper) {
plan = snapper.plan
})
hookMatchCount := make(map[hooks.Hook]int, len(*a.hooks))
for _, h := range *a.hooks {
hookMatchCount[h] = 0
}
anyFsHadErr := false
// TODO channel programs -> allow a little jitter?
for fs, progress := range plan {
suffix := time.Now().In(time.UTC).Format("20060102_150405_000")
snapname := fmt.Sprintf("%s%s", a.prefix, suffix)
ctx := logging.WithInjectedField(a.ctx, "fs", fs.ToString())
ctx = logging.WithInjectedField(ctx, "snap", snapname)
hookEnvExtra := hooks.Env{
hooks.EnvFS: fs.ToString(),
hooks.EnvSnapshot: snapname,
}
jobCallback := hooks.NewCallbackHookForFilesystem("snapshot", fs, func(ctx context.Context) (err error) {
l := getLogger(ctx)
l.Debug("create snapshot")
err = zfs.ZFSSnapshot(ctx, fs, snapname, false) // TODO propagate context to ZFSSnapshot
if err != nil {
l.WithError(err).Error("cannot create snapshot")
}
return
})
fsHadErr := false
var planReport hooks.PlanReport
var plan *hooks.Plan
{
filteredHooks, err := a.hooks.CopyFilteredForFilesystem(fs)
if err != nil {
getLogger(ctx).WithError(err).Error("unexpected filter error")
fsHadErr = true
goto updateFSState
}
// account for running hooks
for _, h := range filteredHooks {
hookMatchCount[h] = hookMatchCount[h] + 1
}
var planErr error
plan, planErr = hooks.NewPlan(&filteredHooks, hooks.PhaseSnapshot, jobCallback, hookEnvExtra)
if planErr != nil {
fsHadErr = true
getLogger(ctx).WithError(planErr).Error("cannot create job hook plan")
goto updateFSState
}
}
u(func(snapper *Snapper) {
progress.name = snapname
progress.startAt = time.Now()
progress.hookPlan = plan
progress.state = SnapStarted
})
{
getLogger(ctx).WithField("report", plan.Report().String()).Debug("begin run job plan")
plan.Run(ctx, a.dryRun)
planReport = plan.Report()
fsHadErr = planReport.HadError() // not just fatal errors
if fsHadErr {
getLogger(ctx).WithField("report", planReport.String()).Error("end run job plan with error")
} else {
getLogger(ctx).WithField("report", planReport.String()).Info("end run job plan successful")
}
}
updateFSState:
anyFsHadErr = anyFsHadErr || fsHadErr
u(func(snapper *Snapper) {
progress.doneAt = time.Now()
progress.state = SnapDone
if fsHadErr {
progress.state = SnapError
}
progress.runResults = planReport
})
}
select {
case a.snapshotsTaken <- struct{}{}:
default: default:
return nil, fmt.Errorf("unknown snapshotting type %T", v) if a.snapshotsTaken != nil {
getLogger(a.ctx).Warn("callback channel is full, discarding snapshot update event")
}
}
for h, mc := range hookMatchCount {
if mc == 0 {
hookIdx := -1
for idx, ah := range *a.hooks {
if ah == h {
hookIdx = idx
break
}
}
getLogger(a.ctx).WithField("hook", h.String()).WithField("hook_number", hookIdx+1).Warn("hook did not match any snapshotted filesystems")
}
}
return u(func(snapper *Snapper) {
if anyFsHadErr {
snapper.state = ErrorWait
snapper.err = errors.New("one or more snapshots could not be created, check logs for details")
} else {
snapper.state = Waiting
snapper.err = nil
}
}).sf()
}
func wait(a args, u updater) state {
var sleepUntil time.Time
u(func(snapper *Snapper) {
lastTick := snapper.lastInvocation
snapper.sleepUntil = lastTick.Add(a.interval)
sleepUntil = snapper.sleepUntil
log := getLogger(a.ctx).WithField("sleep_until", sleepUntil).WithField("duration", a.interval)
logFunc := log.Debug
if snapper.state == ErrorWait || snapper.state == SyncUpErrWait {
logFunc = log.Error
}
logFunc("enter wait-state after error")
})
return waitUntilThenTransitionToPlanning(a, u, sleepUntil)
}
func waitUntilThenTransitionToPlanning(a args, u updater, sleepUntil time.Time) state {
ticker := time.NewTicker(333 * time.Millisecond)
defer ticker.Stop()
lastLog := time.Now()
for {
select {
case <-ticker.C:
now := time.Now()
if now.Before(sleepUntil) {
// rate limit loggin to once per minute
if (now.Sub(lastLog)) > envconst.Duration("ZREPL_SNAPPER_DEBUG_LOG_ISSUE_611", 1*time.Minute) {
getLogger(a.ctx).WithField("sleep_until", sleepUntil).WithField("remaining", sleepUntil.Sub(now)).Error("DEBUG-LOG-ZREPL-ISSUE-611")
lastLog = now
}
continue // continue with ticker
}
return u(func(snapper *Snapper) {
snapper.state = Planning
}).sf()
case <-a.ctx.Done():
return onMainCtxDone(a.ctx, u)
}
} }
} }
func listFSes(ctx context.Context, mf zfs.DatasetFilter) (fss []*zfs.DatasetPath, err error) {
return zfs.ZFSListMapping(ctx, mf)
}
var syncUpWarnNoSnapshotUntilSyncupMinDuration = envconst.Duration("ZREPL_SNAPPER_SYNCUP_WARN_MIN_DURATION", 1*time.Second)
// see docs/snapshotting.rst
func findSyncPoint(ctx context.Context, fss []*zfs.DatasetPath, prefix string, interval time.Duration) (syncPoint time.Time, err error) {
const (
prioHasVersions int = iota
prioNoVersions
)
type snapTime struct {
ds *zfs.DatasetPath
prio int // lower is higher
time time.Time
}
if len(fss) == 0 {
return time.Now(), nil
}
snaptimes := make([]snapTime, 0, len(fss))
hardErrs := 0
now := time.Now()
getLogger(ctx).Debug("examine filesystem state to find sync point")
for _, d := range fss {
ctx := logging.WithInjectedField(ctx, "fs", d.ToString())
syncPoint, err := findSyncPointFSNextOptimalSnapshotTime(ctx, now, interval, prefix, d)
if err == findSyncPointFSNoFilesystemVersionsErr {
snaptimes = append(snaptimes, snapTime{
ds: d,
prio: prioNoVersions,
time: now,
})
} else if err != nil {
hardErrs++
getLogger(ctx).WithError(err).Error("cannot determine optimal sync point for this filesystem")
} else {
getLogger(ctx).WithField("syncPoint", syncPoint).Debug("found optimal sync point for this filesystem")
snaptimes = append(snaptimes, snapTime{
ds: d,
prio: prioHasVersions,
time: syncPoint,
})
}
}
if hardErrs == len(fss) {
return time.Time{}, fmt.Errorf("hard errors in determining sync point for every matching filesystem")
}
if len(snaptimes) == 0 {
panic("implementation error: loop must either inc hardErrs or add result to snaptimes")
}
// sort ascending by (prio,time)
// => those filesystems with versions win over those without any
sort.Slice(snaptimes, func(i, j int) bool {
if snaptimes[i].prio == snaptimes[j].prio {
return snaptimes[i].time.Before(snaptimes[j].time)
}
return snaptimes[i].prio < snaptimes[j].prio
})
winnerSyncPoint := snaptimes[0].time
l := getLogger(ctx).WithField("syncPoint", winnerSyncPoint.String())
l.Info("determined sync point")
if winnerSyncPoint.Sub(now) > syncUpWarnNoSnapshotUntilSyncupMinDuration {
for _, st := range snaptimes {
if st.prio == prioNoVersions {
l.WithField("fs", st.ds.ToString()).Warn("filesystem will not be snapshotted until sync point")
}
}
}
return snaptimes[0].time, nil
}
var findSyncPointFSNoFilesystemVersionsErr = fmt.Errorf("no filesystem versions")
func findSyncPointFSNextOptimalSnapshotTime(ctx context.Context, now time.Time, interval time.Duration, prefix string, d *zfs.DatasetPath) (time.Time, error) {
fsvs, err := zfs.ZFSListFilesystemVersions(ctx, d, zfs.ListFilesystemVersionsOptions{
Types: zfs.Snapshots,
ShortnamePrefix: prefix,
})
if err != nil {
return time.Time{}, errors.Wrap(err, "list filesystem versions")
}
if len(fsvs) <= 0 {
return time.Time{}, findSyncPointFSNoFilesystemVersionsErr
}
// Sort versions by creation
sort.SliceStable(fsvs, func(i, j int) bool {
return fsvs[i].CreateTXG < fsvs[j].CreateTXG
})
latest := fsvs[len(fsvs)-1]
getLogger(ctx).WithField("creation", latest.Creation).Debug("found latest snapshot")
since := now.Sub(latest.Creation)
if since < 0 {
return time.Time{}, fmt.Errorf("snapshot %q is from the future: creation=%q now=%q", latest.ToAbsPath(d), latest.Creation, now)
}
return latest.Creation.Add(interval), nil
}
+48
View File
@@ -0,0 +1,48 @@
package snapper
import (
"context"
"fmt"
"github.com/zrepl/zrepl/config"
"github.com/zrepl/zrepl/zfs"
)
// FIXME: properly abstract snapshotting:
// - split up things that trigger snapshotting from the mechanism
// - timer-based trigger (periodic)
// - call from control socket (manual)
// - mixed modes?
// - support a `zrepl snapshot JOBNAME` subcommand for config.SnapshottingManual
type PeriodicOrManual struct {
s *Snapper
}
func (s *PeriodicOrManual) Run(ctx context.Context, wakeUpCommon chan<- struct{}) {
if s.s != nil {
s.s.Run(ctx, wakeUpCommon)
}
}
// Returns nil if manual
func (s *PeriodicOrManual) Report() *Report {
if s.s != nil {
return s.s.Report()
}
return nil
}
func FromConfig(g *config.Global, fsf zfs.DatasetFilter, in config.SnapshottingEnum) (*PeriodicOrManual, error) {
switch v := in.Ret.(type) {
case *config.SnapshottingPeriodic:
snapper, err := PeriodicFromConfig(g, fsf, v)
if err != nil {
return nil, err
}
return &PeriodicOrManual{snapper}, nil
case *config.SnapshottingManual:
return &PeriodicOrManual{}, nil
default:
return nil, fmt.Errorf("unknown snapshotting type %T", v)
}
}
+118
View File
@@ -0,0 +1,118 @@
package snapper
import (
"fmt"
"sort"
"strings"
"time"
"github.com/zrepl/zrepl/daemon/hooks"
)
type Report struct {
State State
// valid in state SyncUp and Waiting
SleepUntil time.Time
// valid in state Err
Error string
// valid in state Snapshotting
Progress []*ReportFilesystem
}
type ReportFilesystem struct {
Path string
State SnapState
// Valid in SnapStarted and later
SnapName string
StartAt time.Time
Hooks string
HooksHadError bool
// Valid in SnapDone | SnapError
DoneAt time.Time
}
func errOrEmptyString(e error) string {
if e != nil {
return e.Error()
}
return ""
}
func (s *Snapper) Report() *Report {
s.mtx.Lock()
defer s.mtx.Unlock()
pReps := make([]*ReportFilesystem, 0, len(s.plan))
for fs, p := range s.plan {
var hooksStr string
var hooksHadError bool
if p.hookPlan != nil {
hr := p.hookPlan.Report()
// FIXME: technically this belongs into client
// but we can't serialize hooks.Step ATM
rightPad := func(str string, length int, pad string) string {
if len(str) > length {
return str[:length]
}
return str + strings.Repeat(pad, length-len(str))
}
hooksHadError = hr.HadError()
rows := make([][]string, len(hr))
const numCols = 4
lens := make([]int, numCols)
for i, e := range hr {
rows[i] = make([]string, numCols)
rows[i][0] = fmt.Sprintf("%d", i+1)
rows[i][1] = e.Status.String()
runTime := "..."
if e.Status != hooks.StepPending {
runTime = e.End.Sub(e.Begin).Round(time.Millisecond).String()
}
rows[i][2] = runTime
rows[i][3] = ""
if e.Report != nil {
rows[i][3] = e.Report.String()
}
for j, col := range lens {
if len(rows[i][j]) > col {
lens[j] = len(rows[i][j])
}
}
}
rowsFlat := make([]string, len(hr))
for i, r := range rows {
colsPadded := make([]string, len(r))
for j, c := range r[:len(r)-1] {
colsPadded[j] = rightPad(c, lens[j], " ")
}
colsPadded[len(r)-1] = r[len(r)-1]
rowsFlat[i] = strings.Join(colsPadded, " ")
}
hooksStr = strings.Join(rowsFlat, "\n")
}
pReps = append(pReps, &ReportFilesystem{
Path: fs.ToString(),
State: p.state,
SnapName: p.name,
StartAt: p.startAt,
DoneAt: p.doneAt,
Hooks: hooksStr,
HooksHadError: hooksHadError,
})
}
sort.Slice(pReps, func(i, j int) bool {
return strings.Compare(pReps[i].Path, pReps[j].Path) == -1
})
r := &Report{
State: s.state,
SleepUntil: s.sleepUntil,
Error: errOrEmptyString(s.err),
Progress: pReps,
}
return r
}
-21
View File
@@ -1,21 +0,0 @@
package snapper
import (
"context"
"github.com/zrepl/zrepl/daemon/logging"
"github.com/zrepl/zrepl/logger"
)
type Logger = logger.Logger
func getLogger(ctx context.Context) Logger {
return logging.GetLogger(ctx, logging.SubsysSnapshot)
}
func errOrEmptyString(e error) string {
if e != nil {
return e.Error()
}
return ""
}
+2 -2
View File
@@ -1235,7 +1235,7 @@
] ]
}, },
"timezone": "", "timezone": "",
"title": "zrepl", "title": "zrepl 0.3",
"uid": "etQuvBnGz", "uid": "etQuvBnGz",
"version": 8 "version": 7
} }
+1 -5
View File
@@ -9,9 +9,6 @@ ExecStart=/usr/local/bin/zrepl --config /etc/zrepl/zrepl.yml daemon
RuntimeDirectory=zrepl zrepl/stdinserver RuntimeDirectory=zrepl zrepl/stdinserver
RuntimeDirectoryMode=0700 RuntimeDirectoryMode=0700
# Make Go produce coredumps
Environment=GOTRACEBACK='crash'
ProtectSystem=strict ProtectSystem=strict
#PrivateDevices=yes # TODO ZFS needs access to /dev/zfs, could we limit this? #PrivateDevices=yes # TODO ZFS needs access to /dev/zfs, could we limit this?
ProtectKernelTunables=yes ProtectKernelTunables=yes
@@ -30,8 +27,7 @@ ProtectHome=read-only
# SystemCallFilter # SystemCallFilter
# ~@privileged doesn't work with Ubuntu 18.04 ssh # ~@privileged doesn't work with Ubuntu 18.04 ssh
SystemCallFilter=~ @mount @cpu-emulation @keyring @module @obsolete @raw-io @debug @clock @resources SystemCallFilter=~ @mount @cpu-emulation @keyring @module @obsolete @raw-io @debug @clock @resources
# Go1.19 added automatic RLIMIT_NOFILE changes, so, we need to allow that
SystemCallFilter= setrlimit
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target
+5 -55
View File
@@ -16,69 +16,19 @@ 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.
Next Release 0.6 (Unreleased)
------------ ----------------
The plan for the next release is to revisit how zrepl does snapshot management. * `Feature Wishlist on GitHub <https://github.com/zrepl/zrepl/discussions/547>`_
High-level goals:
- Make it easy to decouple snapshot management (snapshotting, pruning) from replication. * |feature| Add ``ZREPL_DESTROY_MAX_BATCH_SIZE`` env var (default 0=unlimited).
- Ability to include/exclude snapshots from replication. * |break| |feature| convert Prometheus metric ``zrepl_version_daemon`` to ``zrepl_start_time`` metric
This is useful for aforementioned decoupling, e.g., separate snapshot prefixes for local & remote replication.
Also, it makes explicit that by default, zrepl replicates all snapshots, and that
replication has no concept of "zrepl-created snapshots", which is a common misconception.
- Use of ``zfs snapshot`` comma syntax or channel programs to take snapshots of multiple
datasets atomically.
- Provide an alternative to the ``grid`` pruning policy.
Most likely something based on hourly/daily/weekly/monthly "trains" plus a count.
- Ability to prune at the granularity of the **group** of snapshots created at a given
time, as opposed to the individual snapshots within a dataset.
Maybe this will be addressed by the alternative to the ``grid`` pruning policy,
as it will likely be more predictable.
Those changes will likely come with some breakage in the config.
However, I want to avoid breaking **use cases** that are satisfied by the current design.
There will be beta/RC releases to give users a chance to evaluate.
0.6
---
* |feature| :ref:`Schedule-based snapshotting<job-snapshotting--cron>` using ``cron`` syntax instead of an interval.
* |feature| Configurable initial replication policy.
When a filesystem is first replicated to a receiver, this control whether just the newest
snapshot will be replicated vs. all existing snapshots. Learn more :ref:`in the docs <conflict_resolution-initial_replication>`.
* |feature| Configurable timestamp format for snapshot names via :ref:`timestamp_format<job-snapshotting-timestamp_format>`
(Thanks, `@ydylla <https://github.com/ydylla>`_).
* |feature| Add ``ZREPL_DESTROY_MAX_BATCH_SIZE`` env var (default 0=unlimited)
(Thanks, `@3nprob <https://github.com/3nprob>`_).
* |feature| Add ``zrepl configcheck --skip-cert-check`` flag (Thanks, `@cole-h <https://github.com/cole-h>`_).
* |bugfix| Fix resuming from interrupted replications that use ``send.raw`` on unencrypted datasets.
* The send options introduced in zrepl 0.4 allow users to specify additional zfs send flags for zrepl to use.
Before this fix, when setting ``send.raw=true`` on a job that replicates unencrypted datasets,
zrepl would not allow an interrupted replication to resume.
The reason were overly cautious checks to support the ``send.encrypted`` option.
* This bugfix removes these checks from the replication planner.
This makes ``send.encrypted`` a sender-side-only concern, much like all other ``send.*`` flags.
* However, this means that the ``zrepl status`` UI no longer indicates whether a replication step uses encrypted sends or not.
The setting is still effective though.
* |break| convert Prometheus metric ``zrepl_version_daemon`` to ``zrepl_start_time`` metric
* The metric still reports the zrepl version in a label. * The metric still reports the zrepl version in a label.
But the metric *value* is now the Unix timestamp at the time the daemon was started. But the metric *value* is now the Unix timestamp at the time the daemon was started.
The Grafana dashboard in :repomasterlink:`dist/grafana` has been updated. The Grafana dashboard in :repomasterlink:`dist/grafana` has been updated.
* |bugfix| transient zrepl status error: ``Post "http://unix/status": EOF`` * |bugfix| transient zrepl status error: ``Post "http://unix/status": EOF``
* |bugfix| don't treat receive-side bookmarks as a replication conflict.
This facilitates chaining of replication jobs. See :issue:`490`.
* |bugfix| workaround for Go/gRPC problem on Illumos where zrepl would
crash when using the ``local`` transport type (:issue:`598`).
* |bugfix| fix active child tasks panic that cold occur during replication plannig (:issue:`193abbe`)
* |bugfix| ``zrepl status`` off-by-one error in display of completed step count (:commit:`ce6701f`)
* |bugfix| Allow using day & week units for ``snapshotting.interval`` (:commit:`ffb1d89`)
* |docs| ``docs/overview`` improvements (Thanks, `@jtagcat <https://github.com/jtagcat>`_).
* |maint| Update to Go 1.19.
0.5 0.5
--- ---
+1 -1
View File
@@ -15,7 +15,7 @@ Conflict Resolution Options
... ...
.. _conflict_resolution-initial_replication: .. _conflict_resolution-initial_replication-option-send_all_snapshots:
``initial_replication`` option ``initial_replication`` option
-2
View File
@@ -1,5 +1,3 @@
.. _miscellaneous:
Miscellaneous Miscellaneous
============= =============
+26 -24
View File
@@ -3,17 +3,16 @@ Overview & Terminology
====================== ======================
All work zrepl does is performed by the zrepl daemon which is configured in a single YAML configuration file loaded on startup. All work zrepl does is performed by the zrepl daemon which is configured in a single YAML configuration file loaded on startup.
The following paths are searched, in this order: The following paths are considered:
1. The path specified via the global ``--config`` flag * If set, the location specified via the global ``--config`` flag
2. ``/etc/zrepl/zrepl.yml`` * ``/etc/zrepl/zrepl.yml``
3. ``/usr/local/etc/zrepl/zrepl.yml`` * ``/usr/local/etc/zrepl/zrepl.yml``
``zrepl configcheck`` can be used to validate the configuration. The ``zrepl configcheck`` subcommand can be used to validate the configuration.
If the configuration is valid, it will output nothing and exit with code ``0``. The command will output nothing and exit with zero status code if the configuration is valid.
The error messages vary in quality and usefulness: please report confusing config errors to the tracking :issue:`155`. The error messages vary in quality and usefulness: please report confusing config errors to the tracking :issue:`155`.
Full example configs such as in the :ref:`quick-start guides <quickstart-toc>` or the :sampleconf:`/` directory might also be helpful.
Full example configs are available at :ref:`quick-start guides <quickstart-toc>` and :sampleconf:`/`.
However, copy-pasting examples is no substitute for reading documentation! However, copy-pasting examples is no substitute for reading documentation!
Config File Structure Config File Structure
@@ -27,8 +26,9 @@ Config File Structure
type: push type: push
- ... - ...
A zrepl configuration file is divided in to two main sections: ``global`` and ``jobs``. zrepl is configured using a single YAML configuration file with two main sections: ``global`` and ``jobs``.
``global`` has sensible defaults. It is covered in :ref:`logging <logging>`, :ref:`monitoring <monitoring>` \& :ref:`miscellaneous <miscellaneous>`. The ``global`` section is filled with sensible defaults and is covered later in this chapter.
The ``jobs`` section is a list of jobs which we are going to explain now.
.. _job-overview: .. _job-overview:
@@ -42,7 +42,8 @@ Jobs are identified by their ``name``, both in log files and the ``zrepl status`
.. NOTE:: .. NOTE::
The job name is persisted in several places on disk and thus :issue:`cannot be changed easily<327>`. The job name is persisted in several places on disk and thus :issue:`cannot be changed easily<327>`.
Replication always happens between a pair of jobs: one **active side** and one **passive side**.
Replication always happens between a pair of jobs: one is the **active side**, and one the **passive side**.
The active side connects to the passive side using a :ref:`transport <transport>` and starts executing the replication logic. The active side connects to the passive side using a :ref:`transport <transport>` and starts executing the replication logic.
The passive side responds to requests from the active side after checking its permissions. The passive side responds to requests from the active side after checking its permissions.
@@ -71,29 +72,30 @@ How the Active Side Works
The active side (:ref:`push <job-push>` and :ref:`pull <job-pull>` job) executes the replication and pruning logic: The active side (:ref:`push <job-push>` and :ref:`pull <job-pull>` job) executes the replication and pruning logic:
1. Wakeup after snapshotting (``push`` job) or pull interval ticker (``pull`` job). * Wakeup because of finished snapshotting (``push`` job) or pull interval ticker (``pull`` job).
2. Connect to the passive side and instantiate an RPC client. * Connect to the corresponding passive side using a :ref:`transport <transport>` and instantiate an RPC client.
3. Replicate data from the sender to the receiver. * Replicate data from the sending to the receiving side (see below).
4. Prune on sender & receiver. * Prune on sender & receiver.
.. TIP:: .. TIP::
The progress of the active side can be watched live using ``zrepl status``. The progress of the active side can be watched live using the ``zrepl status`` subcommand.
.. _overview-passive-side--client-identity: .. _overview-passive-side--client-identity:
How the Passive Side Works How the Passive Side Works
-------------------------- --------------------------
The passive side (:ref:`sink <job-sink>` and :ref:`source <job-source>`) waits for connections from the active side, The passive side (:ref:`sink <job-sink>` and :ref:`source <job-source>`) waits for connections from the corresponding active side,
on the :ref:`transport <transport>` specified with ``serve`` in the job configuration. using the transport listener type specified in the ``serve`` field of the job configuration.
The respective transport then perfoms authentication & authorization, resulting in a stable *client identity*. When a client connects, the transport listener performS listener-specific access control (cert validation, IP ACLs, etc)
The passive side job uses this *client identity* as follows: and determines the *client identity*.
The passive side job then uses this client identity as follows:
* In ``sink`` jobs, to map requests from different *client identities* to their respective sub-filesystem tree ``root_fs/${client_identity}``. * The ``sink`` job maps requests from different client identities to their respective sub-filesystem tree ``root_fs/${client_identity}``.
* *In the future, ``source`` might embed the client identity in :ref:`zrepl's ZFS abstraction names <zrepl-zfs-abstractions>`, to support multi-host replication.* * The ``source`` might, in the future, embed the client identity in :ref:`zrepl's ZFS abstraction names <zrepl-zfs-abstractions>` in order to support multi-host replication.
.. TIP:: .. TIP::
The use of the client identity in the ``sink`` job implies that it must be usable as a ZFS ZFS filesystem name component. The implementation of the ``sink`` job requires that the connecting client identities be a valid ZFS filesystem name components.
.. _overview-how-replication-works: .. _overview-how-replication-works:
@@ -104,7 +106,7 @@ One of the major design goals of the replication module is to avoid any duplicat
As such, the code works on abstract senders and receiver **endpoints**, where typically one will be implemented by a local program object and the other is an RPC client instance. As such, the code works on abstract senders and receiver **endpoints**, where typically one will be implemented by a local program object and the other is an RPC client instance.
Regardless of push- or pull-style setup, the logic executes on the active side, i.e. in the ``push`` or ``pull`` job. Regardless of push- or pull-style setup, the logic executes on the active side, i.e. in the ``push`` or ``pull`` job.
The following high-level steps take place during replication and can be monitored using ``zrepl status``: The following high-level steps take place during replication and can be monitored using the ``zrepl status`` subcommand:
* Plan the replication: * Plan the replication:
+1 -1
View File
@@ -227,7 +227,7 @@ and property replication is enabled, the receiver must :ref:`inherit the followi
.. _job-recv-options--placeholder: .. _job-recv-options--placeholder:
Placeholders Placeholders
~~~~~~~~~~~~ ------------
:: ::
+28 -113
View File
@@ -5,138 +5,53 @@
Taking Snaphots Taking Snaphots
=============== ===============
You can configure zrepl to take snapshots of the filesystems in the ``filesystems`` field specified in ``push``, ``source`` and ``snap`` jobs. The ``push``, ``source`` and ``snap`` jobs can automatically take periodic snapshots of the filesystems matched by the ``filesystems`` filter field.
The snapshot names are composed of a user-defined prefix followed by a UTC date formatted like ``20060102_150405_000``.
We use UTC because it will avoid name conflicts when switching time zones or between summer and winter time.
The following snapshotting types are supported: When a job is started, the snapshotter attempts to get the snapshotting rhythms of the matched ``filesystems`` in sync because snapshotting all filesystems at the same time results in a more consistent backup.
To find that sync point, the most recent snapshot, made by the snapshotter, in any of the matched ``filesystems`` is used.
.. list-table::
:widths: 20 70
:header-rows: 1
* - ``snapshotting.type``
- Comment
* - ``periodic``
- Ensure that snapshots are taken at a particular interval.
* - ``cron``
- Use cron spec to take snapshots at particular points in time.
* - ``manual``
- zrepl does not take any snapshots by itself.
The ``periodic`` and ``cron`` snapshotting types share some common options and behavior:
* **Naming:** The snapshot names are composed of a user-defined ``prefix`` followed by a UTC date formatted like ``20060102_150405_000``.
We use UTC because it will avoid name conflicts when switching time zones or between summer and winter time.
* **Hooks:** You can configure hooks to run before or after zrepl takes the snapshots. See :ref:`below <job-snapshotting-hooks>` for details.
* **Push replication:** After creating all snapshots, the snapshotter will wake up the replication part of the job, if it's a ``push`` job.
Note that snapshotting is decoupled from replication, i.e., if it is down or takes too long, snapshots will still be taken.
Note further that other jobs are not woken up by snapshotting.
.. NOTE::
There is **no concept of ownership** of the snapshots that are created by ``periodic`` or ``cron``.
Thus, there is no distinction between zrepl-created snapshots and user-created snapshots during replication or pruning.
In particular, pruning will take all snapshots into consideration by default.
To constrain pruning to just zrepl-created snapshots:
1. Assign a unique `prefix` to the snapshotter and
2. Use the ``regex`` functionality of the various pruning ``keep`` rules to just consider snapshots with that prefix.
There is currently no way to constrain replication to just zrepl-created snapshots.
Follow and comment at :issue:`403` if you need this functionality.
.. NOTE::
The ``zrepl signal wakeup JOB`` subcommand does not trigger snapshotting.
``periodic`` Snapshotting
-------------------------
::
jobs:
- ...
filesystems: { ... }
snapshotting:
type: periodic
prefix: zrepl_
interval: 10m
# Timestamp format that is used as snapshot suffix.
# Can be any of "dense" (default), "human", "iso-8601", "unix-seconds" or a custom Go time format (see https://go.dev/src/time/format.go)
timestamp_format: dense
hooks: ...
pruning: ...
The ``periodic`` snapshotter ensures that snapshots are taken in the specified ``interval``.
If you use zrepl for backup, this translates into your recovery point objective (RPO).
To meet your RPO, you still need to monitor that replication, which happens asynchronously to snapshotting, actually works.
It is desirable to get all ``filesystems`` snapshotted simultaneously because it results in a more consistent backup.
To accomplish this while still maintaining the ``interval``, the ``periodic`` snapshotter attempts to get the snapshotting rhythms in sync.
To find that sync point, the most recent snapshot, created by the snapshotter, in any of the matched ``filesystems`` is used.
A filesystem that does not have snapshots by the snapshotter has lower priority than filesystem that do, and thus might not be snapshotted (and replicated) until it is snapshotted at the next sync point. A filesystem that does not have snapshots by the snapshotter has lower priority than filesystem that do, and thus might not be snapshotted (and replicated) until it is snapshotted at the next sync point.
The snapshotter uses the ``prefix`` to identify which snapshots it created.
.. _job-snapshotting--cron: For ``push`` jobs, replication is automatically triggered after all filesystems have been snapshotted.
Note that the ``zrepl signal wakeup JOB`` subcommand does not trigger snapshotting.
``cron`` Snapshotting
---------------------
:: ::
jobs: jobs:
- type: snap - type: push
filesystems: { ... } filesystems: {
snapshotting: "<": true,
type: cron "tmp": false
prefix: zrepl_ }
# (second, optional) minute hour day-of-month month day-of-week snapshotting:
# This example takes snapshots daily at 3:00. type: periodic
cron: "0 3 * * *" prefix: zrepl_
# Timestamp format that is used as snapshot suffix. interval: 10m
# Can be any of "dense" (default), "human", "iso-8601", "unix-seconds" or a custom Go time format (see https://go.dev/src/time/format.go) hooks: ...
timestamp_format: dense ...
pruning: ...
In ``cron`` mode, the snapshotter takes snaphots at fixed points in time. There is also a ``manual`` snapshotting type, which covers the following use cases:
See https://en.wikipedia.org/wiki/Cron for details on the syntax.
zrepl uses the ``the github.com/robfig/cron/v3`` Go package for parsing.
An optional field for "seconds" is supported to take snapshots at sub-minute frequencies.
.. _job-snapshotting-timestamp_format: * Existing infrastructure for automatic snapshots: you only want to use this zrepl job for replication.
* Handling snapshotting through a separate ``snap`` job.
Timestamp Format Note that you will have to trigger replication manually using the ``zrepl signal wakeup JOB`` subcommand in that case.
~~~~~~~~~~~~~~~~
The ``cron`` and ``periodic`` snapshotter support configuring a custom timestamp format that is used as suffix for the snapshot name.
It can be used by setting ``timestamp_format`` to any of the following values:
* ``dense`` (default) looks like ``20060102_150405_000``
* ``human`` looks like ``2006-01-02_15:04:05``
* ``iso-8601`` looks like ``2006-01-02T15:04:05.000Z``
* ``unix-seconds`` looks like ``1136214245``
* Any custom Go time format accepted by `time.Time#Format <https://go.dev/src/time/format.go>`_.
``manual`` Snapshotting
-----------------------
:: ::
jobs: jobs:
- type: push - type: push
filesystems: {
"<": true,
"tmp": false
}
snapshotting: snapshotting:
type: manual type: manual
... ...
In ``manual`` mode, zrepl does not take snapshots by itself.
Manual snapshotting is most useful if you have existing infrastructure for snapshot management.
Or, if you want to decouple snapshot management from replication using a zrepl ``snap`` job.
See :ref:`this quickstart guide <quickstart-backup-to-external-disk>` for an example.
To trigger replication after taking snapshots, use the ``zrepl signal wakeup JOB`` command.
.. _job-snapshotting-hooks: .. _job-snapshotting-hooks:
Pre- and Post-Snapshot Hooks Pre- and Post-Snapshot Hooks
+29 -1
View File
@@ -105,10 +105,15 @@ func (s *Sender) ListFilesystems(ctx context.Context, r *pdu.ListFilesystemReq)
} }
rfss := make([]*pdu.Filesystem, len(fss)) rfss := make([]*pdu.Filesystem, len(fss))
for i := range fss { for i := range fss {
encEnabled, err := zfs.ZFSGetEncryptionEnabled(ctx, fss[i].ToString())
if err != nil {
return nil, errors.Wrap(err, "cannot get filesystem encryption status")
}
rfss[i] = &pdu.Filesystem{ rfss[i] = &pdu.Filesystem{
Path: fss[i].ToString(), Path: fss[i].ToString(),
// ResumeToken does not make sense from Sender // ResumeToken does not make sense from Sender
IsPlaceholder: false, // sender FSs are never placeholders IsPlaceholder: false, // sender FSs are never placeholders
IsEncrypted: encEnabled,
} }
} }
res := &pdu.ListFilesystemRes{Filesystems: rfss} res := &pdu.ListFilesystemRes{Filesystems: rfss}
@@ -160,6 +165,23 @@ func (s *Sender) sendMakeArgs(ctx context.Context, r *pdu.SendReq) (sendArgs zfs
if err != nil { if err != nil {
return sendArgs, err return sendArgs, err
} }
switch r.Encrypted {
case pdu.Tri_DontCare:
// use s.encrypt setting
// ok, fallthrough outer
case pdu.Tri_False:
if s.config.Encrypt.B {
return sendArgs, errors.New("only encrypted sends allowed (send -w + encryption!= off), but unencrypted send requested")
}
// fallthrough outer
case pdu.Tri_True:
if !s.config.Encrypt.B {
return sendArgs, errors.New("only unencrypted sends allowed, but encrypted send requested")
}
// fallthrough outer
default:
return sendArgs, fmt.Errorf("unknown pdu.Tri variant %q", r.Encrypted)
}
sendArgsUnvalidated := zfs.ZFSSendArgsUnvalidated{ sendArgsUnvalidated := zfs.ZFSSendArgsUnvalidated{
FS: r.Filesystem, FS: r.Filesystem,
@@ -439,7 +461,7 @@ type FSMap interface { // FIXME unused
} }
// NOTE: when adding members to this struct, remember // NOTE: when adding members to this struct, remember
// to add them to `ReceiverConfig.copyIn()` // to add them to `ReceiverConfig.copyIn()`
type ReceiverConfig struct { type ReceiverConfig struct {
JobID JobID JobID JobID
@@ -636,6 +658,11 @@ func (s *Receiver) ListFilesystems(ctx context.Context, req *pdu.ListFilesystemR
l.WithError(err).Error("cannot get receive resume token") l.WithError(err).Error("cannot get receive resume token")
return nil, err return nil, err
} }
encEnabled, err := zfs.ZFSGetEncryptionEnabled(ctx, a.ToString())
if err != nil {
l.WithError(err).Error("cannot get encryption enabled status")
return nil, err
}
l.WithField("receive_resume_token", token).Debug("receive resume token") l.WithField("receive_resume_token", token).Debug("receive resume token")
a.TrimPrefix(root) a.TrimPrefix(root)
@@ -644,6 +671,7 @@ func (s *Receiver) ListFilesystems(ctx context.Context, req *pdu.ListFilesystemR
Path: a.ToString(), Path: a.ToString(),
IsPlaceholder: ph.IsPlaceholder, IsPlaceholder: ph.IsPlaceholder,
ResumeToken: token, ResumeToken: token,
IsEncrypted: encEnabled,
} }
fss = append(fss, fs) fss = append(fss, fs)
} }
+8 -14
View File
@@ -168,7 +168,7 @@ func (s AbstractionTypeSet) String() string {
for i := range s { for i := range s {
sts = append(sts, string(i)) sts = append(sts, string(i))
} }
sort.Strings(sts) sts = sort.StringSlice(sts)
return strings.Join(sts, ",") return strings.Join(sts, ",")
} }
@@ -516,12 +516,10 @@ func (e ListAbstractionsErrors) Error() string {
} }
func ListAbstractions(ctx context.Context, query ListZFSHoldsAndBookmarksQuery) (out []Abstraction, outErrs []ListAbstractionsError, err error) { func ListAbstractions(ctx context.Context, query ListZFSHoldsAndBookmarksQuery) (out []Abstraction, outErrs []ListAbstractionsError, err error) {
outChan, outErrsChan, drainDone, err := ListAbstractionsStreamed(ctx, query) outChan, outErrsChan, err := ListAbstractionsStreamed(ctx, query)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
defer drainDone()
var wg sync.WaitGroup var wg sync.WaitGroup
wg.Add(1) wg.Add(1)
go func() { go func() {
@@ -542,20 +540,19 @@ func ListAbstractions(ctx context.Context, query ListZFSHoldsAndBookmarksQuery)
} }
// if err != nil, the returned channels are both nil // if err != nil, the returned channels are both nil
// if err == nil, both channels must be fully drained by the caller to avoid leaking goroutines. // if err == nil, both channels must be fully drained by the caller to avoid leaking goroutines
// After draining is done, the caller must call the returned drainDone func. func ListAbstractionsStreamed(ctx context.Context, query ListZFSHoldsAndBookmarksQuery) (<-chan Abstraction, <-chan ListAbstractionsError, error) {
func ListAbstractionsStreamed(ctx context.Context, query ListZFSHoldsAndBookmarksQuery) (_ <-chan Abstraction, _ <-chan ListAbstractionsError, drainDone func(), _ error) {
// impl note: structure the query processing in such a way that // impl note: structure the query processing in such a way that
// a minimum amount of zfs shell-outs needs to be done // a minimum amount of zfs shell-outs needs to be done
if err := query.Validate(); err != nil { if err := query.Validate(); err != nil {
return nil, nil, nil, errors.Wrap(err, "validate query") return nil, nil, errors.Wrap(err, "validate query")
} }
fss, err := query.FS.Filesystems(ctx) fss, err := query.FS.Filesystems(ctx)
if err != nil { if err != nil {
return nil, nil, nil, errors.Wrap(err, "list filesystems") return nil, nil, errors.Wrap(err, "list filesystems")
} }
outErrs := make(chan ListAbstractionsError) outErrs := make(chan ListAbstractionsError)
@@ -577,6 +574,7 @@ func ListAbstractionsStreamed(ctx context.Context, query ListZFSHoldsAndBookmark
sem := semaphore.New(int64(query.Concurrency)) sem := semaphore.New(int64(query.Concurrency))
ctx, endTask := trace.WithTask(ctx, "list-abstractions-streamed-producer") ctx, endTask := trace.WithTask(ctx, "list-abstractions-streamed-producer")
go func() { go func() {
defer endTask()
defer close(out) defer close(out)
defer close(outErrs) defer close(outErrs)
@@ -598,11 +596,7 @@ func ListAbstractionsStreamed(ctx context.Context, query ListZFSHoldsAndBookmark
} }
}() }()
drainDone = func() { return out, outErrs, nil
endTask()
}
return out, outErrs, drainDone, nil
} }
func listAbstractionsImplFS(ctx context.Context, fs string, query *ListZFSHoldsAndBookmarksQuery, emitCandidate putListAbstraction, errCb putListAbstractionErr) { func listAbstractionsImplFS(ctx context.Context, fs string, query *ListZFSHoldsAndBookmarksQuery, emitCandidate putListAbstraction, errCb putListAbstractionErr) {
@@ -23,7 +23,7 @@ func replicationCursorBookmarkNameImpl(fs string, guid uint64, jobid string) (st
var ErrV1ReplicationCursor = fmt.Errorf("bookmark name is a v1-replication cursor") var ErrV1ReplicationCursor = fmt.Errorf("bookmark name is a v1-replication cursor")
// err != nil always means that the bookmark is not a valid replication bookmark //err != nil always means that the bookmark is not a valid replication bookmark
// //
// Returns ErrV1ReplicationCursor as error if the bookmark is a v1 replication cursor // Returns ErrV1ReplicationCursor as error if the bookmark is a v1 replication cursor
func ParseReplicationCursorBookmarkName(fullname string) (uint64, JobID, error) { func ParseReplicationCursorBookmarkName(fullname string) (uint64, JobID, error) {
-1
View File
@@ -29,7 +29,6 @@ require (
github.com/problame/go-netssh v0.0.0-20200601114649-26439f9f0dc5 github.com/problame/go-netssh v0.0.0-20200601114649-26439f9f0dc5
github.com/prometheus/client_golang v1.2.1 github.com/prometheus/client_golang v1.2.1
github.com/prometheus/common v0.7.0 github.com/prometheus/common v0.7.0
github.com/robfig/cron/v3 v3.0.1
github.com/sergi/go-diff v1.0.1-0.20180205163309-da645544ed44 // indirect; go1.12 thinks it needs this github.com/sergi/go-diff v1.0.1-0.20180205163309-da645544ed44 // indirect; go1.12 thinks it needs this
github.com/spf13/cobra v0.0.2 github.com/spf13/cobra v0.0.2
github.com/spf13/pflag v1.0.5 github.com/spf13/pflag v1.0.5
-2
View File
@@ -161,8 +161,6 @@ github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDa
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/sergi/go-diff v1.0.1-0.20180205163309-da645544ed44 h1:tB9NOR21++IjLyVx3/PCPhWMwqGNCMQEH96A6dMZ/gc= github.com/sergi/go-diff v1.0.1-0.20180205163309-da645544ed44 h1:tB9NOR21++IjLyVx3/PCPhWMwqGNCMQEH96A6dMZ/gc=
github.com/sergi/go-diff v1.0.1-0.20180205163309-da645544ed44/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sergi/go-diff v1.0.1-0.20180205163309-da645544ed44/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
+11 -56
View File
@@ -1,7 +1,6 @@
package main package main
import ( import (
"container/list"
"context" "context"
"flag" "flag"
"fmt" "fmt"
@@ -54,40 +53,6 @@ type HarnessArgs struct {
Run string Run string
} }
type invocation struct {
runFunc tests.Case
idstring string
result *testCaseResult
children map[string]*invocation
}
func newInvocation(runFunc tests.Case, id string) *invocation {
return &invocation{
runFunc: runFunc,
idstring: id,
children: make(map[string]*invocation),
}
}
func (i *invocation) String() string {
idsuffix := ""
if i.idstring != "" {
idsuffix = fmt.Sprintf(": %s", i.idstring)
}
return fmt.Sprintf("%s%s", i.runFunc.String(), idsuffix)
}
func (i *invocation) RegisterChild(c *invocation) error {
if c.idstring == "" {
return fmt.Errorf("child must have id string")
}
if oc := i.children[c.idstring]; oc != nil {
return fmt.Errorf("idstring %q is already taken by %s", c.idstring, oc)
}
i.children[c.idstring] = c
return nil
}
func HarnessRun(args HarnessArgs) error { func HarnessRun(args HarnessArgs) error {
runRE := regexp.MustCompile(args.Run) runRE := regexp.MustCompile(args.Run)
@@ -114,19 +79,21 @@ func HarnessRun(args HarnessArgs) error {
ctx = logging.WithLoggers(ctx, logging.SubsystemLoggersWithUniversalLogger(logger)) ctx = logging.WithLoggers(ctx, logging.SubsystemLoggersWithUniversalLogger(logger))
ex := platformtest.NewEx(logger) ex := platformtest.NewEx(logger)
testQueue := list.New() type invocation struct {
runFunc tests.Case
result *testCaseResult
}
invocations := make([]*invocation, 0, len(tests.Cases))
for _, c := range tests.Cases { for _, c := range tests.Cases {
if runRE.MatchString(c.String()) { if runRE.MatchString(c.String()) {
testQueue.PushBack(newInvocation(c, "")) invocations = append(invocations, &invocation{runFunc: c})
} }
} }
completedTests := list.New() for _, inv := range invocations {
for testQueue.Len() > 0 { bold.Printf("BEGIN TEST CASE %s\n", inv.runFunc.String())
inv := testQueue.Remove(testQueue.Front()).(*invocation)
bold.Printf("BEGIN TEST CASE %s\n", inv)
pool, err := platformtest.CreateOrReplaceZpool(ctx, ex, args.CreateArgs) pool, err := platformtest.CreateOrReplaceZpool(ctx, ex, args.CreateArgs)
if err != nil { if err != nil {
@@ -136,15 +103,6 @@ func HarnessRun(args HarnessArgs) error {
ctx := &platformtest.Context{ ctx := &platformtest.Context{
Context: ctx, Context: ctx,
RootDataset: filepath.Join(pool.Name(), "rootds"), RootDataset: filepath.Join(pool.Name(), "rootds"),
QueueSubtest: func(id string, stf func(*platformtest.Context)) {
stinv := newInvocation(stf, id)
err := inv.RegisterChild(stinv)
if err != nil {
panic(err)
}
bold.Printf(" QUEUING SUBTEST %q\n", id)
testQueue.PushFront(stinv)
},
} }
res := runTestCase(ctx, ex, inv.runFunc) res := runTestCase(ctx, ex, inv.runFunc)
@@ -162,8 +120,6 @@ func HarnessRun(args HarnessArgs) error {
panic(fmt.Sprintf("error destroying test pool: %s", err)) panic(fmt.Sprintf("error destroying test pool: %s", err))
} }
completedTests.PushBack(inv)
if res.failed { if res.failed {
boldRed.Printf("TEST FAILED\n") boldRed.Printf("TEST FAILED\n")
} else if res.skipped { } else if res.skipped {
@@ -180,8 +136,7 @@ func HarnessRun(args HarnessArgs) error {
var summary struct { var summary struct {
succ, fail, skip []*invocation succ, fail, skip []*invocation
} }
for completedTests.Len() > 0 { for _, inv := range invocations {
inv := completedTests.Remove(completedTests.Front()).(*invocation)
var bucket *[]*invocation var bucket *[]*invocation
if inv.result.failed { if inv.result.failed {
bucket = &summary.fail bucket = &summary.fail
@@ -202,7 +157,7 @@ func HarnessRun(args HarnessArgs) error {
} }
fmt.Printf("\n") fmt.Printf("\n")
for _, inv := range bucket { for _, inv := range bucket {
fmt.Printf(" %s\n", inv) fmt.Printf(" %s\n", inv.runFunc.String())
} }
} }
printBucket("PASSING TESTS", boldGreen, summary.succ) printBucket("PASSING TESTS", boldGreen, summary.succ)
-7
View File
@@ -11,13 +11,6 @@ import (
type Context struct { type Context struct {
context.Context context.Context
RootDataset string RootDataset string
// Use this callback from a top-level test case to queue the
// execution of sub-tests after this test case is complete.
//
// Note that the testing harness executes the subtest
// _after_ the current top-level test. Hence, the subtest
// cannot use any ZFS state of the top-level test.
QueueSubtest func(id string, stf func(*Context))
} }
var FailNowSentinel = fmt.Errorf("platformtest: FailNow called on context") var FailNowSentinel = fmt.Errorf("platformtest: FailNow called on context")
-1
View File
@@ -36,7 +36,6 @@ var Cases = []Case{BatchDestroy,
ReplicationStepCompletedLostBehavior__GuaranteeResumability, ReplicationStepCompletedLostBehavior__GuaranteeResumability,
ResumableRecvAndTokenHandling, ResumableRecvAndTokenHandling,
ResumeTokenParsing, ResumeTokenParsing,
SendArgsValidationEE_EncryptionAndRaw,
SendArgsValidationEncryptedSendOfUnencryptedDatasetForbidden__EncryptionSupported_false, SendArgsValidationEncryptedSendOfUnencryptedDatasetForbidden__EncryptionSupported_false,
SendArgsValidationEncryptedSendOfUnencryptedDatasetForbidden__EncryptionSupported_true, SendArgsValidationEncryptedSendOfUnencryptedDatasetForbidden__EncryptionSupported_true,
SendArgsValidationResumeTokenDifferentFilesystemForbidden, SendArgsValidationResumeTokenDifferentFilesystemForbidden,
+12 -17
View File
@@ -35,17 +35,16 @@ import (
// of a new sender and receiver instance and one blocking invocation // of a new sender and receiver instance and one blocking invocation
// of the replication engine without encryption // of the replication engine without encryption
type replicationInvocation struct { type replicationInvocation struct {
sjid, rjid endpoint.JobID sjid, rjid endpoint.JobID
sfs string sfs string
sfilter *filters.DatasetMapFilter sfilter *filters.DatasetMapFilter
rfsRoot string rfsRoot string
interceptSender func(e *endpoint.Sender) logic.Sender interceptSender func(e *endpoint.Sender) logic.Sender
interceptReceiver func(e *endpoint.Receiver) logic.Receiver interceptReceiver func(e *endpoint.Receiver) logic.Receiver
guarantee *pdu.ReplicationConfigProtection guarantee *pdu.ReplicationConfigProtection
senderConfigHook func(*endpoint.SenderConfig) senderConfigHook func(*endpoint.SenderConfig)
receiverConfigHook func(*endpoint.ReceiverConfig) receiverConfigHook func(*endpoint.ReceiverConfig)
plannerPolicyHook func(*logic.PlannerPolicy) plannerPolicyHook func(*logic.PlannerPolicy)
skipSendArgsValidation bool
} }
func (i replicationInvocation) Do(ctx *platformtest.Context) *report.Report { func (i replicationInvocation) Do(ctx *platformtest.Context) *report.Report {
@@ -93,6 +92,7 @@ func (i replicationInvocation) Do(ctx *platformtest.Context) *report.Report {
sender := i.interceptSender(endpoint.NewSender(senderConfig)) sender := i.interceptSender(endpoint.NewSender(senderConfig))
receiver := i.interceptReceiver(endpoint.NewReceiver(receiverConfig)) receiver := i.interceptReceiver(endpoint.NewReceiver(receiverConfig))
plannerPolicy := logic.PlannerPolicy{ plannerPolicy := logic.PlannerPolicy{
EncryptedSend: logic.TriFromBool(false),
ReplicationConfig: &pdu.ReplicationConfig{ ReplicationConfig: &pdu.ReplicationConfig{
Protection: i.guarantee, Protection: i.guarantee,
}, },
@@ -105,13 +105,8 @@ func (i replicationInvocation) Do(ctx *platformtest.Context) *report.Report {
i.plannerPolicyHook(&plannerPolicy) i.plannerPolicyHook(&plannerPolicy)
} }
var doCtx context.Context = ctx
if i.skipSendArgsValidation {
doCtx = zfs.ZFSSendArgsSkipValidation(ctx)
}
report, wait := replication.Do( report, wait := replication.Do(
doCtx, ctx,
driver.Config{ driver.Config{
MaxAttempts: 1, MaxAttempts: 1,
StepQueueConcurrency: 1, StepQueueConcurrency: 1,
-228
View File
@@ -2,16 +2,10 @@ package tests
import ( import (
"fmt" "fmt"
"path"
"github.com/kr/pretty"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/zrepl/zrepl/endpoint"
"github.com/zrepl/zrepl/platformtest" "github.com/zrepl/zrepl/platformtest"
"github.com/zrepl/zrepl/replication/logic"
"github.com/zrepl/zrepl/replication/logic/pdu"
"github.com/zrepl/zrepl/replication/report"
"github.com/zrepl/zrepl/util/nodefault" "github.com/zrepl/zrepl/util/nodefault"
"github.com/zrepl/zrepl/zfs" "github.com/zrepl/zrepl/zfs"
) )
@@ -214,225 +208,3 @@ func SendArgsValidationResumeTokenDifferentFilesystemForbidden(ctx *platformtest
require.True(ctx, ok) require.True(ctx, ok)
require.Equal(ctx, mismatchError.What, zfs.ZFSSendArgsResumeTokenMismatchFilesystem) require.Equal(ctx, mismatchError.What, zfs.ZFSSendArgsResumeTokenMismatchFilesystem)
} }
type sendArgsValidationEndToEndTestOutcome string
const (
ValidationAccepts sendArgsValidationEndToEndTestOutcome = "accept"
ValidationRejects sendArgsValidationEndToEndTestOutcome = "rejects"
)
type sendArgsValidationEndToEndTest struct {
encryptedSenderFilesystem bool
senderConfigHook func(config *endpoint.SenderConfig)
expectedOutcome sendArgsValidationEndToEndTestOutcome
outcomeRejectsInspectError func(require.TestingT, *report.FilesystemReport, bool)
inspectReceiverFSAfterSuccessfulCycle func(rfs string)
}
func implSendArgsValidationEndToEndTest(ctx *platformtest.Context, setup sendArgsValidationEndToEndTest) {
senderEncrypted := ""
if setup.encryptedSenderFilesystem {
senderEncrypted = "encrypted"
}
platformtest.Run(ctx, platformtest.PanicErr, ctx.RootDataset, fmt.Sprintf(`
CREATEROOT
+ "sender" %s
+ "receiver"
R zfs create -p "${ROOTDS}/receiver/${ROOTDS}"
`, senderEncrypted))
sjid := endpoint.MustMakeJobID("sender-job")
rjid := endpoint.MustMakeJobID("receiver-job")
sfs := ctx.RootDataset + "/sender"
rfsRoot := ctx.RootDataset + "/receiver"
sfsmp, err := zfs.ZFSGetMountpoint(ctx, sfs)
require.NoError(ctx, err)
require.True(ctx, sfsmp.Mounted)
// Two cycles. one initial replication, one incremental replication.
// Within each cycle: interrupt replication at least once.
// This exercises both the no-resume-token-present and the resume-token-present validation code paths.
initial_then_incremental:
for i := 0; i < 2; i++ {
writeDummyData(path.Join(sfsmp.Mountpoint, "dummy.data"), 2*(1<<20))
mustSnapshot(ctx, fmt.Sprintf("%s@%d", sfs, i))
rep := replicationInvocation{
sjid: sjid,
rjid: rjid,
sfs: sfs,
rfsRoot: rfsRoot,
senderConfigHook: setup.senderConfigHook,
interceptSender: func(e *endpoint.Sender) logic.Sender {
return &PartialSender{Sender: e, failAfterByteCount: 1 << 20}
},
guarantee: pdu.ReplicationConfigProtectionWithKind(pdu.ReplicationGuaranteeKind_GuaranteeResumability),
skipSendArgsValidation: false,
}
rfs := rep.ReceiveSideFilesystem()
// PartialSender interrupts after 1MiB, and we wrote 2 MiB of data
// => Give it 3 attempts to replicate. after that, we should have a stable outcome
var lastReport *report.Report
lastResumeToken := ""
interrupt_current_step:
for j := 0; j < 3; j++ {
lastReport = rep.Do(ctx)
ctx.Logf("\nreport=%s", pretty.Sprint(lastReport))
require.Len(ctx, lastReport.Attempts, 1)
require.Len(ctx, lastReport.Attempts[0].Filesystems, 1)
lastReportFS := lastReport.Attempts[0].Filesystems[0]
var rfsExists bool
rfsResumeToken, err := zfs.ZFSGetReceiveResumeTokenOrEmptyStringIfNotSupported(ctx, mustDatasetPath(rfs))
if err != nil {
_, ok := err.(*zfs.DatasetDoesNotExist) // no shadow
require.True(ctx, ok, "no other errors expected")
rfsExists = false
rfsResumeToken = ""
} else {
rfsExists = true
}
if setup.expectedOutcome == ValidationRejects {
// When expecting rejection, it should manifest immediately, before sending anything.
// This is tested in the j=0 iteration (for both initial and incremental repl (i=0, i=1)).
// But we also want to assert correct behavior in case zrepl observes resume tokens.
// Specifically, cases where the send parameters encoded in the token conflict with the
// configured encryption policy.
// Hence, for scenarios that are expected to reject, after we validated that they reject
// for the non-resuming case (j==0), fabricate a resuming scenario by temporarily disabling
// send args validation. After fabricating the scenario, proceed into j==1 to exercise
// the resume token validation.
if j == 0 {
if i == 0 {
require.False(ctx, rfsExists, "the sender should not have sent anything")
} else {
// we fabricate a scenario where rfsExists below, hence can't assert non-existence anymore
}
ctx.Logf("skipping send args validation to test resuming case")
rep.skipSendArgsValidation = true
setupResumeReport := rep.Do(ctx)
ctx.Logf("setupResumeReport=%s", pretty.Sprint(setupResumeReport))
rep.skipSendArgsValidation = false
rt, err := zfs.ZFSGetReceiveResumeTokenOrEmptyStringIfNotSupported(ctx, mustDatasetPath(rfs))
require.NoError(ctx, err)
require.NotEmpty(ctx, rt, "we disabled send args validation, so the .Do above should have resulted in a resume token on rfs")
lastResumeToken = rt
continue interrupt_current_step // next iteration will test resume case with send args validation enabled
} else { // j > 0
require.Equal(ctx, lastResumeToken, rfsResumeToken, "we expect policy to refuse replication, no progress must happen")
_, err := zfs.ZFSGetFilesystemVersion(ctx, fmt.Sprintf("%s@%d", rfs, i))
_, ok := err.(*zfs.DatasetDoesNotExist)
require.True(ctx, ok, "another check that no progress is happening")
}
setup.outcomeRejectsInspectError(ctx, lastReportFS, j == 0)
// XXX: check rejection cases for incremental replication as well
break initial_then_incremental
} else {
require.Equal(ctx, ValidationAccepts, setup.expectedOutcome)
_, err := zfs.ZFSGetFilesystemVersion(ctx, fmt.Sprintf("%s@%d", rfs, i))
_, notExist := err.(*zfs.DatasetDoesNotExist)
if notExist {
require.NotEmpty(ctx, rfsResumeToken)
continue interrupt_current_step // next iteration will resume
} else {
require.NoError(ctx, err)
// version exists
// make sure all the filesystem versions we created so far were replicated by the replication loop
for j := 0; j <= i; j++ {
_ = fsversion(ctx, rfs, fmt.Sprintf("@%d", j))
}
setup.inspectReceiverFSAfterSuccessfulCycle(rfs)
continue initial_then_incremental
}
}
}
}
}
func SendArgsValidationEE_EncryptionAndRaw(ctx *platformtest.Context) {
type TC struct {
// create sender filesystem with encryption enabled yes/no
SFSEnc bool
SndEnc bool // send flag
SndRaw bool // send flag
RFSEnc bool
Outcome sendArgsValidationEndToEndTestOutcome
RejectErrorNoResume string
RejectErrorResume string
}
tcs := []TC{
// Sender FS is unencrypted
{SFSEnc: false, SndEnc: false, SndRaw: false, RFSEnc: false, Outcome: ValidationAccepts},
{SFSEnc: false, SndEnc: false, SndRaw: true, RFSEnc: false, Outcome: ValidationAccepts}, // allow unencrypted raw sends (#503)
{SFSEnc: false, SndEnc: true, SndRaw: false, RFSEnc: false, Outcome: ValidationRejects,
RejectErrorNoResume: `encrypted send mandated by policy, but filesystem .* is not encrypted`,
RejectErrorResume: `encrypted send mandated by policy, but filesystem .* is not encrypted`,
},
{SFSEnc: false, SndEnc: true, SndRaw: true, RFSEnc: false, Outcome: ValidationRejects,
RejectErrorNoResume: `encrypted send mandated by policy, but filesystem .* is not encrypted`,
RejectErrorResume: `encrypted send mandated by policy, but filesystem .* is not encrypted`,
},
// Sender FS is encrypted
{SFSEnc: true, SndEnc: false, SndRaw: false, RFSEnc: false, Outcome: ValidationAccepts}, // passes because keys are loaded, thus can send plain.
{SFSEnc: true, SndEnc: false, SndRaw: true, RFSEnc: false, Outcome: ValidationRejects,
RejectErrorNoResume: `policy mandates raw\+unencrypted sends, but filesystem .* is encrypted`,
RejectErrorResume: `resume token has rawok=true which would result in encrypted send, but policy mandates unencrypted sends only`,
},
{SFSEnc: true, SndEnc: true, SndRaw: false, RFSEnc: true, Outcome: ValidationAccepts},
{SFSEnc: true, SndEnc: true, SndRaw: true, RFSEnc: true, Outcome: ValidationAccepts},
}
for _, tc := range tcs {
tc := tc // closure would copy by ref otherwise
ctx.QueueSubtest(fmt.Sprintf("%#v", tc), func(ctx *platformtest.Context) {
implSendArgsValidationEndToEndTest(ctx, sendArgsValidationEndToEndTest{
encryptedSenderFilesystem: tc.SFSEnc,
senderConfigHook: func(c *endpoint.SenderConfig) {
c.Encrypt = &nodefault.Bool{B: tc.SndEnc}
c.SendRaw = tc.SndRaw
},
expectedOutcome: tc.Outcome,
outcomeRejectsInspectError: func(ctx require.TestingT, fr *report.FilesystemReport, isResume bool) {
// this callback is only called for ValidationRejects
// validation should be failing during dry send => planning stage
// XXX mock out ZFS to ensure we never call a zfs send that would send data
// if we're expecting validation to fail
require.Equal(ctx, report.FilesystemPlanningErrored, fr.State)
if isResume {
require.NotEmpty(ctx, tc.RejectErrorResume)
require.Regexp(ctx, tc.RejectErrorResume, fr.PlanError)
} else {
require.NotEmpty(ctx, tc.RejectErrorNoResume)
require.Regexp(ctx, tc.RejectErrorNoResume, fr.PlanError)
}
},
inspectReceiverFSAfterSuccessfulCycle: func(rfs string) {
enabled, err := zfs.ZFSGetEncryptionEnabled(ctx, rfs)
require.NoError(ctx, err)
require.Equal(ctx, tc.RFSEnc, enabled, "receiver filesystem encryption settings unexpected")
},
})
})
}
}
+1 -1
View File
@@ -797,7 +797,7 @@ func (a *attempt) errorReport() *errorReport {
r.byClass[class] = errs r.byClass[class] = errs
} }
for _, err := range r.flattened { for _, err := range r.flattened {
if neterr, ok := err.Err.(net.Error); ok && neterr.Timeout() { if neterr, ok := err.Err.(net.Error); ok && neterr.Temporary() {
putClass(err, errorClassTemporaryConnectivityRelated) putClass(err, errorClassTemporaryConnectivityRelated)
continue continue
} }
@@ -13,7 +13,7 @@ func init() {
} }
} }
//nolint:deadcode,unused //nolint[:deadcode,unused]
func debug(format string, args ...interface{}) { func debug(format string, args ...interface{}) {
if debugEnabled { if debugEnabled {
fmt.Fprintf(os.Stderr, "repl: driver: %s\n", fmt.Sprintf(format, args...)) fmt.Fprintf(os.Stderr, "repl: driver: %s\n", fmt.Sprintf(format, args...))
@@ -22,7 +22,7 @@ func debug(format string, args ...interface{}) {
type debugFunc func(format string, args ...interface{}) type debugFunc func(format string, args ...interface{})
//nolint:deadcode,unused //nolint[:deadcode,unused]
func debugPrefix(prefixFormat string, prefixFormatArgs ...interface{}) debugFunc { func debugPrefix(prefixFormat string, prefixFormatArgs ...interface{}) debugFunc {
prefix := fmt.Sprintf(prefixFormat, prefixFormatArgs...) prefix := fmt.Sprintf(prefixFormat, prefixFormatArgs...)
return func(format string, args ...interface{}) { return func(format string, args ...interface{}) {
-19
View File
@@ -88,27 +88,8 @@ func SortVersionListByCreateTXGThenBookmarkLTSnapshot(fsvslice []*FilesystemVers
return sorted return sorted
} }
func StripBookmarksFromVersionList(fsvslice []*FilesystemVersion) []*FilesystemVersion {
fslice := make([]*FilesystemVersion, 0, len(fsvslice))
for _, fv := range fsvslice {
if fv.Type != FilesystemVersion_Bookmark {
fslice = append(fslice, fv)
}
}
return fslice
}
func IncrementalPath(receiver, sender []*FilesystemVersion) (incPath []*FilesystemVersion, conflict error) { func IncrementalPath(receiver, sender []*FilesystemVersion) (incPath []*FilesystemVersion, conflict error) {
// Receive-side bookmarks can't be used as incremental-from,
// and don't cause recv to fail if there is a newer bookmark than incremetal-form on the receiver.
// So, simply mask them out.
// This will also hide them in the report, but it keeps the code in this function simple,
// and a user who complains about them missing in a conflict message will likely require
// more education about bookmarks than a slightly more accurate error message. They'll get
// that when they open an issue.
receiver = StripBookmarksFromVersionList(receiver)
receiver = SortVersionListByCreateTXGThenBookmarkLTSnapshot(receiver) receiver = SortVersionListByCreateTXGThenBookmarkLTSnapshot(receiver)
sender = SortVersionListByCreateTXGThenBookmarkLTSnapshot(sender) sender = SortVersionListByCreateTXGThenBookmarkLTSnapshot(sender)
-14
View File
@@ -152,18 +152,4 @@ func TestIncrementalPath_BookmarkSupport(t *testing.T) {
assert.Equal(t, l("@a,1", "@b,2"), path) assert.Equal(t, l("@a,1", "@b,2"), path)
}) })
// test that receive-side bookmarks younger than the most recent common ancestor do not cause a conflict
doTest(l("@a,1", "#b,2"), l("@a,1", "@c,3"), func(path []*FilesystemVersion, conflict error) {
assert.NoError(t, conflict)
require.Len(t, path, 2)
assert.Equal(t, l("@a,1")[0], path[0])
assert.Equal(t, l("@c,3")[0], path[1])
})
doTest(l("#a,1"), l("@a,1", "@b,2"), func(path []*FilesystemVersion, conflict error) {
assert.Nil(t, path)
ca, ok := conflict.(*ConflictNoCommonAncestor)
require.True(t, ok)
assert.Equal(t, l(), ca.SortedReceiverVersions, "See comment in IncrementalPath() on why we don't include the boomkmark here")
})
} }
+283 -209
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.25.0 // protoc-gen-go v1.25.0
// protoc v3.14.0 // protoc v3.12.4
// source: pdu.proto // source: pdu.proto
package pdu package pdu
@@ -25,6 +25,55 @@ const (
// of the legacy proto package is being used. // of the legacy proto package is being used.
const _ = proto.ProtoPackageIsVersion4 const _ = proto.ProtoPackageIsVersion4
type Tri int32
const (
Tri_DontCare Tri = 0
Tri_False Tri = 1
Tri_True Tri = 2
)
// Enum value maps for Tri.
var (
Tri_name = map[int32]string{
0: "DontCare",
1: "False",
2: "True",
}
Tri_value = map[string]int32{
"DontCare": 0,
"False": 1,
"True": 2,
}
)
func (x Tri) Enum() *Tri {
p := new(Tri)
*p = x
return p
}
func (x Tri) String() string {
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
}
func (Tri) Descriptor() protoreflect.EnumDescriptor {
return file_pdu_proto_enumTypes[0].Descriptor()
}
func (Tri) Type() protoreflect.EnumType {
return &file_pdu_proto_enumTypes[0]
}
func (x Tri) Number() protoreflect.EnumNumber {
return protoreflect.EnumNumber(x)
}
// Deprecated: Use Tri.Descriptor instead.
func (Tri) EnumDescriptor() ([]byte, []int) {
return file_pdu_proto_rawDescGZIP(), []int{0}
}
type ReplicationGuaranteeKind int32 type ReplicationGuaranteeKind int32
const ( const (
@@ -61,11 +110,11 @@ func (x ReplicationGuaranteeKind) String() string {
} }
func (ReplicationGuaranteeKind) Descriptor() protoreflect.EnumDescriptor { func (ReplicationGuaranteeKind) Descriptor() protoreflect.EnumDescriptor {
return file_pdu_proto_enumTypes[0].Descriptor() return file_pdu_proto_enumTypes[1].Descriptor()
} }
func (ReplicationGuaranteeKind) Type() protoreflect.EnumType { func (ReplicationGuaranteeKind) Type() protoreflect.EnumType {
return &file_pdu_proto_enumTypes[0] return &file_pdu_proto_enumTypes[1]
} }
func (x ReplicationGuaranteeKind) Number() protoreflect.EnumNumber { func (x ReplicationGuaranteeKind) Number() protoreflect.EnumNumber {
@@ -74,7 +123,7 @@ func (x ReplicationGuaranteeKind) Number() protoreflect.EnumNumber {
// Deprecated: Use ReplicationGuaranteeKind.Descriptor instead. // Deprecated: Use ReplicationGuaranteeKind.Descriptor instead.
func (ReplicationGuaranteeKind) EnumDescriptor() ([]byte, []int) { func (ReplicationGuaranteeKind) EnumDescriptor() ([]byte, []int) {
return file_pdu_proto_rawDescGZIP(), []int{0} return file_pdu_proto_rawDescGZIP(), []int{1}
} }
type FilesystemVersion_VersionType int32 type FilesystemVersion_VersionType int32
@@ -107,11 +156,11 @@ func (x FilesystemVersion_VersionType) String() string {
} }
func (FilesystemVersion_VersionType) Descriptor() protoreflect.EnumDescriptor { func (FilesystemVersion_VersionType) Descriptor() protoreflect.EnumDescriptor {
return file_pdu_proto_enumTypes[1].Descriptor() return file_pdu_proto_enumTypes[2].Descriptor()
} }
func (FilesystemVersion_VersionType) Type() protoreflect.EnumType { func (FilesystemVersion_VersionType) Type() protoreflect.EnumType {
return &file_pdu_proto_enumTypes[1] return &file_pdu_proto_enumTypes[2]
} }
func (x FilesystemVersion_VersionType) Number() protoreflect.EnumNumber { func (x FilesystemVersion_VersionType) Number() protoreflect.EnumNumber {
@@ -216,6 +265,7 @@ type Filesystem struct {
Path string `protobuf:"bytes,1,opt,name=Path,proto3" json:"Path,omitempty"` Path string `protobuf:"bytes,1,opt,name=Path,proto3" json:"Path,omitempty"`
ResumeToken string `protobuf:"bytes,2,opt,name=ResumeToken,proto3" json:"ResumeToken,omitempty"` ResumeToken string `protobuf:"bytes,2,opt,name=ResumeToken,proto3" json:"ResumeToken,omitempty"`
IsPlaceholder bool `protobuf:"varint,3,opt,name=IsPlaceholder,proto3" json:"IsPlaceholder,omitempty"` IsPlaceholder bool `protobuf:"varint,3,opt,name=IsPlaceholder,proto3" json:"IsPlaceholder,omitempty"`
IsEncrypted bool `protobuf:"varint,4,opt,name=IsEncrypted,proto3" json:"IsEncrypted,omitempty"`
} }
func (x *Filesystem) Reset() { func (x *Filesystem) Reset() {
@@ -271,6 +321,13 @@ func (x *Filesystem) GetIsPlaceholder() bool {
return false return false
} }
func (x *Filesystem) GetIsEncrypted() bool {
if x != nil {
return x.IsEncrypted
}
return false
}
type ListFilesystemVersionsReq struct { type ListFilesystemVersionsReq struct {
state protoimpl.MessageState state protoimpl.MessageState
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
@@ -460,6 +517,7 @@ type SendReq struct {
// ResumeToken is not empty, the GUIDs of From and To MUST correspond to those // ResumeToken is not empty, the GUIDs of From and To MUST correspond to those
// encoded in the ResumeToken. Otherwise, the Sender MUST return an error. // encoded in the ResumeToken. Otherwise, the Sender MUST return an error.
ResumeToken string `protobuf:"bytes,4,opt,name=ResumeToken,proto3" json:"ResumeToken,omitempty"` ResumeToken string `protobuf:"bytes,4,opt,name=ResumeToken,proto3" json:"ResumeToken,omitempty"`
Encrypted Tri `protobuf:"varint,5,opt,name=Encrypted,proto3,enum=Tri" json:"Encrypted,omitempty"`
ReplicationConfig *ReplicationConfig `protobuf:"bytes,6,opt,name=ReplicationConfig,proto3" json:"ReplicationConfig,omitempty"` ReplicationConfig *ReplicationConfig `protobuf:"bytes,6,opt,name=ReplicationConfig,proto3" json:"ReplicationConfig,omitempty"`
} }
@@ -523,6 +581,13 @@ func (x *SendReq) GetResumeToken() string {
return "" return ""
} }
func (x *SendReq) GetEncrypted() Tri {
if x != nil {
return x.Encrypted
}
return Tri_DontCare
}
func (x *SendReq) GetReplicationConfig() *ReplicationConfig { func (x *SendReq) GetReplicationConfig() *ReplicationConfig {
if x != nil { if x != nil {
return x.ReplicationConfig return x.ReplicationConfig
@@ -1331,155 +1396,162 @@ var file_pdu_proto_rawDesc = []byte{
0x65, 0x6d, 0x52, 0x65, 0x73, 0x12, 0x2d, 0x0a, 0x0b, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x65, 0x6d, 0x52, 0x65, 0x73, 0x12, 0x2d, 0x0a, 0x0b, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73,
0x74, 0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x46, 0x69, 0x6c,
0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x52, 0x0b, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x52, 0x0b, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73,
0x74, 0x65, 0x6d, 0x73, 0x22, 0x68, 0x0a, 0x0a, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x74, 0x65, 0x6d, 0x73, 0x22, 0x8a, 0x01, 0x0a, 0x0a, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73,
0x65, 0x6d, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x74, 0x65, 0x6d, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28,
0x52, 0x04, 0x50, 0x61, 0x74, 0x68, 0x12, 0x20, 0x0a, 0x0b, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x65, 0x09, 0x52, 0x04, 0x50, 0x61, 0x74, 0x68, 0x12, 0x20, 0x0a, 0x0b, 0x52, 0x65, 0x73, 0x75, 0x6d,
0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x52, 0x65, 0x73, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x52, 0x65,
0x75, 0x6d, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x24, 0x0a, 0x0d, 0x49, 0x73, 0x50, 0x6c, 0x73, 0x75, 0x6d, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x24, 0x0a, 0x0d, 0x49, 0x73, 0x50,
0x61, 0x63, 0x65, 0x68, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x6c, 0x61, 0x63, 0x65, 0x68, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08,
0x0d, 0x49, 0x73, 0x50, 0x6c, 0x61, 0x63, 0x65, 0x68, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x22, 0x3b, 0x52, 0x0d, 0x49, 0x73, 0x50, 0x6c, 0x61, 0x63, 0x65, 0x68, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x12,
0x20, 0x0a, 0x0b, 0x49, 0x73, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x18, 0x04,
0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x49, 0x73, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65,
0x64, 0x22, 0x3b, 0x0a, 0x19, 0x4c, 0x69, 0x73, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73,
0x74, 0x65, 0x6d, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x12, 0x1e,
0x0a, 0x0a, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01,
0x28, 0x09, 0x52, 0x0a, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x22, 0x4b,
0x0a, 0x19, 0x4c, 0x69, 0x73, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x0a, 0x19, 0x4c, 0x69, 0x73, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d,
0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x12, 0x1e, 0x0a, 0x0a, 0x46, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x12, 0x2e, 0x0a, 0x08, 0x56,
0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e,
0x0a, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x22, 0x4b, 0x0a, 0x19, 0x4c, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f,
0x69, 0x73, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x56, 0x65, 0x72, 0x6e, 0x52, 0x08, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0xd4, 0x01, 0x0a, 0x11,
0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x12, 0x2e, 0x0a, 0x08, 0x56, 0x65, 0x72, 0x73, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f,
0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x46, 0x69, 0x6c, 0x6e, 0x12, 0x32, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32,
0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x08, 0x1e, 0x2e, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x56, 0x65, 0x72, 0x73,
0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0xd4, 0x01, 0x0a, 0x11, 0x46, 0x69, 0x6c, 0x69, 0x6f, 0x6e, 0x2e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x52,
0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x32, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20,
0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1e, 0x2e, 0x46, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x47, 0x75, 0x69,
0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x04, 0x47, 0x75, 0x69, 0x64, 0x12, 0x1c, 0x0a,
0x09, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x58, 0x47, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04,
0x52, 0x09, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x58, 0x47, 0x12, 0x1a, 0x0a, 0x08, 0x43,
0x72, 0x65, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x43,
0x72, 0x65, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x29, 0x0a, 0x0b, 0x56, 0x65, 0x72, 0x73, 0x69,
0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0c, 0x0a, 0x08, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68,
0x6f, 0x74, 0x10, 0x00, 0x12, 0x0c, 0x0a, 0x08, 0x42, 0x6f, 0x6f, 0x6b, 0x6d, 0x61, 0x72, 0x6b,
0x10, 0x01, 0x22, 0xfd, 0x01, 0x0a, 0x07, 0x53, 0x65, 0x6e, 0x64, 0x52, 0x65, 0x71, 0x12, 0x1e,
0x0a, 0x0a, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01,
0x28, 0x09, 0x52, 0x0a, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x12, 0x26,
0x0a, 0x04, 0x46, 0x72, 0x6f, 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x46,
0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e,
0x2e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x54, 0x79, 0x52, 0x04, 0x46, 0x72, 0x6f, 0x6d, 0x12, 0x22, 0x0a, 0x02, 0x54, 0x6f, 0x18, 0x03, 0x20, 0x01,
0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x56,
0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x47, 0x75, 0x69, 0x64, 0x18, 0x03, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x02, 0x54, 0x6f, 0x12, 0x20, 0x0a, 0x0b, 0x52, 0x65,
0x20, 0x01, 0x28, 0x04, 0x52, 0x04, 0x47, 0x75, 0x69, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x43, 0x72, 0x73, 0x75, 0x6d, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52,
0x65, 0x61, 0x74, 0x65, 0x54, 0x58, 0x47, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x43, 0x0b, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x22, 0x0a, 0x09,
0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x58, 0x47, 0x12, 0x1a, 0x0a, 0x08, 0x43, 0x72, 0x65, 0x61, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0e, 0x32,
0x74, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x43, 0x72, 0x65, 0x61, 0x04, 0x2e, 0x54, 0x72, 0x69, 0x52, 0x09, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64,
0x74, 0x69, 0x6f, 0x6e, 0x22, 0x29, 0x0a, 0x0b, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x54, 0x12, 0x40, 0x0a, 0x11, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43,
0x79, 0x70, 0x65, 0x12, 0x0c, 0x0a, 0x08, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x10, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x52, 0x65,
0x00, 0x12, 0x0c, 0x0a, 0x08, 0x42, 0x6f, 0x6f, 0x6b, 0x6d, 0x61, 0x72, 0x6b, 0x10, 0x01, 0x22, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52,
0xd9, 0x01, 0x0a, 0x07, 0x53, 0x65, 0x6e, 0x64, 0x52, 0x65, 0x71, 0x12, 0x1e, 0x0a, 0x0a, 0x46, 0x11, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66,
0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x69, 0x67, 0x22, 0x51, 0x0a, 0x11, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f,
0x0a, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x12, 0x26, 0x0a, 0x04, 0x46, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x3c, 0x0a, 0x0a, 0x70, 0x72, 0x6f, 0x74, 0x65,
0x72, 0x6f, 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x46, 0x69, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x52, 0x65,
0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x04, 0x46, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x50,
0x72, 0x6f, 0x6d, 0x12, 0x22, 0x0a, 0x02, 0x54, 0x6f, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x72, 0x6f, 0x74, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0a, 0x70, 0x72, 0x6f, 0x74, 0x65,
0x12, 0x2e, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x56, 0x65, 0x72, 0x73, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x8f, 0x01, 0x0a, 0x1b, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63,
0x69, 0x6f, 0x6e, 0x52, 0x02, 0x54, 0x6f, 0x12, 0x20, 0x0a, 0x0b, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x50, 0x72, 0x6f, 0x74, 0x65,
0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x52, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x33, 0x0a, 0x07, 0x49, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x6c,
0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x19, 0x2e, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61,
0x74, 0x69, 0x6f, 0x6e, 0x47, 0x75, 0x61, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x65, 0x4b, 0x69, 0x6e,
0x64, 0x52, 0x07, 0x49, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x6c, 0x12, 0x3b, 0x0a, 0x0b, 0x49, 0x6e,
0x63, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32,
0x19, 0x2e, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x47, 0x75, 0x61,
0x72, 0x61, 0x6e, 0x74, 0x65, 0x65, 0x4b, 0x69, 0x6e, 0x64, 0x52, 0x0b, 0x49, 0x6e, 0x63, 0x72,
0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x6c, 0x22, 0x34, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x70, 0x65,
0x72, 0x74, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28,
0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x56, 0x61, 0x6c, 0x75, 0x65,
0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x57, 0x0a,
0x07, 0x53, 0x65, 0x6e, 0x64, 0x52, 0x65, 0x73, 0x12, 0x28, 0x0a, 0x0f, 0x55, 0x73, 0x65, 0x64,
0x52, 0x65, 0x73, 0x75, 0x6d, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28,
0x08, 0x52, 0x0f, 0x55, 0x73, 0x65, 0x64, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x65, 0x54, 0x6f, 0x6b,
0x65, 0x6e, 0x12, 0x22, 0x0a, 0x0c, 0x45, 0x78, 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, 0x53, 0x69,
0x7a, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0c, 0x45, 0x78, 0x70, 0x65, 0x63, 0x74,
0x65, 0x64, 0x53, 0x69, 0x7a, 0x65, 0x22, 0x3e, 0x0a, 0x10, 0x53, 0x65, 0x6e, 0x64, 0x43, 0x6f,
0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x52, 0x65, 0x71, 0x12, 0x2a, 0x0a, 0x0b, 0x4f, 0x72,
0x69, 0x67, 0x69, 0x6e, 0x61, 0x6c, 0x52, 0x65, 0x71, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32,
0x08, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x52, 0x65, 0x71, 0x52, 0x0b, 0x4f, 0x72, 0x69, 0x67, 0x69,
0x6e, 0x61, 0x6c, 0x52, 0x65, 0x71, 0x22, 0x12, 0x0a, 0x10, 0x53, 0x65, 0x6e, 0x64, 0x43, 0x6f,
0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x52, 0x65, 0x73, 0x22, 0xbe, 0x01, 0x0a, 0x0a, 0x52,
0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x52, 0x65, 0x71, 0x12, 0x1e, 0x0a, 0x0a, 0x46, 0x69, 0x6c,
0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x46,
0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x12, 0x22, 0x0a, 0x02, 0x54, 0x6f, 0x18,
0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74,
0x65, 0x6d, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x02, 0x54, 0x6f, 0x12, 0x2a, 0x0a,
0x10, 0x43, 0x6c, 0x65, 0x61, 0x72, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x65, 0x54, 0x6f, 0x6b, 0x65,
0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x43, 0x6c, 0x65, 0x61, 0x72, 0x52, 0x65,
0x73, 0x75, 0x6d, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x40, 0x0a, 0x11, 0x52, 0x65, 0x70, 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, 0x06, 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, 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, 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, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x0c, 0x0a, 0x0a, 0x52,
0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x52, 0x65, 0x73, 0x22, 0x67, 0x0a, 0x13, 0x44, 0x65, 0x73,
0x12, 0x3c, 0x0a, 0x0a, 0x70, 0x72, 0x6f, 0x74, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x74, 0x72, 0x6f, 0x79, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x52, 0x65, 0x71,
0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x12, 0x1e, 0x0a, 0x0a, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x18, 0x01,
0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x50, 0x72, 0x6f, 0x74, 0x65, 0x63, 0x74, 0x69, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d,
0x6f, 0x6e, 0x52, 0x0a, 0x70, 0x72, 0x6f, 0x74, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x8f, 0x12, 0x30, 0x0a, 0x09, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x18, 0x02, 0x20,
0x01, 0x0a, 0x1b, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d,
0x6e, 0x66, 0x69, 0x67, 0x50, 0x72, 0x6f, 0x74, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x33, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f,
0x0a, 0x07, 0x49, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x74, 0x73, 0x22, 0x5a, 0x0a, 0x12, 0x44, 0x65, 0x73, 0x74, 0x72, 0x6f, 0x79, 0x53, 0x6e, 0x61,
0x19, 0x2e, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x47, 0x75, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x52, 0x65, 0x73, 0x12, 0x2e, 0x0a, 0x08, 0x53, 0x6e, 0x61, 0x70,
0x72, 0x61, 0x6e, 0x74, 0x65, 0x65, 0x4b, 0x69, 0x6e, 0x64, 0x52, 0x07, 0x49, 0x6e, 0x69, 0x74, 0x73, 0x68, 0x6f, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x46, 0x69, 0x6c,
0x69, 0x61, 0x6c, 0x12, 0x3b, 0x0a, 0x0b, 0x49, 0x6e, 0x63, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x08,
0x61, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x19, 0x2e, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x45, 0x72, 0x72, 0x6f,
0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x47, 0x75, 0x61, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x65, 0x4b, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x44,
0x69, 0x6e, 0x64, 0x52, 0x0b, 0x49, 0x6e, 0x63, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x6c, 0x0a, 0x13, 0x44, 0x65, 0x73, 0x74, 0x72, 0x6f, 0x79, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f,
0x22, 0x34, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x73, 0x52, 0x65, 0x73, 0x12, 0x2d, 0x0a, 0x07, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73,
0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x44, 0x65, 0x73, 0x74, 0x72, 0x6f, 0x79,
0x12, 0x14, 0x0a, 0x05, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x52, 0x65, 0x73, 0x52, 0x07, 0x52, 0x65, 0x73,
0x05, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x57, 0x0a, 0x07, 0x53, 0x65, 0x6e, 0x64, 0x52, 0x65, 0x75, 0x6c, 0x74, 0x73, 0x22, 0x36, 0x0a, 0x14, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74,
0x73, 0x12, 0x28, 0x0a, 0x0f, 0x55, 0x73, 0x65, 0x64, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x65, 0x54, 0x69, 0x6f, 0x6e, 0x43, 0x75, 0x72, 0x73, 0x6f, 0x72, 0x52, 0x65, 0x71, 0x12, 0x1e, 0x0a, 0x0a,
0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x55, 0x73, 0x65, 0x64, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
0x52, 0x65, 0x73, 0x75, 0x6d, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x22, 0x0a, 0x0c, 0x45, 0x52, 0x0a, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x22, 0x54, 0x0a, 0x14,
0x78, 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, 0x53, 0x69, 0x7a, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28,
0x04, 0x52, 0x0c, 0x45, 0x78, 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, 0x53, 0x69, 0x7a, 0x65, 0x22,
0x3e, 0x0a, 0x10, 0x53, 0x65, 0x6e, 0x64, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64,
0x52, 0x65, 0x71, 0x12, 0x2a, 0x0a, 0x0b, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x61, 0x6c, 0x52,
0x65, 0x71, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x08, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x52,
0x65, 0x71, 0x52, 0x0b, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x61, 0x6c, 0x52, 0x65, 0x71, 0x22,
0x12, 0x0a, 0x10, 0x53, 0x65, 0x6e, 0x64, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64,
0x52, 0x65, 0x73, 0x22, 0xbe, 0x01, 0x0a, 0x0a, 0x52, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x52,
0x65, 0x71, 0x12, 0x1e, 0x0a, 0x0a, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d,
0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74,
0x65, 0x6d, 0x12, 0x22, 0x0a, 0x02, 0x54, 0x6f, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12,
0x2e, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x56, 0x65, 0x72, 0x73, 0x69,
0x6f, 0x6e, 0x52, 0x02, 0x54, 0x6f, 0x12, 0x2a, 0x0a, 0x10, 0x43, 0x6c, 0x65, 0x61, 0x72, 0x52,
0x65, 0x73, 0x75, 0x6d, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08,
0x52, 0x10, 0x43, 0x6c, 0x65, 0x61, 0x72, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x65, 0x54, 0x6f, 0x6b,
0x65, 0x6e, 0x12, 0x40, 0x0a, 0x11, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f,
0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e,
0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69,
0x67, 0x52, 0x11, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f,
0x6e, 0x66, 0x69, 0x67, 0x22, 0x0c, 0x0a, 0x0a, 0x52, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x52,
0x65, 0x73, 0x22, 0x67, 0x0a, 0x13, 0x44, 0x65, 0x73, 0x74, 0x72, 0x6f, 0x79, 0x53, 0x6e, 0x61,
0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x52, 0x65, 0x71, 0x12, 0x1e, 0x0a, 0x0a, 0x46, 0x69, 0x6c,
0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x46,
0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x12, 0x30, 0x0a, 0x09, 0x53, 0x6e, 0x61,
0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x46,
0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e,
0x52, 0x09, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x22, 0x5a, 0x0a, 0x12, 0x44,
0x65, 0x73, 0x74, 0x72, 0x6f, 0x79, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x52, 0x65,
0x73, 0x12, 0x2e, 0x0a, 0x08, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x18, 0x01, 0x20,
0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d,
0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x08, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f,
0x74, 0x12, 0x14, 0x0a, 0x05, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09,
0x52, 0x05, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x44, 0x0a, 0x13, 0x44, 0x65, 0x73, 0x74, 0x72,
0x6f, 0x79, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x52, 0x65, 0x73, 0x12, 0x2d,
0x0a, 0x07, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32,
0x13, 0x2e, 0x44, 0x65, 0x73, 0x74, 0x72, 0x6f, 0x79, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f,
0x74, 0x52, 0x65, 0x73, 0x52, 0x07, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x22, 0x36, 0x0a,
0x14, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x75, 0x72, 0x73,
0x6f, 0x72, 0x52, 0x65, 0x71, 0x12, 0x1e, 0x0a, 0x0a, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73,
0x74, 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x46, 0x69, 0x6c, 0x65, 0x73,
0x79, 0x73, 0x74, 0x65, 0x6d, 0x22, 0x54, 0x0a, 0x14, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61,
0x74, 0x69, 0x6f, 0x6e, 0x43, 0x75, 0x72, 0x73, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x12, 0x14, 0x0a,
0x04, 0x47, 0x75, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x48, 0x00, 0x52, 0x04, 0x47,
0x75, 0x69, 0x64, 0x12, 0x1c, 0x0a, 0x08, 0x4e, 0x6f, 0x74, 0x65, 0x78, 0x69, 0x73, 0x74, 0x18,
0x02, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x08, 0x4e, 0x6f, 0x74, 0x65, 0x78, 0x69, 0x73,
0x74, 0x42, 0x08, 0x0a, 0x06, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x22, 0x23, 0x0a, 0x07, 0x50,
0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x12, 0x18, 0x0a, 0x07, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67,
0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65,
0x22, 0x1d, 0x0a, 0x07, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x45,
0x63, 0x68, 0x6f, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x45, 0x63, 0x68, 0x6f, 0x2a,
0x86, 0x01, 0x0a, 0x18, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x47,
0x75, 0x61, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x65, 0x4b, 0x69, 0x6e, 0x64, 0x12, 0x14, 0x0a, 0x10,
0x47, 0x75, 0x61, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x65, 0x49, 0x6e, 0x76, 0x61, 0x6c, 0x69, 0x64,
0x10, 0x00, 0x12, 0x19, 0x0a, 0x15, 0x47, 0x75, 0x61, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x65, 0x52,
0x65, 0x73, 0x75, 0x6d, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x10, 0x01, 0x12, 0x23, 0x0a,
0x1f, 0x47, 0x75, 0x61, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x65, 0x49, 0x6e, 0x63, 0x72, 0x65, 0x6d,
0x65, 0x6e, 0x74, 0x61, 0x6c, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e,
0x10, 0x02, 0x12, 0x14, 0x0a, 0x10, 0x47, 0x75, 0x61, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x65, 0x4e,
0x6f, 0x74, 0x68, 0x69, 0x6e, 0x67, 0x10, 0x03, 0x32, 0x8f, 0x03, 0x0a, 0x0b, 0x52, 0x65, 0x70,
0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1a, 0x0a, 0x04, 0x50, 0x69, 0x6e, 0x67,
0x12, 0x08, 0x2e, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x1a, 0x08, 0x2e, 0x50, 0x69, 0x6e,
0x67, 0x52, 0x65, 0x73, 0x12, 0x39, 0x0a, 0x0f, 0x4c, 0x69, 0x73, 0x74, 0x46, 0x69, 0x6c, 0x65,
0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x73, 0x12, 0x12, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x46, 0x69,
0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x52, 0x65, 0x71, 0x1a, 0x12, 0x2e, 0x4c, 0x69,
0x73, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x52, 0x65, 0x73, 0x12,
0x50, 0x0a, 0x16, 0x4c, 0x69, 0x73, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65,
0x6d, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x1a, 0x2e, 0x4c, 0x69, 0x73, 0x74,
0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f,
0x6e, 0x73, 0x52, 0x65, 0x71, 0x1a, 0x1a, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x46, 0x69, 0x6c, 0x65,
0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65,
0x73, 0x12, 0x3e, 0x0a, 0x10, 0x44, 0x65, 0x73, 0x74, 0x72, 0x6f, 0x79, 0x53, 0x6e, 0x61, 0x70,
0x73, 0x68, 0x6f, 0x74, 0x73, 0x12, 0x14, 0x2e, 0x44, 0x65, 0x73, 0x74, 0x72, 0x6f, 0x79, 0x53,
0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x52, 0x65, 0x71, 0x1a, 0x14, 0x2e, 0x44, 0x65,
0x73, 0x74, 0x72, 0x6f, 0x79, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x52, 0x65,
0x73, 0x12, 0x41, 0x0a, 0x11, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e,
0x43, 0x75, 0x72, 0x73, 0x6f, 0x72, 0x12, 0x15, 0x2e, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61,
0x74, 0x69, 0x6f, 0x6e, 0x43, 0x75, 0x72, 0x73, 0x6f, 0x72, 0x52, 0x65, 0x71, 0x1a, 0x15, 0x2e,
0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x75, 0x72, 0x73, 0x6f, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x75, 0x72, 0x73, 0x6f,
0x72, 0x52, 0x65, 0x73, 0x12, 0x1d, 0x0a, 0x07, 0x53, 0x65, 0x6e, 0x64, 0x44, 0x72, 0x79, 0x12, 0x72, 0x52, 0x65, 0x73, 0x12, 0x14, 0x0a, 0x04, 0x47, 0x75, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01,
0x08, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x52, 0x65, 0x71, 0x1a, 0x08, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x28, 0x04, 0x48, 0x00, 0x52, 0x04, 0x47, 0x75, 0x69, 0x64, 0x12, 0x1c, 0x0a, 0x08, 0x4e, 0x6f,
0x52, 0x65, 0x73, 0x12, 0x35, 0x0a, 0x0d, 0x53, 0x65, 0x6e, 0x64, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x74, 0x65, 0x78, 0x69, 0x73, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x08,
0x65, 0x74, 0x65, 0x64, 0x12, 0x11, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x4e, 0x6f, 0x74, 0x65, 0x78, 0x69, 0x73, 0x74, 0x42, 0x08, 0x0a, 0x06, 0x52, 0x65, 0x73, 0x75,
0x65, 0x74, 0x65, 0x64, 0x52, 0x65, 0x71, 0x1a, 0x11, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x43, 0x6f, 0x6c, 0x74, 0x22, 0x23, 0x0a, 0x07, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x12, 0x18, 0x0a,
0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x52, 0x65, 0x73, 0x42, 0x07, 0x5a, 0x05, 0x2e, 0x3b, 0x07, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07,
0x70, 0x64, 0x75, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x1d, 0x0a, 0x07, 0x50, 0x69, 0x6e, 0x67, 0x52,
0x65, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x45, 0x63, 0x68, 0x6f, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
0x52, 0x04, 0x45, 0x63, 0x68, 0x6f, 0x2a, 0x28, 0x0a, 0x03, 0x54, 0x72, 0x69, 0x12, 0x0c, 0x0a,
0x08, 0x44, 0x6f, 0x6e, 0x74, 0x43, 0x61, 0x72, 0x65, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x46,
0x61, 0x6c, 0x73, 0x65, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x54, 0x72, 0x75, 0x65, 0x10, 0x02,
0x2a, 0x86, 0x01, 0x0a, 0x18, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e,
0x47, 0x75, 0x61, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x65, 0x4b, 0x69, 0x6e, 0x64, 0x12, 0x14, 0x0a,
0x10, 0x47, 0x75, 0x61, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x65, 0x49, 0x6e, 0x76, 0x61, 0x6c, 0x69,
0x64, 0x10, 0x00, 0x12, 0x19, 0x0a, 0x15, 0x47, 0x75, 0x61, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x65,
0x52, 0x65, 0x73, 0x75, 0x6d, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x10, 0x01, 0x12, 0x23,
0x0a, 0x1f, 0x47, 0x75, 0x61, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x65, 0x49, 0x6e, 0x63, 0x72, 0x65,
0x6d, 0x65, 0x6e, 0x74, 0x61, 0x6c, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f,
0x6e, 0x10, 0x02, 0x12, 0x14, 0x0a, 0x10, 0x47, 0x75, 0x61, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x65,
0x4e, 0x6f, 0x74, 0x68, 0x69, 0x6e, 0x67, 0x10, 0x03, 0x32, 0x8f, 0x03, 0x0a, 0x0b, 0x52, 0x65,
0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1a, 0x0a, 0x04, 0x50, 0x69, 0x6e,
0x67, 0x12, 0x08, 0x2e, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x1a, 0x08, 0x2e, 0x50, 0x69,
0x6e, 0x67, 0x52, 0x65, 0x73, 0x12, 0x39, 0x0a, 0x0f, 0x4c, 0x69, 0x73, 0x74, 0x46, 0x69, 0x6c,
0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x73, 0x12, 0x12, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x46,
0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x52, 0x65, 0x71, 0x1a, 0x12, 0x2e, 0x4c,
0x69, 0x73, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x52, 0x65, 0x73,
0x12, 0x50, 0x0a, 0x16, 0x4c, 0x69, 0x73, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74,
0x65, 0x6d, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x1a, 0x2e, 0x4c, 0x69, 0x73,
0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x56, 0x65, 0x72, 0x73, 0x69,
0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x1a, 0x1a, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x46, 0x69, 0x6c,
0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52,
0x65, 0x73, 0x12, 0x3e, 0x0a, 0x10, 0x44, 0x65, 0x73, 0x74, 0x72, 0x6f, 0x79, 0x53, 0x6e, 0x61,
0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x12, 0x14, 0x2e, 0x44, 0x65, 0x73, 0x74, 0x72, 0x6f, 0x79,
0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x52, 0x65, 0x71, 0x1a, 0x14, 0x2e, 0x44,
0x65, 0x73, 0x74, 0x72, 0x6f, 0x79, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x52,
0x65, 0x73, 0x12, 0x41, 0x0a, 0x11, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f,
0x6e, 0x43, 0x75, 0x72, 0x73, 0x6f, 0x72, 0x12, 0x15, 0x2e, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63,
0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x75, 0x72, 0x73, 0x6f, 0x72, 0x52, 0x65, 0x71, 0x1a, 0x15,
0x2e, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x75, 0x72, 0x73,
0x6f, 0x72, 0x52, 0x65, 0x73, 0x12, 0x1d, 0x0a, 0x07, 0x53, 0x65, 0x6e, 0x64, 0x44, 0x72, 0x79,
0x12, 0x08, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x52, 0x65, 0x71, 0x1a, 0x08, 0x2e, 0x53, 0x65, 0x6e,
0x64, 0x52, 0x65, 0x73, 0x12, 0x35, 0x0a, 0x0d, 0x53, 0x65, 0x6e, 0x64, 0x43, 0x6f, 0x6d, 0x70,
0x6c, 0x65, 0x74, 0x65, 0x64, 0x12, 0x11, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x43, 0x6f, 0x6d, 0x70,
0x6c, 0x65, 0x74, 0x65, 0x64, 0x52, 0x65, 0x71, 0x1a, 0x11, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x43,
0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x52, 0x65, 0x73, 0x42, 0x07, 0x5a, 0x05, 0x2e,
0x3b, 0x70, 0x64, 0x75, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
} }
var ( var (
@@ -1494,69 +1566,71 @@ func file_pdu_proto_rawDescGZIP() []byte {
return file_pdu_proto_rawDescData return file_pdu_proto_rawDescData
} }
var file_pdu_proto_enumTypes = make([]protoimpl.EnumInfo, 2) var file_pdu_proto_enumTypes = make([]protoimpl.EnumInfo, 3)
var file_pdu_proto_msgTypes = make([]protoimpl.MessageInfo, 22) var file_pdu_proto_msgTypes = make([]protoimpl.MessageInfo, 22)
var file_pdu_proto_goTypes = []interface{}{ var file_pdu_proto_goTypes = []interface{}{
(ReplicationGuaranteeKind)(0), // 0: ReplicationGuaranteeKind (Tri)(0), // 0: Tri
(FilesystemVersion_VersionType)(0), // 1: FilesystemVersion.VersionType (ReplicationGuaranteeKind)(0), // 1: ReplicationGuaranteeKind
(*ListFilesystemReq)(nil), // 2: ListFilesystemReq (FilesystemVersion_VersionType)(0), // 2: FilesystemVersion.VersionType
(*ListFilesystemRes)(nil), // 3: ListFilesystemRes (*ListFilesystemReq)(nil), // 3: ListFilesystemReq
(*Filesystem)(nil), // 4: Filesystem (*ListFilesystemRes)(nil), // 4: ListFilesystemRes
(*ListFilesystemVersionsReq)(nil), // 5: ListFilesystemVersionsReq (*Filesystem)(nil), // 5: Filesystem
(*ListFilesystemVersionsRes)(nil), // 6: ListFilesystemVersionsRes (*ListFilesystemVersionsReq)(nil), // 6: ListFilesystemVersionsReq
(*FilesystemVersion)(nil), // 7: FilesystemVersion (*ListFilesystemVersionsRes)(nil), // 7: ListFilesystemVersionsRes
(*SendReq)(nil), // 8: SendReq (*FilesystemVersion)(nil), // 8: FilesystemVersion
(*ReplicationConfig)(nil), // 9: ReplicationConfig (*SendReq)(nil), // 9: SendReq
(*ReplicationConfigProtection)(nil), // 10: ReplicationConfigProtection (*ReplicationConfig)(nil), // 10: ReplicationConfig
(*Property)(nil), // 11: Property (*ReplicationConfigProtection)(nil), // 11: ReplicationConfigProtection
(*SendRes)(nil), // 12: SendRes (*Property)(nil), // 12: Property
(*SendCompletedReq)(nil), // 13: SendCompletedReq (*SendRes)(nil), // 13: SendRes
(*SendCompletedRes)(nil), // 14: SendCompletedRes (*SendCompletedReq)(nil), // 14: SendCompletedReq
(*ReceiveReq)(nil), // 15: ReceiveReq (*SendCompletedRes)(nil), // 15: SendCompletedRes
(*ReceiveRes)(nil), // 16: ReceiveRes (*ReceiveReq)(nil), // 16: ReceiveReq
(*DestroySnapshotsReq)(nil), // 17: DestroySnapshotsReq (*ReceiveRes)(nil), // 17: ReceiveRes
(*DestroySnapshotRes)(nil), // 18: DestroySnapshotRes (*DestroySnapshotsReq)(nil), // 18: DestroySnapshotsReq
(*DestroySnapshotsRes)(nil), // 19: DestroySnapshotsRes (*DestroySnapshotRes)(nil), // 19: DestroySnapshotRes
(*ReplicationCursorReq)(nil), // 20: ReplicationCursorReq (*DestroySnapshotsRes)(nil), // 20: DestroySnapshotsRes
(*ReplicationCursorRes)(nil), // 21: ReplicationCursorRes (*ReplicationCursorReq)(nil), // 21: ReplicationCursorReq
(*PingReq)(nil), // 22: PingReq (*ReplicationCursorRes)(nil), // 22: ReplicationCursorRes
(*PingRes)(nil), // 23: PingRes (*PingReq)(nil), // 23: PingReq
(*PingRes)(nil), // 24: PingRes
} }
var file_pdu_proto_depIdxs = []int32{ var file_pdu_proto_depIdxs = []int32{
4, // 0: ListFilesystemRes.Filesystems:type_name -> Filesystem 5, // 0: ListFilesystemRes.Filesystems:type_name -> Filesystem
7, // 1: ListFilesystemVersionsRes.Versions:type_name -> FilesystemVersion 8, // 1: ListFilesystemVersionsRes.Versions:type_name -> FilesystemVersion
1, // 2: FilesystemVersion.Type:type_name -> FilesystemVersion.VersionType 2, // 2: FilesystemVersion.Type:type_name -> FilesystemVersion.VersionType
7, // 3: SendReq.From:type_name -> FilesystemVersion 8, // 3: SendReq.From:type_name -> FilesystemVersion
7, // 4: SendReq.To:type_name -> FilesystemVersion 8, // 4: SendReq.To:type_name -> FilesystemVersion
9, // 5: SendReq.ReplicationConfig:type_name -> ReplicationConfig 0, // 5: SendReq.Encrypted:type_name -> Tri
10, // 6: ReplicationConfig.protection:type_name -> ReplicationConfigProtection 10, // 6: SendReq.ReplicationConfig:type_name -> ReplicationConfig
0, // 7: ReplicationConfigProtection.Initial:type_name -> ReplicationGuaranteeKind 11, // 7: ReplicationConfig.protection:type_name -> ReplicationConfigProtection
0, // 8: ReplicationConfigProtection.Incremental:type_name -> ReplicationGuaranteeKind 1, // 8: ReplicationConfigProtection.Initial:type_name -> ReplicationGuaranteeKind
8, // 9: SendCompletedReq.OriginalReq:type_name -> SendReq 1, // 9: ReplicationConfigProtection.Incremental:type_name -> ReplicationGuaranteeKind
7, // 10: ReceiveReq.To:type_name -> FilesystemVersion 9, // 10: SendCompletedReq.OriginalReq:type_name -> SendReq
9, // 11: ReceiveReq.ReplicationConfig:type_name -> ReplicationConfig 8, // 11: ReceiveReq.To:type_name -> FilesystemVersion
7, // 12: DestroySnapshotsReq.Snapshots:type_name -> FilesystemVersion 10, // 12: ReceiveReq.ReplicationConfig:type_name -> ReplicationConfig
7, // 13: DestroySnapshotRes.Snapshot:type_name -> FilesystemVersion 8, // 13: DestroySnapshotsReq.Snapshots:type_name -> FilesystemVersion
18, // 14: DestroySnapshotsRes.Results:type_name -> DestroySnapshotRes 8, // 14: DestroySnapshotRes.Snapshot:type_name -> FilesystemVersion
22, // 15: Replication.Ping:input_type -> PingReq 19, // 15: DestroySnapshotsRes.Results:type_name -> DestroySnapshotRes
2, // 16: Replication.ListFilesystems:input_type -> ListFilesystemReq 23, // 16: Replication.Ping:input_type -> PingReq
5, // 17: Replication.ListFilesystemVersions:input_type -> ListFilesystemVersionsReq 3, // 17: Replication.ListFilesystems:input_type -> ListFilesystemReq
17, // 18: Replication.DestroySnapshots:input_type -> DestroySnapshotsReq 6, // 18: Replication.ListFilesystemVersions:input_type -> ListFilesystemVersionsReq
20, // 19: Replication.ReplicationCursor:input_type -> ReplicationCursorReq 18, // 19: Replication.DestroySnapshots:input_type -> DestroySnapshotsReq
8, // 20: Replication.SendDry:input_type -> SendReq 21, // 20: Replication.ReplicationCursor:input_type -> ReplicationCursorReq
13, // 21: Replication.SendCompleted:input_type -> SendCompletedReq 9, // 21: Replication.SendDry:input_type -> SendReq
23, // 22: Replication.Ping:output_type -> PingRes 14, // 22: Replication.SendCompleted:input_type -> SendCompletedReq
3, // 23: Replication.ListFilesystems:output_type -> ListFilesystemRes 24, // 23: Replication.Ping:output_type -> PingRes
6, // 24: Replication.ListFilesystemVersions:output_type -> ListFilesystemVersionsRes 4, // 24: Replication.ListFilesystems:output_type -> ListFilesystemRes
19, // 25: Replication.DestroySnapshots:output_type -> DestroySnapshotsRes 7, // 25: Replication.ListFilesystemVersions:output_type -> ListFilesystemVersionsRes
21, // 26: Replication.ReplicationCursor:output_type -> ReplicationCursorRes 20, // 26: Replication.DestroySnapshots:output_type -> DestroySnapshotsRes
12, // 27: Replication.SendDry:output_type -> SendRes 22, // 27: Replication.ReplicationCursor:output_type -> ReplicationCursorRes
14, // 28: Replication.SendCompleted:output_type -> SendCompletedRes 13, // 28: Replication.SendDry:output_type -> SendRes
22, // [22:29] is the sub-list for method output_type 15, // 29: Replication.SendCompleted:output_type -> SendCompletedRes
15, // [15:22] is the sub-list for method input_type 23, // [23:30] is the sub-list for method output_type
15, // [15:15] is the sub-list for extension type_name 16, // [16:23] is the sub-list for method input_type
15, // [15:15] is the sub-list for extension extendee 16, // [16:16] is the sub-list for extension type_name
0, // [0:15] 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() }
@@ -1839,7 +1913,7 @@ func file_pdu_proto_init() {
File: protoimpl.DescBuilder{ File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(), GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_pdu_proto_rawDesc, RawDescriptor: file_pdu_proto_rawDesc,
NumEnums: 2, NumEnums: 3,
NumMessages: 22, NumMessages: 22,
NumExtensions: 0, NumExtensions: 0,
NumServices: 1, NumServices: 1,
+8
View File
@@ -21,6 +21,7 @@ message Filesystem {
string Path = 1; string Path = 1;
string ResumeToken = 2; string ResumeToken = 2;
bool IsPlaceholder = 3; bool IsPlaceholder = 3;
bool IsEncrypted = 4;
} }
message ListFilesystemVersionsReq { string Filesystem = 1; } message ListFilesystemVersionsReq { string Filesystem = 1; }
@@ -39,6 +40,12 @@ message FilesystemVersion {
string Creation = 5; // RFC 3339 string Creation = 5; // RFC 3339
} }
enum Tri {
DontCare = 0;
False = 1;
True = 2;
}
message SendReq { message SendReq {
string Filesystem = 1; string Filesystem = 1;
// May be empty / null to request a full transfer of To // May be empty / null to request a full transfer of To
@@ -52,6 +59,7 @@ message SendReq {
// ResumeToken is not empty, the GUIDs of From and To MUST correspond to those // ResumeToken is not empty, the GUIDs of From and To MUST correspond to those
// encoded in the ResumeToken. Otherwise, the Sender MUST return an error. // encoded in the ResumeToken. Otherwise, the Sender MUST return an error.
string ResumeToken = 4; string ResumeToken = 4;
Tri Encrypted = 5;
ReplicationConfig ReplicationConfig = 6; ReplicationConfig ReplicationConfig = 6;
} }
+44 -6
View File
@@ -156,7 +156,8 @@ type Step struct {
parent *Filesystem parent *Filesystem
from, to *pdu.FilesystemVersion // from may be nil, indicating full send from, to *pdu.FilesystemVersion // from may be nil, indicating full send
resumeToken string // empty means no resume token shall be used encrypt tri
resumeToken string // empty means no resume token shall be used
expectedSize uint64 // 0 means no size estimate present / possible expectedSize uint64 // 0 means no size estimate present / possible
@@ -200,10 +201,22 @@ func (s *Step) ReportInfo() *report.StepInfo {
if s.from != nil { if s.from != nil {
from = s.from.RelName() from = s.from.RelName()
} }
var encrypted report.EncryptedEnum
switch s.encrypt {
case DontCare:
encrypted = report.EncryptedSenderDependent
case True:
encrypted = report.EncryptedTrue
case False:
encrypted = report.EncryptedFalse
default:
panic(fmt.Sprintf("unknown variant %s", s.encrypt))
}
return &report.StepInfo{ return &report.StepInfo{
From: from, From: from,
To: s.to.RelName(), To: s.to.RelName(),
Resumed: s.resumeToken != "", Resumed: s.resumeToken != "",
Encrypted: encrypted,
BytesExpected: s.expectedSize, BytesExpected: s.expectedSize,
BytesReplicated: byteCounter, BytesReplicated: byteCounter,
} }
@@ -333,6 +346,10 @@ func (fs *Filesystem) doPlanning(ctx context.Context) ([]*Step, error) {
log(ctx).Debug("assessing filesystem") log(ctx).Debug("assessing filesystem")
if fs.policy.EncryptedSend == True && !fs.senderFS.GetIsEncrypted() {
return nil, fmt.Errorf("sender filesystem is not encrypted but policy mandates encrypted send")
}
sfsvsres, err := fs.sender.ListFilesystemVersions(ctx, &pdu.ListFilesystemVersionsReq{Filesystem: fs.Path}) sfsvsres, err := fs.sender.ListFilesystemVersions(ctx, &pdu.ListFilesystemVersionsReq{Filesystem: fs.Path})
if err != nil { if err != nil {
log(ctx).WithError(err).Error("cannot get remote filesystem versions") log(ctx).WithError(err).Error("cannot get remote filesystem versions")
@@ -406,7 +423,24 @@ func (fs *Filesystem) doPlanning(ctx context.Context) ([]*Step, error) {
} }
} }
if toVersion == nil { encryptionMatches := false
switch fs.policy.EncryptedSend {
case True:
encryptionMatches = resumeToken.RawOK && resumeToken.CompressOK
case False:
encryptionMatches = !resumeToken.RawOK && !resumeToken.CompressOK
case DontCare:
encryptionMatches = true
}
log(ctx).WithField("fromVersion", fromVersion).
WithField("toVersion", toVersion).
WithField("encryptionMatches", encryptionMatches).
Debug("result of resume-token-matching to sender's versions")
if !encryptionMatches {
return nil, fmt.Errorf("resume token `rawok`=%v and `compressok`=%v are incompatible with encryption policy=%v", resumeToken.RawOK, resumeToken.CompressOK, fs.policy.EncryptedSend)
} else if toVersion == nil {
return nil, fmt.Errorf("resume token `toguid` = %v not found on sender (`toname` = %q)", resumeToken.ToGUID, resumeToken.ToName) return nil, fmt.Errorf("resume token `toguid` = %v not found on sender (`toname` = %q)", resumeToken.ToGUID, resumeToken.ToName)
} else if fromVersion == toVersion { } else if fromVersion == toVersion {
return nil, fmt.Errorf("resume token `fromguid` and `toguid` match same version on sener") return nil, fmt.Errorf("resume token `fromguid` and `toguid` match same version on sener")
@@ -418,8 +452,9 @@ func (fs *Filesystem) doPlanning(ctx context.Context) ([]*Step, error) {
sender: fs.sender, sender: fs.sender,
receiver: fs.receiver, receiver: fs.receiver,
from: fromVersion, from: fromVersion,
to: toVersion, to: toVersion,
encrypt: fs.policy.EncryptedSend,
resumeToken: resumeTokenRaw, resumeToken: resumeTokenRaw,
} }
@@ -445,6 +480,7 @@ func (fs *Filesystem) doPlanning(ctx context.Context) ([]*Step, error) {
receiver: fs.receiver, receiver: fs.receiver,
from: remainingSFSVs[i], from: remainingSFSVs[i],
to: remainingSFSVs[i+1], to: remainingSFSVs[i+1],
encrypt: fs.policy.EncryptedSend,
}) })
} }
} else { // resumeToken == nil } else { // resumeToken == nil
@@ -474,8 +510,9 @@ func (fs *Filesystem) doPlanning(ctx context.Context) ([]*Step, error) {
sender: fs.sender, sender: fs.sender,
receiver: fs.receiver, receiver: fs.receiver,
from: path[i], // nil in case of initial repl from: path[i], // nil in case of initial repl
to: path[i+1], to: path[i+1],
encrypt: fs.policy.EncryptedSend,
}) })
} }
} }
@@ -555,6 +592,7 @@ func (s *Step) buildSendRequest() (sr *pdu.SendReq) {
Filesystem: fs, Filesystem: fs,
From: s.from, // may be nil From: s.from, // may be nil
To: s.to, To: s.to,
Encrypted: s.encrypt.ToPDU(),
ResumeToken: s.resumeToken, ResumeToken: s.resumeToken,
ReplicationConfig: s.parent.policy.ReplicationConfig, ReplicationConfig: s.parent.policy.ReplicationConfig,
} }
@@ -63,6 +63,7 @@ func ConflictResolutionFromConfig(in *config.ConflictResolution) (*ConflictResol
} }
type PlannerPolicy struct { type PlannerPolicy struct {
EncryptedSend tri // all sends must be encrypted (send -w, and encryption!=off)
ConflictResolution *ConflictResolution `validate:"ne=nil"` ConflictResolution *ConflictResolution `validate:"ne=nil"`
ReplicationConfig *pdu.ReplicationConfig `validate:"ne=nil"` ReplicationConfig *pdu.ReplicationConfig `validate:"ne=nil"`
SizeEstimationConcurrency int `validate:"gte=1"` SizeEstimationConcurrency int `validate:"gte=1"`
@@ -0,0 +1,46 @@
package logic
import (
"fmt"
"github.com/zrepl/zrepl/replication/logic/pdu"
)
type tri int
const (
DontCare = 0x0
False = 0x1
True = 0x2
)
func (t tri) String() string {
switch t {
case DontCare:
return "dontcare"
case False:
return "false"
case True:
return "true"
}
panic(fmt.Sprintf("unknown variant %v", int(t)))
}
func (t tri) ToPDU() pdu.Tri {
switch t {
case DontCare:
return pdu.Tri_DontCare
case False:
return pdu.Tri_False
case True:
return pdu.Tri_True
}
panic(fmt.Sprintf("unknown variant %v", int(t)))
}
func TriFromBool(b bool) tri {
if b {
return True
}
return False
}
+9
View File
@@ -97,9 +97,18 @@ type StepReport struct {
Info *StepInfo Info *StepInfo
} }
type EncryptedEnum string
const (
EncryptedTrue EncryptedEnum = "yes"
EncryptedFalse EncryptedEnum = "no"
EncryptedSenderDependent EncryptedEnum = "sender-dependent"
)
type StepInfo struct { type StepInfo struct {
From, To string From, To string
Resumed bool Resumed bool
Encrypted EncryptedEnum
BytesExpected uint64 BytesExpected uint64
BytesReplicated uint64 BytesReplicated uint64
} }
+1 -1
View File
@@ -14,7 +14,7 @@ func init() {
} }
} }
//nolint:deadcode,unused //nolint[:deadcode,unused]
func debug(format string, args ...interface{}) { func debug(format string, args ...interface{}) {
if debugEnabled { if debugEnabled {
fmt.Fprintf(os.Stderr, "rpc/dataconn: %s\n", fmt.Sprintf(format, args...)) fmt.Fprintf(os.Stderr, "rpc/dataconn: %s\n", fmt.Sprintf(format, args...))
@@ -24,9 +24,6 @@ func (e HeartbeatTimeout) Error() string {
return "heartbeat timeout" return "heartbeat timeout"
} }
// This function is deprecated in net.Error and since this
// function is not involved in .Accept() code path, nothing
// really needs this method to be here.
func (e HeartbeatTimeout) Temporary() bool { return true } func (e HeartbeatTimeout) Temporary() bool { return true }
func (e HeartbeatTimeout) Timeout() bool { return true } func (e HeartbeatTimeout) Timeout() bool { return true }
@@ -13,7 +13,7 @@ func init() {
} }
} }
//nolint:deadcode,unused //nolint[:deadcode,unused]
func debug(format string, args ...interface{}) { func debug(format string, args ...interface{}) {
if debugEnabled { if debugEnabled {
fmt.Fprintf(os.Stderr, "rpc/dataconn/heartbeatconn: %s\n", fmt.Sprintf(format, args...)) fmt.Fprintf(os.Stderr, "rpc/dataconn/heartbeatconn: %s\n", fmt.Sprintf(format, args...))
@@ -6,12 +6,12 @@
// In commit 082335df5d85e1b0b9faa35ff182c71886142d3e and earlier, heartbeatconn would fail // In commit 082335df5d85e1b0b9faa35ff182c71886142d3e and earlier, heartbeatconn would fail
// this benchmark with a writev I/O timeout (here the ss(8) output at the time of failure) // this benchmark with a writev I/O timeout (here the ss(8) output at the time of failure)
// //
// ESTAB 33369 0 127.0.0.1:12345 127.0.0.1:57282 users:(("heartbeatconn_i",pid=25953,fd=5)) // ESTAB 33369 0 127.0.0.1:12345 127.0.0.1:57282 users:(("heartbeatconn_i",pid=25953,fd=5))
// cubic wscale:7,7 rto:203 rtt:2.992/5.849 ato:162 mss:32768 pmtu:65535 rcvmss:32741 advmss:65483 cwnd:10 bytes_sent:48 bytes_acked:48 bytes_received:195401 segs_out:44 segs_in:57 data_segs_out:6 data_segs_in:34 send 876.1Mbps lastsnd:125 lastrcv:9390 lastack:125 pacing_rate 1752.0Mbps delivery_rate 6393.8Mbps delivered:7 app_limited busy:42ms rcv_rtt:1 rcv_space:65483 rcv_ssthresh:65483 minrtt:0.029 // cubic wscale:7,7 rto:203 rtt:2.992/5.849 ato:162 mss:32768 pmtu:65535 rcvmss:32741 advmss:65483 cwnd:10 bytes_sent:48 bytes_acked:48 bytes_received:195401 segs_out:44 segs_in:57 data_segs_out:6 data_segs_in:34 send 876.1Mbps lastsnd:125 lastrcv:9390 lastack:125 pacing_rate 1752.0Mbps delivery_rate 6393.8Mbps delivered:7 app_limited busy:42ms rcv_rtt:1 rcv_space:65483 rcv_ssthresh:65483 minrtt:0.029
// -- // --
// ESTAB 0 3956805 127.0.0.1:57282 127.0.0.1:12345 users:(("heartbeatconn_i",pid=26100,fd=3)) // ESTAB 0 3956805 127.0.0.1:57282 127.0.0.1:12345 users:(("heartbeatconn_i",pid=26100,fd=3))
// cubic wscale:7,7 rto:211 backoff:5 rtt:10.38/16.937 ato:40 mss:32768 pmtu:65535 rcvmss:536 advmss:65483 cwnd:10 bytes_sent:195401 bytes_acked:195402 bytes_received:48 segs_out:57 segs_in:45 data_segs_out:34 data_segs_in:6 send 252.5Mbps lastsnd:9390 lastrcv:125 lastack:125 pacing_rate 505.1Mbps delivery_rate 1971.0Mbps delivered:35 busy:30127ms rwnd_limited:30086ms(99.9%) rcv_space:65495 rcv_ssthresh:65495 notsent:3956805 minrtt:0.007 // cubic wscale:7,7 rto:211 backoff:5 rtt:10.38/16.937 ato:40 mss:32768 pmtu:65535 rcvmss:536 advmss:65483 cwnd:10 bytes_sent:195401 bytes_acked:195402 bytes_received:48 segs_out:57 segs_in:45 data_segs_out:34 data_segs_in:6 send 252.5Mbps lastsnd:9390 lastrcv:125 lastack:125 pacing_rate 505.1Mbps delivery_rate 1971.0Mbps delivered:35 busy:30127ms rwnd_limited:30086ms(99.9%) rcv_space:65495 rcv_ssthresh:65495 notsent:3956805 minrtt:0.007
// panic: writev tcp 127.0.0.1:57282->127.0.0.1:12345: i/o timeout // panic: writev tcp 127.0.0.1:57282->127.0.0.1:12345: i/o timeout
// //
// The assumed reason for those writev timeouts is the following: // The assumed reason for those writev timeouts is the following:
// - Sporadic server stalls (sever data handling, usually I/O) cause TCP exponential backoff on the client for client->server // - Sporadic server stalls (sever data handling, usually I/O) cause TCP exponential backoff on the client for client->server
@@ -22,23 +22,27 @@
// The fix contained in the commit this message was committed with resets the deadline whenever // The fix contained in the commit this message was committed with resets the deadline whenever
// a heartbeat is received from the server. // a heartbeat is received from the server.
// //
//
// How to run this integration test: // How to run this integration test:
// //
// Terminal 1:
// $ ZREPL_RPC_DATACONN_HEARTBEATCONN_DEBUG=1 go run heartbeatconn_integration_variablereceiverate.go -mode server -addr 127.0.0.1:12345
// rpc/dataconn/heartbeatconn: send heartbeat
// rpc/dataconn/heartbeatconn: send heartbeat
// ...
// //
// Terminal 2: // Terminal 1:
// $ ZREPL_RPC_DATACONN_HEARTBEATCONN_DEBUG=1 go run heartbeatconn_integration_variablereceiverate.go -mode client -addr 127.0.0.1:12345 // $ ZREPL_RPC_DATACONN_HEARTBEATCONN_DEBUG=1 go run heartbeatconn_integration_variablereceiverate.go -mode server -addr 127.0.0.1:12345
// rpc/dataconn/heartbeatconn: received heartbeat, resetting write timeout // rpc/dataconn/heartbeatconn: send heartbeat
// rpc/dataconn/heartbeatconn: renew frameconn write timeout returned errT=<nil> err=%!s(<nil>) // rpc/dataconn/heartbeatconn: send heartbeat
// rpc/dataconn/heartbeatconn: send heartbeat // ...
// rpc/dataconn/heartbeatconn: received heartbeat, resetting write timeout //
// rpc/dataconn/heartbeatconn: renew frameconn write timeout returned errT=<nil> err=%!s(<nil>) // Terminal 2:
// rpc/dataconn/heartbeatconn: received heartbeat, resetting write timeout // $ ZREPL_RPC_DATACONN_HEARTBEATCONN_DEBUG=1 go run heartbeatconn_integration_variablereceiverate.go -mode client -addr 127.0.0.1:12345
// ... // rpc/dataconn/heartbeatconn: received heartbeat, resetting write timeout
// rpc/dataconn/heartbeatconn: renew frameconn write timeout returned errT=<nil> err=%!s(<nil>)
// rpc/dataconn/heartbeatconn: send heartbeat
// rpc/dataconn/heartbeatconn: received heartbeat, resetting write timeout
// rpc/dataconn/heartbeatconn: renew frameconn write timeout returned errT=<nil> err=%!s(<nil>)
// rpc/dataconn/heartbeatconn: received heartbeat, resetting write timeout
// ...
//
// You should observe
package main package main
import ( import (
@@ -2,13 +2,15 @@
// //
// With stdin / stdout on client and server, simulating zfs send|recv piping // With stdin / stdout on client and server, simulating zfs send|recv piping
// //
// ./microbenchmark -appmode server | pv -r > /dev/null // ./microbenchmark -appmode server | pv -r > /dev/null
// ./microbenchmark -appmode client -direction recv < /dev/zero // ./microbenchmark -appmode client -direction recv < /dev/zero
//
// //
// Without the overhead of pipes (just protocol performance, mostly useful with perf bc no bw measurement) // Without the overhead of pipes (just protocol performance, mostly useful with perf bc no bw measurement)
// //
// ./microbenchmark -appmode client -direction recv -devnoopWriter -devnoopReader // ./microbenchmark -appmode client -direction recv -devnoopWriter -devnoopReader
// ./microbenchmark -appmode server -devnoopReader -devnoopWriter // ./microbenchmark -appmode server -devnoopReader -devnoopWriter
//
package main package main
import ( import (
+11 -7
View File
@@ -29,7 +29,7 @@ func WithLogger(ctx context.Context, log Logger) context.Context {
return context.WithValue(ctx, contextKeyLogger, log) return context.WithValue(ctx, contextKeyLogger, log)
} }
//nolint:deadcode,unused //nolint[:deadcode,unused]
func getLog(ctx context.Context) Logger { func getLog(ctx context.Context) Logger {
log, ok := ctx.Value(contextKeyLogger).(Logger) log, ok := ctx.Value(contextKeyLogger).(Logger)
if !ok { if !ok {
@@ -181,19 +181,23 @@ func (e *ReadStreamError) Error() string {
var _ net.Error = &ReadStreamError{} var _ net.Error = &ReadStreamError{}
func (e ReadStreamError) Timeout() bool { func (e ReadStreamError) netErr() net.Error {
if netErr, ok := e.Err.(net.Error); ok { if netErr, ok := e.Err.(net.Error); ok {
return netErr
}
return nil
}
func (e ReadStreamError) Timeout() bool {
if netErr := e.netErr(); netErr != nil {
return netErr.Timeout() return netErr.Timeout()
} }
return false return false
} }
// This function is deprecated in net.Error and since this
// function is not involved in .Accept() code path, nothing
// really needs this method to be here.
func (e ReadStreamError) Temporary() bool { func (e ReadStreamError) Temporary() bool {
if te, ok := e.Err.(interface{ Temporary() bool }); ok { if netErr := e.netErr(); netErr != nil {
return te.Temporary() return netErr.Temporary()
} }
return false return false
} }
+1 -6
View File
@@ -232,12 +232,7 @@ var _ net.Error = (*closeStateErrConnectionClosed)(nil)
func (e *closeStateErrConnectionClosed) Error() string { func (e *closeStateErrConnectionClosed) Error() string {
return "connection closed" return "connection closed"
} }
func (e *closeStateErrConnectionClosed) Timeout() bool { return false }
func (e *closeStateErrConnectionClosed) Timeout() bool { return false }
// This function is deprecated in net.Error and since this
// function is not involved in .Accept() code path, nothing
// really needs this method to be here.
func (e *closeStateErrConnectionClosed) Temporary() bool { return false } func (e *closeStateErrConnectionClosed) Temporary() bool { return false }
func (s *closeState) CloseEntry() error { func (s *closeState) CloseEntry() error {
+1 -1
View File
@@ -13,7 +13,7 @@ func init() {
} }
} }
//nolint:deadcode,unused //nolint[:deadcode,unused]
func debug(format string, args ...interface{}) { func debug(format string, args ...interface{}) {
if debugEnabled { if debugEnabled {
fmt.Fprintf(os.Stderr, "rpc/dataconn/stream: %s\n", fmt.Sprintf(format, args...)) fmt.Fprintf(os.Stderr, "rpc/dataconn/stream: %s\n", fmt.Sprintf(format, args...))
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.25.0 // protoc-gen-go v1.25.0
// protoc v3.14.0 // protoc v3.12.4
// source: grpcauth.proto // source: grpcauth.proto
package pdu package pdu
@@ -1,5 +1,25 @@
// Package netadaptor implements an adaptor from // Package netadaptor implements an adaptor from
// transport.AuthenticatedListener to net.Listener. // transport.AuthenticatedListener to net.Listener.
//
// In contrast to transport.AuthenticatedListener,
// net.Listener is commonly expected (e.g. by net/http.Server.Serve),
// to return errors that fulfill the Temporary interface:
// interface Temporary { Temporary() bool }
// Common behavior of packages consuming net.Listener is to return
// from the serve-loop if an error returned by Accept is not Temporary,
// i.e., does not implement the interface or is !net.Error.Temporary().
//
// The zrepl transport infrastructure was written with the
// idea that Accept() may return any kind of error, and the consumer
// would just log the error and continue calling Accept().
// We have to adapt these listeners' behavior to the expectations
// of net/http.Server.
//
// Hence, Listener does not return an error at all but blocks the
// caller of Accept() until we get a (successfully authenticated)
// connection without errors from the transport.
// Accept errors returned from the transport are logged as errors
// to the logger passed on initialization.
package netadaptor package netadaptor
import ( import (
+2 -2
View File
@@ -188,8 +188,8 @@ func (c *Client) WaitForConnectivity(ctx context.Context) error {
data, dataErr := c.dataClient.ReqPing(ctx, &req) data, dataErr := c.dataClient.ReqPing(ctx, &req)
// dataClient uses transport.Connecter, which doesn't expose WaitForReady(true) // dataClient uses transport.Connecter, which doesn't expose WaitForReady(true)
// => we need to mask dial timeouts // => we need to mask dial timeouts
if err, ok := dataErr.(interface{ Timeout() bool }); ok && err.Timeout() { if err, ok := dataErr.(interface{ Temporary() bool }); ok && err.Temporary() {
// Rate-limit pings here in case Timeout() == true is a mis-classification // Rate-limit pings here in case Temporary() is a mis-classification
// or returns immediately (this is a tight loop in that case) // or returns immediately (this is a tight loop in that case)
// TODO keep this in lockstep with controlClient // TODO keep this in lockstep with controlClient
// => don't use FailFast for control, but check that both control and data worked // => don't use FailFast for control, but check that both control and data worked
+1 -1
View File
@@ -14,7 +14,7 @@ func init() {
} }
} }
//nolint:deadcode,unused //nolint[:deadcode,unused]
func debug(format string, args ...interface{}) { func debug(format string, args ...interface{}) {
if debugEnabled { if debugEnabled {
fmt.Fprintf(os.Stderr, "rpc: %s\n", fmt.Sprintf(format, args...)) fmt.Fprintf(os.Stderr, "rpc: %s\n", fmt.Sprintf(format, args...))
+64 -62
View File
@@ -3,7 +3,7 @@
// The zrepl documentation refers to the client as the // The zrepl documentation refers to the client as the
// `active side` and to the server as the `passive side`. // `active side` and to the server as the `passive side`.
// //
// # Design Considerations // Design Considerations
// //
// zrepl has non-standard requirements to remote procedure calls (RPC): // zrepl has non-standard requirements to remote procedure calls (RPC):
// whereas the coordination of replication (the planning phase) mostly // whereas the coordination of replication (the planning phase) mostly
@@ -35,7 +35,7 @@
// //
// Hence, this package attempts to combine the best of both worlds: // Hence, this package attempts to combine the best of both worlds:
// //
// # GRPC for Coordination and Dataconn for Bulk Data Transfer // GRPC for Coordination and Dataconn for Bulk Data Transfer
// //
// This package's Client uses its transport.Connecter to maintain // This package's Client uses its transport.Connecter to maintain
// separate control and data connections to the Server. // separate control and data connections to the Server.
@@ -47,66 +47,68 @@
// The following ASCII diagram gives an overview of how the individual // The following ASCII diagram gives an overview of how the individual
// building blocks are glued together: // building blocks are glued together:
// //
// +------------+ // +------------+
// | rpc.Client | // | rpc.Client |
// +------------+ // +------------+
// | | // | |
// +--------+ +------------+ // +--------+ +------------+
// | | // | |
// +---------v-----------+ +--------v------+ // +---------v-----------+ +--------v------+
// |pdu.ReplicationClient| |dataconn.Client| // |pdu.ReplicationClient| |dataconn.Client|
// +---------------------+ +--------v------+ // +---------------------+ +--------v------+
// | label: label: | // | label: label: |
// | zrepl_control zrepl_data | // | zrepl_control zrepl_data |
// +--------+ +------------+ // +--------+ +------------+
// | | // | |
// +--v---------v---+ // +--v---------v---+
// | transportmux | // | transportmux |
// +-------+--------+ // +-------+--------+
// | uses // | uses
// +-------v--------+ // +-------v--------+
// |versionhandshake| // |versionhandshake|
// +-------+--------+ // +-------+--------+
// | uses // | uses
// +------v------+ // +------v------+
// | transport | // | transport |
// +------+------+ // +------+------+
// | // |
// NETWORK // NETWORK
// | // |
// +------+------+ // +------+------+
// | transport | // | transport |
// +------^------+ // +------^------+
// | uses // | uses
// +-------+--------+ // +-------+--------+
// |versionhandshake| // |versionhandshake|
// +-------^--------+ // +-------^--------+
// | uses // | uses
// +-------+--------+ // +-------+--------+
// | transportmux | // | transportmux |
// +--^--------^----+ // +--^--------^----+
// | | // | |
// +--------+ --------------+ --- // +--------+ --------------+ ---
// | | | // | | |
// | label: label: | | // | label: label: | |
// | zrepl_control zrepl_data | | // | zrepl_control zrepl_data | |
// +-----+----+ +-----------+---+ | // +-----+----+ +-----------+---+ |
// |netadaptor| |dataconn.Server| | rpc.Server // |netadaptor| |dataconn.Server| | rpc.Server
// | + | +------+--------+ | // | + | +------+--------+ |
// |grpcclient| | | // |grpcclient| | |
// |identity | | | // |identity | | |
// +-----+----+ | | // +-----+----+ | |
// | | | // | | |
// +---------v-----------+ | | // +---------v-----------+ | |
// |pdu.ReplicationServer| | | // |pdu.ReplicationServer| | |
// +---------+-----------+ | | // +---------+-----------+ | |
// | | --- // | | ---
// +----------+ +------------+ // +----------+ +------------+
// | | // | |
// +---v--v-----+ // +---v--v-----+
// | Handler | // | Handler |
// +------------+ // +------------+
// (usually endpoint.{Sender,Receiver}) // (usually endpoint.{Sender,Receiver})
//
//
package rpc package rpc
// edit trick for the ASCII art above: // edit trick for the ASCII art above:
+1 -1
View File
@@ -9,7 +9,7 @@ import (
type Logger = logger.Logger type Logger = logger.Logger
// All fields must be non-nil /// All fields must be non-nil
type Loggers struct { type Loggers struct {
General Logger General Logger
Control Logger Control Logger
+3 -42
View File
@@ -33,47 +33,8 @@ var _ net.Error = &HandshakeError{}
func (e HandshakeError) Error() string { return e.msg } func (e HandshakeError) Error() string { return e.msg }
// When a net.Listener.Accept() returns an error, the server must // Like with net.OpErr (Go issue 6163), a client failing to handshake
// decide whether to retry calling Accept() or not. // should be a temporary Accept error toward the Listener .
// On some platforms (e.g., Linux), Accept() can return errors
// related to the specific protocol connection that was supposed
// to be returned as asocket FD. Obviously, we want to ignore,
// maybe log, those errors and retry Accept() immediately to
// serve other connections.
// But there are also conditions where we get Accept() errors because
// the process has run out of file descriptors. In that case, retrying
// won't help. We need to close some file descriptor to make progress.
// Note that there could be lots of open file descriptors because we
// have accepted, and not yet closed, lots of connections in the past.
// And then, of course there can be errors where we just want
// to return, e.g., if there's a programming error and we're getting
// an EBADFD or whatever.
//
// So, the serve loops in net/http.Server.Serve() or gRPC's server.Serve()
// must inspect the error and decide what to do.
// The vehicle for this is the
//
// interface { Temporary() bool }
//
// Behavior in both of the aforementioned Serve() loops:
//
// - if the error doesn't implement the interface, stop serving and return
// - `Temporary() == true`: retry with back-off
// - `Temporary() == false`: stop serving and return
//
// So, to make this package's HandshakeListener work with these
// Serve() loops, we return Temporary() == true if the handshake fails.
// In the aforementioned categories, that's the case of a per-connection
// protocol error.
//
// Note: the net.Error interface has deprecated the Temporary() method
// in go.dev/issue/45729, but there is no replacement for users of .Accept().
// Existing users of .Accept() continue to check for the interface.
// So, we need to continue supporting Temporary() until there's a different
// mechanism for serve loops to decide whether to retry or not.
// The following mailing list post proposes to eliminate the retries
// completely, but it seems like the effort has stalled.
// https://groups.google.com/g/golang-nuts/c/-JcZzOkyqYI/m/xwaZzjCgAwAJ
func (e HandshakeError) Temporary() bool { func (e HandshakeError) Temporary() bool {
if e.isAcceptError { if e.isAcceptError {
return true return true
@@ -191,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, 7) return DoHandshakeVersion(conn, deadline, 6)
} }
const HandshakeMessageMaxLen = 16 * 4096 const HandshakeMessageMaxLen = 16 * 4096
+7 -6
View File
@@ -3,12 +3,13 @@
// //
// Intended Usage // Intended Usage
// //
// defer s.lock().unlock() // defer s.lock().unlock()
// // drop lock while waiting for wait group // // drop lock while waiting for wait group
// func() { // func() {
// defer a.l.Unlock().Lock() // defer a.l.Unlock().Lock()
// fssesDone.Wait() // fssesDone.Wait()
// }() // }()
//
package chainlock package chainlock
import "sync" import "sync"
+12 -11
View File
@@ -7,25 +7,26 @@
// //
// Example: // Example:
// //
// type Config struct { // type Config struct {
// // This field must be set to a non-nil value, // // This field must be set to a non-nil value,
// // forcing the caller to make their mind up // // forcing the caller to make their mind up
// // about this field. // // about this field.
// CriticalSetting *nodefault.Bool // CriticalSetting *nodefault.Bool
// } // }
// //
// An function that takes such a Config should _not_ check for nil-ness: // An function that takes such a Config should _not_ check for nil-ness:
// and instead unconditionally dereference: // and instead unconditionally dereference:
// //
// func f(c Config) { // func f(c Config) {
// if (c.CriticalSetting) { } // if (c.CriticalSetting) { }
// } // }
// //
// If the caller of f forgot to specify the .CriticalSetting // If the caller of f forgot to specify the .CriticalSetting
// field, the Go runtime will issue a nil-pointer deref panic // field, the Go runtime will issue a nil-pointer deref panic
// and it'll be clear that the caller did not read the docs of Config. // and it'll be clear that the caller did not read the docs of Config.
// //
// f(Config{}) // crashes // f(Config{}) // crashes
//
// f Config{ CriticalSetting: &nodefault.Bool{B: false}} // doesn't crash
// //
// f Config{ CriticalSetting: &nodefault.Bool{B: false}} // doesn't crash
package nodefault package nodefault
@@ -1,96 +0,0 @@
package suspendresumesafetimer
import (
"context"
"time"
"github.com/zrepl/zrepl/util/envconst"
)
// The returned error is guaranteed to be the ctx.Err()
func SleepUntil(ctx context.Context, sleepUntil time.Time) error {
// We use .Round(0) to strip the monotonic clock reading from the time.Time
// returned by time.Now(). That will make the before/after check in the ticker
// for-loop compare wall-clock times instead of monotonic time.
// Comparing wall clock time is necessary because monotonic time does not progress
// while the system is suspended.
//
// Background
//
// A time.Time carries a wallclock timestamp and optionally a monotonic clock timestamp.
// time.Now() returns a time.Time that carries both.
// time.Time.Add() applies the same delta to both timestamps in the time.Time.
// x.Sub(y) will return the *monotonic* delta if both x and y carry a monotonic timestamp.
// time.Until(x) == x.Sub(now) where `now` will have a monotonic timestamp.
// So, time.Until(x) with an `x` that has monotonic timestamp will return monotonic delta.
//
// Why Do We Care?
//
// On systems that suspend/resume, wall clock time progresses during suspend but
// monotonic time does not.
//
// So, suppose the following sequence of events:
// x <== time.Now()
// System suspends for 1 hour
// delta <== time.Now().Sub(x)
// `delta` will be near 0 because time.Until() subtracts the monotonic
// timestamps, and monotonic time didn't progress during suspend.
//
// Now strip the timestamp using .Round(0)
// x <== time.Now().Round(0)
// System suspends for 1 hour
// delta <== time.Now().Sub(x)
// `delta` will be 1 hour because time.Sub() subtracted wallclock timestamps
// because x didn't have a monotonic timestamp because we stripped it using .Round(0).
//
//
sleepUntil = sleepUntil.Round(0)
// Set up a timer so that, if the system doesn't suspend/resume,
// we get a precise wake-up time from the native Go timer.
monotonicClockTimer := time.NewTimer(time.Until(sleepUntil))
defer func() {
if !monotonicClockTimer.Stop() {
// non-blocking read since we can come here when
// we've already drained the channel through
// case <-monotonicClockTimer.C
// in the `for` loop below.
select {
case <-monotonicClockTimer.C:
default:
}
}
}()
// Set up a ticker so that we're guaranteed to wake up periodically.
// We'll then get the current wall-clock time and check ourselves
// whether we're past the requested expiration time.
// Pick a 10 second check interval by default since it's rare that
// suspend/resume is done more frequently.
ticker := time.NewTicker(envconst.Duration("ZREPL_WALLCLOCKTIMER_MAX_DELAY", 10*time.Second))
defer ticker.Stop()
for {
select {
case <-monotonicClockTimer.C:
return nil
case <-ticker.C:
now := time.Now()
if now.Before(sleepUntil) {
// Continue waiting.
// Reset the monotonic timer to reset drift.
if !monotonicClockTimer.Stop() {
<-monotonicClockTimer.C
}
monotonicClockTimer.Reset(time.Until(sleepUntil))
continue
}
return nil
case <-ctx.Done():
return ctx.Err()
}
}
}
+3 -2
View File
@@ -53,6 +53,7 @@ var componentValidChar = regexp.MustCompile(`^[0-9a-zA-Z-_\.: ]+$`)
// characters: // characters:
// //
// [-_.: ] // [-_.: ]
//
func ComponentNamecheck(datasetPathComponent string) error { func ComponentNamecheck(datasetPathComponent string) error {
if len(datasetPathComponent) == 0 { if len(datasetPathComponent) == 0 {
return fmt.Errorf("path component must not be empty") return fmt.Errorf("path component must not be empty")
@@ -90,8 +91,8 @@ func (e *PathValidationError) Error() string {
// combines // combines
// //
// lib/libzfs/libzfs_dataset.c: zfs_validate_name // lib/libzfs/libzfs_dataset.c: zfs_validate_name
// module/zcommon/zfs_namecheck.c: entity_namecheck // module/zcommon/zfs_namecheck.c: entity_namecheck
// //
// The '%' character is not allowed because it's reserved for zfs-internal use // The '%' character is not allowed because it's reserved for zfs-internal use
func EntityNamecheck(path string, t EntityType) (err *PathValidationError) { func EntityNamecheck(path string, t EntityType) (err *PathValidationError) {
+20 -73
View File
@@ -343,6 +343,7 @@ const (
type SendStream struct { type SendStream struct {
cmd *zfscmd.Cmd cmd *zfscmd.Cmd
kill context.CancelFunc
stdoutReader io.ReadCloser // not *os.File for mocking during platformtest stdoutReader io.ReadCloser // not *os.File for mocking during platformtest
stderrBuf *circlog.CircularLog stderrBuf *circlog.CircularLog
@@ -415,29 +416,8 @@ func (s *SendStream) killAndWait() error {
debug("sendStream: killAndWait enter") debug("sendStream: killAndWait enter")
defer debug("sendStream: killAndWait leave") defer debug("sendStream: killAndWait leave")
// ensure this function is only called once
if s.state != sendStreamOpen {
panic(s.state)
}
// send SIGKILL // send SIGKILL
// In an earlier version, we used the starting context.Context's cancel function s.kill()
// for this, but in Go > 1.19, doing so will cause .Wait() to return the
// context cancel error instead of the *exec.ExitError.
err := s.cmd.Process().Kill()
if err != nil {
if err == os.ErrProcessDone {
// This can happen if
// (1) the process has already been .Wait()ed, or
// (2) some other goroutine cancels the context, likely further up
// the context tree.
// Case (1) can't happen to us because we only call
// this function in sendStreamOpen state.
// In Case (2), it's still our job to .Wait(), so, fallthrough.
} else {
return err
}
}
// Close our read-end of the pipe. // Close our read-end of the pipe.
// //
@@ -652,14 +632,6 @@ func (e ZFSSendArgsValidationError) Error() string {
return e.Msg.Error() return e.Msg.Error()
} }
type zfsSendArgsSkipValidationKeyType struct{}
var zfsSendArgsSkipValidationKey = zfsSendArgsSkipValidationKeyType{}
func ZFSSendArgsSkipValidation(ctx context.Context) context.Context {
return context.WithValue(ctx, zfsSendArgsSkipValidationKey, true)
}
// - Recursively call Validate on each field. // - Recursively call Validate on each field.
// - Make sure that if ResumeToken != "", it reflects the same operation as the other parameters would. // - Make sure that if ResumeToken != "", it reflects the same operation as the other parameters would.
// //
@@ -687,16 +659,6 @@ func (a ZFSSendArgsUnvalidated) Validate(ctx context.Context) (v ZFSSendArgsVali
// fallthrough // fallthrough
} }
validated := ZFSSendArgsValidated{
ZFSSendArgsUnvalidated: a,
FromVersion: fromVersion,
ToVersion: toVersion,
}
if ctx.Value(zfsSendArgsSkipValidationKey) != nil {
return validated, nil
}
if err := a.ZFSSendFlags.Validate(); err != nil { if err := a.ZFSSendFlags.Validate(); err != nil {
return v, newGenericValidationError(a, errors.Wrap(err, "send flags invalid")) return v, newGenericValidationError(a, errors.Wrap(err, "send flags invalid"))
} }
@@ -711,19 +673,18 @@ func (a ZFSSendArgsUnvalidated) Validate(ctx context.Context) (v ZFSSendArgsVali
if a.Encrypted.B && !fsEncrypted { if a.Encrypted.B && !fsEncrypted {
return v, newValidationError(a, ZFSSendArgsEncryptedSendRequestedButFSUnencrypted, return v, newValidationError(a, ZFSSendArgsEncryptedSendRequestedButFSUnencrypted,
errors.Errorf("encrypted send mandated by policy, but filesystem %q is not encrypted", a.FS)) errors.Errorf("encrypted send requested, but filesystem %q is not encrypted", a.FS))
}
if a.Raw && fsEncrypted && !a.Encrypted.B {
return v, newValidationError(a, ZFSSendArgsGenericValidationError,
errors.Errorf("policy mandates raw+unencrypted sends, but filesystem %q is encrypted", a.FS))
} }
if err := a.validateEncryptionFlagsCorrespondToResumeToken(ctx, valCtx); err != nil { if err := a.validateEncryptionFlagsCorrespondToResumeToken(ctx, valCtx); err != nil {
return v, newValidationError(a, ZFSSendArgsResumeTokenMismatch, err) return v, newValidationError(a, ZFSSendArgsResumeTokenMismatch, err)
} }
return validated, nil return ZFSSendArgsValidated{
ZFSSendArgsUnvalidated: a,
FromVersion: fromVersion,
ToVersion: toVersion,
}, nil
} }
func (f ZFSSendFlags) Validate() error { func (f ZFSSendFlags) Validate() error {
@@ -891,34 +852,21 @@ func (a ZFSSendArgsUnvalidated) validateEncryptionFlagsCorrespondToResumeToken(c
return gen.fmt("resume token `toguid` != expected: %v != %v", t.ToGUID, a.To.GUID) return gen.fmt("resume token `toguid` != expected: %v != %v", t.ToGUID, a.To.GUID)
} }
// ensure resume stream will be encrypted/unencrypted as specified in policy
if err := valCtx.encEnabled.ValidateNoDefault(); err != nil {
panic(valCtx)
}
wouldSendEncryptedIfFilesystemIsEncrypted := t.RawOK
filesystemIsEncrypted := valCtx.encEnabled.B
resumeWillBeEncryptedSend := filesystemIsEncrypted && wouldSendEncryptedIfFilesystemIsEncrypted
if a.Encrypted.B { if a.Encrypted.B {
if resumeWillBeEncryptedSend { if !(t.RawOK && t.CompressOK) {
return nil // encrypted send in policy, and that's what's going to happen
} else {
if !filesystemIsEncrypted {
// NB: a.Encrypted.B && !valCtx.encEnabled.B
// is handled in the caller, because it doesn't concern the resume token (different kind of error)
panic("caller should have already raised an error")
}
// XXX we have no test coverage for this case. We'd need to forge a resume token for that.
return ZFSSendArgsResumeTokenMismatchEncryptionNotSet.fmt( return ZFSSendArgsResumeTokenMismatchEncryptionNotSet.fmt(
"resume token does not have rawok=true which would result in an unencrypted send, but policy mandates encrypted sends only") "resume token must have `rawok` and `compressok` = true but got %v %v", t.RawOK, t.CompressOK)
} }
// fallthrough
} else { } else {
if resumeWillBeEncryptedSend { if t.RawOK || t.CompressOK {
return ZFSSendArgsResumeTokenMismatchEncryptionSet.fmt( return ZFSSendArgsResumeTokenMismatchEncryptionSet.fmt(
"resume token has rawok=true which would result in encrypted send, but policy mandates unencrypted sends only") "resume token must not have `rawok` or `compressok` set but got %v %v", t.RawOK, t.CompressOK)
} else {
return nil // unencrypted send in policy, and that's what's going to happen
} }
// fallthrough
} }
return nil
} }
var zfsSendStderrCaptureMaxSize = envconst.Int("ZREPL_ZFS_SEND_STDERR_MAX_CAPTURE_SIZE", 1<<15) var zfsSendStderrCaptureMaxSize = envconst.Int("ZREPL_ZFS_SEND_STDERR_MAX_CAPTURE_SIZE", 1<<15)
@@ -983,10 +931,10 @@ func ZFSSend(ctx context.Context, sendArgs ZFSSendArgsValidated) (*SendStream, e
stream := &SendStream{ stream := &SendStream{
cmd: cmd, cmd: cmd,
kill: cancel,
stdoutReader: stdoutReader, stdoutReader: stdoutReader,
stderrBuf: stderrBuf, stderrBuf: stderrBuf,
} }
_ = cancel // the SendStream.killAndWait() will kill the process
return stream, nil return stream, nil
} }
@@ -1042,10 +990,8 @@ func (s *DrySendInfo) unmarshalZFSOutput(output []byte) (err error) {
} }
// unmarshal info line, looks like this: // unmarshal info line, looks like this:
// // full zroot/test/a@1 5389768
// full zroot/test/a@1 5389768 // incremental zroot/test/a@1 zroot/test/a@2 5383936
// incremental zroot/test/a@1 zroot/test/a@2 5383936
//
// => see test cases // => see test cases
func (s *DrySendInfo) unmarshalInfoLine(l string) (regexMatched bool, err error) { func (s *DrySendInfo) unmarshalInfoLine(l string) (regexMatched bool, err error) {
@@ -1877,6 +1823,7 @@ var ErrBookmarkCloningNotSupported = fmt.Errorf("bookmark cloning feature is not
// unless a bookmark with the name `bookmark` exists and has the same idenitty (zfs.FilesystemVersionEqualIdentity) // unless a bookmark with the name `bookmark` exists and has the same idenitty (zfs.FilesystemVersionEqualIdentity)
// //
// v must be validated by the caller // v must be validated by the caller
//
func ZFSBookmark(ctx context.Context, fs string, v FilesystemVersion, bookmark string) (bm FilesystemVersion, err error) { func ZFSBookmark(ctx context.Context, fs string, v FilesystemVersion, bookmark string) (bm FilesystemVersion, err error) {
bm = FilesystemVersion{ bm = FilesystemVersion{
+1 -1
View File
@@ -13,7 +13,7 @@ func init() {
} }
} }
//nolint:deadcode,unused //nolint[:deadcode,unused]
func debug(format string, args ...interface{}) { func debug(format string, args ...interface{}) {
if debugEnabled { if debugEnabled {
fmt.Fprintf(os.Stderr, "zfs: %s\n", fmt.Sprintf(format, args...)) fmt.Fprintf(os.Stderr, "zfs: %s\n", fmt.Sprintf(format, args...))