Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4a0104a44f | |||
| f0146d03d3 | |||
| 742e28d2a3 | |||
| 58f364dc04 | |||
| a5c9450b2d | |||
| b222574ff3 | |||
| 28e1c486f5 | |||
| 82370f3555 | |||
| 4f6c57fbba | |||
| 5a558e3b71 | |||
| b33f670b9d |
@@ -98,16 +98,14 @@ jobs:
|
||||
-X POST \
|
||||
-d '{"context":"zrepl/publish-ci-artifacts", "state": "success", "description":"CI Build Artifacts for '"$JOB_NAME"'", "target_url":"https://minio.cschwarz.com/minio/zrepl-ci-artifacts/'"$COMMIT"'/"}'
|
||||
|
||||
# kick off binary packaging workflow
|
||||
- run:
|
||||
shell: /bin/bash -eo pipefail
|
||||
shell: /bin/bash -euo pipefail
|
||||
command: |
|
||||
if [ -n "$CIRCLE_PR_NUMBER" ]; then # CIRCLE_PR_NUMBER is guaranteed to be only present in forked PRs (external)
|
||||
echo "Forked PR detected. Sry, can't trust you with credentials."
|
||||
exit 0
|
||||
fi
|
||||
set -u # from now on
|
||||
GITHUB_ACCESS_TOKEN="$ZREPL_DEBIAN_BINARYPACKAGIN_TRIGGER_BUILD_GITHUB_TOKEN" .circleci/trigger_debian_binary_packaging_workflow.bash "$CIRCLE_SHA1" "${CIRCLE_JOB##build-}"
|
||||
# Trigger Debian Package Build
|
||||
curl -v -X POST https://api.github.com/repos/zrepl/debian-binary-packaging/dispatches \
|
||||
-H 'Accept: application/vnd.github.v3+json' \
|
||||
-H "Authorization: token $ZREPL_DEBIAN_BINARYPACKAGIN_TRIGGER_BUILD_GITHUB_TOKEN" \
|
||||
--data '{"event_type": "push", "client_payload": { "zrepl_main_repo_commit": "'"$CIRCLE_SHA1"'", "go_version": "'"${CIRCLE_JOB##build-}"'" }}'
|
||||
|
||||
build-1.11:
|
||||
<<: *build-latest
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
COMMIT="$1"
|
||||
GO_VERSION="$2"
|
||||
|
||||
curl -v -X POST https://api.github.com/repos/zrepl/debian-binary-packaging/dispatches \
|
||||
-H 'Accept: application/vnd.github.v3+json' \
|
||||
-H "Authorization: token $GITHUB_ACCESS_TOKEN" \
|
||||
--data '{"event_type": "push", "client_payload": { "zrepl_main_repo_commit": "'"$COMMIT"'", "go_version": "'"$GO_VERSION"'" }}'
|
||||
@@ -1,4 +1,3 @@
|
||||
github: problame
|
||||
patreon: zrepl
|
||||
liberapay: zrepl
|
||||
custom: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=R5QSXJVYHGX96
|
||||
|
||||
@@ -147,26 +147,11 @@ zrepl-bin:
|
||||
generate-platform-test-list:
|
||||
$(GO_BUILD) -o $(ARTIFACTDIR)/generate-platform-test-list ./platformtest/tests/gen
|
||||
|
||||
COVER_PLATFORM_BIN_PATH := $(ARTIFACTDIR)/platformtest-cover-$(ZREPL_TARGET_TUPLE)
|
||||
cover-platform-bin:
|
||||
test-platform-bin:
|
||||
$(GO_ENV_VARS) $(GO) test $(GO_BUILDFLAGS) \
|
||||
-c -o "$(COVER_PLATFORM_BIN_PATH)" \
|
||||
-c -o "$(ARTIFACTDIR)/platformtest-$(ZREPL_TARGET_TUPLE)" \
|
||||
-covermode=atomic -cover -coverpkg github.com/zrepl/zrepl/... \
|
||||
./platformtest/harness
|
||||
cover-platform:
|
||||
# do not track dependency on cover-platform-bin to allow build of binary outside of test VM
|
||||
export _TEST_PLATFORM_CMD="$(COVER_PLATFORM_BIN_PATH) \
|
||||
-test.coverprofile \"$(ARTIFACTDIR)/platformtest.cover\" \
|
||||
-test.v \
|
||||
__DEVEL--i-heard-you-like-tests"; \
|
||||
$(MAKE) _test-or-cover-platform-impl
|
||||
|
||||
TEST_PLATFORM_BIN_PATH := $(ARTIFACTDIR)/platformtest-$(ZREPL_TARGET_TUPLE)
|
||||
test-platform-bin:
|
||||
$(GO_BUILD) -o "$(TEST_PLATFORM_BIN_PATH)" ./platformtest/harness
|
||||
test-platform:
|
||||
export _TEST_PLATFORM_CMD="\"$(TEST_PLATFORM_BIN_PATH)\""; \
|
||||
$(MAKE) _test-or-cover-platform-impl
|
||||
|
||||
ZREPL_PLATFORMTEST_POOLNAME := zreplplatformtest
|
||||
ZREPL_PLATFORMTEST_IMAGEPATH := /tmp/zreplplatformtest.pool.img
|
||||
@@ -174,14 +159,14 @@ ZREPL_PLATFORMTEST_MOUNTPOINT := /tmp/zreplplatformtest.pool
|
||||
ZREPL_PLATFORMTEST_ZFS_LOG := /tmp/zreplplatformtest.zfs.log
|
||||
# ZREPL_PLATFORMTEST_STOP_AND_KEEP := -failure.stop-and-keep-pool
|
||||
ZREPL_PLATFORMTEST_ARGS :=
|
||||
_test-or-cover-platform-impl: $(ARTIFACTDIR)
|
||||
ifndef _TEST_PLATFORM_CMD
|
||||
$(error _TEST_PLATFORM_CMD is undefined, caller 'cover-platform' or 'test-platform' should have defined it)
|
||||
endif
|
||||
test-platform: $(ARTIFACTDIR) # do not track dependency on test-platform-bin to allow build of platformtest outside of test VM
|
||||
rm -f "$(ZREPL_PLATFORMTEST_ZFS_LOG)"
|
||||
rm -f "$(ARTIFACTDIR)/platformtest.cover"
|
||||
platformtest/logmockzfs/logzfsenv "$(ZREPL_PLATFORMTEST_ZFS_LOG)" `which zfs` \
|
||||
$(_TEST_PLATFORM_CMD) \
|
||||
"$(ARTIFACTDIR)/platformtest-$(ZREPL_TARGET_TUPLE)" \
|
||||
-test.coverprofile "$(ARTIFACTDIR)/platformtest.cover" \
|
||||
-test.v \
|
||||
__DEVEL--i-heard-you-like-tests \
|
||||
-poolname "$(ZREPL_PLATFORMTEST_POOLNAME)" \
|
||||
-imagepath "$(ZREPL_PLATFORMTEST_IMAGEPATH)" \
|
||||
-mountpoint "$(ZREPL_PLATFORMTEST_MOUNTPOINT)" \
|
||||
@@ -193,11 +178,10 @@ cover-merge: $(ARTIFACTDIR)
|
||||
cover-html: cover-merge
|
||||
$(GO) tool cover -html "$(ARTIFACTDIR)/merged.cover" -o "$(ARTIFACTDIR)/merged.cover.html"
|
||||
|
||||
cover-full:
|
||||
test-full:
|
||||
test "$$(id -u)" = "0" || echo "MUST RUN AS ROOT" 1>&2
|
||||
$(MAKE) test-go COVER=1
|
||||
$(MAKE) cover-platform-bin
|
||||
$(MAKE) cover-platform
|
||||
$(MAKE) test-platform
|
||||
$(MAKE) cover-html
|
||||
|
||||
##################### DEV TARGETS #####################
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
[](https://golang.org/)
|
||||
[](https://zrepl.github.io)
|
||||
[](https://www.patreon.com/zrepl)
|
||||
[](https://github.com/sponsors/problame)
|
||||
[](https://liberapay.com/zrepl/donate)
|
||||
[](https://liberapay.com/zrepl/donate)
|
||||
[](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=R5QSXJVYHGX96)
|
||||
[](https://twitter.com/intent/tweet?text=Wow:&url=https%3A%2F%2Fgithub.com%2Fzrepl%2Fzrepl)
|
||||
|
||||
@@ -21,7 +20,7 @@ zrepl is a one-stop ZFS backup & replication solution.
|
||||
|
||||
## Feature Requests
|
||||
|
||||
1. Does your feature request require default values / some kind of configuration?
|
||||
1. Does you feature request require default values / some kind of configuration?
|
||||
If so, think of an expressive configuration example.
|
||||
2. Think of at least one use case that generalizes from your concrete application.
|
||||
3. Open an issue on GitHub with example conf & use case attached.
|
||||
|
||||
@@ -63,7 +63,7 @@ func doZabsList(ctx context.Context, sc *cli.Subcommand, args []string) error {
|
||||
defer line.Lock().Unlock()
|
||||
if zabsListFlags.Json {
|
||||
enc.SetIndent("", " ")
|
||||
if err := enc.Encode(a); err != nil {
|
||||
if err := enc.Encode(abstractions); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Println()
|
||||
|
||||
+21
-27
@@ -52,12 +52,11 @@ func (j JobEnum) Name() string {
|
||||
}
|
||||
|
||||
type ActiveJob struct {
|
||||
Type string `yaml:"type"`
|
||||
Name string `yaml:"name"`
|
||||
Connect ConnectEnum `yaml:"connect"`
|
||||
Pruning PruningSenderReceiver `yaml:"pruning"`
|
||||
Debug JobDebugSettings `yaml:"debug,optional"`
|
||||
Replication *Replication `yaml:"replication,optional,fromdefaults"`
|
||||
Type string `yaml:"type"`
|
||||
Name string `yaml:"name"`
|
||||
Connect ConnectEnum `yaml:"connect"`
|
||||
Pruning PruningSenderReceiver `yaml:"pruning"`
|
||||
Debug JobDebugSettings `yaml:"debug,optional"`
|
||||
}
|
||||
|
||||
type PassiveJob struct {
|
||||
@@ -77,7 +76,13 @@ type SnapJob struct {
|
||||
}
|
||||
|
||||
type SendOptions struct {
|
||||
Encrypted bool `yaml:"encrypted,optional,default=false"`
|
||||
Encrypted bool `yaml:"encrypted"`
|
||||
}
|
||||
|
||||
var _ yaml.Defaulter = (*SendOptions)(nil)
|
||||
|
||||
func (l *SendOptions) SetDefault() {
|
||||
*l = SendOptions{Encrypted: false}
|
||||
}
|
||||
|
||||
type RecvOptions struct {
|
||||
@@ -88,13 +93,10 @@ type RecvOptions struct {
|
||||
// Reencrypt bool `yaml:"reencrypt"`
|
||||
}
|
||||
|
||||
type Replication struct {
|
||||
Protection *ReplicationOptionsProtection `yaml:"protection,optional,fromdefaults"`
|
||||
}
|
||||
var _ yaml.Defaulter = (*RecvOptions)(nil)
|
||||
|
||||
type ReplicationOptionsProtection struct {
|
||||
Initial string `yaml:"initial,optional,default=guarantee_resumability"`
|
||||
Incremental string `yaml:"incremental,optional,default=guarantee_resumability"`
|
||||
func (l *RecvOptions) SetDefault() {
|
||||
*l = RecvOptions{}
|
||||
}
|
||||
|
||||
type PushJob struct {
|
||||
@@ -104,9 +106,6 @@ type PushJob struct {
|
||||
Send *SendOptions `yaml:"send,fromdefaults,optional"`
|
||||
}
|
||||
|
||||
func (j *PushJob) GetFilesystems() FilesystemsFilter { return j.Filesystems }
|
||||
func (j *PushJob) GetSendOptions() *SendOptions { return j.Send }
|
||||
|
||||
type PullJob struct {
|
||||
ActiveJob `yaml:",inline"`
|
||||
RootFS string `yaml:"root_fs"`
|
||||
@@ -114,10 +113,6 @@ type PullJob struct {
|
||||
Recv *RecvOptions `yaml:"recv,fromdefaults,optional"`
|
||||
}
|
||||
|
||||
func (j *PullJob) GetRootFS() string { return j.RootFS }
|
||||
func (j *PullJob) GetAppendClientIdentity() bool { return false }
|
||||
func (j *PullJob) GetRecvOptions() *RecvOptions { return j.Recv }
|
||||
|
||||
type PositiveDurationOrManual struct {
|
||||
Interval time.Duration
|
||||
Manual bool
|
||||
@@ -155,10 +150,6 @@ type SinkJob struct {
|
||||
Recv *RecvOptions `yaml:"recv,optional,fromdefaults"`
|
||||
}
|
||||
|
||||
func (j *SinkJob) GetRootFS() string { return j.RootFS }
|
||||
func (j *SinkJob) GetAppendClientIdentity() bool { return true }
|
||||
func (j *SinkJob) GetRecvOptions() *RecvOptions { return j.Recv }
|
||||
|
||||
type SourceJob struct {
|
||||
PassiveJob `yaml:",inline"`
|
||||
Snapshotting SnapshottingEnum `yaml:"snapshotting"`
|
||||
@@ -166,9 +157,6 @@ type SourceJob struct {
|
||||
Send *SendOptions `yaml:"send,optional,fromdefaults"`
|
||||
}
|
||||
|
||||
func (j *SourceJob) GetFilesystems() FilesystemsFilter { return j.Filesystems }
|
||||
func (j *SourceJob) GetSendOptions() *SendOptions { return j.Send }
|
||||
|
||||
type FilesystemsFilter map[string]bool
|
||||
|
||||
type SnapshottingEnum struct {
|
||||
@@ -321,6 +309,11 @@ type PruneKeepNotReplicated struct {
|
||||
KeepSnapshotAtCursor bool `yaml:"keep_snapshot_at_cursor,optional,default=true"`
|
||||
}
|
||||
|
||||
type PruneKeepStepHolds struct {
|
||||
Type string `yaml:"type"`
|
||||
AdditionalJobIds []string `yaml:"additional_job_ids,optional"`
|
||||
}
|
||||
|
||||
type PruneKeepLastN struct {
|
||||
Type string `yaml:"type"`
|
||||
Count int `yaml:"count"`
|
||||
@@ -492,6 +485,7 @@ func (t *ServeEnum) UnmarshalYAML(u func(interface{}, bool) error) (err error) {
|
||||
|
||||
func (t *PruningEnum) UnmarshalYAML(u func(interface{}, bool) error) (err error) {
|
||||
t.Ret, err = enumUnmarshal(u, map[string]interface{}{
|
||||
"step_holds": &PruneKeepStepHolds{},
|
||||
"not_replicated": &PruneKeepNotReplicated{},
|
||||
"last_n": &PruneKeepLastN{},
|
||||
"grid": &PruneGrid{},
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/zrepl/yaml-config"
|
||||
)
|
||||
|
||||
type A struct {
|
||||
B *B `yaml:"b,optional,fromdefaults"`
|
||||
A1 string `yaml:"a1,optional"`
|
||||
}
|
||||
|
||||
type B struct {
|
||||
C *C `yaml:"c,optional,fromdefaults"`
|
||||
D string `yaml:"d,default=ddd"`
|
||||
E string `yaml:"e,optional"`
|
||||
}
|
||||
|
||||
type C struct {
|
||||
Q string `yaml:"q,optional"`
|
||||
R string `yaml:"r,default=r"`
|
||||
}
|
||||
|
||||
func TestDepFromDefaults(t *testing.T) {
|
||||
|
||||
type testcase struct {
|
||||
name string
|
||||
yaml string
|
||||
expect *A
|
||||
}
|
||||
|
||||
tcs := []testcase{
|
||||
{
|
||||
name: "empty",
|
||||
yaml: `{}`,
|
||||
expect: &A{
|
||||
B: &B{
|
||||
C: &C{
|
||||
R: "r",
|
||||
},
|
||||
D: "ddd",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "a1 set",
|
||||
yaml: `{"a1":"blah"}`,
|
||||
expect: &A{
|
||||
A1: "blah",
|
||||
B: &B{
|
||||
C: &C{
|
||||
R: "r",
|
||||
},
|
||||
D: "ddd",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "D set",
|
||||
yaml: `
|
||||
b:
|
||||
d: 4d
|
||||
`,
|
||||
expect: &A{
|
||||
B: &B{
|
||||
D: "4d",
|
||||
C: &C{
|
||||
R: "r",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for tci := range tcs {
|
||||
t.Run(fmt.Sprintf("%d-%s", tci, tcs[tci].name), func(t *testing.T) {
|
||||
tc := tcs[tci]
|
||||
|
||||
var a A
|
||||
err := yaml.UnmarshalStrict([]byte(tc.yaml), &a)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, tc.expect, &a)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
@@ -60,9 +60,9 @@ jobs:
|
||||
})
|
||||
|
||||
t.Run("encrypted_unspecified", func(t *testing.T) {
|
||||
c = testValidConfig(t, fill(encrypted_unspecified))
|
||||
encrypted := c.Jobs[0].Ret.(*PushJob).Send.Encrypted
|
||||
assert.Equal(t, false, encrypted)
|
||||
c, err := testConfig(t, fill(encrypted_unspecified))
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, c)
|
||||
})
|
||||
|
||||
t.Run("send_not_specified", func(t *testing.T) {
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
# This config serves as an example for a local zrepl installation that
|
||||
# backups the entire zpool `system` to `backuppool/zrepl/sink`
|
||||
#
|
||||
# The requirements covered by this setup are described in the zrepl documentation's
|
||||
# quick start section which inlines this example.
|
||||
#
|
||||
# CUSTOMIZATIONS YOU WILL LIKELY WANT TO APPLY:
|
||||
# - adjust the name of the production pool `system` in the `filesystems` filter of jobs `snapjob` and `push_to_derive`
|
||||
# - adjust the name of the backup pool `backuppool` in the `backuppool_sink` job
|
||||
# - adjust the occurences of `myhostname` to the name of the system you are backing up (cannot be easily changed once you start replicating)
|
||||
# - make sure the `zrepl_` prefix is not being used by any other zfs tools you might have installed (it likely isn't)
|
||||
|
||||
jobs:
|
||||
|
||||
# this job takes care of snapshot creation + pruning
|
||||
- name: snapjob
|
||||
type: snap
|
||||
filesystems: {
|
||||
"system<": true,
|
||||
}
|
||||
# create snapshots with prefix `zrepl_` every 15 minutes
|
||||
snapshotting:
|
||||
type: periodic
|
||||
interval: 15m
|
||||
prefix: zrepl_
|
||||
pruning:
|
||||
keep:
|
||||
# fade-out scheme for snapshots starting with `zrepl_`
|
||||
# - keep all created in the last hour
|
||||
# - then destroy snapshots such that we keep 24 each 1 hour apart
|
||||
# - then destroy snapshots such that we keep 14 each 1 day apart
|
||||
# - then destroy all older snapshots
|
||||
- type: grid
|
||||
grid: 1x1h(keep=all) | 24x1h | 14x1d
|
||||
regex: "^zrepl_.*"
|
||||
# keep all snapshots that don't have the `zrepl_` prefix
|
||||
- type: regex
|
||||
negate: true
|
||||
regex: "^zrepl_.*"
|
||||
|
||||
# This job pushes to the local sink defined in job `backuppool_sink`.
|
||||
# We trigger replication manually from the command line / udev rules using
|
||||
# `zrepl signal wakeup push_to_drive`
|
||||
- type: push
|
||||
name: push_to_drive
|
||||
connect:
|
||||
type: local
|
||||
listener_name: backuppool_sink
|
||||
client_identity: myhostname
|
||||
filesystems: {
|
||||
"system<": true
|
||||
}
|
||||
send:
|
||||
encrypted: true
|
||||
replication:
|
||||
protection:
|
||||
initial: guarantee_resumability
|
||||
# Downgrade protection to guarantee_incremental which uses zfs bookmarks instead of zfs holds.
|
||||
# Thus, when we yank out the backup drive during replication
|
||||
# - we might not be able to resume the interrupted replication step because the partially received `to` snapshot of a `from`->`to` step may be pruned any time
|
||||
# - but in exchange we get back the disk space allocated by `to` when we prune it
|
||||
# - and because we still have the bookmarks created by `guarantee_incremental`, we can still do incremental replication of `from`->`to2` in the future
|
||||
incremental: guarantee_incremental
|
||||
snapshotting:
|
||||
type: manual
|
||||
pruning:
|
||||
# no-op prune rule on sender (keep all snapshots), job `snapshot` takes care of this
|
||||
keep_sender:
|
||||
- type: regex
|
||||
regex: ".*"
|
||||
# retain
|
||||
keep_receiver:
|
||||
# longer retention on the backup drive, we have more space there
|
||||
- type: grid
|
||||
grid: 1x1h(keep=all) | 24x1h | 360x1d
|
||||
regex: "^zrepl_.*"
|
||||
# retain all non-zrepl snapshots on the backup drive
|
||||
- type: regex
|
||||
negate: true
|
||||
regex: "^zrepl_.*"
|
||||
|
||||
# This job receives from job `push_to_drive` into `backuppool/zrepl/sink/myhostname`
|
||||
- type: sink
|
||||
name: backuppool_sink
|
||||
root_fs: "backuppool/zrepl/sink"
|
||||
serve:
|
||||
type: local
|
||||
listener_name: backuppool_sink
|
||||
@@ -1,12 +0,0 @@
|
||||
jobs:
|
||||
- name: sink
|
||||
type: sink
|
||||
serve:
|
||||
type: tls
|
||||
listen: ":8888"
|
||||
ca: "/etc/zrepl/prod.crt"
|
||||
cert: "/etc/zrepl/backups.crt"
|
||||
key: "/etc/zrepl/backups.key"
|
||||
client_cns:
|
||||
- "prod"
|
||||
root_fs: "storage/zrepl/sink"
|
||||
@@ -1,28 +0,0 @@
|
||||
jobs:
|
||||
- name: prod_to_backups
|
||||
type: push
|
||||
connect:
|
||||
type: tls
|
||||
address: "backups.example.com:8888"
|
||||
ca: /etc/zrepl/backups.crt
|
||||
cert: /etc/zrepl/prod.crt
|
||||
key: /etc/zrepl/prod.key
|
||||
server_cn: "backups"
|
||||
filesystems: {
|
||||
"zroot/var/db": true,
|
||||
"zroot/usr/home<": true,
|
||||
"zroot/usr/home/paranoid": false
|
||||
}
|
||||
snapshotting:
|
||||
type: periodic
|
||||
prefix: zrepl_
|
||||
interval: 10m
|
||||
pruning:
|
||||
keep_sender:
|
||||
- type: not_replicated
|
||||
- type: last_n
|
||||
count: 10
|
||||
keep_receiver:
|
||||
- type: grid
|
||||
grid: 1x1h(keep=all) | 24x1h | 30x1d | 6x30d
|
||||
regex: "^zrepl_"
|
||||
+1
-7
@@ -120,13 +120,7 @@ func (j *controlJob) Run(ctx context.Context) {
|
||||
jsonResponder{log, func() (interface{}, error) {
|
||||
jobs := j.jobs.status()
|
||||
globalZFS := zfscmd.GetReport()
|
||||
envconstReport := envconst.GetReport()
|
||||
s := Status{
|
||||
Jobs: jobs,
|
||||
Global: GlobalStatus{
|
||||
ZFSCmds: globalZFS,
|
||||
Envconst: envconstReport,
|
||||
}}
|
||||
s := Status{Jobs: jobs, Global: GlobalStatus{ZFSCmds: globalZFS}}
|
||||
return s, nil
|
||||
}})
|
||||
|
||||
|
||||
+1
-4
@@ -14,7 +14,6 @@ import (
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/zrepl/zrepl/daemon/logging/trace"
|
||||
"github.com/zrepl/zrepl/endpoint"
|
||||
"github.com/zrepl/zrepl/util/envconst"
|
||||
|
||||
"github.com/zrepl/zrepl/config"
|
||||
"github.com/zrepl/zrepl/daemon/job"
|
||||
@@ -95,7 +94,6 @@ func Run(ctx context.Context, conf *config.Config) error {
|
||||
}
|
||||
|
||||
// register global (=non job-local) metrics
|
||||
version.PrometheusRegister(prometheus.DefaultRegisterer)
|
||||
zfscmd.RegisterMetrics(prometheus.DefaultRegisterer)
|
||||
trace.RegisterMetrics(prometheus.DefaultRegisterer)
|
||||
endpoint.RegisterMetrics(prometheus.DefaultRegisterer)
|
||||
@@ -152,8 +150,7 @@ type Status struct {
|
||||
}
|
||||
|
||||
type GlobalStatus struct {
|
||||
ZFSCmds *zfscmd.Report
|
||||
Envconst *envconst.Report
|
||||
ZFSCmds *zfscmd.Report
|
||||
}
|
||||
|
||||
func (s *jobs) status() map[string]*job.Status {
|
||||
|
||||
+28
-35
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/zrepl/zrepl/daemon/logging/trace"
|
||||
|
||||
"github.com/zrepl/zrepl/config"
|
||||
"github.com/zrepl/zrepl/daemon/filters"
|
||||
"github.com/zrepl/zrepl/daemon/job/reset"
|
||||
"github.com/zrepl/zrepl/daemon/job/wakeup"
|
||||
"github.com/zrepl/zrepl/daemon/pruner"
|
||||
@@ -34,10 +35,9 @@ type ActiveSide struct {
|
||||
|
||||
prunerFactory *pruner.PrunerFactory
|
||||
|
||||
promRepStateSecs *prometheus.HistogramVec // labels: state
|
||||
promPruneSecs *prometheus.HistogramVec // labels: prune_side
|
||||
promBytesReplicated *prometheus.CounterVec // labels: filesystem
|
||||
promReplicationErrors prometheus.Gauge
|
||||
promRepStateSecs *prometheus.HistogramVec // labels: state
|
||||
promPruneSecs *prometheus.HistogramVec // labels: prune_side
|
||||
promBytesReplicated *prometheus.CounterVec // labels: filesystem
|
||||
|
||||
tasksMtx sync.Mutex
|
||||
tasks activeSideTasks
|
||||
@@ -145,24 +145,22 @@ func (m *modePush) ResetConnectBackoff() {
|
||||
|
||||
func modePushFromConfig(g *config.Global, in *config.PushJob, jobID endpoint.JobID) (*modePush, error) {
|
||||
m := &modePush{}
|
||||
var err error
|
||||
|
||||
m.senderConfig, err = buildSenderConfig(in, jobID)
|
||||
fsf, err := filters.DatasetMapFilterFromConfig(in.Filesystems)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "sender config")
|
||||
return nil, errors.Wrap(err, "cannot build filesystem filter")
|
||||
}
|
||||
|
||||
replicationConfig, err := logic.ReplicationConfigFromConfig(in.Replication)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "field `replication`")
|
||||
m.senderConfig = &endpoint.SenderConfig{
|
||||
FSF: fsf,
|
||||
Encrypt: &zfs.NilBool{B: in.Send.Encrypted},
|
||||
JobID: jobID,
|
||||
}
|
||||
|
||||
m.plannerPolicy = &logic.PlannerPolicy{
|
||||
EncryptedSend: logic.TriFromBool(in.Send.Encrypted),
|
||||
ReplicationConfig: *replicationConfig,
|
||||
EncryptedSend: logic.TriFromBool(in.Send.Encrypted),
|
||||
}
|
||||
|
||||
if m.snapper, err = snapper.FromConfig(g, m.senderConfig.FSF, in.Snapshotting); err != nil {
|
||||
if m.snapper, err = snapper.FromConfig(g, fsf, in.Snapshotting); err != nil {
|
||||
return nil, errors.Wrap(err, "cannot build snapper")
|
||||
}
|
||||
|
||||
@@ -174,6 +172,7 @@ type modePull struct {
|
||||
receiver *endpoint.Receiver
|
||||
receiverConfig endpoint.ReceiverConfig
|
||||
sender *rpc.Client
|
||||
rootFS *zfs.DatasetPath
|
||||
plannerPolicy *logic.PlannerPolicy
|
||||
interval config.PositiveDurationOrManual
|
||||
}
|
||||
@@ -247,19 +246,26 @@ func modePullFromConfig(g *config.Global, in *config.PullJob, jobID endpoint.Job
|
||||
m = &modePull{}
|
||||
m.interval = in.Interval
|
||||
|
||||
replicationConfig, err := logic.ReplicationConfigFromConfig(in.Replication)
|
||||
m.rootFS, err = zfs.NewDatasetPath(in.RootFS)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "field `replication`")
|
||||
return nil, errors.New("RootFS is not a valid zfs filesystem path")
|
||||
}
|
||||
if m.rootFS.Length() <= 0 {
|
||||
return nil, errors.New("RootFS must not be empty") // duplicates error check of receiver
|
||||
}
|
||||
|
||||
m.plannerPolicy = &logic.PlannerPolicy{
|
||||
EncryptedSend: logic.DontCare,
|
||||
ReplicationConfig: *replicationConfig,
|
||||
EncryptedSend: logic.DontCare,
|
||||
}
|
||||
|
||||
m.receiverConfig, err = buildReceiverConfig(in, jobID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
m.receiverConfig = endpoint.ReceiverConfig{
|
||||
JobID: jobID,
|
||||
RootWithoutClientComponent: m.rootFS,
|
||||
AppendClientIdentity: false, // !
|
||||
UpdateLastReceivedHold: true,
|
||||
}
|
||||
if err := m.receiverConfig.Validate(); err != nil {
|
||||
return nil, errors.Wrap(err, "cannot build receiver config")
|
||||
}
|
||||
|
||||
return m, nil
|
||||
@@ -300,14 +306,6 @@ func activeSide(g *config.Global, in *config.ActiveJob, configJob interface{}) (
|
||||
ConstLabels: prometheus.Labels{"zrepl_job": j.name.String()},
|
||||
}, []string{"filesystem"})
|
||||
|
||||
j.promReplicationErrors = prometheus.NewGauge(prometheus.GaugeOpts{
|
||||
Namespace: "zrepl",
|
||||
Subsystem: "replication",
|
||||
Name: "filesystem_errors",
|
||||
Help: "number of filesystems that failed replication in the latest replication attempt, or -1 if the job failed before enumerating the filesystems",
|
||||
ConstLabels: prometheus.Labels{"zrepl_job": j.name.String()},
|
||||
})
|
||||
|
||||
j.connecter, err = fromconfig.ConnecterFromConfig(g, in.Connect)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "cannot build client")
|
||||
@@ -332,7 +330,6 @@ func (j *ActiveSide) RegisterMetrics(registerer prometheus.Registerer) {
|
||||
registerer.MustRegister(j.promRepStateSecs)
|
||||
registerer.MustRegister(j.promPruneSecs)
|
||||
registerer.MustRegister(j.promBytesReplicated)
|
||||
registerer.MustRegister(j.promReplicationErrors)
|
||||
}
|
||||
|
||||
func (j *ActiveSide) Name() string { return j.name.String() }
|
||||
@@ -367,7 +364,7 @@ func (j *ActiveSide) OwnedDatasetSubtreeRoot() (rfs *zfs.DatasetPath, ok bool) {
|
||||
_ = j.mode.(*modePush) // make sure we didn't introduce a new job type
|
||||
return nil, false
|
||||
}
|
||||
return pull.receiverConfig.RootWithoutClientComponent.Copy(), true
|
||||
return pull.rootFS.Copy(), true
|
||||
}
|
||||
|
||||
func (j *ActiveSide) SenderConfig() *endpoint.SenderConfig {
|
||||
@@ -465,10 +462,6 @@ func (j *ActiveSide) do(ctx context.Context) {
|
||||
GetLogger(ctx).Info("start replication")
|
||||
repWait(true) // wait blocking
|
||||
repCancel() // always cancel to free up context resources
|
||||
|
||||
replicationReport := j.tasks.replicationReport()
|
||||
j.promReplicationErrors.Set(float64(replicationReport.GetFailedFilesystemsCountInLatestAttempt()))
|
||||
|
||||
endSpan()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
package job
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
"github.com/zrepl/zrepl/config"
|
||||
"github.com/zrepl/zrepl/daemon/filters"
|
||||
"github.com/zrepl/zrepl/endpoint"
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
|
||||
type SendingJobConfig interface {
|
||||
GetFilesystems() config.FilesystemsFilter
|
||||
GetSendOptions() *config.SendOptions // must not be nil
|
||||
}
|
||||
|
||||
func buildSenderConfig(in SendingJobConfig, jobID endpoint.JobID) (*endpoint.SenderConfig, error) {
|
||||
|
||||
fsf, err := filters.DatasetMapFilterFromConfig(in.GetFilesystems())
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "cannot build filesystem filter")
|
||||
}
|
||||
|
||||
return &endpoint.SenderConfig{
|
||||
FSF: fsf,
|
||||
Encrypt: &zfs.NilBool{B: in.GetSendOptions().Encrypted},
|
||||
JobID: jobID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type ReceivingJobConfig interface {
|
||||
GetRootFS() string
|
||||
GetAppendClientIdentity() bool
|
||||
GetRecvOptions() *config.RecvOptions
|
||||
}
|
||||
|
||||
func buildReceiverConfig(in ReceivingJobConfig, jobID endpoint.JobID) (rc endpoint.ReceiverConfig, err error) {
|
||||
rootFs, err := zfs.NewDatasetPath(in.GetRootFS())
|
||||
if err != nil {
|
||||
return rc, errors.New("root_fs is not a valid zfs filesystem path")
|
||||
}
|
||||
if rootFs.Length() <= 0 {
|
||||
return rc, errors.New("root_fs must not be empty") // duplicates error check of receiver
|
||||
}
|
||||
|
||||
rc = endpoint.ReceiverConfig{
|
||||
JobID: jobID,
|
||||
RootWithoutClientComponent: rootFs,
|
||||
AppendClientIdentity: in.GetAppendClientIdentity(),
|
||||
}
|
||||
if err := rc.Validate(); err != nil {
|
||||
return rc, errors.Wrap(err, "cannot build receiver config")
|
||||
}
|
||||
|
||||
return rc, nil
|
||||
}
|
||||
@@ -2,16 +2,12 @@ package job
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/kr/pretty"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/zrepl/zrepl/config"
|
||||
"github.com/zrepl/zrepl/transport/tls"
|
||||
)
|
||||
|
||||
func TestValidateReceivingSidesDoNotOverlap(t *testing.T) {
|
||||
@@ -112,35 +108,3 @@ jobs:
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestSampleConfigsAreBuiltWithoutErrors(t *testing.T) {
|
||||
paths, err := filepath.Glob("../../config/samples/*")
|
||||
if err != nil {
|
||||
t.Errorf("glob failed: %+v", err)
|
||||
}
|
||||
|
||||
for _, p := range paths {
|
||||
|
||||
if path.Ext(p) != ".yml" {
|
||||
t.Logf("skipping file %s", p)
|
||||
continue
|
||||
}
|
||||
|
||||
t.Run(p, func(t *testing.T) {
|
||||
c, err := config.ParseConfig(p)
|
||||
if err != nil {
|
||||
t.Errorf("error parsing %s:\n%+v", p, err)
|
||||
}
|
||||
|
||||
t.Logf("file: %s", p)
|
||||
t.Log(pretty.Sprint(c))
|
||||
|
||||
tls.FakeCertificateLoading(t)
|
||||
jobs, err := JobsFromConfig(c)
|
||||
t.Logf("jobs: %#v", jobs)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+21
-6
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/zrepl/zrepl/daemon/logging/trace"
|
||||
|
||||
"github.com/zrepl/zrepl/config"
|
||||
"github.com/zrepl/zrepl/daemon/filters"
|
||||
"github.com/zrepl/zrepl/daemon/logging"
|
||||
"github.com/zrepl/zrepl/daemon/snapper"
|
||||
"github.com/zrepl/zrepl/endpoint"
|
||||
@@ -47,9 +48,19 @@ func (m *modeSink) SnapperReport() *snapper.Report { return nil }
|
||||
func modeSinkFromConfig(g *config.Global, in *config.SinkJob, jobID endpoint.JobID) (m *modeSink, err error) {
|
||||
m = &modeSink{}
|
||||
|
||||
m.receiverConfig, err = buildReceiverConfig(in, jobID)
|
||||
rootDataset, err := zfs.NewDatasetPath(in.RootFS)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, errors.New("root dataset is not a valid zfs filesystem path")
|
||||
}
|
||||
|
||||
m.receiverConfig = endpoint.ReceiverConfig{
|
||||
JobID: jobID,
|
||||
RootWithoutClientComponent: rootDataset,
|
||||
AppendClientIdentity: true, // !
|
||||
UpdateLastReceivedHold: true,
|
||||
}
|
||||
if err := m.receiverConfig.Validate(); err != nil {
|
||||
return nil, errors.Wrap(err, "cannot build receiver config")
|
||||
}
|
||||
|
||||
return m, nil
|
||||
@@ -63,13 +74,17 @@ type modeSource struct {
|
||||
func modeSourceFromConfig(g *config.Global, in *config.SourceJob, jobID endpoint.JobID) (m *modeSource, err error) {
|
||||
// FIXME exact dedup of modePush
|
||||
m = &modeSource{}
|
||||
|
||||
m.senderConfig, err = buildSenderConfig(in, jobID)
|
||||
fsf, err := filters.DatasetMapFilterFromConfig(in.Filesystems)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "send options")
|
||||
return nil, errors.Wrap(err, "cannot build filesystem filter")
|
||||
}
|
||||
m.senderConfig = &endpoint.SenderConfig{
|
||||
FSF: fsf,
|
||||
Encrypt: &zfs.NilBool{B: in.Send.Encrypted},
|
||||
JobID: jobID,
|
||||
}
|
||||
|
||||
if m.snapper, err = snapper.FromConfig(g, m.senderConfig.FSF, in.Snapshotting); err != nil {
|
||||
if m.snapper, err = snapper.FromConfig(g, fsf, in.Snapshotting); err != nil {
|
||||
return nil, errors.Wrap(err, "cannot build snapper")
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
// Code generated by "enumer -type=FSState -json"; DO NOT EDIT.
|
||||
|
||||
//
|
||||
package pruner
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
const _FSStateName = "FSStateInitializedFSStatePlanningFSStatePlanErrFSStateExecutingFSStateExecuteErrFSStateExecuteSuccess"
|
||||
|
||||
var _FSStateIndex = [...]uint8{0, 18, 33, 47, 63, 80, 101}
|
||||
|
||||
func (i FSState) String() string {
|
||||
if i < 0 || i >= FSState(len(_FSStateIndex)-1) {
|
||||
return fmt.Sprintf("FSState(%d)", i)
|
||||
}
|
||||
return _FSStateName[_FSStateIndex[i]:_FSStateIndex[i+1]]
|
||||
}
|
||||
|
||||
var _FSStateValues = []FSState{0, 1, 2, 3, 4, 5}
|
||||
|
||||
var _FSStateNameToValueMap = map[string]FSState{
|
||||
_FSStateName[0:18]: 0,
|
||||
_FSStateName[18:33]: 1,
|
||||
_FSStateName[33:47]: 2,
|
||||
_FSStateName[47:63]: 3,
|
||||
_FSStateName[63:80]: 4,
|
||||
_FSStateName[80:101]: 5,
|
||||
}
|
||||
|
||||
// FSStateString retrieves an enum value from the enum constants string name.
|
||||
// Throws an error if the param is not part of the enum.
|
||||
func FSStateString(s string) (FSState, error) {
|
||||
if val, ok := _FSStateNameToValueMap[s]; ok {
|
||||
return val, nil
|
||||
}
|
||||
return 0, fmt.Errorf("%s does not belong to FSState values", s)
|
||||
}
|
||||
|
||||
// FSStateValues returns all values of the enum
|
||||
func FSStateValues() []FSState {
|
||||
return _FSStateValues
|
||||
}
|
||||
|
||||
// IsAFSState returns "true" if the value is listed in the enum definition. "false" otherwise
|
||||
func (i FSState) IsAFSState() bool {
|
||||
for _, v := range _FSStateValues {
|
||||
if i == v {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// MarshalJSON implements the json.Marshaler interface for FSState
|
||||
func (i FSState) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(i.String())
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements the json.Unmarshaler interface for FSState
|
||||
func (i *FSState) UnmarshalJSON(data []byte) error {
|
||||
var s string
|
||||
if err := json.Unmarshal(data, &s); err != nil {
|
||||
return fmt.Errorf("FSState should be a string, got %s", data)
|
||||
}
|
||||
|
||||
var err error
|
||||
*i, err = FSStateString(s)
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,454 @@
|
||||
package pruner
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/zrepl/zrepl/daemon/logging"
|
||||
"github.com/zrepl/zrepl/daemon/logging/trace"
|
||||
"github.com/zrepl/zrepl/endpoint"
|
||||
"github.com/zrepl/zrepl/pruning"
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
|
||||
type Pruner struct {
|
||||
fsfilter endpoint.FSFilter
|
||||
jid endpoint.JobID
|
||||
side Side
|
||||
keepRules []pruning.KeepRule
|
||||
|
||||
// all channels consumed by the run loop
|
||||
reportReqs chan reportRequest
|
||||
stopReqs chan stopRequest
|
||||
done chan struct{}
|
||||
fsListRes chan fsListRes
|
||||
|
||||
state State
|
||||
|
||||
listFilesystemsError error // only in state StateListFilesystemsError
|
||||
fsPruners []*FSPruner // only in state StateFanOutFilesystems
|
||||
}
|
||||
|
||||
//go:generate enumer -type=State -json
|
||||
type State int
|
||||
|
||||
const (
|
||||
StateInitialized State = iota
|
||||
StateListFilesystems
|
||||
StateListFilesystemsError
|
||||
StateFanOutFilesystems
|
||||
StateDone
|
||||
)
|
||||
|
||||
type Report struct {
|
||||
State State
|
||||
ListFilesystemsError error // only valid in StateListFilesystemsError
|
||||
Filesystems []*FSReport // valid from StateFanOutFilesystems
|
||||
}
|
||||
|
||||
type reportRequest struct {
|
||||
ctx context.Context
|
||||
reply chan *Report
|
||||
}
|
||||
|
||||
type runRequest struct {
|
||||
complete chan struct{}
|
||||
}
|
||||
|
||||
type stopRequest struct {
|
||||
complete chan struct{}
|
||||
}
|
||||
|
||||
type fsListRes struct {
|
||||
filesystems []*zfs.DatasetPath
|
||||
err error
|
||||
}
|
||||
|
||||
type Side interface {
|
||||
// may return both nil, indicating there is no replication position
|
||||
GetReplicationPosition(ctx context.Context, fs string) (*zfs.FilesystemVersion, error)
|
||||
isSide() Side
|
||||
}
|
||||
|
||||
func NewPruner(fsfilter endpoint.FSFilter, jid endpoint.JobID, side Side, keepRules []pruning.KeepRule) *Pruner {
|
||||
return &Pruner{
|
||||
fsfilter,
|
||||
jid,
|
||||
side,
|
||||
keepRules,
|
||||
make(chan reportRequest),
|
||||
make(chan stopRequest),
|
||||
make(chan struct{}),
|
||||
make(chan fsListRes),
|
||||
StateInitialized,
|
||||
nil,
|
||||
nil,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Pruner) Run(ctx context.Context) *Report {
|
||||
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
if p.state != StateInitialized {
|
||||
panic("Run can onl[y be called once")
|
||||
}
|
||||
|
||||
go func() {
|
||||
fss, err := zfs.ZFSListMapping(ctx, p.fsfilter)
|
||||
p.fsListRes <- fsListRes{fss, err}
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case res := <-p.fsListRes:
|
||||
if res.err != nil {
|
||||
p.state = StateListFilesystemsError
|
||||
p.listFilesystemsError = res.err
|
||||
close(p.done)
|
||||
continue
|
||||
}
|
||||
|
||||
p.state = StateFanOutFilesystems
|
||||
|
||||
p.fsPruners = make([]*FSPruner, len(res.filesystems))
|
||||
_, add, end := trace.WithTaskGroup(ctx, "pruner-fan-out-fs")
|
||||
for i, fs := range res.filesystems {
|
||||
p.fsPruners[i] = NewFSPruner(p.jid, p.side, p.keepRules, fs)
|
||||
add(func(ctx context.Context) {
|
||||
p.fsPruners[i].Run(ctx)
|
||||
})
|
||||
}
|
||||
go func() {
|
||||
end()
|
||||
close(p.done)
|
||||
}()
|
||||
|
||||
case req := <-p.stopReqs:
|
||||
cancel()
|
||||
go func() {
|
||||
<-p.done
|
||||
close(req.complete)
|
||||
}()
|
||||
case req := <-p.reportReqs:
|
||||
req.reply <- p.report(req.ctx)
|
||||
case <-p.done:
|
||||
p.state = StateDone
|
||||
return p.report(ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Pruner) Report(ctx context.Context) *Report {
|
||||
req := reportRequest{
|
||||
ctx: ctx,
|
||||
reply: make(chan *Report, 1),
|
||||
}
|
||||
select {
|
||||
case p.reportReqs <- req:
|
||||
return <-req.reply
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
case <-p.done:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Pruner) report(ctx context.Context) *Report {
|
||||
fsreports := make([]*FSReport, len(p.fsPruners))
|
||||
for i := range fsreports {
|
||||
fsreports[i] = p.fsPruners[i].report()
|
||||
}
|
||||
return &Report{
|
||||
State: p.state,
|
||||
ListFilesystemsError: p.listFilesystemsError,
|
||||
Filesystems: fsreports,
|
||||
}
|
||||
}
|
||||
|
||||
// implements pruning.Snapshot
|
||||
type snapshot struct {
|
||||
replicated bool
|
||||
stepHolds []pruning.StepHold
|
||||
zfs.FilesystemVersion
|
||||
|
||||
state SnapState
|
||||
destroyOp *zfs.DestroySnapOp
|
||||
}
|
||||
|
||||
//go:generate enumer -type=SnapState -json
|
||||
type SnapState int
|
||||
|
||||
const (
|
||||
SnapStateInitialized SnapState = iota
|
||||
SnapStateKeeping
|
||||
SnapStateDeletePending
|
||||
SnapStateDeleteAttempted
|
||||
)
|
||||
|
||||
// implements pruning.StepHold
|
||||
type stepHold struct {
|
||||
endpoint.Abstraction
|
||||
}
|
||||
|
||||
func (s snapshot) Replicated() bool { return s.replicated }
|
||||
func (s snapshot) StepHolds() []pruning.StepHold { return s.stepHolds }
|
||||
|
||||
func (s stepHold) GetJobID() endpoint.JobID { return *s.Abstraction.GetJobID() }
|
||||
|
||||
type FSPruner struct {
|
||||
jid endpoint.JobID
|
||||
side Side
|
||||
keepRules []pruning.KeepRule
|
||||
fsp *zfs.DatasetPath
|
||||
|
||||
state FSState
|
||||
|
||||
// all channels consumed by the run loop
|
||||
planned chan fsPlanRes
|
||||
executed chan fsExecuteRes
|
||||
done chan struct{}
|
||||
reportReqs chan fsReportReq
|
||||
|
||||
keepList []*snapshot // valid in FSStateExecuting and forward
|
||||
destroyList []*snapshot // valid in FSStateExecuting and forward, field .destroyOp is invalid until FSStateExecuting is left
|
||||
|
||||
}
|
||||
|
||||
type fsPlanRes struct {
|
||||
keepList []*snapshot
|
||||
destroyList []*snapshot
|
||||
err error
|
||||
}
|
||||
|
||||
type fsExecuteRes struct {
|
||||
completedDestroyOps []*zfs.DestroySnapOp // same len() as FSPruner.destroyList
|
||||
}
|
||||
|
||||
type fsReportReq struct {
|
||||
res chan *FSReport
|
||||
}
|
||||
|
||||
type FSReport struct {
|
||||
State FSState
|
||||
KeepList []*SnapReport
|
||||
Destroy []*SnapReport
|
||||
}
|
||||
|
||||
type SnapReport struct {
|
||||
State SnapState
|
||||
Name string
|
||||
Replicated bool
|
||||
StepHoldCount int
|
||||
DestroyError error
|
||||
}
|
||||
|
||||
//go:generate enumer -type=FSState -json
|
||||
type FSState int
|
||||
|
||||
const (
|
||||
FSStateInitialized FSState = iota
|
||||
FSStatePlanning
|
||||
FSStatePlanErr
|
||||
FSStateExecuting
|
||||
FSStateExecuteErr
|
||||
FSStateExecuteSuccess
|
||||
)
|
||||
|
||||
func (s FSState) IsTerminal() bool {
|
||||
return s == FSStatePlanErr || s == FSStateExecuteErr || s == FSStateExecuteSuccess
|
||||
}
|
||||
|
||||
func NewFSPruner(jid endpoint.JobID, side Side, keepRules []pruning.KeepRule, fsp *zfs.DatasetPath) *FSPruner {
|
||||
return &FSPruner{
|
||||
jid, side, keepRules, fsp,
|
||||
FSStateInitialized,
|
||||
make(chan fsPlanRes),
|
||||
make(chan fsExecuteRes),
|
||||
make(chan struct{}),
|
||||
make(chan fsReportReq),
|
||||
nil, nil,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *FSPruner) Run(ctx context.Context) *FSReport {
|
||||
|
||||
defer func() {
|
||||
}()
|
||||
|
||||
p.state = FSStatePlanning
|
||||
|
||||
go func() { p.planned <- p.plan(ctx) }()
|
||||
|
||||
out:
|
||||
for !p.state.IsTerminal() {
|
||||
select {
|
||||
case res := <-p.planned:
|
||||
|
||||
if res.err != nil {
|
||||
p.state = FSStatePlanErr
|
||||
continue
|
||||
}
|
||||
p.state = FSStateExecuting
|
||||
p.keepList = res.keepList
|
||||
p.destroyList = res.destroyList
|
||||
|
||||
go func() { p.executed <- p.execute(ctx, p.destroyList) }()
|
||||
|
||||
case res := <-p.executed:
|
||||
|
||||
if len(res.completedDestroyOps) != len(p.destroyList) {
|
||||
panic("impl error: completedDestroyOps is a vector corresponding to entries in p.destroyList")
|
||||
}
|
||||
|
||||
var erronous []*zfs.DestroySnapOp
|
||||
for i, op := range res.completedDestroyOps {
|
||||
if *op.ErrOut != nil {
|
||||
erronous = append(erronous, op)
|
||||
}
|
||||
p.destroyList[i].destroyOp = op
|
||||
p.destroyList[i].state = SnapStateDeleteAttempted
|
||||
}
|
||||
if len(erronous) > 0 {
|
||||
p.state = FSStateExecuteErr
|
||||
} else {
|
||||
p.state = FSStateExecuteSuccess
|
||||
}
|
||||
|
||||
close(p.done)
|
||||
|
||||
case <-p.reportReqs:
|
||||
panic("unimp")
|
||||
case <-p.done:
|
||||
break out
|
||||
}
|
||||
}
|
||||
|
||||
// TODO render last FS report
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *FSPruner) plan(ctx context.Context) fsPlanRes {
|
||||
fs := p.fsp.ToString()
|
||||
vs, err := zfs.ZFSListFilesystemVersions(ctx, p.fsp, zfs.ListFilesystemVersionsOptions{})
|
||||
if err != nil {
|
||||
return fsPlanRes{err: errors.Wrap(err, "list filesystem versions")}
|
||||
}
|
||||
|
||||
allJobsStepHolds, absErrs, err := endpoint.ListAbstractions(ctx, endpoint.ListZFSHoldsAndBookmarksQuery{
|
||||
FS: endpoint.ListZFSHoldsAndBookmarksQueryFilesystemFilter{
|
||||
FS: &fs,
|
||||
},
|
||||
What: endpoint.AbstractionTypeSet{
|
||||
endpoint.AbstractionStepHold: true,
|
||||
},
|
||||
Concurrency: 1,
|
||||
})
|
||||
if err != nil {
|
||||
return fsPlanRes{err: errors.Wrap(err, "list abstractions")}
|
||||
}
|
||||
if len(absErrs) > 0 {
|
||||
logging.GetLogger(ctx, logging.SubsysPruning).WithError(endpoint.ListAbstractionsErrors(absErrs)).
|
||||
Error("error listing some step holds, prune attempt might fail with 'dataset is busy' errors")
|
||||
}
|
||||
|
||||
repPos, err := p.side.GetReplicationPosition(ctx, p.fsp.ToString())
|
||||
if err != nil {
|
||||
return fsPlanRes{err: errors.Wrap(err, "get replication position")}
|
||||
}
|
||||
|
||||
vsAsSnaps := make([]pruning.Snapshot, len(vs))
|
||||
for i := range vs {
|
||||
var repPosCreateTxgOrZero uint64
|
||||
if repPos != nil {
|
||||
repPosCreateTxgOrZero = repPos.GetCreateTXG()
|
||||
}
|
||||
s := &snapshot{
|
||||
state: SnapStateInitialized,
|
||||
FilesystemVersion: vs[i],
|
||||
replicated: vs[i].GetCreateTXG() <= repPosCreateTxgOrZero,
|
||||
}
|
||||
for _, h := range allJobsStepHolds {
|
||||
if zfs.FilesystemVersionEqualIdentity(vs[i], h.GetFilesystemVersion()) {
|
||||
s.stepHolds = append(s.stepHolds, stepHold{h})
|
||||
}
|
||||
}
|
||||
vsAsSnaps[i] = s
|
||||
}
|
||||
|
||||
downcastToSnapshots := func(l []pruning.Snapshot) (r []*snapshot) {
|
||||
r = make([]*snapshot, len(l))
|
||||
for i, e := range l {
|
||||
r[i] = e.(*snapshot)
|
||||
}
|
||||
return r
|
||||
}
|
||||
pruningResult := pruning.PruneSnapshots(vsAsSnaps, p.keepRules)
|
||||
remove, keep := downcastToSnapshots(pruningResult.Remove), downcastToSnapshots(pruningResult.Keep)
|
||||
if len(remove)+len(keep) != len(vsAsSnaps) {
|
||||
for _, s := range vsAsSnaps {
|
||||
r, _ := json.MarshalIndent(s.(*snapshot).report(), "", " ")
|
||||
fmt.Fprintf(os.Stderr, "%s\n", string(r))
|
||||
}
|
||||
panic("indecisive")
|
||||
}
|
||||
|
||||
for _, s := range remove {
|
||||
s.state = SnapStateDeletePending
|
||||
}
|
||||
for _, s := range keep {
|
||||
s.state = SnapStateKeeping
|
||||
}
|
||||
|
||||
return fsPlanRes{keepList: keep, destroyList: remove, err: nil}
|
||||
}
|
||||
|
||||
func (p *FSPruner) execute(ctx context.Context, destroyList []*snapshot) fsExecuteRes {
|
||||
ops := make([]*zfs.DestroySnapOp, len(destroyList))
|
||||
for i, fsv := range p.destroyList {
|
||||
ops[i] = &zfs.DestroySnapOp{
|
||||
Filesystem: p.fsp.ToString(),
|
||||
Name: fsv.GetName(),
|
||||
ErrOut: new(error),
|
||||
}
|
||||
}
|
||||
zfs.ZFSDestroyFilesystemVersions(ctx, ops)
|
||||
|
||||
return fsExecuteRes{completedDestroyOps: ops}
|
||||
}
|
||||
|
||||
func (p *FSPruner) report() *FSReport {
|
||||
return &FSReport{
|
||||
State: p.state,
|
||||
KeepList: p.reportRenderSnapReports(p.keepList),
|
||||
Destroy: p.reportRenderSnapReports(p.destroyList),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *FSPruner) reportRenderSnapReports(l []*snapshot) (r []*SnapReport) {
|
||||
r = make([]*SnapReport, len(l))
|
||||
for i := range l {
|
||||
r[i] = l[i].report()
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func (s *snapshot) report() *SnapReport {
|
||||
var snapErr error
|
||||
if s.state == SnapStateDeleteAttempted {
|
||||
if *s.destroyOp.ErrOut != nil {
|
||||
snapErr = (*s.destroyOp.ErrOut)
|
||||
}
|
||||
}
|
||||
return &SnapReport{
|
||||
State: s.state,
|
||||
Name: s.Name,
|
||||
Replicated: s.Replicated(),
|
||||
StepHoldCount: len(s.stepHolds),
|
||||
DestroyError: snapErr,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package pruner
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zrepl/zrepl/endpoint"
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
|
||||
type SideSender struct {
|
||||
jobID endpoint.JobID
|
||||
}
|
||||
|
||||
func NewSideSender(jid endpoint.JobID) *SideSender {
|
||||
return &SideSender{jid}
|
||||
}
|
||||
|
||||
func (s *SideSender) isSide() Side { return nil }
|
||||
|
||||
var _ Side = (*SideSender)(nil)
|
||||
|
||||
func (s *SideSender) GetReplicationPosition(ctx context.Context, fs string) (*zfs.FilesystemVersion, error) {
|
||||
if fs == "" {
|
||||
panic("must not pass zero value for fs")
|
||||
}
|
||||
return endpoint.GetMostRecentReplicationCursorOfJob(ctx, fs, s.jobID)
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
// Code generated by "enumer -type=SnapState -json"; DO NOT EDIT.
|
||||
|
||||
//
|
||||
package pruner
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
const _SnapStateName = "SnapStateInitializedSnapStateKeepingSnapStateDeletePendingSnapStateDeleteAttempted"
|
||||
|
||||
var _SnapStateIndex = [...]uint8{0, 20, 36, 58, 82}
|
||||
|
||||
func (i SnapState) String() string {
|
||||
if i < 0 || i >= SnapState(len(_SnapStateIndex)-1) {
|
||||
return fmt.Sprintf("SnapState(%d)", i)
|
||||
}
|
||||
return _SnapStateName[_SnapStateIndex[i]:_SnapStateIndex[i+1]]
|
||||
}
|
||||
|
||||
var _SnapStateValues = []SnapState{0, 1, 2, 3}
|
||||
|
||||
var _SnapStateNameToValueMap = map[string]SnapState{
|
||||
_SnapStateName[0:20]: 0,
|
||||
_SnapStateName[20:36]: 1,
|
||||
_SnapStateName[36:58]: 2,
|
||||
_SnapStateName[58:82]: 3,
|
||||
}
|
||||
|
||||
// SnapStateString retrieves an enum value from the enum constants string name.
|
||||
// Throws an error if the param is not part of the enum.
|
||||
func SnapStateString(s string) (SnapState, error) {
|
||||
if val, ok := _SnapStateNameToValueMap[s]; ok {
|
||||
return val, nil
|
||||
}
|
||||
return 0, fmt.Errorf("%s does not belong to SnapState values", s)
|
||||
}
|
||||
|
||||
// SnapStateValues returns all values of the enum
|
||||
func SnapStateValues() []SnapState {
|
||||
return _SnapStateValues
|
||||
}
|
||||
|
||||
// IsASnapState returns "true" if the value is listed in the enum definition. "false" otherwise
|
||||
func (i SnapState) IsASnapState() bool {
|
||||
for _, v := range _SnapStateValues {
|
||||
if i == v {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// MarshalJSON implements the json.Marshaler interface for SnapState
|
||||
func (i SnapState) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(i.String())
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements the json.Unmarshaler interface for SnapState
|
||||
func (i *SnapState) UnmarshalJSON(data []byte) error {
|
||||
var s string
|
||||
if err := json.Unmarshal(data, &s); err != nil {
|
||||
return fmt.Errorf("SnapState should be a string, got %s", data)
|
||||
}
|
||||
|
||||
var err error
|
||||
*i, err = SnapStateString(s)
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
// Code generated by "enumer -type=State -json"; DO NOT EDIT.
|
||||
|
||||
//
|
||||
package pruner
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
const _StateName = "StateInitializedStateListFilesystemsStateListFilesystemsErrorStateFanOutFilesystemsStateDone"
|
||||
|
||||
var _StateIndex = [...]uint8{0, 16, 36, 61, 83, 92}
|
||||
|
||||
func (i State) String() string {
|
||||
if i < 0 || i >= State(len(_StateIndex)-1) {
|
||||
return fmt.Sprintf("State(%d)", i)
|
||||
}
|
||||
return _StateName[_StateIndex[i]:_StateIndex[i+1]]
|
||||
}
|
||||
|
||||
var _StateValues = []State{0, 1, 2, 3, 4}
|
||||
|
||||
var _StateNameToValueMap = map[string]State{
|
||||
_StateName[0:16]: 0,
|
||||
_StateName[16:36]: 1,
|
||||
_StateName[36:61]: 2,
|
||||
_StateName[61:83]: 3,
|
||||
_StateName[83:92]: 4,
|
||||
}
|
||||
|
||||
// StateString retrieves an enum value from the enum constants string name.
|
||||
// Throws an error if the param is not part of the enum.
|
||||
func StateString(s string) (State, error) {
|
||||
if val, ok := _StateNameToValueMap[s]; ok {
|
||||
return val, nil
|
||||
}
|
||||
return 0, fmt.Errorf("%s does not belong to State values", s)
|
||||
}
|
||||
|
||||
// StateValues returns all values of the enum
|
||||
func StateValues() []State {
|
||||
return _StateValues
|
||||
}
|
||||
|
||||
// IsAState returns "true" if the value is listed in the enum definition. "false" otherwise
|
||||
func (i State) IsAState() bool {
|
||||
for _, v := range _StateValues {
|
||||
if i == v {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// MarshalJSON implements the json.Marshaler interface for State
|
||||
func (i State) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(i.String())
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements the json.Unmarshaler interface for State
|
||||
func (i *State) UnmarshalJSON(data []byte) error {
|
||||
var s string
|
||||
if err := json.Unmarshal(data, &s); err != nil {
|
||||
return fmt.Errorf("State should be a string, got %s", data)
|
||||
}
|
||||
|
||||
var err error
|
||||
*i, err = StateString(s)
|
||||
return err
|
||||
}
|
||||
+51
-29
@@ -20,15 +20,14 @@ import (
|
||||
)
|
||||
|
||||
// Try to keep it compatible with github.com/zrepl/zrepl/endpoint.Endpoint
|
||||
type History interface {
|
||||
ReplicationCursor(ctx context.Context, req *pdu.ReplicationCursorReq) (*pdu.ReplicationCursorRes, error)
|
||||
type Endpoint interface {
|
||||
ListFilesystems(ctx context.Context, req *pdu.ListFilesystemReq) (*pdu.ListFilesystemRes, error)
|
||||
ListFilesystemVersions(ctx context.Context, req *pdu.ListFilesystemVersionsReq) (*pdu.ListFilesystemVersionsRes, error)
|
||||
}
|
||||
|
||||
// Try to keep it compatible with github.com/zrepl/zrepl/endpoint.Endpoint
|
||||
type Target interface {
|
||||
ListFilesystems(ctx context.Context, req *pdu.ListFilesystemReq) (*pdu.ListFilesystemRes, error)
|
||||
ListFilesystemVersions(ctx context.Context, req *pdu.ListFilesystemVersionsReq) (*pdu.ListFilesystemVersionsRes, error)
|
||||
Endpoint
|
||||
DestroySnapshots(ctx context.Context, req *pdu.DestroySnapshotsReq) (*pdu.DestroySnapshotsRes, error)
|
||||
}
|
||||
|
||||
@@ -46,13 +45,14 @@ func GetLogger(ctx context.Context) Logger {
|
||||
}
|
||||
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
target Target
|
||||
receiver History
|
||||
rules []pruning.KeepRule
|
||||
retryWait time.Duration
|
||||
considerSnapAtCursorReplicated bool
|
||||
promPruneSecs prometheus.Observer
|
||||
ctx context.Context
|
||||
target Target
|
||||
sender, receiver Endpoint
|
||||
rules []pruning.KeepRule
|
||||
retryWait time.Duration
|
||||
considerSnapAtCursorReplicated bool
|
||||
convertAnyStepHoldToStepBookmark bool
|
||||
promPruneSecs prometheus.Observer
|
||||
}
|
||||
|
||||
type Pruner struct {
|
||||
@@ -70,11 +70,12 @@ type Pruner struct {
|
||||
}
|
||||
|
||||
type PrunerFactory struct {
|
||||
senderRules []pruning.KeepRule
|
||||
receiverRules []pruning.KeepRule
|
||||
retryWait time.Duration
|
||||
considerSnapAtCursorReplicated bool
|
||||
promPruneSecs *prometheus.HistogramVec
|
||||
senderRules []pruning.KeepRule
|
||||
receiverRules []pruning.KeepRule
|
||||
retryWait time.Duration
|
||||
considerSnapAtCursorReplicated bool
|
||||
convertAnyStepHoldToStepBookmark bool
|
||||
promPruneSecs *prometheus.HistogramVec
|
||||
}
|
||||
|
||||
type LocalPrunerFactory struct {
|
||||
@@ -122,25 +123,35 @@ func NewPrunerFactory(in config.PruningSenderReceiver, promPruneSecs *prometheus
|
||||
}
|
||||
considerSnapAtCursorReplicated = considerSnapAtCursorReplicated || !knr.KeepSnapshotAtCursor
|
||||
}
|
||||
|
||||
convertAnyStepHoldToStepBookmark := false
|
||||
for _, r := range in.KeepSender {
|
||||
_, ok := r.Ret.(*config.PruneKeepStepHolds)
|
||||
convertAnyStepHoldToStepBookmark = convertAnyStepHoldToStepBookmark || ok
|
||||
}
|
||||
|
||||
f := &PrunerFactory{
|
||||
senderRules: keepRulesSender,
|
||||
receiverRules: keepRulesReceiver,
|
||||
retryWait: envconst.Duration("ZREPL_PRUNER_RETRY_INTERVAL", 10*time.Second),
|
||||
considerSnapAtCursorReplicated: considerSnapAtCursorReplicated,
|
||||
promPruneSecs: promPruneSecs,
|
||||
senderRules: keepRulesSender,
|
||||
receiverRules: keepRulesReceiver,
|
||||
retryWait: envconst.Duration("ZREPL_PRUNER_RETRY_INTERVAL", 10*time.Second),
|
||||
considerSnapAtCursorReplicated: considerSnapAtCursorReplicated,
|
||||
convertAnyStepHoldToStepBookmark: convertAnyStepHoldToStepBookmark,
|
||||
promPruneSecs: promPruneSecs,
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func (f *PrunerFactory) BuildSenderPruner(ctx context.Context, target Target, receiver History) *Pruner {
|
||||
func (f *PrunerFactory) BuildSenderPruner(ctx context.Context, sender Target, receiver Endpoint) *Pruner {
|
||||
p := &Pruner{
|
||||
args: args{
|
||||
context.WithValue(ctx, contextKeyPruneSide, "sender"),
|
||||
target,
|
||||
sender,
|
||||
sender,
|
||||
receiver,
|
||||
f.senderRules,
|
||||
f.retryWait,
|
||||
f.considerSnapAtCursorReplicated,
|
||||
f.convertAnyStepHoldToStepBookmark,
|
||||
f.promPruneSecs.WithLabelValues("sender"),
|
||||
},
|
||||
state: Plan,
|
||||
@@ -148,15 +159,17 @@ func (f *PrunerFactory) BuildSenderPruner(ctx context.Context, target Target, re
|
||||
return p
|
||||
}
|
||||
|
||||
func (f *PrunerFactory) BuildReceiverPruner(ctx context.Context, target Target, receiver History) *Pruner {
|
||||
func (f *PrunerFactory) BuildReceiverPruner(ctx context.Context, receiver Target, sender Endpoint) *Pruner {
|
||||
p := &Pruner{
|
||||
args: args{
|
||||
context.WithValue(ctx, contextKeyPruneSide, "receiver"),
|
||||
target,
|
||||
receiver,
|
||||
sender,
|
||||
receiver,
|
||||
f.receiverRules,
|
||||
f.retryWait,
|
||||
false, // senseless here anyways
|
||||
false, // senseless here anyways
|
||||
f.promPruneSecs.WithLabelValues("receiver"),
|
||||
},
|
||||
state: Plan,
|
||||
@@ -164,15 +177,17 @@ func (f *PrunerFactory) BuildReceiverPruner(ctx context.Context, target Target,
|
||||
return p
|
||||
}
|
||||
|
||||
func (f *LocalPrunerFactory) BuildLocalPruner(ctx context.Context, target Target, receiver History) *Pruner {
|
||||
func (f *LocalPrunerFactory) BuildLocalPruner(ctx context.Context, target Target) *Pruner {
|
||||
p := &Pruner{
|
||||
args: args{
|
||||
context.WithValue(ctx, contextKeyPruneSide, "local"),
|
||||
target,
|
||||
receiver,
|
||||
target,
|
||||
target,
|
||||
f.keepRules,
|
||||
f.retryWait,
|
||||
false, // considerSnapAtCursorReplicated is not relevant for local pruning
|
||||
false, // convertAnyStepHoldToStepBookmark is not relevant for local pruning
|
||||
f.promPruneSecs.WithLabelValues("local"),
|
||||
},
|
||||
state: Plan,
|
||||
@@ -341,11 +356,13 @@ func (s snapshot) Replicated() bool { return s.replicated }
|
||||
|
||||
func (s snapshot) Date() time.Time { return s.date }
|
||||
|
||||
func (s snapshot) CreateTXG() uint64 { return s.fsv.GetCreateTXG() }
|
||||
|
||||
func doOneAttempt(a *args, u updater) {
|
||||
|
||||
ctx, target, receiver := a.ctx, a.target, a.receiver
|
||||
ctx, sender, receiver, target := a.ctx, a.sender, a.receiver, a.target
|
||||
|
||||
sfssres, err := receiver.ListFilesystems(ctx, &pdu.ListFilesystemReq{})
|
||||
sfssres, err := sender.ListFilesystems(ctx, &pdu.ListFilesystemReq{})
|
||||
if err != nil {
|
||||
u(func(p *Pruner) {
|
||||
p.state = PlanErr
|
||||
@@ -407,6 +424,10 @@ tfss_loop:
|
||||
|
||||
pfs.snaps = make([]pruning.Snapshot, 0, len(tfsvs))
|
||||
|
||||
receiver.ListFilesystemVersions(ctx, &pdu.ListFilesystemVersionsReq{
|
||||
Filesystem: tfs.Path,
|
||||
})
|
||||
|
||||
rcReq := &pdu.ReplicationCursorReq{
|
||||
Filesystem: tfs.Path,
|
||||
}
|
||||
@@ -415,6 +436,7 @@ tfss_loop:
|
||||
pfsPlanErrAndLog(err, "cannot get replication cursor bookmark")
|
||||
continue tfss_loop
|
||||
}
|
||||
|
||||
if rc.GetNotexist() {
|
||||
err := errors.New("replication cursor bookmark does not exist (one successful replication is required before pruning works)")
|
||||
pfsPlanErrAndLog(err, "")
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/zrepl/zrepl/daemon/logging/trace"
|
||||
|
||||
"github.com/zrepl/zrepl/config"
|
||||
"github.com/zrepl/zrepl/daemon/filters"
|
||||
"github.com/zrepl/zrepl/daemon/hooks"
|
||||
"github.com/zrepl/zrepl/daemon/logging"
|
||||
"github.com/zrepl/zrepl/logger"
|
||||
@@ -48,7 +49,7 @@ type args struct {
|
||||
ctx context.Context
|
||||
prefix string
|
||||
interval time.Duration
|
||||
fsf zfs.DatasetFilter
|
||||
fsf *filters.DatasetMapFilter
|
||||
snapshotsTaken chan<- struct{}
|
||||
hooks *hooks.List
|
||||
dryRun bool
|
||||
@@ -108,7 +109,7 @@ 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) {
|
||||
func PeriodicFromConfig(g *config.Global, fsf *filters.DatasetMapFilter, in *config.SnapshottingPeriodic) (*Snapper, error) {
|
||||
if in.Prefix == "" {
|
||||
return nil, errors.New("prefix must not be empty")
|
||||
}
|
||||
@@ -382,7 +383,7 @@ func wait(a args, u updater) state {
|
||||
}
|
||||
}
|
||||
|
||||
func listFSes(ctx context.Context, mf zfs.DatasetFilter) (fss []*zfs.DatasetPath, err error) {
|
||||
func listFSes(ctx context.Context, mf *filters.DatasetMapFilter) (fss []*zfs.DatasetPath, err error) {
|
||||
return zfs.ZFSListMapping(ctx, mf)
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/zrepl/zrepl/config"
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
"github.com/zrepl/zrepl/daemon/filters"
|
||||
)
|
||||
|
||||
// FIXME: properly abstract snapshotting:
|
||||
@@ -32,7 +32,7 @@ func (s *PeriodicOrManual) Report() *Report {
|
||||
return nil
|
||||
}
|
||||
|
||||
func FromConfig(g *config.Global, fsf zfs.DatasetFilter, in config.SnapshottingEnum) (*PeriodicOrManual, error) {
|
||||
func FromConfig(g *config.Global, fsf *filters.DatasetMapFilter, in config.SnapshottingEnum) (*PeriodicOrManual, error) {
|
||||
switch v := in.Ret.(type) {
|
||||
case *config.SnapshottingPeriodic:
|
||||
snapper, err := PeriodicFromConfig(g, fsf, v)
|
||||
|
||||
+595
@@ -0,0 +1,595 @@
|
||||
{
|
||||
"annotations": {
|
||||
"list": [
|
||||
{
|
||||
"$$hashKey": "object:3351",
|
||||
"builtIn": 1,
|
||||
"datasource": "-- Grafana --",
|
||||
"enable": true,
|
||||
"hide": true,
|
||||
"iconColor": "rgba(0, 211, 255, 1)",
|
||||
"name": "Annotations & Alerts",
|
||||
"type": "dashboard"
|
||||
}
|
||||
]
|
||||
},
|
||||
"editable": true,
|
||||
"gnetId": null,
|
||||
"graphTooltip": 1,
|
||||
"id": 7,
|
||||
"iteration": 1552746418826,
|
||||
"links": [],
|
||||
"panels": [
|
||||
{
|
||||
"content": "# zrepl Prometheus Metrics\n\nzrepl exposes Prometheus metrics and ships with this Grafana dashboard.\nThe exported metrics are suitable for health checks:\n\n* The log should generally be warning & error-free\n * The `Log Messages that require attention` graph visualizes log message counts indicating problems.\n* The number of goroutines should not grow unboundedly over time.\n * During replication, the number of goroutines can be way higher than during idle time.\n * If the goroutine count grows with each replication, there is clearly a goroutine leak. Please open a bug report.\n* The sys memory consumption should not grow unboundedly over time.\n * Note that the Go runtime pre-allocates some of its heap from the OS.\n * zrepl actually uses much less memory than allocated from the OS.\n * Since Go 1.11, Go pre-allocates more aggressively.\n* Monitor that some data is replicated, although that metric does not guarantee that replication was successful.\n\n**In general, note that the exported metrics are not stable unless declared otherwise.**",
|
||||
"gridPos": {
|
||||
"h": 9,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"id": 35,
|
||||
"mode": "markdown",
|
||||
"title": "Panel Title",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"aliasColors": {},
|
||||
"bars": false,
|
||||
"dashLength": 10,
|
||||
"dashes": false,
|
||||
"datasource": null,
|
||||
"fill": 1,
|
||||
"gridPos": {
|
||||
"h": 9,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 9
|
||||
},
|
||||
"id": 15,
|
||||
"legend": {
|
||||
"avg": false,
|
||||
"current": false,
|
||||
"max": false,
|
||||
"min": false,
|
||||
"show": true,
|
||||
"total": false,
|
||||
"values": false
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [],
|
||||
"spaceLength": 10,
|
||||
"stack": true,
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"$$hashKey": "object:3436",
|
||||
"expr": "up{job='$prom_job_name'}",
|
||||
"format": "time_series",
|
||||
"intervalFactor": 1,
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"thresholds": [],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "zrepl Instances Up",
|
||||
"tooltip": {
|
||||
"shared": true,
|
||||
"sort": 0,
|
||||
"value_type": "individual"
|
||||
},
|
||||
"type": "graph",
|
||||
"xaxis": {
|
||||
"buckets": null,
|
||||
"mode": "time",
|
||||
"name": null,
|
||||
"show": true,
|
||||
"values": []
|
||||
},
|
||||
"yaxes": [
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": "5",
|
||||
"min": "0",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"aliasColors": {},
|
||||
"bars": false,
|
||||
"dashLength": 10,
|
||||
"dashes": false,
|
||||
"datasource": null,
|
||||
"fill": 1,
|
||||
"gridPos": {
|
||||
"h": 9,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 9
|
||||
},
|
||||
"id": 22,
|
||||
"legend": {
|
||||
"avg": false,
|
||||
"current": false,
|
||||
"max": false,
|
||||
"min": false,
|
||||
"show": true,
|
||||
"total": false,
|
||||
"values": false
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [],
|
||||
"spaceLength": 10,
|
||||
"stack": false,
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"expr": "increase(zrepl_daemon_log_entries{job='$prom_job_name',level=~'warn|error'}[$__interval])",
|
||||
"format": "time_series",
|
||||
"intervalFactor": 1,
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"thresholds": [],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Log Messages that require attention",
|
||||
"tooltip": {
|
||||
"shared": true,
|
||||
"sort": 0,
|
||||
"value_type": "individual"
|
||||
},
|
||||
"type": "graph",
|
||||
"xaxis": {
|
||||
"buckets": null,
|
||||
"mode": "time",
|
||||
"name": null,
|
||||
"show": true,
|
||||
"values": []
|
||||
},
|
||||
"yaxes": [
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": "0",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"aliasColors": {},
|
||||
"bars": false,
|
||||
"dashLength": 10,
|
||||
"dashes": false,
|
||||
"datasource": null,
|
||||
"fill": 1,
|
||||
"gridPos": {
|
||||
"h": 9,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 18
|
||||
},
|
||||
"id": 33,
|
||||
"legend": {
|
||||
"avg": false,
|
||||
"current": false,
|
||||
"max": false,
|
||||
"min": false,
|
||||
"show": true,
|
||||
"total": false,
|
||||
"values": false
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [
|
||||
{
|
||||
"alias": "/replicated bytes in last.*/",
|
||||
"yaxis": 2
|
||||
}
|
||||
],
|
||||
"spaceLength": 10,
|
||||
"stack": false,
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"expr": "sum(rate(zrepl_replication_bytes_replicated{job='$prom_job_name'}[$__interval])) by (zrepl_job)",
|
||||
"format": "time_series",
|
||||
"hide": false,
|
||||
"interval": "",
|
||||
"intervalFactor": 1,
|
||||
"legendFormat": "replication data rate zrepl_job={{zrepl_job}}",
|
||||
"refId": "A"
|
||||
},
|
||||
{
|
||||
"expr": "sum(increase(zrepl_replication_bytes_replicated{job='$prom_job_name'}[10m])) by (zrepl_job)",
|
||||
"format": "time_series",
|
||||
"hide": false,
|
||||
"interval": "",
|
||||
"intervalFactor": 1,
|
||||
"legendFormat": "replicated bytes in last 10min zrepl_job={{zrepl_job}}",
|
||||
"refId": "B"
|
||||
}
|
||||
],
|
||||
"thresholds": [],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Replication Data Rate and Volume(integrates last 10min)",
|
||||
"tooltip": {
|
||||
"shared": true,
|
||||
"sort": 0,
|
||||
"value_type": "individual"
|
||||
},
|
||||
"type": "graph",
|
||||
"xaxis": {
|
||||
"buckets": null,
|
||||
"mode": "time",
|
||||
"name": null,
|
||||
"show": true,
|
||||
"values": []
|
||||
},
|
||||
"yaxes": [
|
||||
{
|
||||
"format": "Bps",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"format": "bytes",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"aliasColors": {},
|
||||
"bars": false,
|
||||
"dashLength": 10,
|
||||
"dashes": false,
|
||||
"datasource": null,
|
||||
"fill": 1,
|
||||
"gridPos": {
|
||||
"h": 9,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 18
|
||||
},
|
||||
"id": 23,
|
||||
"legend": {
|
||||
"avg": false,
|
||||
"current": false,
|
||||
"max": false,
|
||||
"min": false,
|
||||
"show": true,
|
||||
"total": false,
|
||||
"values": false
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [],
|
||||
"spaceLength": 10,
|
||||
"stack": false,
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"expr": "sum(increase(zrepl_daemon_log_entries{job='$prom_job_name',zrepl_job=~\"^[^_].*\"}[$__interval])) by (instance,zrepl_job)",
|
||||
"format": "time_series",
|
||||
"intervalFactor": 1,
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"thresholds": [],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Log Activity (without internal jobs)",
|
||||
"tooltip": {
|
||||
"shared": true,
|
||||
"sort": 0,
|
||||
"value_type": "individual"
|
||||
},
|
||||
"type": "graph",
|
||||
"xaxis": {
|
||||
"buckets": null,
|
||||
"mode": "time",
|
||||
"name": null,
|
||||
"show": true,
|
||||
"values": []
|
||||
},
|
||||
"yaxes": [
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": "0",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"aliasColors": {},
|
||||
"bars": false,
|
||||
"dashLength": 10,
|
||||
"dashes": false,
|
||||
"datasource": null,
|
||||
"fill": 1,
|
||||
"gridPos": {
|
||||
"h": 9,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 27
|
||||
},
|
||||
"id": 17,
|
||||
"legend": {
|
||||
"avg": false,
|
||||
"current": false,
|
||||
"max": false,
|
||||
"min": false,
|
||||
"show": true,
|
||||
"total": false,
|
||||
"values": false
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [],
|
||||
"spaceLength": 10,
|
||||
"stack": false,
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"$$hashKey": "object:3535",
|
||||
"expr": "go_memstats_sys_bytes{job='$prom_job_name'}",
|
||||
"format": "time_series",
|
||||
"hide": false,
|
||||
"intervalFactor": 1,
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"thresholds": [],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Memory Allocated by the Go runtime from the OS (should not grow unboundedly)",
|
||||
"tooltip": {
|
||||
"shared": true,
|
||||
"sort": 0,
|
||||
"value_type": "individual"
|
||||
},
|
||||
"type": "graph",
|
||||
"xaxis": {
|
||||
"buckets": null,
|
||||
"mode": "time",
|
||||
"name": null,
|
||||
"show": true,
|
||||
"values": []
|
||||
},
|
||||
"yaxes": [
|
||||
{
|
||||
"format": "bytes",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": "0",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"aliasColors": {},
|
||||
"bars": false,
|
||||
"dashLength": 10,
|
||||
"dashes": false,
|
||||
"datasource": null,
|
||||
"fill": 1,
|
||||
"gridPos": {
|
||||
"h": 9,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 27
|
||||
},
|
||||
"id": 19,
|
||||
"legend": {
|
||||
"avg": false,
|
||||
"current": false,
|
||||
"max": false,
|
||||
"min": false,
|
||||
"show": true,
|
||||
"total": false,
|
||||
"values": false
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [],
|
||||
"spaceLength": 10,
|
||||
"stack": false,
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"expr": "go_goroutines{job='$prom_job_name'}",
|
||||
"format": "time_series",
|
||||
"intervalFactor": 1,
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"thresholds": [],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "number of goroutines (should not grow unboundedly)",
|
||||
"tooltip": {
|
||||
"shared": true,
|
||||
"sort": 0,
|
||||
"value_type": "individual"
|
||||
},
|
||||
"type": "graph",
|
||||
"xaxis": {
|
||||
"buckets": null,
|
||||
"mode": "time",
|
||||
"name": null,
|
||||
"show": true,
|
||||
"values": []
|
||||
},
|
||||
"yaxes": [
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": "0",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"refresh": "1m",
|
||||
"schemaVersion": 16,
|
||||
"style": "dark",
|
||||
"tags": [],
|
||||
"templating": {
|
||||
"list": [
|
||||
{
|
||||
"allValue": null,
|
||||
"current": {
|
||||
"text": "zrepl",
|
||||
"value": "zrepl"
|
||||
},
|
||||
"datasource": "prometheus",
|
||||
"hide": 0,
|
||||
"includeAll": false,
|
||||
"label": "Prometheus Job Name",
|
||||
"multi": false,
|
||||
"name": "prom_job_name",
|
||||
"options": [],
|
||||
"query": "label_values(up, job)",
|
||||
"refresh": 1,
|
||||
"regex": "",
|
||||
"sort": 1,
|
||||
"tagValuesQuery": "",
|
||||
"tags": [],
|
||||
"tagsQuery": "",
|
||||
"type": "query",
|
||||
"useTags": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"time": {
|
||||
"from": "now-2d",
|
||||
"to": "now"
|
||||
},
|
||||
"timepicker": {
|
||||
"refresh_intervals": [
|
||||
"5s",
|
||||
"10s",
|
||||
"30s",
|
||||
"1m",
|
||||
"5m",
|
||||
"15m",
|
||||
"30m",
|
||||
"1h",
|
||||
"2h",
|
||||
"1d"
|
||||
],
|
||||
"time_options": [
|
||||
"5m",
|
||||
"15m",
|
||||
"1h",
|
||||
"6h",
|
||||
"12h",
|
||||
"24h",
|
||||
"2d",
|
||||
"7d",
|
||||
"30d"
|
||||
]
|
||||
},
|
||||
"timezone": "",
|
||||
"title": "zrepl 0.1",
|
||||
"uid": "xTljn4qmk",
|
||||
"version": 6
|
||||
}
|
||||
-1241
File diff suppressed because it is too large
Load Diff
+21
-35
@@ -34,60 +34,38 @@ This is a big one! Headlining features:
|
||||
|
||||
* **Resumable Send & Recv Support**
|
||||
No knobs required, automatically used where supported.
|
||||
* **Encrypted Send & Recv Support** for OpenZFS native encryption,
|
||||
:ref:`configurable <job-send-options>` at the job level, i.e., for all filesystems a job is responsible for.
|
||||
* **Replication Guarantees**
|
||||
Automatic use of ZFS holds and bookmarks to protect a replicated filesystem from losing synchronization between sender and receiver.
|
||||
By default, zrepl guarantees that incremental replication will always be possible and interrupted steps will always be resumable.
|
||||
* **Hold-Protected Send & Recv**
|
||||
Automatic ZFS holds to ensure that we can always resume a replication step.
|
||||
* **Encrypted Send & Recv Support** for OpenZFS native encryption.
|
||||
:ref:`Configurable <job-send-options>` at the job level, i.e., for all filesystems a job is responsible for.
|
||||
* **Receive-side hold on last received dataset**
|
||||
The counterpart to the replication cursor bookmark on the send-side.
|
||||
Ensures that incremental replication will always be possible between a sender and receiver.
|
||||
|
||||
.. TIP::
|
||||
Actual changelog:
|
||||
|
||||
We highly recommend studying the updated :ref:`overview section of the configuration chapter <overview-how-replication-works>` to understand how replication works.
|
||||
* |break_config| **more restrictive job names than in prior zrepl versions**
|
||||
Starting with this version, job names are going to be embedded into ZFS holds and bookmark names.
|
||||
|
||||
Quick-start guides:
|
||||
|
||||
* We have added :ref:`another quick-start guide for a typical workstation use case for zrepl <quickstart-backup-to-external-disk>`.
|
||||
Check it out to learn how you can use zrepl to back up your workstation's OpenZFS natively-encrypted root filesystem to an external disk.
|
||||
|
||||
Additional changelog:
|
||||
|
||||
* |break| |break_config| **more restrictive job names than in prior zrepl versions**
|
||||
Starting with this version, job names are going to be embedded into ZFS holds and bookmark names (see :ref:`this section for details <zrepl-zfs-abstractions>`).
|
||||
Therefore you might need to adjust your job names.
|
||||
**Note that jobs** cannot be renamed easily **once you start using zrepl 0.3.**
|
||||
* |break| |mig| replication cursor representation changed
|
||||
|
||||
* zrepl now manages the :ref:`replication cursor bookmark <zrepl-zfs-abstractions>` per job-filesystem tuple instead of a single replication cursor per filesystem.
|
||||
* zrepl now manages the :ref:`replication cursor bookmark <replication-cursor-and-last-received-hold>` per job-filesystem tuple instead of a single replication cursor per filesystem.
|
||||
In the future, this will permit multiple sending jobs to send from the same filesystems.
|
||||
* ZFS does not allow bookmark renaming, thus we cannot migrate the old replication cursors.
|
||||
* zrepl 0.3 will automatically create cursors in the new format for new replications, and warn if it still finds ones in the old format.
|
||||
* Run ``zrepl migrate replication-cursor:v1-v2`` to safely destroy old-format cursors.
|
||||
The migration will ensure that only those old-format cursors are destroyed that have been superseeded by new-format cursors.
|
||||
|
||||
* |feature| New option ``listen_freebind`` (tcp, tls, prometheus listener)
|
||||
* |feature| :issue:`341` Prometheus metric for failing replications + corresponding Grafana panel
|
||||
* |feature| :issue:`265` transport/tcp: support for CIDR masks in client IP whitelist
|
||||
* |feature| documented subcommand to generate ``bash`` and ``zsh`` completions
|
||||
* |feature| :issue:`307` ``chrome://trace`` -compatible activity tracing of zrepl daemon activity
|
||||
* |feature| logging: trace IDs for better log entry correlation with concurrent replication jobs
|
||||
* |feature| experimental environment variable for parallel replication (see :issue:`306` )
|
||||
* |bugfix| missing logger context vars in control connection handlers
|
||||
* |bugfix| improved error messages on ``zfs send`` errors
|
||||
* |feature| New option ``listen_freebind`` (tcp, tls, prometheus listener)
|
||||
* |bugfix| |docs| snapshotting: clarify sync-up behavior and warn about filesystems
|
||||
* |bugfix| transport/ssh: do not leak zombie ssh process on connection failures
|
||||
that will not be snapshotted until the sync-up phase is over
|
||||
* |docs| Installation: :ref:`FreeBSD jail with iocage <installation-freebsd-jail-with-iocage>`
|
||||
* |docs| Document new replication features in the :ref:`config overview <overview-how-replication-works>` and :repomasterlink:`replication/design.md`.
|
||||
* |feature| documented subcommand to generate ``bash`` and ``zsh`` completions
|
||||
* **[MAINTAINER NOTICE]** New platform tests in this version, please make sure you run them for your distro!
|
||||
* **[MAINTAINER NOTICE]** Please add the shell completions to the zrepl packages.
|
||||
|
||||
.. NOTE::
|
||||
| zrepl is a spare-time project primarily developed by `Christian Schwarz <https://cschwarz.com>`_.
|
||||
| You can support maintenance and feature development through one of the following services:
|
||||
| |Donate via Patreon| |Donate via GitHub Sponsors| |Donate via Liberapay| |Donate via PayPal|
|
||||
| Note that PayPal processing fees are relatively high for small donations.
|
||||
| For SEPA wire transfer and **commercial support**, please `contact Christian directly <https://cschwarz.com>`_.
|
||||
|
||||
0.2.1
|
||||
-----
|
||||
|
||||
@@ -124,6 +102,14 @@ Additional changelog:
|
||||
The pool name ``zreplplatformtest`` is reserved for this use case.
|
||||
Only run ``make platformtest`` on test systems, e.g. a FreeBSD VM image.
|
||||
|
||||
.. NOTE::
|
||||
| zrepl is a spare-time project primarily developed by `Christian Schwarz <https://cschwarz.com>`_.
|
||||
| You can support maintenance and feature development through one of the following services:
|
||||
| |Donate via Patreon| |Donate via Liberapay| |Donate via PayPal|
|
||||
| Note that PayPal processing fees are relatively high for small donations.
|
||||
| For SEPA wire transfer and **commercial support**, please `contact Christian directly <https://cschwarz.com>`_.
|
||||
|
||||
|
||||
0.1.1
|
||||
-----
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ Configuration
|
||||
configuration/transports
|
||||
configuration/filter_syntax
|
||||
configuration/sendrecvoptions
|
||||
configuration/replication
|
||||
configuration/snapshotting
|
||||
configuration/prune
|
||||
configuration/logging
|
||||
|
||||
@@ -19,7 +19,7 @@ Job Type ``push``
|
||||
* - ``type``
|
||||
- = ``push``
|
||||
* - ``name``
|
||||
- unique name of the job :issue:`(must not change)<327>`
|
||||
- unique name of the job
|
||||
* - ``connect``
|
||||
- |connect-transport|
|
||||
* - ``filesystems``
|
||||
@@ -47,7 +47,7 @@ Job Type ``sink``
|
||||
* - ``type``
|
||||
- = ``sink``
|
||||
* - ``name``
|
||||
- unique name of the job :issue:`(must not change)<327>`
|
||||
- unique name of the job
|
||||
* - ``serve``
|
||||
- |serve-transport|
|
||||
* - ``root_fs``
|
||||
@@ -70,7 +70,7 @@ Job Type ``pull``
|
||||
* - ``type``
|
||||
- = ``pull``
|
||||
* - ``name``
|
||||
- unique name of the job :issue:`(must not change)<327>`
|
||||
- unique name of the job
|
||||
* - ``connect``
|
||||
- |connect-transport|
|
||||
* - ``root_fs``
|
||||
@@ -98,7 +98,7 @@ Job Type ``source``
|
||||
* - ``type``
|
||||
- = ``source``
|
||||
* - ``name``
|
||||
- unique name of the job :issue:`(must not change)<327>`
|
||||
- unique name of the job
|
||||
* - ``serve``
|
||||
- |serve-transport|
|
||||
* - ``filesystems``
|
||||
@@ -137,7 +137,7 @@ Job type that only takes snapshots and performs pruning on the local machine.
|
||||
* - ``type``
|
||||
- = ``snap``
|
||||
* - ``name``
|
||||
- unique name of the job :issue:`(must not change)<327>`
|
||||
- unique name of the job
|
||||
* - ``filesystems``
|
||||
- |filter-spec| for filesystems to be snapshotted
|
||||
* - ``snapshotting``
|
||||
|
||||
@@ -12,7 +12,7 @@ The following paths are considered:
|
||||
The ``zrepl configcheck`` subcommand can be used to validate the configuration.
|
||||
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`.
|
||||
Full example configs such as in the :ref:`quick-start guides <quickstart-toc>` or the :sampleconf:`/` directory might also be helpful.
|
||||
Full example configs such as in the :ref:`tutorial` or the :sampleconf:`/` directory might also be helpful.
|
||||
However, copy-pasting examples is no substitute for reading documentation!
|
||||
|
||||
Config File Structure
|
||||
@@ -39,10 +39,6 @@ A *job* is the unit of activity tracked by the zrepl daemon.
|
||||
The ``type`` of a job determines its role in a replication setup and in snapshot management.
|
||||
Jobs are identified by their ``name``, both in log files and the ``zrepl status`` command.
|
||||
|
||||
.. NOTE::
|
||||
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 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 passive side responds to requests from the active side after checking its permissions.
|
||||
@@ -103,7 +99,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.
|
||||
Regardless of push- or pull-style setup, the logic executes on the active side, i.e. in the ``push`` or ``pull`` job.
|
||||
|
||||
The following high-level steps take place during replication and can be monitored using the ``zrepl status`` subcommand:
|
||||
The following steps take place during replication and can be monitored using the ``zrepl status`` subcommand:
|
||||
|
||||
* Plan the replication:
|
||||
|
||||
@@ -111,9 +107,9 @@ The following high-level steps take place during replication and can be monitore
|
||||
* Build the **replication plan**
|
||||
|
||||
* Per filesystem, compute a diff between sender and receiver snapshots
|
||||
* Build a list of **replication steps**
|
||||
* Build a list of replication steps
|
||||
|
||||
* If possible, use incremental and resumable sends
|
||||
* If possible, use incremental and resumable sends (``zfs send -i``)
|
||||
* Otherwise, use full send of most recent snapshot on sender
|
||||
|
||||
* Retry on errors that are likely temporary (i.e. network failures).
|
||||
@@ -132,35 +128,15 @@ The following high-level steps take place during replication and can be monitore
|
||||
|
||||
The idea behind the execution order of replication steps is that if the sender snapshots all filesystems simultaneously at fixed intervals, the receiver will have all filesystems snapshotted at time ``T1`` before the first snapshot at ``T2 = T1 + $interval`` is replicated.
|
||||
|
||||
ZFS Background Knowledge
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
This section gives some background knowledge about ZFS features that zrepl uses to provide guarantees for a replication filesystem.
|
||||
Specifically, zrepl guarantees by default that **incremental replication is always possible and that started replication steps can always be resumed if they are interrupted.**
|
||||
.. _replication-cursor-and-last-received-hold:
|
||||
|
||||
**ZFS Send Modes & Bookmarks**
|
||||
ZFS supports full sends (``zfs send fs@to``) and incremental sends (``zfs send -i @from fs@to``).
|
||||
Full sends are used to create a new filesystem on the receiver with the send-side state of ``fs@to``.
|
||||
Incremental sends only transfer the delta between ``@from`` and ``@to``.
|
||||
Incremental sends require that ``@from`` be present on the receiving side when receiving the incremental stream.
|
||||
Incremental sends can also use a ZFS bookmark as *from* on the sending side (``zfs send -i #bm_from fs@to``), where ``#bm_from`` was created using ``zfs bookmark fs@from fs#bm_from``.
|
||||
The receiving side must always have the actual snapshot ``@from``, regardless of whether the sending side uses ``@from`` or a bookmark of it.
|
||||
|
||||
**Resumable Send & Recv**
|
||||
The ``-s`` flag for ``zfs recv`` tells zfs to save the partially received send stream in case it is interrupted.
|
||||
To resume the replication, the receiving side filesystem's ``receive_resume_token`` must be passed to a new ``zfs send -t <value> | zfs recv`` command.
|
||||
A full send can only be resumed if ``@to`` still exists.
|
||||
An incremental send can only be resumed if ``@to`` still exists *and* either ``@from`` still exists *or* a bookmark ``#fbm`` of ``@from`` still exists.
|
||||
|
||||
**ZFS Holds**
|
||||
ZFS holds prevent a snapshot from being deleted through ``zfs destroy``, letting the destroy fail with a ``datset is busy`` error.
|
||||
Holds are created and referred to by a *tag*. They can be thought of as a named, persistent lock on the snapshot.
|
||||
|
||||
|
||||
.. _zrepl-zfs-abstractions:
|
||||
|
||||
ZFS Abstractions Managed By zrepl
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
With the background knowledge from the previous paragraph, we now summarize the different on-disk ZFS objects that zrepl manages to provide its functionality.
|
||||
**Replication cursor** bookmark and **last-received-hold** are managed by zrepl to ensure that future replications can always be done incrementally:
|
||||
the replication cursor is a send-side bookmark of the most recent successfully replicated snapshot,
|
||||
and the last-received-hold is a hold of that snapshot on the receiving side.
|
||||
The replication cursor has the format ``#zrepl_CUSOR_G_<GUID>_J_<JOBNAME>``.
|
||||
The last-received-hold tag has the format ``#zrepl_last_received_J_<JOBNAME>``.
|
||||
Encoding the job name in the names ensures that multiple sending jobs can replicate the same filesystem to different receivers without interference.
|
||||
The ``zrepl holds list`` provides a listing of all bookmarks and holds managed by zrepl.
|
||||
|
||||
.. _replication-placeholder-property:
|
||||
|
||||
@@ -172,57 +148,16 @@ Thus, zrepl creates the parent filesystems as placeholders on the receiving side
|
||||
If at some point ``S/H`` and ``S`` shall be replicated, the receiving side invalidates the placeholder flag automatically.
|
||||
The ``zrepl test placeholder`` command can be used to check whether a filesystem is a placeholder.
|
||||
|
||||
.. _replication-cursor-and-last-received-hold:
|
||||
|
||||
The **replication cursor** bookmark and **last-received-hold** are managed by zrepl to ensure that future replications can always be done incrementally.
|
||||
The replication cursor is a send-side bookmark of the most recent successfully replicated snapshot,
|
||||
and the last-received-hold is a hold of that snapshot on the receiving side.
|
||||
Both are moved atomically after the receiving side has confirmed that a replication step is complete.
|
||||
|
||||
The replication cursor has the format ``#zrepl_CUSOR_G_<GUID>_J_<JOBNAME>``.
|
||||
The last-received-hold tag has the format ``zrepl_last_received_J_<JOBNAME>``.
|
||||
Encoding the job name in the names ensures that multiple sending jobs can replicate the same filesystem to different receivers without interference.
|
||||
|
||||
.. _tentative-replication-cursor-bookmarks:
|
||||
|
||||
**Tentative replication cursor bookmarks** are short-lived boomkarks that protect the atomic moving-forward of the replication cursor and last-received-hold (see :issue:`this issue <340>`).
|
||||
They are only necessary if step holds are not used as per the :ref:`replication.protection <replication-option-protection>` setting.
|
||||
The tentative replication cursor has the format ``#zrepl_CUSORTENTATIVE_G_<GUID>_J_<JOBNAME>``.
|
||||
The ``zrepl zfs-abstraction list`` command provides a listing of all bookmarks and holds managed by zrepl.
|
||||
|
||||
.. _step-holds:
|
||||
|
||||
**Step holds** are zfs holds managed by zrepl to ensure that a replication step can always be resumed if it is interrupted, e.g., due to network outage.
|
||||
zrepl creates step holds before it attempts a replication step and releases them after the receiver confirms that the replication step is complete.
|
||||
For an initial replication ``full @initial_snap``, zrepl puts a zfs hold on ``@initial_snap``.
|
||||
For an incremental send ``@from -> @to``, zrepl puts a zfs hold on both ``@from`` and ``@to``.
|
||||
Note that ``@from`` is not strictly necessary for resumability -- a bookmark on the sending side would be sufficient --, but size-estimation in currently used OpenZFS versions only works if ``@from`` is a snapshot.
|
||||
The hold tag has the format ``zrepl_STEP_J_<JOBNAME>``.
|
||||
A job only ever has one active send per filesystem.
|
||||
Thus, there are never more than two step holds for a given pair of ``(job,filesystem)``.
|
||||
|
||||
**Step bookmarks** are zrepl's equivalent for holds on bookmarks (ZFS does not support putting holds on bookmarks).
|
||||
They are intended for a situation where a replication step uses a bookmark ``#bm`` as incremental ``from`` where ``#bm`` is not managed by zrepl.
|
||||
To ensure resumability, zrepl copies ``#bm`` to step bookmark ``#zrepl_STEP_G_<GUID>_J_<JOBNAME>``.
|
||||
If the replication is interrupted and ``#bm`` is deleted by the user, the step bookmark remains as an incremental source for the resumable send.
|
||||
Note that zrepl does not yet support creating step bookmarks because the `corresponding ZFS feature for copying bookmarks <https://github.com/openzfs/zfs/pull/9571>`_ is not yet widely available .
|
||||
Subscribe to zrepl :issue:`326` for details.
|
||||
|
||||
The ``zrepl zfs-abstraction list`` command provides a listing of all bookmarks and holds managed by zrepl.
|
||||
|
||||
.. NOTE::
|
||||
|
||||
More details can be found in the design document :repomasterlink:`replication/design.md`.
|
||||
|
||||
Limitations
|
||||
^^^^^^^^^^^
|
||||
|
||||
.. ATTENTION::
|
||||
|
||||
Currently, zrepl does not replicate filesystem properties.
|
||||
When receiving a filesystem, it is never mounted (`-u` flag) and `mountpoint=none` is set.
|
||||
This is temporary and being worked on :issue:`24`.
|
||||
|
||||
.. NOTE::
|
||||
|
||||
More details can be found in the design document :repomasterlink:`replication/design.md`.
|
||||
|
||||
|
||||
.. _jobs-multiple-jobs:
|
||||
|
||||
@@ -242,7 +177,7 @@ Multiple Jobs & More than 2 Machines
|
||||
If you would like to see improvements to multi-job setups, please `open an issue on GitHub <https://github.com/zrepl/zrepl/issues/new>`_.
|
||||
|
||||
No Overlapping
|
||||
^^^^^^^^^^^^^^
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
Jobs run independently of each other.
|
||||
If two jobs match the same filesystem with their ``filesystems`` filter, they will operate on that filesystem independently and potentially in parallel.
|
||||
@@ -250,14 +185,14 @@ For example, if job A prunes snapshots that job B is planning to replicate, the
|
||||
However, the next replication attempt will re-examine the situation from scratch and should work.
|
||||
|
||||
N push jobs to 1 sink
|
||||
^^^^^^^^^^^^^^^^^^^^^
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The :ref:`sink job <job-sink>` namespaces by client identity.
|
||||
It is thus safe to push to one sink job with different client identities.
|
||||
If the push jobs have the same client identity, the filesystems matched by the push jobs must be disjoint to avoid races.
|
||||
|
||||
N pull jobs from 1 source
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Multiple pull jobs pulling from the same source have potential for race conditions during pruning:
|
||||
each pull job prunes the source side independently, causing replication-prune and prune-prune races.
|
||||
|
||||
@@ -46,8 +46,6 @@ Example Configuration:
|
||||
grid: 1x1h(keep=all) | 24x1h | 35x1d | 6x30d
|
||||
regex: "^zrepl_.*"
|
||||
# manually created snapshots will be kept forever on receiver
|
||||
- type: regex
|
||||
regex: "^manual_.*"
|
||||
|
||||
.. DANGER::
|
||||
You might have **existing snapshots** of filesystems affected by pruning which you want to keep, i.e. not be destroyed by zrepl.
|
||||
@@ -84,82 +82,35 @@ Policy ``grid``
|
||||
- type: grid
|
||||
regex: "^zrepl_.*"
|
||||
grid: 1x1h(keep=all) | 24x1h | 35x1d | 6x30d
|
||||
│ │ │
|
||||
└─ 1 repetition of a one-hour interval with keep=all
|
||||
│ │
|
||||
└─ 24 repetitions of a one-hour interval with keep=1
|
||||
│
|
||||
└─ 6 repetitions of a 30-day interval with keep=1
|
||||
│ │
|
||||
└─ one hour interval
|
||||
│
|
||||
└─ 24 adjacent one-hour intervals
|
||||
...
|
||||
|
||||
The retention grid can be thought of as a time-based sieve that thins out snapshots as they get older.
|
||||
The ``grid`` field specifies a list of adjacent time intervals.
|
||||
The retention grid can be thought of as a time-based sieve:
|
||||
The ``grid`` field specifies a list of adjacent time intervals:
|
||||
the left edge of the leftmost (first) interval is the ``creation`` date of the youngest snapshot.
|
||||
All intervals to its right describe time intervals further in the past.
|
||||
|
||||
Each interval carries a maximum number of snapshots to keep.
|
||||
It is specified via ``(keep=N)``, where ``N`` is either ``all`` (all snapshots are kept) or a positive integer.
|
||||
The default value is **keep=1**.
|
||||
|
||||
The following procedure happens during pruning:
|
||||
|
||||
#. The list of snapshots is filtered by the regular expression in ``regex``.
|
||||
Only snapshots names that match the regex are considered for this rule, all others are not affected.
|
||||
#. The filtered list of snapshots is sorted by ``creation``.
|
||||
#. The left edge of the first interval is aligned to the ``creation`` date of the youngest snapshot.
|
||||
#. A list of buckets is created, one for each interval.
|
||||
#. The snapshots are placed into the bucket that matches their ``creation`` date.
|
||||
#. The filtered list of snapshots is sorted by ``creation``
|
||||
#. The left edge of the first interval is aligned to the ``creation`` date of the youngest snapshot
|
||||
#. A list of buckets is created, one for each interval
|
||||
#. The list of snapshots is split up into the buckets.
|
||||
#. For each bucket
|
||||
|
||||
#. the contained snapshot list is sorted by creation.
|
||||
#. snapshots from the list, oldest first, are destroyed until the specified ``keep`` count is reached.
|
||||
#. all remaining snapshots on the list are kept.
|
||||
|
||||
The syntax to describe the list of time intervals ("buckets") is as follows:
|
||||
|
||||
::
|
||||
|
||||
Repeat x Duration (keep=all)
|
||||
|
||||
* The **duration** specifies the length of the interval.
|
||||
* The **keep** count specifies the number of snapshots that fit into the bucket.
|
||||
It can be either a positive integer or ``all`` (all snapshots are kept).
|
||||
* The **repeat** count repeats the bucket definition for the specified number of times.
|
||||
|
||||
**Example**:
|
||||
|
||||
::
|
||||
|
||||
This grid spec produces the following list of adjacent buckets. For the sake of simplicity,
|
||||
we subject all snapshots to the grid pruning policy by settings `regex: .*`.
|
||||
|
||||
`
|
||||
grid: 1x1h(keep=all) | 2x2h | 1x3h
|
||||
regex: .*
|
||||
`
|
||||
|
||||
0h 1h 2h 3h 4h 5h 6h 7h 8h
|
||||
| | | | | | | | |
|
||||
|-Bucket 1-|------Bucket 2-------|-------Bucket 3------|------------Bucket 4------------|
|
||||
| keep=all | keep=1 | keep=1 | keep=1 |
|
||||
|
||||
|
||||
|
||||
Let us consider the following set of snapshots @a-zA-C , taken at an interval of ~15min:
|
||||
|
||||
|
||||
| a b c d e f g h i j k l m n o p q r s t u v w x y z A B C |
|
||||
|
||||
|
||||
The `grid` algorithm maps them to their respective buckets:
|
||||
|
||||
Bucket 1: a, b, c
|
||||
Bucket 2: d,e,f,g,h,i,j
|
||||
Bucket 3: k,l,m,n,o,p,q,r
|
||||
Bucket 4: q,r,s,t,u,v,w,x,y,z,A,B,C
|
||||
|
||||
It then applies the per-bucket pruning logic described above which resulting in the
|
||||
following list of remaining snapshots.
|
||||
|
||||
| a b c d k s |
|
||||
|
||||
Note that it only makes sense to grow (not shorten) the interval duration for buckets
|
||||
further in the past since each bucket acts like a low-pass filter for incoming snapshots
|
||||
and adding a less-low-pass-filter after a low-pass one has no effect.
|
||||
|
||||
|
||||
.. _prune-keep-last-n:
|
||||
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
.. include:: ../global.rst.inc
|
||||
|
||||
|
||||
Replication Options
|
||||
===================
|
||||
|
||||
|
||||
::
|
||||
|
||||
jobs:
|
||||
- type: push
|
||||
filesystems: ...
|
||||
replication:
|
||||
protection:
|
||||
initial: guarantee_resumability # guarantee_{resumability,incremental,nothing}
|
||||
incremental: guarantee_resumability # guarantee_{resumability,incremental,nothing}
|
||||
...
|
||||
|
||||
.. _replication-option-protection:
|
||||
|
||||
``protection`` option
|
||||
--------------------------
|
||||
|
||||
The ``protection`` variable controls the degree to which a replicated filesystem is protected from getting out of sync through a zrepl pruner or external tools that destroy snapshots.
|
||||
zrepl can guarantee :ref:`resumability <step-holds>` or just :ref:`incremental replication <replication-cursor-and-last-received-hold>`.
|
||||
|
||||
``guarantee_resumability`` is the **default** value and guarantees that a replication step is always resumable and that incremental replication will always be possible.
|
||||
The implementation uses replication cursors, last-received-hold and step holds.
|
||||
|
||||
``guarantee_incremental`` only guarantees that incremental replication will always be possible.
|
||||
If a step ``from -> to`` is interrupted and its `to` snapshot is destroyed, zrepl will remove the half-received ``to``'s resume state and start a new step ``from -> to2``.
|
||||
The implementation uses replication cursors, tentative replication cursors and last-received-hold.
|
||||
|
||||
``guarantee_nothing`` does not make any guarantees with regards to keeping sending and receiving side in sync.
|
||||
No bookmarks or holds are created to protect sender and receiver from diverging.
|
||||
|
||||
**Tradeoffs**
|
||||
|
||||
Using ``guarantee_incremental`` instead of ``guarantee_resumability`` obviously removes the resumability guarantee.
|
||||
This means that replication progress is no longer monotonic which might lead to a replication setup that never makes progress if mid-step interruptions are too frequent (e.g. frequent network outages).
|
||||
However, the advantage and :issue:`reason for existence <288>` of the ``incremental`` mode is that it allows the pruner to delete snapshots of interrupted replication steps
|
||||
which is useful if replication happens so rarely (or fails so frequently) that the amount of disk space exclusively referenced by the step's snapshots becomes intolerable.
|
||||
|
||||
.. NOTE::
|
||||
|
||||
When changing this flag, obsoleted zrepl-managed bookmarks and holds will be destroyed on the next replication step that is attempted for each filesystem.
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import subprocess
|
||||
import re
|
||||
|
||||
output = subprocess.run(["git", "tag", "-l"], capture_output=True, check=True, text=True)
|
||||
tagRE = re.compile(r"^v(?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(-rc(?P<rc>\d+))?$")
|
||||
class Tag:
|
||||
orig: str
|
||||
major: int
|
||||
minor: int
|
||||
patch: int
|
||||
rc: int
|
||||
|
||||
def __str__(self):
|
||||
return self.orig
|
||||
|
||||
def __repr__(self):
|
||||
return self.orig
|
||||
|
||||
tags = []
|
||||
for line in output.stdout.split("\n"):
|
||||
m = tagRE.match(line)
|
||||
if not m:
|
||||
continue
|
||||
m = m.groupdict()
|
||||
|
||||
t = Tag()
|
||||
t.orig = line
|
||||
t.major = int(m["major"])
|
||||
t.minor = int(m["minor"])
|
||||
t.patch = int(m["patch"])
|
||||
t.rc = int(m["rc"] if m["rc"] is not None else 0)
|
||||
|
||||
tags.append(t)
|
||||
|
||||
by_major_minor = {}
|
||||
|
||||
for tag in tags:
|
||||
key = (tag.major, tag.minor)
|
||||
l = by_major_minor.get(key, [])
|
||||
l.append(tag)
|
||||
by_major_minor[key] = l
|
||||
|
||||
latest_by_major_minor = []
|
||||
for (mm, l) in by_major_minor.items():
|
||||
# sort ascending by patch-level (rc's weigh less)
|
||||
l.sort(key=lambda tag: (tag.patch, int(tag.rc == 0), tag.rc))
|
||||
latest_by_major_minor.append(l[-1])
|
||||
latest_by_major_minor.sort(key=lambda tag: (tag.major, tag.minor))
|
||||
|
||||
# print(by_major_minor)
|
||||
# print(latest_by_major_minor)
|
||||
|
||||
cmdline = []
|
||||
|
||||
for latest_patch in latest_by_major_minor:
|
||||
cmdline.append("--whitelist-tags")
|
||||
cmdline.append(f"^{re.escape(latest_patch.orig)}$")
|
||||
|
||||
# we want to render the latest non-rc version as the default page
|
||||
# (latest_by_major_minor is already sorted)
|
||||
default_version = latest_by_major_minor[-1]
|
||||
for tag in reversed(latest_by_major_minor):
|
||||
if tag.rc == 0:
|
||||
default_version = tag
|
||||
break
|
||||
|
||||
cmdline.extend(["--root-ref", f"{default_version}"])
|
||||
cmdline.extend(["--banner-main-ref", f"{default_version}"])
|
||||
cmdline.extend(["--show-banner"])
|
||||
cmdline.extend(["--sort", "semver"])
|
||||
|
||||
cmdline.extend(["--whitelist-branches", "master"])
|
||||
|
||||
print(" ".join(cmdline))
|
||||
+2
-8
@@ -11,12 +11,10 @@
|
||||
:target: https://zrepl.github.io
|
||||
.. |Donate via PayPal| image:: https://img.shields.io/badge/donate-paypal-yellow.svg
|
||||
:target: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=R5QSXJVYHGX96
|
||||
.. |Donate via Liberapay| image:: https://img.shields.io/liberapay/patrons/zrepl.svg?logo=liberapay
|
||||
.. |Donate via Liberapay| image:: https://img.shields.io/liberapay/receives/zrepl.svg?logo=liberapay
|
||||
:target: https://liberapay.com/zrepl/donate
|
||||
.. |Donate via Patreon| image:: https://img.shields.io/endpoint.svg?url=https%3A%2F%2Fshieldsio-patreon.herokuapp.com%2Fzrepl%2Fpledges&style=flat&color=yellow
|
||||
:target: https://www.patreon.com/zrepl
|
||||
.. |Donate via GitHub Sponsors| image:: https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&style=flat&color=yellow
|
||||
:target: https://github.com/sponsors/problame
|
||||
.. |Twitter| image:: https://img.shields.io/twitter/url/https/github.com/zrepl/zrepl.svg?style=social
|
||||
:target: https://twitter.com/intent/tweet?text=Wow:&url=https%3A%2F%2Fgithub.com%2Fzrepl%2Fzrepl
|
||||
|
||||
@@ -27,8 +25,4 @@
|
||||
.. |recv-options| replace:: :ref:`recv options<job-recv-options>`
|
||||
.. |snapshotting-spec| replace:: :ref:`snapshotting specification <job-snapshotting-spec>`
|
||||
.. |pruning-spec| replace:: :ref:`pruning specification <prune>`
|
||||
.. |filter-spec| replace:: :ref:`filter specification<pattern-filter>`
|
||||
|
||||
.. |br| raw:: html
|
||||
|
||||
<br/>
|
||||
.. |filter-spec| replace:: :ref:`filter specification<pattern-filter>`
|
||||
+3
-3
@@ -5,7 +5,7 @@
|
||||
|
||||
.. include:: global.rst.inc
|
||||
|
||||
|GitHub license| |Language: Go| |Twitter| |Donate via Patreon| |Donate via GitHub Sponsors| |Donate via Liberapay| |Donate via PayPal|
|
||||
|GitHub license| |Language: Go| |Twitter| |Donate via Patreon| |Donate via Liberapay| |Donate via PayPal|
|
||||
|
||||
|
||||
zrepl - ZFS replication
|
||||
@@ -44,7 +44,7 @@ zrepl - ZFS replication
|
||||
Getting started
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
The :ref:`10 minute quick-start guides <quickstart-toc>` give you a first impression.
|
||||
The :ref:`10 minutes tutorial setup <tutorial>` gives you a first impression.
|
||||
|
||||
Main Features
|
||||
~~~~~~~~~~~~~
|
||||
@@ -125,7 +125,7 @@ Table of Contents
|
||||
:maxdepth: 2
|
||||
:caption: Contents:
|
||||
|
||||
quickstart
|
||||
tutorial
|
||||
installation
|
||||
configuration
|
||||
usage
|
||||
|
||||
@@ -7,7 +7,7 @@ Installation
|
||||
|
||||
.. TIP::
|
||||
|
||||
Note: check out the :ref:`quick-start guides <quickstart-toc>` if you want a first impression of zrepl.
|
||||
Note: check out the :ref:`tutorial` if you want a first impression of zrepl.
|
||||
|
||||
.. toctree::
|
||||
|
||||
|
||||
@@ -42,4 +42,4 @@ Either way, all build results are located in the ``artifacts/`` directory.
|
||||
.. NOTE::
|
||||
|
||||
It is your job to install the appropriate binary in the zrepl users's ``$PATH``, e.g. ``/usr/local/bin/zrepl``.
|
||||
Otherwise, the examples in the :ref:`quick-start guides <quickstart-toc>` may need to be adjusted.
|
||||
Otherwise, the examples in the :ref:`tutorial` may need to be adjusted.
|
||||
|
||||
@@ -126,7 +126,7 @@ Modify the ``/usr/local/etc/zrepl/zrepl.yml`` configuration file.
|
||||
|
||||
.. TIP::
|
||||
|
||||
Note: check out the :ref:`quick-start guides <quickstart-toc>` for examples of a ``sink`` job.
|
||||
Note: check out the :ref:`tutorial` for examples of a ``sink`` job.
|
||||
|
||||
Now ``zrepl`` can be started.
|
||||
|
||||
|
||||
@@ -5,4 +5,4 @@ What next?
|
||||
|
||||
Read the :ref:`configuration chapter<configuration_toc>` and then continue with the :ref:`usage chapter<usage>`.
|
||||
|
||||
**Reminder**: If you want a quick introduction, please read the :ref:`quick-start guides <quickstart-toc>`.
|
||||
**Reminder**: If you want a quick introduction, please read the :ref:`tutorial`.
|
||||
|
||||
+3
-2
@@ -51,10 +51,11 @@ git rm -rf .
|
||||
popd
|
||||
|
||||
echo "building site"
|
||||
|
||||
set -e
|
||||
sphinx-versioning build \
|
||||
$(python3 gen-sphinx-versioning-flags.py) \
|
||||
--show-banner \
|
||||
--whitelist-branches '^master$' \
|
||||
--whitelist-tags '(v)?\d+\.[1-9][0-9]*.\d+$' \
|
||||
docs ./public_git \
|
||||
-- -c sphinxconf # older conf.py throw errors because they used
|
||||
# version = subprocess.show_output(["git", "describe"])
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
.. include:: global.rst.inc
|
||||
|
||||
.. _quickstart-toc:
|
||||
|
||||
***********************
|
||||
Quick Start by Use Case
|
||||
***********************
|
||||
|
||||
The goal of this quick-start guide is to give you an impression of how zrepl can accomodate your use case.
|
||||
|
||||
Install zrepl
|
||||
=============
|
||||
|
||||
Follow the :ref:`OS-specific installation instructions <installation_toc>` and come back here.
|
||||
|
||||
Overview Of How zrepl Works
|
||||
============================
|
||||
|
||||
Check out the :ref:`overview section <job-overview>` to get a rough idea of what you are going to configure in the next step, then come back here.
|
||||
|
||||
Configuration Examples
|
||||
======================
|
||||
|
||||
zrepl is configured through a YAML configuration file in ``/etc/zrepl/zrepl.yml``.
|
||||
We have prepared example use cases that show-case typical deployments and different functionality of zrepl.
|
||||
We encourage you to read through all of the examples to get an idea of what zrepl has to offer, and how you can mix-and-match configurations for your use case.
|
||||
Keep the :ref:`full config documentation <configuration_toc>` handy if a config snippet is unclear.
|
||||
|
||||
**Example Use Cases**
|
||||
|
||||
.. toctree::
|
||||
:titlesonly:
|
||||
|
||||
quickstart/continuous_server_backup
|
||||
quickstart/backup_to_external_disk
|
||||
|
||||
Use ``zrepl configcheck`` to validate your configuration.
|
||||
No output indicates that everything is fine.
|
||||
|
||||
.. NOTE::
|
||||
|
||||
Please open an issue on GitHub if your use case for zrepl is significantly different from those listed above.
|
||||
Or even better, write it up in the same style as above and open a PR!
|
||||
|
||||
.. _quickstart-apply-config:
|
||||
|
||||
Apply Configuration Changes
|
||||
===========================
|
||||
|
||||
We hope that you have found a configuration that fits your use case.
|
||||
Use ``zrepl configcheck`` once again to make sure the config is correct (output indicates that everything is fine).
|
||||
Then restart the zrepl daemon on all systems involved in the replication, likely using ``service zrepl restart`` or ``systemctl restart zrepl``.
|
||||
|
||||
Watch it Work
|
||||
=============
|
||||
|
||||
Run ``zrepl status`` on the active side of the replication setup to monitor snaphotting, replication and pruning activity.
|
||||
To re-trigger replication (snapshots are separate!), use ``zrepl signal wakeup JOBNAME``.
|
||||
(refer to the example use case document if you are uncertain which job you want to wake up).
|
||||
|
||||
You can also use basic UNIX tools to inspect see what's going on.
|
||||
If you like tmux, here is a handy script that works on FreeBSD: ::
|
||||
|
||||
pkg install gnu-watch tmux
|
||||
tmux new -s zrepl -d
|
||||
tmux split-window -t zrepl "tail -f /var/log/messages"
|
||||
tmux split-window -t zrepl "gnu-watch 'zfs list -t snapshot -o name,creation -s creation'"
|
||||
tmux split-window -t zrepl "zrepl status"
|
||||
tmux select-layout -t zrepl tiled
|
||||
tmux attach -t zrepl
|
||||
|
||||
The Linux equivalent might look like this: ::
|
||||
|
||||
# make sure tmux is installed & let's assume you use systemd + journald
|
||||
tmux new -s zrepl -d
|
||||
tmux split-window -t zrepl "journalctl -f -u zrepl.service"
|
||||
tmux split-window -t zrepl "watch 'zfs list -t snapshot -o name,creation -s creation'"
|
||||
tmux split-window -t zrepl "zrepl status"
|
||||
tmux select-layout -t zrepl tiled
|
||||
tmux attach -t zrepl
|
||||
|
||||
What Next?
|
||||
==========
|
||||
|
||||
* Read more about :ref:`configuration format, options & job types <configuration_toc>`
|
||||
* Configure :ref:`logging <logging>` \& :ref:`monitoring <monitoring>`.
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
.. include:: ../global.rst.inc
|
||||
|
||||
.. _quickstart-backup-to-external-disk:
|
||||
|
||||
Local Snapshots + Offline Backup to an External Disk
|
||||
====================================================
|
||||
|
||||
This config example shows how we can use zrepl to make periodic snapshots of our local workstation and back it up to a zpool on an external disk which we occassionally connect.
|
||||
|
||||
The local snapshots should be taken every 15 minutes for pain-free recovery from CLI disasters (``rm -rf /`` and the like).
|
||||
However, we do not want to keep the snapshots around for very long because our workstation is a little tight on disk space.
|
||||
Thus, we only keep one hour worth of high-resolution snapshots, then fade them out to one per hour for a day (24 hours), then one per day for 14 days.
|
||||
|
||||
At the end of each work day, we connect our external disk that serves as our workstation's local offline backup.
|
||||
We want zrepl to inspect the filesystems and snapshots on the external pool, figure out which snapshots were created since the last time we connected the external disk, and use incremental replication to efficiently mirror our workstation to our backup disk.
|
||||
Afterwards, we want to clean up old snapshots on the backup pool: we want to keep all snapshots younger than one hour, 24 for each hor of the first day, then 360 daily backups.
|
||||
|
||||
A few additional requirements:
|
||||
|
||||
* Snapshot creation and pruning on our workstation should happen in the background, without interaction from our side.
|
||||
* However, we want to explicitly trigger replication via the command line.
|
||||
* We want to use OpenZFS native encryption to protect our data on the external disk.
|
||||
It is absolutely critical that only encrypted data leaves our workstation.
|
||||
**zrepl should provide an easy config knob for this and prevent replication of unencrypted datasets to the external disk.**
|
||||
* We want to be able to put off the backups for more than three weeks, i.e., longer than the lifetime of the automatically created snapshots on our workstation.
|
||||
**zrepl should use bookmarks and holds to achieve this goal**.
|
||||
* When we yank out the drive during replication and go on a long vacation, we do *not* want the partially replicated snapshot to stick around as it would hold on to too much disk space over time.
|
||||
Therefore, we want zrepl to deviate from its :ref:`default behavior <replication-option-protection>` and sacrifice resumability, but nonetheless retain the ability to do incremental replication once we return from our vacation.
|
||||
**zrepl should provide an easy config knob to disable step holds for incremental replication**.
|
||||
|
||||
The following config snippet implements the setup described above.
|
||||
You will likely want to customize some aspects mentioned in the top comment in the file.
|
||||
|
||||
.. literalinclude:: ../../config/samples/quickstart_backup_to_external_disk.yml
|
||||
|
||||
:ref:`Click here <quickstart-apply-config>` to go back to the quickstart guide.
|
||||
@@ -1,97 +0,0 @@
|
||||
.. include:: ../global.rst.inc
|
||||
|
||||
.. _quickstart-continuous-replication:
|
||||
|
||||
Continuous Backup of a Server
|
||||
=============================
|
||||
|
||||
This config example shows how we can backup our ZFS-based server to another machine using a zrepl push job.
|
||||
|
||||
* Production server ``prod`` with filesystems to back up:
|
||||
|
||||
* ``zroot/var/db``
|
||||
* ``zroot/usr/home`` and all its child filesystems
|
||||
* **except** ``zroot/usr/home/paranoid`` belonging to a user doing backups themselves
|
||||
|
||||
* Backup server ``backups`` with
|
||||
|
||||
* Filesystem ``storage/zrepl/sink/prod`` + children dedicated to backups of ``prod``
|
||||
|
||||
Our backup solution should fulfill the following requirements:
|
||||
|
||||
* Periodically snapshot the filesystems on ``prod`` *every 10 minutes*
|
||||
* Incrementally replicate these snapshots to ``storage/zrepl/sink/prod/*`` on ``backups``
|
||||
* Keep only very few snapshots on ``prod`` to save disk space
|
||||
* Keep a fading history (24 hourly, 30 daily, 6 monthly) of snapshots on ``backups``
|
||||
* The network is untrusted - zrepl should use TLS to protect its communication and our data.
|
||||
|
||||
Analysis
|
||||
--------
|
||||
|
||||
We can model this situation as two jobs:
|
||||
|
||||
* A **push job** on ``prod``
|
||||
|
||||
* Creates the snapshots
|
||||
* Keeps a short history of local snapshots to enable incremental replication to ``backups``
|
||||
* Connects to the ``zrepl daemon`` process on ``backups``
|
||||
* Pushes snapshots ``backups``
|
||||
* Prunes snapshots on ``backups`` after replication is complete
|
||||
|
||||
* A **sink job** on ``backups``
|
||||
|
||||
* Accepts connections & responds to requests from ``prod``
|
||||
* Limits client ``prod`` access to filesystem sub-tree ``storage/zrepl/sink/prod``
|
||||
|
||||
Generate TLS Certificates
|
||||
-------------------------
|
||||
|
||||
We use the :ref:`TLS client authentication transport <transport-tcp+tlsclientauth>` to protect our data on the wire.
|
||||
To get things going quickly, we skip setting up a CA and generate two self-signed certificates as described :ref:`here <transport-tcp+tlsclientauth-2machineopenssl>`.
|
||||
For convenience, we generate the key pairs on our local machine and distribute them using ssh:
|
||||
|
||||
.. code-block:: bash
|
||||
:emphasize-lines: 6,13
|
||||
|
||||
openssl req -x509 -sha256 -nodes \
|
||||
-newkey rsa:4096 \
|
||||
-days 365 \
|
||||
-keyout backups.key \
|
||||
-out backups.crt
|
||||
# ... and use "backups" as Common Name (CN)
|
||||
|
||||
openssl req -x509 -sha256 -nodes \
|
||||
-newkey rsa:4096 \
|
||||
-days 365 \
|
||||
-keyout prod.key \
|
||||
-out prod.crt
|
||||
# ... and use "prod" as Common Name (CN)
|
||||
|
||||
ssh root@backups "mkdir /etc/zrepl"
|
||||
scp backups.key backups.crt prod.crt root@backups:/etc/zrepl
|
||||
|
||||
ssh root@prod "mkdir /etc/zrepl"
|
||||
scp prod.key prod.crt backups.crt root@prod:/etc/zrepl
|
||||
|
||||
Note that alternative transports exist, e.g. via :ref:`TCP without TLS <transport-tcp>` or :ref:`ssh <transport-ssh+stdinserver>`.
|
||||
|
||||
Configure server ``prod``
|
||||
-------------------------
|
||||
|
||||
We define a **push job** named ``prod_to_backups`` in ``/etc/zrepl/zrepl.yml`` on host ``prod`` :
|
||||
|
||||
.. literalinclude:: ../../config/samples/quickstart_continuous_server_backup_sender.yml
|
||||
|
||||
.. _tutorial-configure-prod:
|
||||
|
||||
Configure server ``backups``
|
||||
----------------------------
|
||||
|
||||
We define a corresponding **sink job** named ``sink`` in ``/etc/zrepl/zrepl.yml`` on host ``backups`` :
|
||||
|
||||
.. literalinclude:: ../../config/samples/quickstart_continuous_server_backup_receiver.yml
|
||||
|
||||
Go Back To Quickstart Guide
|
||||
---------------------------
|
||||
|
||||
:ref:`Click here <quickstart-apply-config>` to go back to the quickstart guide.
|
||||
+1
-4
@@ -2,7 +2,7 @@
|
||||
|
||||
.. _supporters:
|
||||
|
||||
|Donate via Patreon| |Donate via GitHub Sponsors| |Donate via Liberapay| |Donate via PayPal|
|
||||
|Donate via Patreon| |Donate via Liberapay| |Donate via PayPal|
|
||||
|
||||
zrepl is a spare-time project primarily developed by `Christian Schwarz <https://cschwarz.com>`_.
|
||||
You can support maintenance and feature development through one of the services listed above.
|
||||
@@ -32,9 +32,6 @@ We would like to thank the following people and organizations for supporting zre
|
||||
|
||||
<div class="fa fa-code" style="width: 1em;"></div>
|
||||
|
||||
* |supporter-std| `@jwittlincohen <https://github.com/jwittlincohen>`_
|
||||
* |supporter-std| `Michael D. Schmitt <https://waterbendingscroll.dancingdragons.org>`_
|
||||
* |supporter-std| `Hans Schulz <https://github.com/schulzh>`_
|
||||
* |supporter-std| Henning Kessler
|
||||
* |supporter-std| `John Ramsden <https://github.com/johnramsden>`_
|
||||
* |supporter-std| `DrLuke <https://github.com/drluke>`_
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
.. include:: global.rst.inc
|
||||
|
||||
.. _tutorial:
|
||||
|
||||
Tutorial
|
||||
========
|
||||
|
||||
|
||||
This tutorial shows how zrepl can be used to implement a ZFS-based push backup.
|
||||
We assume the following scenario:
|
||||
|
||||
* Production server ``prod`` with filesystems to back up:
|
||||
|
||||
* ``zroot/var/db``
|
||||
* ``zroot/usr/home`` and all its child filesystems
|
||||
* **except** ``zroot/usr/home/paranoid`` belonging to a user doing backups themselves
|
||||
|
||||
* Backup server ``backups`` with
|
||||
|
||||
* Filesystem ``storage/zrepl/sink/prod`` + children dedicated to backups of ``prod``
|
||||
|
||||
Our backup solution should fulfill the following requirements:
|
||||
|
||||
* Periodically snapshot the filesystems on ``prod`` *every 10 minutes*
|
||||
* Incrementally replicate these snapshots to ``storage/zrepl/sink/prod/*`` on ``backups``
|
||||
* Keep only very few snapshots on ``prod`` to save disk space
|
||||
* Keep a fading history (24 hourly, 30 daily, 6 monthly) of snapshots on ``backups``
|
||||
|
||||
Analysis
|
||||
--------
|
||||
|
||||
We can model this situation as two jobs:
|
||||
|
||||
* A **push job** on ``prod``
|
||||
|
||||
* Creates the snapshots
|
||||
* Keeps a short history of local snapshots to enable incremental replication to ``backups``
|
||||
* Connects to the ``zrepl daemon`` process on ``backups``
|
||||
* Pushes snapshots ``backups``
|
||||
* Prunes snapshots on ``backups`` after replication is complete
|
||||
|
||||
* A **sink job** on ``backups``
|
||||
|
||||
* Accepts connections & responds to requests from ``prod``
|
||||
* Limits client ``prod`` access to filesystem sub-tree ``storage/zrepl/sink/prod``
|
||||
|
||||
Install zrepl
|
||||
-------------
|
||||
|
||||
Follow the :ref:`OS-specific installation instructions <installation_toc>` and come back here.
|
||||
|
||||
Generate TLS Certificates
|
||||
-------------------------
|
||||
|
||||
We use the :ref:`TLS client authentication transport <transport-tcp+tlsclientauth>` to protect our data on the wire.
|
||||
To get things going quickly, we skip setting up a CA and generate two self-signed certificates as described :ref:`here <transport-tcp+tlsclientauth-2machineopenssl>`.
|
||||
For convenience, we generate the key pairs on our local machine and distribute them using ssh:
|
||||
|
||||
.. code-block:: bash
|
||||
:emphasize-lines: 6,13
|
||||
|
||||
openssl req -x509 -sha256 -nodes \
|
||||
-newkey rsa:4096 \
|
||||
-days 365 \
|
||||
-keyout backups.key \
|
||||
-out backups.crt
|
||||
# ... and use "backups" as Common Name (CN)
|
||||
|
||||
openssl req -x509 -sha256 -nodes \
|
||||
-newkey rsa:4096 \
|
||||
-days 365 \
|
||||
-keyout prod.key \
|
||||
-out prod.crt
|
||||
# ... and use "prod" as Common Name (CN)
|
||||
|
||||
ssh root@backups "mkdir /etc/zrepl"
|
||||
scp backups.key backups.crt prod.crt root@backups:/etc/zrepl
|
||||
|
||||
ssh root@prod "mkdir /etc/zrepl"
|
||||
scp prod.key prod.crt backups.crt root@prod:/etc/zrepl
|
||||
|
||||
|
||||
Configure server ``prod``
|
||||
-------------------------
|
||||
|
||||
We define a **push job** named ``prod_to_backups`` in ``/etc/zrepl/zrepl.yml`` on host ``prod`` : ::
|
||||
|
||||
jobs:
|
||||
- name: prod_to_backups
|
||||
type: push
|
||||
connect:
|
||||
type: tls
|
||||
address: "backups.example.com:8888"
|
||||
ca: /etc/zrepl/backups.crt
|
||||
cert: /etc/zrepl/prod.crt
|
||||
key: /etc/zrepl/prod.key
|
||||
server_cn: "backups"
|
||||
filesystems: {
|
||||
"zroot/var/db": true,
|
||||
"zroot/usr/home<": true,
|
||||
"zroot/usr/home/paranoid": false
|
||||
}
|
||||
snapshotting:
|
||||
type: periodic
|
||||
prefix: zrepl_
|
||||
interval: 10m
|
||||
pruning:
|
||||
keep_sender:
|
||||
- type: not_replicated
|
||||
- type: last_n
|
||||
count: 10
|
||||
keep_receiver:
|
||||
- type: grid
|
||||
grid: 1x1h(keep=all) | 24x1h | 30x1d | 6x30d
|
||||
regex: "^zrepl_"
|
||||
|
||||
.. _tutorial-configure-prod:
|
||||
|
||||
Configure server ``backups``
|
||||
----------------------------
|
||||
|
||||
We define a corresponding **sink job** named ``sink`` in ``/etc/zrepl/zrepl.yml`` on host ``backups`` : ::
|
||||
|
||||
jobs:
|
||||
- name: sink
|
||||
type: sink
|
||||
serve:
|
||||
type: tls
|
||||
listen: ":8888"
|
||||
ca: "/etc/zrepl/prod.crt"
|
||||
cert: "/etc/zrepl/backups.crt"
|
||||
key: "/etc/zrepl/backups.key"
|
||||
client_cns:
|
||||
- "prod"
|
||||
root_fs: "storage/zrepl/sink"
|
||||
|
||||
|
||||
Apply Configuration Changes
|
||||
---------------------------
|
||||
|
||||
We use ``zrepl configcheck`` to catch any configuration errors: no output indicates that everything is fine.
|
||||
If that is the case, restart the zrepl daemon on **both** ``prod`` and ``backups`` using ``service zrepl restart`` or ``systemctl restart zrepl``.
|
||||
|
||||
|
||||
Watch it Work
|
||||
-------------
|
||||
|
||||
Run ``zrepl status`` on ``prod`` to monitor the replication and pruning activity.
|
||||
To re-trigger replication (snapshots are separate!), use ``zrepl signal wakeup prod_to_backups`` on ``prod``.
|
||||
|
||||
If you like tmux, here is a handy script that works on FreeBSD: ::
|
||||
|
||||
pkg install gnu-watch tmux
|
||||
tmux new -s zrepl -d
|
||||
tmux split-window -t zrepl "tail -f /var/log/messages"
|
||||
tmux split-window -t zrepl "gnu-watch 'zfs list -t snapshot -o name,creation -s creation | grep zrepl_'"
|
||||
tmux split-window -t zrepl "zrepl status"
|
||||
tmux select-layout -t zrepl tiled
|
||||
tmux attach -t zrepl
|
||||
|
||||
The Linux equivalent might look like this: ::
|
||||
|
||||
# make sure tmux is installed & let's assume you use systemd + journald
|
||||
tmux new -s zrepl -d
|
||||
tmux split-window -t zrepl "journalctl -f -u zrepl.service"
|
||||
tmux split-window -t zrepl "watch 'zfs list -t snapshot -o name,creation -s creation | grep zrepl_'"
|
||||
tmux split-window -t zrepl "zrepl status"
|
||||
tmux select-layout -t zrepl tiled
|
||||
tmux attach -t zrepl
|
||||
|
||||
Summary
|
||||
-------
|
||||
|
||||
Congratulations, you have a working push backup. Where to go next?
|
||||
|
||||
* Read more about :ref:`configuration format, options & job types <configuration_toc>`
|
||||
* Configure :ref:`logging <logging>` \& :ref:`monitoring <monitoring>`.
|
||||
+1
-1
@@ -38,7 +38,7 @@ CLI Overview
|
||||
* - ``zrepl migrate``
|
||||
- | perform on-disk state / ZFS property migrations
|
||||
| (see :ref:`changelog <changelog>` for details)
|
||||
* - ``zrepl zfs-abstraction``
|
||||
* - ``zrepl zfs-abstractions``
|
||||
- list and remove zrepl's abstractions on top of ZFS, e.g. holds and step bookmarks (see :ref:`overview <replication-cursor-and-last-received-hold>` )
|
||||
|
||||
.. _usage-zrepl-daemon:
|
||||
|
||||
+77
-92
@@ -112,6 +112,13 @@ func (s *Sender) ListFilesystemVersions(ctx context.Context, r *pdu.ListFilesyst
|
||||
for i := range fsvs {
|
||||
rfsvs[i] = pdu.FilesystemVersionFromZFS(&fsvs[i])
|
||||
}
|
||||
|
||||
sendAbstractions := sendAbstractionsCacheSingleton.GetByFS(ctx, lp.ToString())
|
||||
rSabsInfo := make([]*pdu.SendAbstraction, len(sendAbstractions))
|
||||
for i := range rSabsInfo {
|
||||
rSabsInfo[i] = SendAbstractionToPDU(sendAbstractions[i])
|
||||
}
|
||||
|
||||
res := &pdu.ListFilesystemVersionsRes{Versions: rfsvs}
|
||||
return res, nil
|
||||
|
||||
@@ -208,30 +215,36 @@ func (s *Sender) Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, io.Rea
|
||||
return res, nil, nil
|
||||
}
|
||||
|
||||
// create holds or bookmarks of `From` and `To` to guarantee one of the following:
|
||||
// - that the replication step can always be resumed (`holds`),
|
||||
// - that the replication step can be interrupted and a future replication
|
||||
// step with same or different `To` but same `From` is still possible (`bookmarks`)
|
||||
// - nothing (`none`)
|
||||
//
|
||||
// ...
|
||||
//
|
||||
// ... actually create the abstractions
|
||||
replicationGuaranteeOptions, err := replicationGuaranteeOptionsFromPDU(r.GetReplicationConfig().Protection)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
replicationGuaranteeStrategy := replicationGuaranteeOptions.Strategy(sendArgs.From != nil)
|
||||
liveAbs, err := replicationGuaranteeStrategy.SenderPreSend(ctx, s.jobId, &sendArgs)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
for _, a := range liveAbs {
|
||||
if a != nil {
|
||||
abstractionsCacheSingleton.Put(a)
|
||||
// create a replication cursor for `From` (usually an idempotent no-op because SendCompleted already created it before)
|
||||
var fromReplicationCursor Abstraction
|
||||
if sendArgs.From != nil {
|
||||
// For all but the first replication, this should always be a no-op because SendCompleted already moved the cursor
|
||||
fromReplicationCursor, err = CreateReplicationCursor(ctx, sendArgs.FS, *sendArgs.FromVersion, s.jobId) // no shadow
|
||||
if err == zfs.ErrBookmarkCloningNotSupported {
|
||||
getLogger(ctx).Debug("not creating replication cursor from bookmark because ZFS does not support it")
|
||||
// fallthrough
|
||||
} else if err != nil {
|
||||
return nil, nil, errors.Wrap(err, "cannot set replication cursor to `from` version before starting send")
|
||||
}
|
||||
}
|
||||
|
||||
var fromHold, toHold Abstraction
|
||||
// make sure `From` doesn't go away in order to make this step resumable
|
||||
if sendArgs.From != nil {
|
||||
fromHold, err = HoldStep(ctx, sendArgs.FS, *sendArgs.FromVersion, s.jobId) // no shadow
|
||||
if err == zfs.ErrBookmarkCloningNotSupported {
|
||||
getLogger(ctx).Debug("not creating step bookmark because ZFS does not support it")
|
||||
// fallthrough
|
||||
} else if err != nil {
|
||||
return nil, nil, errors.Wrapf(err, "cannot hold `from` version %q before starting send", *sendArgs.FromVersion)
|
||||
}
|
||||
}
|
||||
// make sure `To` doesn't go away in order to make this step resumable
|
||||
toHold, err = HoldStep(ctx, sendArgs.FS, sendArgs.ToVersion, s.jobId)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrapf(err, "cannot hold `to` version %q before starting send", sendArgs.ToVersion)
|
||||
}
|
||||
|
||||
// cleanup the mess that _this function_ might have created in prior failed attempts:
|
||||
//
|
||||
// In summary, we delete every endpoint ZFS abstraction created on this filesystem for this job id,
|
||||
@@ -252,6 +265,7 @@ func (s *Sender) Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, io.Rea
|
||||
ctx, endSpan := trace.WithSpan(ctx, "cleanup-stale-abstractions")
|
||||
defer endSpan()
|
||||
|
||||
liveAbs := []Abstraction{fromHold, toHold, fromReplicationCursor}
|
||||
keep := func(a Abstraction) (keep bool) {
|
||||
keep = false
|
||||
for _, k := range liveAbs {
|
||||
@@ -268,31 +282,23 @@ func (s *Sender) Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, io.Rea
|
||||
}
|
||||
for _, staleVersion := range obsoleteAbs {
|
||||
for _, mustLiveVersion := range mustLiveVersions {
|
||||
isSendArg := zfs.FilesystemVersionEqualIdentity(mustLiveVersion, staleVersion.GetFilesystemVersion())
|
||||
stepHoldBasedGuaranteeStrategy := false
|
||||
k := replicationGuaranteeStrategy.Kind()
|
||||
switch k {
|
||||
case ReplicationGuaranteeKindResumability:
|
||||
stepHoldBasedGuaranteeStrategy = true
|
||||
case ReplicationGuaranteeKindIncremental:
|
||||
case ReplicationGuaranteeKindNone:
|
||||
default:
|
||||
panic(fmt.Sprintf("this is supposed to be an exhaustive match, got %v", k))
|
||||
}
|
||||
isSnapshot := mustLiveVersion.IsSnapshot()
|
||||
if isSendArg && (!isSnapshot || stepHoldBasedGuaranteeStrategy) {
|
||||
if zfs.FilesystemVersionEqualIdentity(mustLiveVersion, staleVersion.GetFilesystemVersion()) {
|
||||
panic(fmt.Sprintf("impl error: %q would be destroyed because it is considered stale but it is part of of sendArgs=%s", mustLiveVersion.String(), pretty.Sprint(sendArgs)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
destroyTypes := AbstractionTypeSet{
|
||||
AbstractionStepHold: true,
|
||||
AbstractionTentativeReplicationCursorBookmark: true,
|
||||
}
|
||||
abstractionsCacheSingleton.TryBatchDestroy(ctx, s.jobId, sendArgs.FS, destroyTypes, keep, check)
|
||||
sendAbstractionsCacheSingleton.TryBatchDestroy(ctx, s.jobId, sendArgs.FS, keep, check)
|
||||
}()
|
||||
|
||||
if fromHold != nil {
|
||||
sendAbstractionsCacheSingleton.Put(fromHold)
|
||||
}
|
||||
sendAbstractionsCacheSingleton.Put(toHold)
|
||||
if fromReplicationCursor != nil {
|
||||
sendAbstractionsCacheSingleton.Put(fromReplicationCursor)
|
||||
}
|
||||
|
||||
sendStream, err := zfs.ZFSSend(ctx, sendArgs)
|
||||
if err != nil {
|
||||
// it's ok to not destroy the abstractions we just created here, a new send attempt will take care of it
|
||||
@@ -325,32 +331,35 @@ func (p *Sender) SendCompleted(ctx context.Context, r *pdu.SendCompletedReq) (*p
|
||||
return nil, errors.Wrap(err, "validate `to` exists")
|
||||
}
|
||||
|
||||
replicationGuaranteeOptions, err := replicationGuaranteeOptionsFromPDU(orig.GetReplicationConfig().Protection)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
liveAbs, err := replicationGuaranteeOptions.Strategy(from != nil).SenderPostRecvConfirmed(ctx, p.jobId, fs, to)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, a := range liveAbs {
|
||||
if a != nil {
|
||||
abstractionsCacheSingleton.Put(a)
|
||||
log := func(ctx context.Context) Logger {
|
||||
log := getLogger(ctx).WithField("to_guid", to.Guid).
|
||||
WithField("fs", fs).
|
||||
WithField("to", to.RelName)
|
||||
if from != nil {
|
||||
log = log.WithField("from", from.RelName).WithField("from_guid", from.Guid)
|
||||
}
|
||||
return log
|
||||
}
|
||||
keep := func(a Abstraction) (keep bool) {
|
||||
keep = false
|
||||
for _, k := range liveAbs {
|
||||
keep = keep || AbstractionEquals(a, k)
|
||||
|
||||
toReplicationCursor, err := CreateReplicationCursor(ctx, fs, to, p.jobId)
|
||||
if err != nil {
|
||||
if err == zfs.ErrBookmarkCloningNotSupported {
|
||||
log(ctx).Debug("not setting replication cursor, bookmark cloning not supported")
|
||||
} else {
|
||||
msg := "cannot move replication cursor, keeping hold on `to` until successful"
|
||||
log(ctx).WithError(err).Error(msg)
|
||||
err = errors.Wrap(err, msg)
|
||||
// it is correct to not destroy from and to step holds if we can't move the cursor!
|
||||
return &pdu.SendCompletedRes{}, err
|
||||
}
|
||||
return keep
|
||||
} else {
|
||||
log(ctx).WithField("to_cursor", toReplicationCursor.String()).Info("successfully created `to` replication cursor")
|
||||
}
|
||||
destroyTypes := AbstractionTypeSet{
|
||||
AbstractionStepHold: true,
|
||||
AbstractionTentativeReplicationCursorBookmark: true,
|
||||
AbstractionReplicationCursorBookmarkV2: true,
|
||||
|
||||
keep := func(a Abstraction) bool {
|
||||
return AbstractionEquals(a, toReplicationCursor)
|
||||
}
|
||||
abstractionsCacheSingleton.TryBatchDestroy(ctx, p.jobId, fs, destroyTypes, keep, nil)
|
||||
sendAbstractionsCacheSingleton.TryBatchDestroy(ctx, p.jobId, fs, keep, nil)
|
||||
|
||||
return &pdu.SendCompletedRes{}, nil
|
||||
|
||||
@@ -426,6 +435,8 @@ type ReceiverConfig struct {
|
||||
|
||||
RootWithoutClientComponent *zfs.DatasetPath // TODO use
|
||||
AppendClientIdentity bool
|
||||
|
||||
UpdateLastReceivedHold bool
|
||||
}
|
||||
|
||||
func (c *ReceiverConfig) copyIn() {
|
||||
@@ -666,7 +677,7 @@ func (s *Receiver) Receive(ctx context.Context, req *pdu.ReceiveReq, receive io.
|
||||
f := zfs.NewDatasetPathForest()
|
||||
f.Add(lp)
|
||||
getLogger(ctx).Debug("begin tree-walk")
|
||||
f.WalkTopDown(func(v *zfs.DatasetPathVisit) (visitChildTree bool) {
|
||||
f.WalkTopDown(func(v zfs.DatasetPathVisit) (visitChildTree bool) {
|
||||
if v.Path.Equal(lp) {
|
||||
return false
|
||||
}
|
||||
@@ -694,7 +705,7 @@ func (s *Receiver) Receive(ctx context.Context, req *pdu.ReceiveReq, receive io.
|
||||
}
|
||||
l := getLogger(ctx).WithField("placeholder_fs", v.Path)
|
||||
l.Debug("create placeholder filesystem")
|
||||
err := zfs.ZFSCreatePlaceholderFilesystem(ctx, v.Path, v.Parent.Path)
|
||||
err := zfs.ZFSCreatePlaceholderFilesystem(ctx, v.Path)
|
||||
if err != nil {
|
||||
l.WithError(err).Error("cannot create placeholder filesystem")
|
||||
visitErr = err
|
||||
@@ -850,38 +861,12 @@ func (s *Receiver) Receive(ctx context.Context, req *pdu.ReceiveReq, receive io.
|
||||
return nil, errors.Wrap(err, msg)
|
||||
}
|
||||
|
||||
replicationGuaranteeOptions, err := replicationGuaranteeOptionsFromPDU(req.GetReplicationConfig().Protection)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
replicationGuaranteeStrategy := replicationGuaranteeOptions.Strategy(ph.FSExists)
|
||||
liveAbs, err := replicationGuaranteeStrategy.ReceiverPostRecv(ctx, s.conf.JobID, lp.ToString(), toRecvd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, a := range liveAbs {
|
||||
if a != nil {
|
||||
abstractionsCacheSingleton.Put(a)
|
||||
if s.conf.UpdateLastReceivedHold {
|
||||
log.Debug("move last-received-hold")
|
||||
if err := MoveLastReceivedHold(ctx, lp.ToString(), toRecvd, s.conf.JobID); err != nil {
|
||||
return nil, errors.Wrap(err, "cannot move last-received-hold")
|
||||
}
|
||||
}
|
||||
keep := func(a Abstraction) (keep bool) {
|
||||
keep = false
|
||||
for _, k := range liveAbs {
|
||||
keep = keep || AbstractionEquals(a, k)
|
||||
}
|
||||
return keep
|
||||
}
|
||||
check := func(obsoleteAbs []Abstraction) {
|
||||
for _, abs := range obsoleteAbs {
|
||||
if zfs.FilesystemVersionEqualIdentity(abs.GetFilesystemVersion(), toRecvd) {
|
||||
panic(fmt.Sprintf("would destroy endpoint abstraction around the filesystem version we just received %s", abs))
|
||||
}
|
||||
}
|
||||
}
|
||||
destroyTypes := AbstractionTypeSet{
|
||||
AbstractionLastReceivedHold: true,
|
||||
}
|
||||
abstractionsCacheSingleton.TryBatchDestroy(ctx, s.conf.JobID, lp.ToString(), destroyTypes, keep, check)
|
||||
|
||||
return &pdu.ReceiveRes{}, nil
|
||||
}
|
||||
|
||||
@@ -1,208 +0,0 @@
|
||||
package endpoint
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/zrepl/zrepl/daemon/logging/trace"
|
||||
"github.com/zrepl/zrepl/util/chainlock"
|
||||
)
|
||||
|
||||
var abstractionsCacheMetrics struct {
|
||||
count prometheus.Gauge
|
||||
}
|
||||
|
||||
func init() {
|
||||
abstractionsCacheMetrics.count = prometheus.NewGauge(prometheus.GaugeOpts{
|
||||
Namespace: "zrepl",
|
||||
Subsystem: "endpoint",
|
||||
Name: "abstractions_cache_entry_count",
|
||||
Help: "number of abstractions tracked in the abstractionsCache data structure",
|
||||
})
|
||||
}
|
||||
|
||||
var abstractionsCacheSingleton = newAbstractionsCache()
|
||||
|
||||
func AbstractionsCacheInvalidate(fs string) {
|
||||
abstractionsCacheSingleton.InvalidateFSCache(fs)
|
||||
}
|
||||
|
||||
type abstractionsCacheDidLoadFSState int
|
||||
|
||||
const (
|
||||
abstractionsCacheDidLoadFSStateNo abstractionsCacheDidLoadFSState = iota // 0-value has meaning
|
||||
abstractionsCacheDidLoadFSStateInProgress
|
||||
abstractionsCacheDidLoadFSStateDone
|
||||
)
|
||||
|
||||
type abstractionsCache struct {
|
||||
mtx chainlock.L
|
||||
abstractions []Abstraction
|
||||
didLoadFS map[string]abstractionsCacheDidLoadFSState
|
||||
didLoadFSChanged *sync.Cond
|
||||
}
|
||||
|
||||
func newAbstractionsCache() *abstractionsCache {
|
||||
c := &abstractionsCache{
|
||||
didLoadFS: make(map[string]abstractionsCacheDidLoadFSState),
|
||||
}
|
||||
c.didLoadFSChanged = c.mtx.NewCond()
|
||||
return c
|
||||
}
|
||||
|
||||
func (s *abstractionsCache) Put(a Abstraction) {
|
||||
defer s.mtx.Lock().Unlock()
|
||||
|
||||
var zeroJobId JobID
|
||||
if a.GetJobID() == nil {
|
||||
panic("abstraction must not have nil job id")
|
||||
} else if *a.GetJobID() == zeroJobId {
|
||||
panic(fmt.Sprintf("abstraction must not have zero-value job id: %s", a))
|
||||
}
|
||||
|
||||
s.abstractions = append(s.abstractions, a)
|
||||
abstractionsCacheMetrics.count.Set(float64(len(s.abstractions)))
|
||||
}
|
||||
|
||||
func (s *abstractionsCache) InvalidateFSCache(fs string) {
|
||||
// FIXME: O(n)
|
||||
newAbs := make([]Abstraction, 0, len(s.abstractions))
|
||||
for _, a := range s.abstractions {
|
||||
if a.GetFS() != fs {
|
||||
newAbs = append(newAbs, a)
|
||||
}
|
||||
}
|
||||
s.abstractions = newAbs
|
||||
abstractionsCacheMetrics.count.Set(float64(len(s.abstractions)))
|
||||
|
||||
s.didLoadFS[fs] = abstractionsCacheDidLoadFSStateNo
|
||||
s.didLoadFSChanged.Broadcast()
|
||||
|
||||
}
|
||||
|
||||
// - logs errors in getting on-disk abstractions
|
||||
// - only fetches on-disk abstractions once, but every time from the in-memory store
|
||||
//
|
||||
// That means that for precise results, all abstractions created by the endpoint must be .Put into this cache.
|
||||
func (s *abstractionsCache) GetAndDeleteByJobIDAndFS(ctx context.Context, jobID JobID, fs string, types AbstractionTypeSet, keep func(a Abstraction) bool) (ret []Abstraction) {
|
||||
defer s.mtx.Lock().Unlock()
|
||||
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
|
||||
var zeroJobId JobID
|
||||
if jobID == zeroJobId {
|
||||
panic("must not pass zero-value job id")
|
||||
}
|
||||
if fs == "" {
|
||||
panic("must not pass zero-value fs")
|
||||
}
|
||||
|
||||
s.tryLoadOnDiskAbstractions(ctx, fs)
|
||||
|
||||
// FIXME O(n)
|
||||
var remaining []Abstraction
|
||||
for _, a := range s.abstractions {
|
||||
aJobId := *a.GetJobID()
|
||||
aFS := a.GetFS()
|
||||
if aJobId == jobID && aFS == fs && types[a.GetType()] && !keep(a) {
|
||||
ret = append(ret, a)
|
||||
} else {
|
||||
remaining = append(remaining, a)
|
||||
}
|
||||
}
|
||||
s.abstractions = remaining
|
||||
abstractionsCacheMetrics.count.Set(float64(len(s.abstractions)))
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
// caller must hold s.mtx
|
||||
func (s *abstractionsCache) tryLoadOnDiskAbstractions(ctx context.Context, fs string) {
|
||||
for s.didLoadFS[fs] != abstractionsCacheDidLoadFSStateDone {
|
||||
if s.didLoadFS[fs] == abstractionsCacheDidLoadFSStateInProgress {
|
||||
s.didLoadFSChanged.Wait()
|
||||
continue
|
||||
}
|
||||
if s.didLoadFS[fs] != abstractionsCacheDidLoadFSStateNo {
|
||||
panic(fmt.Sprintf("unreachable: %v", s.didLoadFS[fs]))
|
||||
}
|
||||
|
||||
s.didLoadFS[fs] = abstractionsCacheDidLoadFSStateInProgress
|
||||
defer s.didLoadFSChanged.Broadcast()
|
||||
|
||||
var onDiskAbs []Abstraction
|
||||
var err error
|
||||
s.mtx.DropWhile(func() {
|
||||
onDiskAbs, err = s.tryLoadOnDiskAbstractionsImpl(ctx, fs) // no shadow
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
s.didLoadFS[fs] = abstractionsCacheDidLoadFSStateNo
|
||||
getLogger(ctx).WithField("fs", fs).WithError(err).Error("cannot list abstractions for filesystem")
|
||||
} else {
|
||||
s.didLoadFS[fs] = abstractionsCacheDidLoadFSStateDone
|
||||
s.abstractions = append(s.abstractions, onDiskAbs...)
|
||||
getLogger(ctx).WithField("fs", fs).WithField("abstractions", onDiskAbs).Debug("loaded step abstractions for filesystem")
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// caller should _not hold s.mtx
|
||||
func (s *abstractionsCache) tryLoadOnDiskAbstractionsImpl(ctx context.Context, fs string) ([]Abstraction, error) {
|
||||
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
|
||||
|
||||
q := ListZFSHoldsAndBookmarksQuery{
|
||||
FS: ListZFSHoldsAndBookmarksQueryFilesystemFilter{
|
||||
FS: &fs,
|
||||
},
|
||||
JobID: nil,
|
||||
What: AbstractionTypeSet{
|
||||
AbstractionStepHold: true,
|
||||
AbstractionTentativeReplicationCursorBookmark: true,
|
||||
AbstractionReplicationCursorBookmarkV2: true,
|
||||
AbstractionLastReceivedHold: true,
|
||||
},
|
||||
Concurrency: 1,
|
||||
}
|
||||
abs, absErrs, err := ListAbstractions(ctx, q)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// safe to ignore absErrs here, this is best-effort cleanup
|
||||
if len(absErrs) > 0 {
|
||||
return nil, ListAbstractionsErrors(absErrs)
|
||||
}
|
||||
return abs, nil
|
||||
}
|
||||
|
||||
func (s *abstractionsCache) TryBatchDestroy(ctx context.Context, jobId JobID, fs string, types AbstractionTypeSet, keep func(a Abstraction) bool, check func(willDestroy []Abstraction)) {
|
||||
// no s.mtx, we only use the public interface in this function
|
||||
|
||||
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
|
||||
|
||||
obsoleteAbs := s.GetAndDeleteByJobIDAndFS(ctx, jobId, fs, types, keep)
|
||||
|
||||
if check != nil {
|
||||
check(obsoleteAbs)
|
||||
}
|
||||
|
||||
hadErr := false
|
||||
for res := range BatchDestroy(ctx, obsoleteAbs) {
|
||||
if res.DestroyErr != nil {
|
||||
hadErr = true
|
||||
getLogger(ctx).
|
||||
WithField("abstraction", res.Abstraction).
|
||||
WithError(res.DestroyErr).
|
||||
Error("cannot destroy abstraction")
|
||||
} else {
|
||||
getLogger(ctx).
|
||||
WithField("abstraction", res.Abstraction).
|
||||
Info("destroyed abstraction")
|
||||
}
|
||||
}
|
||||
if hadErr {
|
||||
s.InvalidateFSCache(fs)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,213 +0,0 @@
|
||||
package endpoint
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/zrepl/zrepl/replication/logic/pdu"
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
|
||||
type ReplicationGuaranteeOptions struct {
|
||||
Initial ReplicationGuaranteeKind
|
||||
Incremental ReplicationGuaranteeKind
|
||||
}
|
||||
|
||||
func replicationGuaranteeOptionsFromPDU(in *pdu.ReplicationConfigProtection) (o ReplicationGuaranteeOptions, _ error) {
|
||||
if in == nil {
|
||||
return o, errors.New("pdu.ReplicationConfigProtection must not be nil")
|
||||
}
|
||||
initial, err := replicationGuaranteeKindFromPDU(in.GetInitial())
|
||||
if err != nil {
|
||||
return o, errors.Wrap(err, "pdu.ReplicationConfigProtection: field Initial")
|
||||
}
|
||||
incremental, err := replicationGuaranteeKindFromPDU(in.GetIncremental())
|
||||
if err != nil {
|
||||
return o, errors.Wrap(err, "pdu.ReplicationConfigProtection: field Incremental")
|
||||
}
|
||||
o = ReplicationGuaranteeOptions{
|
||||
Initial: initial,
|
||||
Incremental: incremental,
|
||||
}
|
||||
return o, nil
|
||||
}
|
||||
|
||||
func replicationGuaranteeKindFromPDU(in pdu.ReplicationGuaranteeKind) (k ReplicationGuaranteeKind, _ error) {
|
||||
switch in {
|
||||
case pdu.ReplicationGuaranteeKind_GuaranteeNothing:
|
||||
return ReplicationGuaranteeKindNone, nil
|
||||
case pdu.ReplicationGuaranteeKind_GuaranteeIncrementalReplication:
|
||||
return ReplicationGuaranteeKindIncremental, nil
|
||||
case pdu.ReplicationGuaranteeKind_GuaranteeResumability:
|
||||
return ReplicationGuaranteeKindResumability, nil
|
||||
|
||||
case pdu.ReplicationGuaranteeKind_GuaranteeInvalid:
|
||||
fallthrough
|
||||
default:
|
||||
return k, errors.Errorf("%q", in.String())
|
||||
}
|
||||
}
|
||||
|
||||
func (o ReplicationGuaranteeOptions) Strategy(incremental bool) ReplicationGuaranteeStrategy {
|
||||
g := o.Initial
|
||||
if incremental {
|
||||
g = o.Incremental
|
||||
}
|
||||
return ReplicationGuaranteeFromKind(g)
|
||||
}
|
||||
|
||||
//go:generate enumer -type=ReplicationGuaranteeKind -json -transform=snake -trimprefix=ReplicationGuaranteeKind
|
||||
type ReplicationGuaranteeKind int
|
||||
|
||||
const (
|
||||
ReplicationGuaranteeKindResumability ReplicationGuaranteeKind = 1 << iota
|
||||
ReplicationGuaranteeKindIncremental
|
||||
ReplicationGuaranteeKindNone
|
||||
)
|
||||
|
||||
type ReplicationGuaranteeStrategy interface {
|
||||
Kind() ReplicationGuaranteeKind
|
||||
SenderPreSend(ctx context.Context, jid JobID, sendArgs *zfs.ZFSSendArgsValidated) (keep []Abstraction, err error)
|
||||
ReceiverPostRecv(ctx context.Context, jid JobID, fs string, toRecvd zfs.FilesystemVersion) (keep []Abstraction, err error)
|
||||
SenderPostRecvConfirmed(ctx context.Context, jid JobID, fs string, to zfs.FilesystemVersion) (keep []Abstraction, err error)
|
||||
}
|
||||
|
||||
func ReplicationGuaranteeFromKind(k ReplicationGuaranteeKind) ReplicationGuaranteeStrategy {
|
||||
switch k {
|
||||
case ReplicationGuaranteeKindNone:
|
||||
return ReplicationGuaranteeNone{}
|
||||
case ReplicationGuaranteeKindIncremental:
|
||||
return ReplicationGuaranteeIncremental{}
|
||||
case ReplicationGuaranteeKindResumability:
|
||||
return ReplicationGuaranteeResumability{}
|
||||
default:
|
||||
panic(fmt.Sprintf("unreachable: %q %T", k, k))
|
||||
}
|
||||
}
|
||||
|
||||
type ReplicationGuaranteeNone struct{}
|
||||
|
||||
func (g ReplicationGuaranteeNone) Kind() ReplicationGuaranteeKind {
|
||||
return ReplicationGuaranteeKindNone
|
||||
}
|
||||
|
||||
func (g ReplicationGuaranteeNone) SenderPreSend(ctx context.Context, jid JobID, sendArgs *zfs.ZFSSendArgsValidated) (keep []Abstraction, err error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (g ReplicationGuaranteeNone) ReceiverPostRecv(ctx context.Context, jid JobID, fs string, toRecvd zfs.FilesystemVersion) (keep []Abstraction, err error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (g ReplicationGuaranteeNone) SenderPostRecvConfirmed(ctx context.Context, jid JobID, fs string, to zfs.FilesystemVersion) (keep []Abstraction, err error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
type ReplicationGuaranteeIncremental struct{}
|
||||
|
||||
func (g ReplicationGuaranteeIncremental) Kind() ReplicationGuaranteeKind {
|
||||
return ReplicationGuaranteeKindIncremental
|
||||
}
|
||||
|
||||
func (g ReplicationGuaranteeIncremental) SenderPreSend(ctx context.Context, jid JobID, sendArgs *zfs.ZFSSendArgsValidated) (keep []Abstraction, err error) {
|
||||
if sendArgs.FromVersion != nil {
|
||||
from, err := CreateTentativeReplicationCursor(ctx, sendArgs.FS, *sendArgs.FromVersion, jid)
|
||||
if err != nil {
|
||||
if err == zfs.ErrBookmarkCloningNotSupported {
|
||||
getLogger(ctx).WithField("replication_guarantee", g).
|
||||
WithField("bookmark", sendArgs.From.FullPath(sendArgs.FS)).
|
||||
Info("bookmark cloning is not supported, speculating that `from` will not be destroyed until step is done")
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
keep = append(keep, from)
|
||||
}
|
||||
to, err := CreateTentativeReplicationCursor(ctx, sendArgs.FS, sendArgs.ToVersion, jid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
keep = append(keep, to)
|
||||
|
||||
return keep, nil
|
||||
}
|
||||
|
||||
func (g ReplicationGuaranteeIncremental) ReceiverPostRecv(ctx context.Context, jid JobID, fs string, toRecvd zfs.FilesystemVersion) (keep []Abstraction, err error) {
|
||||
return receiverPostRecvCommon(ctx, jid, fs, toRecvd)
|
||||
}
|
||||
|
||||
func (g ReplicationGuaranteeIncremental) SenderPostRecvConfirmed(ctx context.Context, jid JobID, fs string, to zfs.FilesystemVersion) (keep []Abstraction, err error) {
|
||||
return senderPostRecvConfirmedCommon(ctx, jid, fs, to)
|
||||
}
|
||||
|
||||
type ReplicationGuaranteeResumability struct{}
|
||||
|
||||
func (g ReplicationGuaranteeResumability) Kind() ReplicationGuaranteeKind {
|
||||
return ReplicationGuaranteeKindResumability
|
||||
}
|
||||
|
||||
func (g ReplicationGuaranteeResumability) SenderPreSend(ctx context.Context, jid JobID, sendArgs *zfs.ZFSSendArgsValidated) (keep []Abstraction, err error) {
|
||||
// try to hold the FromVersion
|
||||
if sendArgs.FromVersion != nil {
|
||||
if sendArgs.FromVersion.Type == zfs.Bookmark {
|
||||
getLogger(ctx).WithField("replication_guarantee", g).WithField("fromVersion", sendArgs.FromVersion.FullPath(sendArgs.FS)).
|
||||
Debug("cannot hold a bookmark, speculating that `from` will not be destroyed until step is done")
|
||||
} else {
|
||||
from, err := HoldStep(ctx, sendArgs.FS, *sendArgs.FromVersion, jid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
keep = append(keep, from)
|
||||
}
|
||||
// fallthrough
|
||||
}
|
||||
|
||||
to, err := HoldStep(ctx, sendArgs.FS, sendArgs.ToVersion, jid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
keep = append(keep, to)
|
||||
|
||||
return keep, nil
|
||||
}
|
||||
|
||||
func (g ReplicationGuaranteeResumability) ReceiverPostRecv(ctx context.Context, jid JobID, fs string, toRecvd zfs.FilesystemVersion) (keep []Abstraction, err error) {
|
||||
return receiverPostRecvCommon(ctx, jid, fs, toRecvd)
|
||||
}
|
||||
|
||||
func (g ReplicationGuaranteeResumability) SenderPostRecvConfirmed(ctx context.Context, jid JobID, fs string, to zfs.FilesystemVersion) (keep []Abstraction, err error) {
|
||||
return senderPostRecvConfirmedCommon(ctx, jid, fs, to)
|
||||
}
|
||||
|
||||
// helper function used by multiple strategies
|
||||
func senderPostRecvConfirmedCommon(ctx context.Context, jid JobID, fs string, to zfs.FilesystemVersion) (keep []Abstraction, err error) {
|
||||
|
||||
log := getLogger(ctx).WithField("toVersion", to.FullPath(fs))
|
||||
|
||||
toReplicationCursor, err := CreateReplicationCursor(ctx, fs, to, jid)
|
||||
if err != nil {
|
||||
if err == zfs.ErrBookmarkCloningNotSupported {
|
||||
log.Debug("not setting replication cursor, bookmark cloning not supported")
|
||||
} else {
|
||||
msg := "cannot move replication cursor, keeping hold on `to` until successful"
|
||||
log.WithError(err).Error(msg)
|
||||
err = errors.Wrap(err, msg)
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
log.WithField("to_cursor", toReplicationCursor.String()).Info("successfully created `to` replication cursor")
|
||||
}
|
||||
|
||||
return []Abstraction{toReplicationCursor}, nil
|
||||
}
|
||||
|
||||
// helper function used by multiple strategies
|
||||
func receiverPostRecvCommon(ctx context.Context, jid JobID, fs string, toRecvd zfs.FilesystemVersion) (keep []Abstraction, err error) {
|
||||
getLogger(ctx).Debug("create new last-received-hold")
|
||||
lrh, err := CreateLastReceivedHold(ctx, fs, toRecvd, jid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []Abstraction{lrh}, nil
|
||||
}
|
||||
@@ -3,5 +3,5 @@ package endpoint
|
||||
import "github.com/prometheus/client_golang/prometheus"
|
||||
|
||||
func RegisterMetrics(r prometheus.Registerer) {
|
||||
r.MustRegister(abstractionsCacheMetrics.count)
|
||||
r.MustRegister(sendAbstractionsCacheMetrics.count)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
package endpoint
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/zrepl/zrepl/daemon/logging/trace"
|
||||
"github.com/zrepl/zrepl/util/chainlock"
|
||||
)
|
||||
|
||||
var sendAbstractionsCacheMetrics struct {
|
||||
count prometheus.Gauge
|
||||
}
|
||||
|
||||
func init() {
|
||||
sendAbstractionsCacheMetrics.count = prometheus.NewGauge(prometheus.GaugeOpts{
|
||||
Namespace: "zrepl",
|
||||
Subsystem: "endpoint",
|
||||
Name: "send_abstractions_cache_entry_count",
|
||||
Help: "number of send abstractions tracked in the sendAbstractionsCache data structure",
|
||||
})
|
||||
}
|
||||
|
||||
var sendAbstractionsCacheSingleton = newSendAbstractionsCache()
|
||||
|
||||
func SendAbstractionsCacheInvalidate(fs string) {
|
||||
sendAbstractionsCacheSingleton.InvalidateFSCache(fs)
|
||||
}
|
||||
|
||||
type sendAbstractionsCacheDidLoadFSState int
|
||||
|
||||
const (
|
||||
sendAbstractionsCacheDidLoadFSStateNo sendAbstractionsCacheDidLoadFSState = iota // 0-value has meaning
|
||||
sendAbstractionsCacheDidLoadFSStateInProgress
|
||||
sendAbstractionsCacheDidLoadFSStateDone
|
||||
)
|
||||
|
||||
type sendAbstractionsCache struct {
|
||||
mtx chainlock.L
|
||||
abstractions []Abstraction
|
||||
didLoadFS map[string]sendAbstractionsCacheDidLoadFSState
|
||||
didLoadFSChanged *sync.Cond
|
||||
}
|
||||
|
||||
func newSendAbstractionsCache() *sendAbstractionsCache {
|
||||
c := &sendAbstractionsCache{
|
||||
didLoadFS: make(map[string]sendAbstractionsCacheDidLoadFSState),
|
||||
}
|
||||
c.didLoadFSChanged = c.mtx.NewCond()
|
||||
return c
|
||||
}
|
||||
|
||||
func (s *sendAbstractionsCache) Put(a Abstraction) {
|
||||
defer s.mtx.Lock().Unlock()
|
||||
|
||||
var zeroJobId JobID
|
||||
if a.GetJobID() == nil {
|
||||
panic("abstraction must not have nil job id")
|
||||
} else if *a.GetJobID() == zeroJobId {
|
||||
panic(fmt.Sprintf("abstraction must not have zero-value job id: %s", a))
|
||||
}
|
||||
|
||||
s.abstractions = append(s.abstractions, a)
|
||||
sendAbstractionsCacheMetrics.count.Set(float64(len(s.abstractions)))
|
||||
}
|
||||
|
||||
func (s *sendAbstractionsCache) InvalidateFSCache(fs string) {
|
||||
// FIXME: O(n)
|
||||
newAbs := make([]Abstraction, 0, len(s.abstractions))
|
||||
for _, a := range s.abstractions {
|
||||
if a.GetFS() != fs {
|
||||
newAbs = append(newAbs, a)
|
||||
}
|
||||
}
|
||||
s.abstractions = newAbs
|
||||
sendAbstractionsCacheMetrics.count.Set(float64(len(s.abstractions)))
|
||||
|
||||
s.didLoadFS[fs] = sendAbstractionsCacheDidLoadFSStateNo
|
||||
s.didLoadFSChanged.Broadcast()
|
||||
|
||||
}
|
||||
|
||||
func (s *sendAbstractionsCache) GetByFS(ctx context.Context, fs string) (ret []Abstraction) {
|
||||
defer s.mtx.Lock().Unlock()
|
||||
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
|
||||
if fs == "" {
|
||||
panic("must not pass zero-value fs")
|
||||
}
|
||||
|
||||
s.tryLoadOnDiskSendAbstractions(ctx, fs)
|
||||
|
||||
for _, a := range s.abstractions {
|
||||
if a.GetFS() == fs {
|
||||
ret = append(ret, a)
|
||||
}
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
// - logs errors in getting on-disk abstractions
|
||||
// - only fetches on-disk abstractions once, but every time from the in-memory store
|
||||
//
|
||||
// That means that for precise results, all abstractions created by the endpoint must be .Put into this cache.
|
||||
func (s *sendAbstractionsCache) GetAndDeleteByJobIDAndFS(ctx context.Context, jobID JobID, fs string) (ret []Abstraction) {
|
||||
defer s.mtx.Lock().Unlock()
|
||||
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
|
||||
var zeroJobId JobID
|
||||
if jobID == zeroJobId {
|
||||
panic("must not pass zero-value job id")
|
||||
}
|
||||
if fs == "" {
|
||||
panic("must not pass zero-value fs")
|
||||
}
|
||||
|
||||
s.tryLoadOnDiskSendAbstractions(ctx, fs)
|
||||
|
||||
// FIXME O(n)
|
||||
var remaining []Abstraction
|
||||
for _, a := range s.abstractions {
|
||||
aJobId := *a.GetJobID()
|
||||
aFS := a.GetFS()
|
||||
if aJobId == jobID && aFS == fs {
|
||||
ret = append(ret, a)
|
||||
} else {
|
||||
remaining = append(remaining, a)
|
||||
}
|
||||
}
|
||||
s.abstractions = remaining
|
||||
sendAbstractionsCacheMetrics.count.Set(float64(len(s.abstractions)))
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
// caller must hold s.mtx
|
||||
func (s *sendAbstractionsCache) tryLoadOnDiskSendAbstractions(ctx context.Context, fs string) {
|
||||
for s.didLoadFS[fs] != sendAbstractionsCacheDidLoadFSStateDone {
|
||||
if s.didLoadFS[fs] == sendAbstractionsCacheDidLoadFSStateInProgress {
|
||||
s.didLoadFSChanged.Wait()
|
||||
continue
|
||||
}
|
||||
if s.didLoadFS[fs] != sendAbstractionsCacheDidLoadFSStateNo {
|
||||
panic(fmt.Sprintf("unreachable: %v", s.didLoadFS[fs]))
|
||||
}
|
||||
|
||||
s.didLoadFS[fs] = sendAbstractionsCacheDidLoadFSStateInProgress
|
||||
defer s.didLoadFSChanged.Broadcast()
|
||||
|
||||
var onDiskAbs []Abstraction
|
||||
var err error
|
||||
s.mtx.DropWhile(func() {
|
||||
onDiskAbs, err = s.tryLoadOnDiskSendAbstractionsImpl(ctx, fs) // no shadow
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
s.didLoadFS[fs] = sendAbstractionsCacheDidLoadFSStateNo
|
||||
getLogger(ctx).WithField("fs", fs).WithError(err).Error("cannot list send step abstractions for filesystem")
|
||||
} else {
|
||||
s.didLoadFS[fs] = sendAbstractionsCacheDidLoadFSStateDone
|
||||
s.abstractions = append(s.abstractions, onDiskAbs...)
|
||||
getLogger(ctx).WithField("fs", fs).WithField("abstractions", onDiskAbs).Debug("loaded step abstractions for filesystem")
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// caller should _not hold s.mtx
|
||||
func (s *sendAbstractionsCache) tryLoadOnDiskSendAbstractionsImpl(ctx context.Context, fs string) ([]Abstraction, error) {
|
||||
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
|
||||
|
||||
q := ListZFSHoldsAndBookmarksQuery{
|
||||
FS: ListZFSHoldsAndBookmarksQueryFilesystemFilter{
|
||||
FS: &fs,
|
||||
},
|
||||
JobID: nil,
|
||||
What: AbstractionTypeSet{
|
||||
AbstractionStepHold: true,
|
||||
AbstractionStepBookmark: true,
|
||||
AbstractionReplicationCursorBookmarkV2: true,
|
||||
},
|
||||
Concurrency: 1,
|
||||
}
|
||||
abs, absErrs, err := ListAbstractions(ctx, q)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// safe to ignore absErrs here, this is best-effort cleanup
|
||||
if len(absErrs) > 0 {
|
||||
return nil, ListAbstractionsErrors(absErrs)
|
||||
}
|
||||
return abs, nil
|
||||
}
|
||||
|
||||
func (s *sendAbstractionsCache) TryBatchDestroy(ctx context.Context, jobId JobID, fs string, keep func(a Abstraction) bool, check func(willDestroy []Abstraction)) {
|
||||
// no s.mtx, we only use the public interface in this function
|
||||
|
||||
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
|
||||
|
||||
allSendStepAbstractions := s.GetAndDeleteByJobIDAndFS(ctx, jobId, fs)
|
||||
|
||||
var obsoleteAbs []Abstraction
|
||||
for _, a := range allSendStepAbstractions {
|
||||
if !keep(a) {
|
||||
obsoleteAbs = append(obsoleteAbs, a)
|
||||
}
|
||||
}
|
||||
|
||||
if check != nil {
|
||||
check(obsoleteAbs)
|
||||
}
|
||||
|
||||
hadErr := false
|
||||
for res := range BatchDestroy(ctx, obsoleteAbs) {
|
||||
if res.DestroyErr != nil {
|
||||
hadErr = true
|
||||
getLogger(ctx).
|
||||
WithField("abstraction", res.Abstraction).
|
||||
WithError(res.DestroyErr).
|
||||
Error("cannot destroy stale send step abstraction")
|
||||
} else {
|
||||
getLogger(ctx).
|
||||
WithField("abstraction", res.Abstraction).
|
||||
Info("destroyed stale send step abstraction")
|
||||
}
|
||||
}
|
||||
if hadErr {
|
||||
s.InvalidateFSCache(fs)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -23,19 +23,19 @@ type AbstractionType string
|
||||
// There are a lot of exhaustive switches on AbstractionType in the code base.
|
||||
// When adding a new abstraction type, make sure to search and update them!
|
||||
const (
|
||||
AbstractionStepHold AbstractionType = "step-hold"
|
||||
AbstractionLastReceivedHold AbstractionType = "last-received-hold"
|
||||
AbstractionTentativeReplicationCursorBookmark AbstractionType = "tentative-replication-cursor-bookmark-v2"
|
||||
AbstractionReplicationCursorBookmarkV1 AbstractionType = "replication-cursor-bookmark-v1"
|
||||
AbstractionReplicationCursorBookmarkV2 AbstractionType = "replication-cursor-bookmark-v2"
|
||||
AbstractionStepBookmark AbstractionType = "step-bookmark"
|
||||
AbstractionStepHold AbstractionType = "step-hold"
|
||||
AbstractionLastReceivedHold AbstractionType = "last-received-hold"
|
||||
AbstractionReplicationCursorBookmarkV1 AbstractionType = "replication-cursor-bookmark-v1"
|
||||
AbstractionReplicationCursorBookmarkV2 AbstractionType = "replication-cursor-bookmark-v2"
|
||||
)
|
||||
|
||||
var AbstractionTypesAll = map[AbstractionType]bool{
|
||||
AbstractionStepHold: true,
|
||||
AbstractionLastReceivedHold: true,
|
||||
AbstractionTentativeReplicationCursorBookmark: true,
|
||||
AbstractionReplicationCursorBookmarkV1: true,
|
||||
AbstractionReplicationCursorBookmarkV2: true,
|
||||
AbstractionStepBookmark: true,
|
||||
AbstractionStepHold: true,
|
||||
AbstractionLastReceivedHold: true,
|
||||
AbstractionReplicationCursorBookmarkV1: true,
|
||||
AbstractionReplicationCursorBookmarkV2: true,
|
||||
}
|
||||
|
||||
// Implementation Note:
|
||||
@@ -80,12 +80,12 @@ func AbstractionEquals(a, b Abstraction) bool {
|
||||
|
||||
func (t AbstractionType) Validate() error {
|
||||
switch t {
|
||||
case AbstractionStepBookmark:
|
||||
return nil
|
||||
case AbstractionStepHold:
|
||||
return nil
|
||||
case AbstractionLastReceivedHold:
|
||||
return nil
|
||||
case AbstractionTentativeReplicationCursorBookmark:
|
||||
return nil
|
||||
case AbstractionReplicationCursorBookmarkV1:
|
||||
return nil
|
||||
case AbstractionReplicationCursorBookmarkV2:
|
||||
@@ -185,8 +185,8 @@ type BookmarkExtractor func(fs *zfs.DatasetPath, v zfs.FilesystemVersion) Abstra
|
||||
// returns nil if the abstraction type is not bookmark-based
|
||||
func (t AbstractionType) BookmarkExtractor() BookmarkExtractor {
|
||||
switch t {
|
||||
case AbstractionTentativeReplicationCursorBookmark:
|
||||
return TentativeReplicationCursorExtractor
|
||||
case AbstractionStepBookmark:
|
||||
return StepBookmarkExtractor
|
||||
case AbstractionReplicationCursorBookmarkV1:
|
||||
return ReplicationCursorV1Extractor
|
||||
case AbstractionReplicationCursorBookmarkV2:
|
||||
@@ -205,7 +205,7 @@ type HoldExtractor = func(fs *zfs.DatasetPath, v zfs.FilesystemVersion, tag stri
|
||||
// returns nil if the abstraction type is not hold-based
|
||||
func (t AbstractionType) HoldExtractor() HoldExtractor {
|
||||
switch t {
|
||||
case AbstractionTentativeReplicationCursorBookmark:
|
||||
case AbstractionStepBookmark:
|
||||
return nil
|
||||
case AbstractionReplicationCursorBookmarkV1:
|
||||
return nil
|
||||
@@ -220,23 +220,6 @@ func (t AbstractionType) HoldExtractor() HoldExtractor {
|
||||
}
|
||||
}
|
||||
|
||||
func (t AbstractionType) BookmarkNamer() func(fs string, guid uint64, jobId JobID) (string, error) {
|
||||
switch t {
|
||||
case AbstractionTentativeReplicationCursorBookmark:
|
||||
return TentativeReplicationCursorBookmarkName
|
||||
case AbstractionReplicationCursorBookmarkV1:
|
||||
panic("shouldn't be creating new ones")
|
||||
case AbstractionReplicationCursorBookmarkV2:
|
||||
return ReplicationCursorBookmarkName
|
||||
case AbstractionStepHold:
|
||||
return nil
|
||||
case AbstractionLastReceivedHold:
|
||||
return nil
|
||||
default:
|
||||
panic(fmt.Sprintf("unimpl: %q", t))
|
||||
}
|
||||
}
|
||||
|
||||
type ListZFSHoldsAndBookmarksQuery struct {
|
||||
FS ListZFSHoldsAndBookmarksQueryFilesystemFilter
|
||||
// What abstraction types should match (any contained in the set)
|
||||
@@ -714,9 +697,11 @@ func ListStale(ctx context.Context, q ListZFSHoldsAndBookmarksQuery) (*Staleness
|
||||
return nil, &ListStaleQueryError{errors.New("ListStale cannot have Until != nil set on query")}
|
||||
}
|
||||
|
||||
// if asking for step holds must also ask for replication cursor bookmarks (for firstNotStale)
|
||||
// if asking for step holds, must also as for step bookmarks (same kind of abstraction)
|
||||
// as well as replication cursor bookmarks (for firstNotStale)
|
||||
ifAnyThenAll := AbstractionTypeSet{
|
||||
AbstractionStepHold: true,
|
||||
AbstractionStepBookmark: true,
|
||||
AbstractionReplicationCursorBookmarkV2: true,
|
||||
}
|
||||
if q.What.ContainsAnyOf(ifAnyThenAll) && !q.What.ContainsAll(ifAnyThenAll) {
|
||||
@@ -745,7 +730,7 @@ type fsAjobAtype struct {
|
||||
}
|
||||
|
||||
// For step holds and bookmarks, only those older than the most recent replication cursor
|
||||
// of their (filesystem,job) are considered because younger ones cannot be stale by definition
|
||||
// of their (filesystem,job) is considered because younger ones cannot be stale by definition
|
||||
// (if we destroy them, we might actually lose the hold on the `To` for an ongoing incremental replication)
|
||||
//
|
||||
// For replication cursors and last-received-holds, only the most recent one is kept.
|
||||
@@ -787,6 +772,8 @@ func listStaleFiltering(abs []Abstraction, sinceBound *CreateTXGRangeBound) *Sta
|
||||
}
|
||||
|
||||
// stepFirstNotStaleCandidate.step
|
||||
case AbstractionStepBookmark:
|
||||
fallthrough
|
||||
case AbstractionStepHold:
|
||||
if c.step == nil || (*c.step).GetCreateTXG() < a.GetCreateTXG() {
|
||||
a := a
|
||||
@@ -810,7 +797,7 @@ func listStaleFiltering(abs []Abstraction, sinceBound *CreateTXGRangeBound) *Sta
|
||||
for k := range by {
|
||||
l := by[k]
|
||||
|
||||
if k.Type == AbstractionStepHold {
|
||||
if k.Type == AbstractionStepHold || k.Type == AbstractionStepBookmark {
|
||||
// all older than the most recent cursor are stale, others are always live
|
||||
|
||||
// if we don't have a replication cursor yet, use untilBound = nil
|
||||
|
||||
+140
-51
@@ -4,9 +4,12 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sort"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/zrepl/zrepl/util/errorarray"
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
|
||||
@@ -50,28 +53,6 @@ func ParseReplicationCursorBookmarkName(fullname string) (uint64, JobID, error)
|
||||
return guid, jobID, err
|
||||
}
|
||||
|
||||
const tentativeReplicationCursorBookmarkNamePrefix = "zrepl_CURSORTENTATIVE_"
|
||||
|
||||
// v must be validated by caller
|
||||
func TentativeReplicationCursorBookmarkName(fs string, guid uint64, id JobID) (string, error) {
|
||||
return tentativeReplicationCursorBookmarkNameImpl(fs, guid, id.String())
|
||||
}
|
||||
|
||||
func tentativeReplicationCursorBookmarkNameImpl(fs string, guid uint64, jobid string) (string, error) {
|
||||
return makeJobAndGuidBookmarkName(tentativeReplicationCursorBookmarkNamePrefix, fs, guid, jobid)
|
||||
}
|
||||
|
||||
// name is the full bookmark name, including dataset path
|
||||
//
|
||||
// err != nil always means that the bookmark is not a step bookmark
|
||||
func ParseTentativeReplicationCursorBookmarkName(fullname string) (guid uint64, jobID JobID, err error) {
|
||||
guid, jobID, err = parseJobAndGuidBookmarkName(fullname, tentativeReplicationCursorBookmarkNamePrefix)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "parse step bookmark name") // no shadow!
|
||||
}
|
||||
return guid, jobID, err
|
||||
}
|
||||
|
||||
// may return nil for both values, indicating there is no cursor
|
||||
func GetMostRecentReplicationCursorOfJob(ctx context.Context, fs string, jobID JobID) (*zfs.FilesystemVersion, error) {
|
||||
fsp, err := zfs.NewDatasetPath(fs)
|
||||
@@ -141,23 +122,23 @@ func GetReplicationCursors(ctx context.Context, dp *zfs.DatasetPath, jobID JobID
|
||||
//
|
||||
// returns ErrBookmarkCloningNotSupported if version is a bookmark and bookmarking bookmarks is not supported by ZFS
|
||||
func CreateReplicationCursor(ctx context.Context, fs string, target zfs.FilesystemVersion, jobID JobID) (a Abstraction, err error) {
|
||||
return createBookmarkAbstraction(ctx, AbstractionReplicationCursorBookmarkV2, fs, target, jobID)
|
||||
}
|
||||
|
||||
func CreateTentativeReplicationCursor(ctx context.Context, fs string, target zfs.FilesystemVersion, jobID JobID) (a Abstraction, err error) {
|
||||
return createBookmarkAbstraction(ctx, AbstractionTentativeReplicationCursorBookmark, fs, target, jobID)
|
||||
}
|
||||
|
||||
func createBookmarkAbstraction(ctx context.Context, abstractionType AbstractionType, fs string, target zfs.FilesystemVersion, jobID JobID) (a Abstraction, err error) {
|
||||
|
||||
bookmarkNamer := abstractionType.BookmarkNamer()
|
||||
if bookmarkNamer == nil {
|
||||
panic(abstractionType)
|
||||
bookmarkname, err := ReplicationCursorBookmarkName(fs, target.GetGuid(), jobID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "determine replication cursor name")
|
||||
}
|
||||
|
||||
bookmarkname, err := bookmarkNamer(fs, target.GetGuid(), jobID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "determine %s name", abstractionType)
|
||||
if target.IsBookmark() && target.GetName() == bookmarkname {
|
||||
return &bookmarkBasedAbstraction{
|
||||
Type: AbstractionReplicationCursorBookmarkV2,
|
||||
FS: fs,
|
||||
FilesystemVersion: target,
|
||||
JobID: jobID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
if !target.IsSnapshot() {
|
||||
return nil, zfs.ErrBookmarkCloningNotSupported
|
||||
}
|
||||
|
||||
// idempotently create bookmark (guid is encoded in it)
|
||||
@@ -171,13 +152,125 @@ func createBookmarkAbstraction(ctx context.Context, abstractionType AbstractionT
|
||||
}
|
||||
|
||||
return &bookmarkBasedAbstraction{
|
||||
Type: abstractionType,
|
||||
Type: AbstractionReplicationCursorBookmarkV2,
|
||||
FS: fs,
|
||||
FilesystemVersion: cursorBookmark,
|
||||
JobID: jobID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
const (
|
||||
ReplicationCursorBookmarkNamePrefix = "zrepl_last_received_J_"
|
||||
)
|
||||
|
||||
var lastReceivedHoldTagRE = regexp.MustCompile("^zrepl_last_received_J_(.+)$")
|
||||
|
||||
// err != nil always means that the bookmark is not a step bookmark
|
||||
func ParseLastReceivedHoldTag(tag string) (JobID, error) {
|
||||
match := lastReceivedHoldTagRE.FindStringSubmatch(tag)
|
||||
if match == nil {
|
||||
return JobID{}, errors.Errorf("parse last-received-hold tag: does not match regex %s", lastReceivedHoldTagRE.String())
|
||||
}
|
||||
jobId, err := MakeJobID(match[1])
|
||||
if err != nil {
|
||||
return JobID{}, errors.Wrap(err, "parse last-received-hold tag: invalid job id field")
|
||||
}
|
||||
return jobId, nil
|
||||
}
|
||||
|
||||
func LastReceivedHoldTag(jobID JobID) (string, error) {
|
||||
return lastReceivedHoldImpl(jobID.String())
|
||||
}
|
||||
|
||||
func lastReceivedHoldImpl(jobid string) (string, error) {
|
||||
tag := fmt.Sprintf("%s%s", ReplicationCursorBookmarkNamePrefix, jobid)
|
||||
if err := zfs.ValidHoldTag(tag); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return tag, nil
|
||||
}
|
||||
|
||||
func CreateLastReceivedHold(ctx context.Context, fs string, to zfs.FilesystemVersion, jobID JobID) (Abstraction, error) {
|
||||
|
||||
if !to.IsSnapshot() {
|
||||
return nil, errors.Errorf("last-received-hold: target must be a snapshot: %s", to.FullPath(fs))
|
||||
}
|
||||
|
||||
tag, err := LastReceivedHoldTag(jobID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "last-received-hold: hold tag")
|
||||
}
|
||||
|
||||
// we never want to be without a hold
|
||||
// => hold new one before releasing old hold
|
||||
|
||||
err = zfs.ZFSHold(ctx, fs, to, tag)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "last-received-hold: hold newly received")
|
||||
}
|
||||
|
||||
return &holdBasedAbstraction{
|
||||
Type: AbstractionLastReceivedHold,
|
||||
FS: fs,
|
||||
FilesystemVersion: to,
|
||||
JobID: jobID,
|
||||
Tag: tag,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func MoveLastReceivedHold(ctx context.Context, fs string, to zfs.FilesystemVersion, jobID JobID) error {
|
||||
|
||||
_, err := CreateLastReceivedHold(ctx, fs, to, jobID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
q := ListZFSHoldsAndBookmarksQuery{
|
||||
What: AbstractionTypeSet{
|
||||
AbstractionLastReceivedHold: true,
|
||||
},
|
||||
FS: ListZFSHoldsAndBookmarksQueryFilesystemFilter{
|
||||
FS: &fs,
|
||||
},
|
||||
JobID: &jobID,
|
||||
CreateTXG: CreateTXGRange{
|
||||
Since: nil,
|
||||
Until: &CreateTXGRangeBound{
|
||||
CreateTXG: to.GetCreateTXG(),
|
||||
Inclusive: &zfs.NilBool{B: false},
|
||||
},
|
||||
},
|
||||
Concurrency: 1,
|
||||
}
|
||||
abs, absErrs, err := ListAbstractions(ctx, q)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "last-received-hold: list")
|
||||
}
|
||||
if len(absErrs) > 0 {
|
||||
return errors.Wrap(ListAbstractionsErrors(absErrs), "last-received-hold: list")
|
||||
}
|
||||
|
||||
getLogger(ctx).WithField("last-received-holds", fmt.Sprintf("%s", abs)).Debug("releasing last-received-holds")
|
||||
|
||||
var errs []error
|
||||
for res := range BatchDestroy(ctx, abs) {
|
||||
log := getLogger(ctx).
|
||||
WithField("last-received-hold", res.Abstraction)
|
||||
if res.DestroyErr != nil {
|
||||
errs = append(errs, res.DestroyErr)
|
||||
log.WithError(err).
|
||||
Error("cannot release last-received-hold")
|
||||
} else {
|
||||
log.Info("released last-received-hold")
|
||||
}
|
||||
}
|
||||
if len(errs) == 0 {
|
||||
return nil
|
||||
} else {
|
||||
return errorarray.Wrap(errs, "last-received-hold: release")
|
||||
}
|
||||
}
|
||||
|
||||
func ReplicationCursorV2Extractor(fs *zfs.DatasetPath, v zfs.FilesystemVersion) (_ Abstraction) {
|
||||
if v.Type != zfs.Bookmark {
|
||||
panic("impl error")
|
||||
@@ -215,28 +308,24 @@ func ReplicationCursorV1Extractor(fs *zfs.DatasetPath, v zfs.FilesystemVersion)
|
||||
return nil
|
||||
}
|
||||
|
||||
var _ BookmarkExtractor = TentativeReplicationCursorExtractor
|
||||
var _ HoldExtractor = LastReceivedHoldExtractor
|
||||
|
||||
func TentativeReplicationCursorExtractor(fs *zfs.DatasetPath, v zfs.FilesystemVersion) (_ Abstraction) {
|
||||
if v.Type != zfs.Bookmark {
|
||||
func LastReceivedHoldExtractor(fs *zfs.DatasetPath, v zfs.FilesystemVersion, holdTag string) Abstraction {
|
||||
var err error
|
||||
|
||||
if v.Type != zfs.Snapshot {
|
||||
panic("impl error")
|
||||
}
|
||||
|
||||
fullname := v.ToAbsPath(fs)
|
||||
|
||||
guid, jobid, err := ParseTentativeReplicationCursorBookmarkName(fullname)
|
||||
if guid != v.Guid {
|
||||
// TODO log this possibly tinkered-with bookmark
|
||||
return nil
|
||||
}
|
||||
jobID, err := ParseLastReceivedHoldTag(holdTag)
|
||||
if err == nil {
|
||||
bm := &bookmarkBasedAbstraction{
|
||||
Type: AbstractionTentativeReplicationCursorBookmark,
|
||||
return &holdBasedAbstraction{
|
||||
Type: AbstractionLastReceivedHold,
|
||||
FS: fs.ToString(),
|
||||
FilesystemVersion: v,
|
||||
JobID: jobid,
|
||||
Tag: holdTag,
|
||||
JobID: jobID,
|
||||
}
|
||||
return bm
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
package endpoint
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
|
||||
const (
|
||||
LastReceivedHoldTagNamePrefix = "zrepl_last_received_J_"
|
||||
)
|
||||
|
||||
var lastReceivedHoldTagRE = regexp.MustCompile("^zrepl_last_received_J_(.+)$")
|
||||
|
||||
var _ HoldExtractor = LastReceivedHoldExtractor
|
||||
|
||||
func LastReceivedHoldExtractor(fs *zfs.DatasetPath, v zfs.FilesystemVersion, holdTag string) Abstraction {
|
||||
var err error
|
||||
|
||||
if v.Type != zfs.Snapshot {
|
||||
panic("impl error")
|
||||
}
|
||||
|
||||
jobID, err := ParseLastReceivedHoldTag(holdTag)
|
||||
if err == nil {
|
||||
return &holdBasedAbstraction{
|
||||
Type: AbstractionLastReceivedHold,
|
||||
FS: fs.ToString(),
|
||||
FilesystemVersion: v,
|
||||
Tag: holdTag,
|
||||
JobID: jobID,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// err != nil always means that the bookmark is not a step bookmark
|
||||
func ParseLastReceivedHoldTag(tag string) (JobID, error) {
|
||||
match := lastReceivedHoldTagRE.FindStringSubmatch(tag)
|
||||
if match == nil {
|
||||
return JobID{}, errors.Errorf("parse last-received-hold tag: does not match regex %s", lastReceivedHoldTagRE.String())
|
||||
}
|
||||
jobId, err := MakeJobID(match[1])
|
||||
if err != nil {
|
||||
return JobID{}, errors.Wrap(err, "parse last-received-hold tag: invalid job id field")
|
||||
}
|
||||
return jobId, nil
|
||||
}
|
||||
|
||||
func LastReceivedHoldTag(jobID JobID) (string, error) {
|
||||
return lastReceivedHoldImpl(jobID.String())
|
||||
}
|
||||
|
||||
func lastReceivedHoldImpl(jobid string) (string, error) {
|
||||
tag := fmt.Sprintf("%s%s", LastReceivedHoldTagNamePrefix, jobid)
|
||||
if err := zfs.ValidHoldTag(tag); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return tag, nil
|
||||
}
|
||||
|
||||
func CreateLastReceivedHold(ctx context.Context, fs string, to zfs.FilesystemVersion, jobID JobID) (Abstraction, error) {
|
||||
|
||||
if !to.IsSnapshot() {
|
||||
return nil, errors.Errorf("last-received-hold: target must be a snapshot: %s", to.FullPath(fs))
|
||||
}
|
||||
|
||||
tag, err := LastReceivedHoldTag(jobID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "last-received-hold: hold tag")
|
||||
}
|
||||
|
||||
// we never want to be without a hold
|
||||
// => hold new one before releasing old hold
|
||||
|
||||
err = zfs.ZFSHold(ctx, fs, to, tag)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "last-received-hold: hold newly received")
|
||||
}
|
||||
|
||||
return &holdBasedAbstraction{
|
||||
Type: AbstractionLastReceivedHold,
|
||||
FS: fs,
|
||||
FilesystemVersion: to,
|
||||
JobID: jobID,
|
||||
Tag: tag,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package endpoint
|
||||
|
||||
import "github.com/zrepl/zrepl/replication/logic/pdu"
|
||||
|
||||
func SendAbstractionToPDU(a Abstraction) *pdu.SendAbstraction {
|
||||
var ty pdu.SendAbstraction_SendAbstractionType
|
||||
switch a.GetType() {
|
||||
case AbstractionLastReceivedHold:
|
||||
panic(a)
|
||||
case AbstractionReplicationCursorBookmarkV1:
|
||||
panic(a)
|
||||
case AbstractionReplicationCursorBookmarkV2:
|
||||
ty = pdu.SendAbstraction_ReplicationCursorV2
|
||||
case AbstractionStepHold:
|
||||
ty = pdu.SendAbstraction_StepHold
|
||||
case AbstractionStepBookmark:
|
||||
ty = pdu.SendAbstraction_StepBookmark
|
||||
default:
|
||||
panic(a)
|
||||
}
|
||||
|
||||
version := a.GetFilesystemVersion()
|
||||
return &pdu.SendAbstraction{
|
||||
Type: ty,
|
||||
JobID: (*a.GetJobID()).String(),
|
||||
Version: pdu.FilesystemVersionFromZFS(&version),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
package endpoint
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
|
||||
var stepHoldTagRE = regexp.MustCompile("^zrepl_STEP_J_(.+)")
|
||||
|
||||
func StepHoldTag(jobid JobID) (string, error) {
|
||||
return stepHoldTagImpl(jobid.String())
|
||||
}
|
||||
|
||||
func stepHoldTagImpl(jobid string) (string, error) {
|
||||
t := fmt.Sprintf("zrepl_STEP_J_%s", jobid)
|
||||
if err := zfs.ValidHoldTag(t); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// err != nil always means that the bookmark is not a step bookmark
|
||||
func ParseStepHoldTag(tag string) (JobID, error) {
|
||||
match := stepHoldTagRE.FindStringSubmatch(tag)
|
||||
if match == nil {
|
||||
return JobID{}, fmt.Errorf("parse hold tag: match regex %q", stepHoldTagRE)
|
||||
}
|
||||
jobID, err := MakeJobID(match[1])
|
||||
if err != nil {
|
||||
return JobID{}, errors.Wrap(err, "parse hold tag: invalid job id field")
|
||||
}
|
||||
return jobID, nil
|
||||
}
|
||||
|
||||
const stepBookmarkNamePrefix = "zrepl_STEP"
|
||||
|
||||
// v must be validated by caller
|
||||
func StepBookmarkName(fs string, guid uint64, id JobID) (string, error) {
|
||||
return stepBookmarkNameImpl(fs, guid, id.String())
|
||||
}
|
||||
|
||||
func stepBookmarkNameImpl(fs string, guid uint64, jobid string) (string, error) {
|
||||
return makeJobAndGuidBookmarkName(stepBookmarkNamePrefix, fs, guid, jobid)
|
||||
}
|
||||
|
||||
// name is the full bookmark name, including dataset path
|
||||
//
|
||||
// err != nil always means that the bookmark is not a step bookmark
|
||||
func ParseStepBookmarkName(fullname string) (guid uint64, jobID JobID, err error) {
|
||||
guid, jobID, err = parseJobAndGuidBookmarkName(fullname, stepBookmarkNamePrefix)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "parse step bookmark name") // no shadow!
|
||||
}
|
||||
return guid, jobID, err
|
||||
}
|
||||
|
||||
// idempotently hold / step-bookmark `version`
|
||||
//
|
||||
// returns ErrBookmarkCloningNotSupported if version is a bookmark and bookmarking bookmarks is not supported by ZFS
|
||||
func HoldStep(ctx context.Context, fs string, v zfs.FilesystemVersion, jobID JobID) (Abstraction, error) {
|
||||
if v.IsSnapshot() {
|
||||
|
||||
tag, err := StepHoldTag(jobID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "step hold tag")
|
||||
}
|
||||
|
||||
if err := zfs.ZFSHold(ctx, fs, v, tag); err != nil {
|
||||
return nil, errors.Wrap(err, "step hold: zfs")
|
||||
}
|
||||
|
||||
return &holdBasedAbstraction{
|
||||
Type: AbstractionStepHold,
|
||||
FS: fs,
|
||||
Tag: tag,
|
||||
JobID: jobID,
|
||||
FilesystemVersion: v,
|
||||
}, nil
|
||||
}
|
||||
|
||||
if !v.IsBookmark() {
|
||||
panic(fmt.Sprintf("version must bei either snapshot or bookmark, got %#v", v))
|
||||
}
|
||||
|
||||
bmname, err := StepBookmarkName(fs, v.Guid, jobID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "create step bookmark: determine bookmark name")
|
||||
}
|
||||
// idempotently create bookmark
|
||||
stepBookmark, err := zfs.ZFSBookmark(ctx, fs, v, bmname)
|
||||
if err != nil {
|
||||
if err == zfs.ErrBookmarkCloningNotSupported {
|
||||
// TODO we could actually try to find a local snapshot that has the requested GUID
|
||||
// however, the replication algorithm prefers snapshots anyways, so this quest
|
||||
// is most likely not going to be successful. Also, there's the possibility that
|
||||
// the caller might want to filter what snapshots are eligibile, and this would
|
||||
// complicate things even further.
|
||||
return nil, err // TODO go1.13 use wrapping
|
||||
}
|
||||
return nil, errors.Wrap(err, "create step bookmark: zfs")
|
||||
}
|
||||
return &bookmarkBasedAbstraction{
|
||||
Type: AbstractionStepBookmark,
|
||||
FS: fs,
|
||||
FilesystemVersion: stepBookmark,
|
||||
JobID: jobID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
var _ BookmarkExtractor = StepBookmarkExtractor
|
||||
|
||||
func StepBookmarkExtractor(fs *zfs.DatasetPath, v zfs.FilesystemVersion) (_ Abstraction) {
|
||||
if v.Type != zfs.Bookmark {
|
||||
panic("impl error")
|
||||
}
|
||||
|
||||
fullname := v.ToAbsPath(fs)
|
||||
|
||||
guid, jobid, err := ParseStepBookmarkName(fullname)
|
||||
if guid != v.Guid {
|
||||
// TODO log this possibly tinkered-with bookmark
|
||||
return nil
|
||||
}
|
||||
if err == nil {
|
||||
bm := &bookmarkBasedAbstraction{
|
||||
Type: AbstractionStepBookmark,
|
||||
FS: fs.ToString(),
|
||||
FilesystemVersion: v,
|
||||
JobID: jobid,
|
||||
}
|
||||
return bm
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var _ HoldExtractor = StepHoldExtractor
|
||||
|
||||
func StepHoldExtractor(fs *zfs.DatasetPath, v zfs.FilesystemVersion, holdTag string) Abstraction {
|
||||
if v.Type != zfs.Snapshot {
|
||||
panic("impl error")
|
||||
}
|
||||
|
||||
jobID, err := ParseStepHoldTag(holdTag)
|
||||
if err == nil {
|
||||
return &holdBasedAbstraction{
|
||||
Type: AbstractionStepHold,
|
||||
FS: fs.ToString(),
|
||||
Tag: holdTag,
|
||||
FilesystemVersion: v,
|
||||
JobID: jobID,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
package endpoint
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
|
||||
var stepHoldTagRE = regexp.MustCompile("^zrepl_STEP_J_(.+)")
|
||||
|
||||
func StepHoldTag(jobid JobID) (string, error) {
|
||||
return stepHoldTagImpl(jobid.String())
|
||||
}
|
||||
|
||||
func stepHoldTagImpl(jobid string) (string, error) {
|
||||
t := fmt.Sprintf("zrepl_STEP_J_%s", jobid)
|
||||
if err := zfs.ValidHoldTag(t); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// err != nil always means that the bookmark is not a step bookmark
|
||||
func ParseStepHoldTag(tag string) (JobID, error) {
|
||||
match := stepHoldTagRE.FindStringSubmatch(tag)
|
||||
if match == nil {
|
||||
return JobID{}, fmt.Errorf("parse hold tag: match regex %q", stepHoldTagRE)
|
||||
}
|
||||
jobID, err := MakeJobID(match[1])
|
||||
if err != nil {
|
||||
return JobID{}, errors.Wrap(err, "parse hold tag: invalid job id field")
|
||||
}
|
||||
return jobID, nil
|
||||
}
|
||||
|
||||
// idempotently hold `version`
|
||||
func HoldStep(ctx context.Context, fs string, v zfs.FilesystemVersion, jobID JobID) (Abstraction, error) {
|
||||
if !v.IsSnapshot() {
|
||||
panic(fmt.Sprintf("version must be a snapshot got %#v", v))
|
||||
}
|
||||
|
||||
tag, err := StepHoldTag(jobID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "step hold tag")
|
||||
}
|
||||
|
||||
if err := zfs.ZFSHold(ctx, fs, v, tag); err != nil {
|
||||
return nil, errors.Wrap(err, "step hold: zfs")
|
||||
}
|
||||
|
||||
return &holdBasedAbstraction{
|
||||
Type: AbstractionStepHold,
|
||||
FS: fs,
|
||||
Tag: tag,
|
||||
JobID: jobID,
|
||||
FilesystemVersion: v,
|
||||
}, nil
|
||||
|
||||
}
|
||||
|
||||
var _ HoldExtractor = StepHoldExtractor
|
||||
|
||||
func StepHoldExtractor(fs *zfs.DatasetPath, v zfs.FilesystemVersion, holdTag string) Abstraction {
|
||||
if v.Type != zfs.Snapshot {
|
||||
panic("impl error")
|
||||
}
|
||||
|
||||
jobID, err := ParseStepHoldTag(holdTag)
|
||||
if err == nil {
|
||||
return &holdBasedAbstraction{
|
||||
Type: AbstractionStepHold,
|
||||
FS: fs.ToString(),
|
||||
Tag: holdTag,
|
||||
FilesystemVersion: v,
|
||||
JobID: jobID,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+2
-2
@@ -24,9 +24,9 @@ func MakeJobID(s string) (JobID, error) {
|
||||
return JobID{}, errors.Wrap(err, "must be usable as a dataset path component")
|
||||
}
|
||||
|
||||
if _, err := tentativeReplicationCursorBookmarkNameImpl("pool/ds", 0xface601d, s); err != nil {
|
||||
if _, err := stepBookmarkNameImpl("pool/ds", 0xface601d, s); err != nil {
|
||||
// note that this might still fail due to total maximum name length, but we can't enforce that
|
||||
return JobID{}, errors.Wrap(err, "must be usable for a tentative replication cursor bookmark")
|
||||
return JobID{}, errors.Wrap(err, "must be usable for a step bookmark")
|
||||
}
|
||||
|
||||
if _, err := stepHoldTagImpl(s); err != nil {
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
// Code generated by "enumer -type=ReplicationGuaranteeKind -json -transform=snake -trimprefix=ReplicationGuaranteeKind"; DO NOT EDIT.
|
||||
|
||||
//
|
||||
package endpoint
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
const (
|
||||
_ReplicationGuaranteeKindName_0 = "resumabilityincremental"
|
||||
_ReplicationGuaranteeKindName_1 = "none"
|
||||
)
|
||||
|
||||
var (
|
||||
_ReplicationGuaranteeKindIndex_0 = [...]uint8{0, 12, 23}
|
||||
_ReplicationGuaranteeKindIndex_1 = [...]uint8{0, 4}
|
||||
)
|
||||
|
||||
func (i ReplicationGuaranteeKind) String() string {
|
||||
switch {
|
||||
case 1 <= i && i <= 2:
|
||||
i -= 1
|
||||
return _ReplicationGuaranteeKindName_0[_ReplicationGuaranteeKindIndex_0[i]:_ReplicationGuaranteeKindIndex_0[i+1]]
|
||||
case i == 4:
|
||||
return _ReplicationGuaranteeKindName_1
|
||||
default:
|
||||
return fmt.Sprintf("ReplicationGuaranteeKind(%d)", i)
|
||||
}
|
||||
}
|
||||
|
||||
var _ReplicationGuaranteeKindValues = []ReplicationGuaranteeKind{1, 2, 4}
|
||||
|
||||
var _ReplicationGuaranteeKindNameToValueMap = map[string]ReplicationGuaranteeKind{
|
||||
_ReplicationGuaranteeKindName_0[0:12]: 1,
|
||||
_ReplicationGuaranteeKindName_0[12:23]: 2,
|
||||
_ReplicationGuaranteeKindName_1[0:4]: 4,
|
||||
}
|
||||
|
||||
// ReplicationGuaranteeKindString retrieves an enum value from the enum constants string name.
|
||||
// Throws an error if the param is not part of the enum.
|
||||
func ReplicationGuaranteeKindString(s string) (ReplicationGuaranteeKind, error) {
|
||||
if val, ok := _ReplicationGuaranteeKindNameToValueMap[s]; ok {
|
||||
return val, nil
|
||||
}
|
||||
return 0, fmt.Errorf("%s does not belong to ReplicationGuaranteeKind values", s)
|
||||
}
|
||||
|
||||
// ReplicationGuaranteeKindValues returns all values of the enum
|
||||
func ReplicationGuaranteeKindValues() []ReplicationGuaranteeKind {
|
||||
return _ReplicationGuaranteeKindValues
|
||||
}
|
||||
|
||||
// IsAReplicationGuaranteeKind returns "true" if the value is listed in the enum definition. "false" otherwise
|
||||
func (i ReplicationGuaranteeKind) IsAReplicationGuaranteeKind() bool {
|
||||
for _, v := range _ReplicationGuaranteeKindValues {
|
||||
if i == v {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// MarshalJSON implements the json.Marshaler interface for ReplicationGuaranteeKind
|
||||
func (i ReplicationGuaranteeKind) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(i.String())
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements the json.Unmarshaler interface for ReplicationGuaranteeKind
|
||||
func (i *ReplicationGuaranteeKind) UnmarshalJSON(data []byte) error {
|
||||
var s string
|
||||
if err := json.Unmarshal(data, &s); err != nil {
|
||||
return fmt.Errorf("ReplicationGuaranteeKind should be a string, got %s", data)
|
||||
}
|
||||
|
||||
var err error
|
||||
*i, err = ReplicationGuaranteeKindString(s)
|
||||
return err
|
||||
}
|
||||
@@ -22,7 +22,7 @@ require (
|
||||
github.com/onsi/gomega v1.7.0 // indirect
|
||||
github.com/pkg/errors v0.8.1
|
||||
github.com/pkg/profile v1.2.1
|
||||
github.com/problame/go-netssh v0.0.0-20200601114649-26439f9f0dc5
|
||||
github.com/problame/go-netssh v0.0.0-20191209123953-18d8aa6923c7
|
||||
github.com/prometheus/client_golang v1.2.1
|
||||
github.com/prometheus/common v0.7.0
|
||||
github.com/sergi/go-diff v1.0.1-0.20180205163309-da645544ed44 // go1.12 thinks it needs this
|
||||
|
||||
@@ -212,8 +212,6 @@ github.com/problame/go-netssh v0.0.0-20191026123024-f34099f4f6b1 h1:HH8yzlaZq/A8
|
||||
github.com/problame/go-netssh v0.0.0-20191026123024-f34099f4f6b1/go.mod h1:JRs3nyBjvOv/9YHC+Vlw9z1Jfzl5s5t0BNnTzmclj1c=
|
||||
github.com/problame/go-netssh v0.0.0-20191209123953-18d8aa6923c7 h1:Oik8tLrLhoMq4fduDRuNNOAQ+q0M0dXkjAYLUf4+mAc=
|
||||
github.com/problame/go-netssh v0.0.0-20191209123953-18d8aa6923c7/go.mod h1:Ba6RaFpK1+IHWs1wLZ6sCBuXkt4iAVLeOG5GinXHusM=
|
||||
github.com/problame/go-netssh v0.0.0-20200601114649-26439f9f0dc5 h1:1eSrO2WAeSXjMol8WRKW9LekL1252VIMaKQwWkmEdvs=
|
||||
github.com/problame/go-netssh v0.0.0-20200601114649-26439f9f0dc5/go.mod h1:Ba6RaFpK1+IHWs1wLZ6sCBuXkt4iAVLeOG5GinXHusM=
|
||||
github.com/prometheus/client_golang v0.0.0-20180410130117-e11c6ff8170b/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||
|
||||
@@ -82,7 +82,7 @@ func (l Level) String() string {
|
||||
case Error:
|
||||
return "error"
|
||||
default:
|
||||
return fmt.Sprintf("unknown level %d", l)
|
||||
return string(l)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,8 +2,9 @@ package tests
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/kr/pretty"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/zrepl/zrepl/platformtest"
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
@@ -17,7 +18,9 @@ func BatchDestroy(ctx *platformtest.Context) {
|
||||
+ "foo bar@1"
|
||||
+ "foo bar@2"
|
||||
+ "foo bar@3"
|
||||
+ "foo bar@4"
|
||||
R zfs hold zrepl_platformtest "${ROOTDS}/foo bar@2"
|
||||
R zfs hold zrepl_platformtest "${ROOTDS}/foo bar@4"
|
||||
`)
|
||||
|
||||
reqs := []*zfs.DestroySnapOp{
|
||||
@@ -31,24 +34,40 @@ func BatchDestroy(ctx *platformtest.Context) {
|
||||
Filesystem: fmt.Sprintf("%s/foo bar", ctx.RootDataset),
|
||||
Name: "2",
|
||||
},
|
||||
&zfs.DestroySnapOp{
|
||||
ErrOut: new(error),
|
||||
Filesystem: fmt.Sprintf("%s/foo bar", ctx.RootDataset),
|
||||
Name: "non existent",
|
||||
},
|
||||
&zfs.DestroySnapOp{
|
||||
ErrOut: new(error),
|
||||
Filesystem: fmt.Sprintf("%s/foo bar", ctx.RootDataset),
|
||||
Name: "4",
|
||||
},
|
||||
}
|
||||
zfs.ZFSDestroyFilesystemVersions(ctx, reqs)
|
||||
|
||||
pretty.Println(reqs)
|
||||
|
||||
if *reqs[0].ErrOut != nil {
|
||||
panic("expecting no error")
|
||||
}
|
||||
err := (*reqs[1].ErrOut).Error()
|
||||
if !strings.Contains(err, fmt.Sprintf("%s/foo bar@2", ctx.RootDataset)) {
|
||||
panic(fmt.Sprintf("expecting error about being unable to destroy @2: %T\n%s", err, err))
|
||||
}
|
||||
|
||||
eBusy, ok := (*reqs[1].ErrOut).(*zfs.ErrDestroySnapshotDatasetIsBusy)
|
||||
require.True(ctx, ok)
|
||||
require.Equal(ctx, reqs[1].Name, eBusy.Name)
|
||||
|
||||
require.Nil(ctx, *reqs[2].ErrOut, "destroying non-existent snap is not an error (idempotence)")
|
||||
|
||||
eBusy, ok = (*reqs[3].ErrOut).(*zfs.ErrDestroySnapshotDatasetIsBusy)
|
||||
require.True(ctx, ok)
|
||||
require.Equal(ctx, reqs[3].Name, eBusy.Name)
|
||||
|
||||
platformtest.Run(ctx, platformtest.PanicErr, ctx.RootDataset, `
|
||||
!N "foo bar@3"
|
||||
!E "foo bar@1"
|
||||
!E "foo bar@2"
|
||||
R zfs release zrepl_platformtest "${ROOTDS}/foo bar@2"
|
||||
- "foo bar@2"
|
||||
- "foo bar@1"
|
||||
- "foo bar"
|
||||
!E "foo bar@4"
|
||||
`)
|
||||
|
||||
}
|
||||
|
||||
@@ -14,19 +14,13 @@ var Cases = []Case{BatchDestroy,
|
||||
ListFilesystemVersionsUserrefs,
|
||||
ListFilesystemVersionsZeroExistIsNotAnError,
|
||||
ListFilesystemsNoFilter,
|
||||
Pruner2NotReplicated,
|
||||
ReceiveForceIntoEncryptedErr,
|
||||
ReceiveForceRollbackWorksUnencrypted,
|
||||
ReplicationFailingInitialParentProhibitsChildReplication,
|
||||
ReplicationIncrementalCleansUpStaleAbstractionsWithCacheOnSecondReplication,
|
||||
ReplicationIncrementalCleansUpStaleAbstractionsWithoutCacheOnSecondReplication,
|
||||
ReplicationIncrementalDestroysStepHoldsIffIncrementalStepHoldsAreDisabledButStepHoldsExist,
|
||||
ReplicationIncrementalIsPossibleIfCommonSnapshotIsDestroyed,
|
||||
ReplicationIsResumableFullSend__both_GuaranteeResumability,
|
||||
ReplicationIsResumableFullSend__initial_GuaranteeIncrementalReplication_incremental_GuaranteeIncrementalReplication,
|
||||
ReplicationIsResumableFullSend__initial_GuaranteeResumability_incremental_GuaranteeIncrementalReplication,
|
||||
ReplicationReceiverErrorWhileStillSending,
|
||||
ReplicationStepCompletedLostBehavior__GuaranteeIncrementalReplication,
|
||||
ReplicationStepCompletedLostBehavior__GuaranteeResumability,
|
||||
ReplicationIsResumableFullSend,
|
||||
ResumableRecvAndTokenHandling,
|
||||
ResumeTokenParsing,
|
||||
SendArgsValidationEncryptedSendOfUnencryptedDatasetForbidden,
|
||||
|
||||
@@ -0,0 +1,331 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/zrepl/zrepl/config"
|
||||
"github.com/zrepl/zrepl/daemon/filters"
|
||||
"github.com/zrepl/zrepl/daemon/pruner"
|
||||
"github.com/zrepl/zrepl/endpoint"
|
||||
"github.com/zrepl/zrepl/platformtest"
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
|
||||
func PrunerNotReplicated(ctx *platformtest.Context) {
|
||||
|
||||
platformtest.Run(ctx, platformtest.PanicErr, ctx.RootDataset, `
|
||||
DESTROYROOT
|
||||
CREATEROOT
|
||||
+ "foo bar"
|
||||
+ "foo bar@1"
|
||||
+ "foo bar@2"
|
||||
+ "foo bar@3"
|
||||
+ "foo bar@4"
|
||||
+ "foo bar@5"
|
||||
`)
|
||||
|
||||
c, err := config.ParseConfigBytes([]byte(fmt.Sprintf(`
|
||||
jobs:
|
||||
- name: prunetest
|
||||
type: push
|
||||
filesystems: {
|
||||
"%s/foo bar<": true
|
||||
}
|
||||
connect:
|
||||
type: tcp
|
||||
address: 255.255.255.255:255
|
||||
snapshotting:
|
||||
type: manual
|
||||
pruning:
|
||||
keep_sender:
|
||||
- type: not_replicated
|
||||
- type: last_n
|
||||
count: 1
|
||||
keep_receiver:
|
||||
- type: last_n
|
||||
count: 2
|
||||
`, ctx.RootDataset)))
|
||||
require.NoError(ctx, err)
|
||||
|
||||
pushJob := c.Jobs[0].Ret.(*config.PushJob)
|
||||
|
||||
dummyHistVec := prometheus.NewHistogramVec(prometheus.HistogramOpts{
|
||||
Namespace: "foo",
|
||||
Subsystem: "foo",
|
||||
Name: "foo",
|
||||
Help: "foo",
|
||||
}, []string{"foo"})
|
||||
|
||||
prunerFactory, err := pruner.NewPrunerFactory(pushJob.Pruning, dummyHistVec)
|
||||
require.NoError(ctx, err)
|
||||
|
||||
senderJid := endpoint.MustMakeJobID("sender-job")
|
||||
|
||||
fsfilter, err := filters.DatasetMapFilterFromConfig(pushJob.Filesystems)
|
||||
require.NoError(ctx, err)
|
||||
|
||||
sender := endpoint.NewSender(endpoint.SenderConfig{
|
||||
FSF: fsfilter,
|
||||
Encrypt: &zfs.NilBool{
|
||||
B: false,
|
||||
},
|
||||
JobID: senderJid,
|
||||
})
|
||||
|
||||
fs := ctx.RootDataset + "/foo bar"
|
||||
|
||||
// create a replication cursor to make pruning work at all
|
||||
_, err = endpoint.CreateReplicationCursor(ctx, fs, fsversion(ctx, fs, "@2"), senderJid)
|
||||
require.NoError(ctx, err)
|
||||
|
||||
p := prunerFactory.BuildSenderPruner(ctx, sender, sender)
|
||||
|
||||
p.Prune()
|
||||
|
||||
report := p.Report()
|
||||
|
||||
reportJSON, err := json.MarshalIndent(report, "", " ")
|
||||
require.NoError(ctx, err)
|
||||
ctx.Logf("%s\n", string(reportJSON))
|
||||
|
||||
require.Equal(ctx, pruner.Done.String(), report.State)
|
||||
require.Len(ctx, report.Completed, 1)
|
||||
fsReport := report.Completed[0]
|
||||
require.Equal(ctx, fs, fsReport.Filesystem)
|
||||
require.Empty(ctx, fsReport.SkipReason)
|
||||
require.Empty(ctx, fsReport.LastError)
|
||||
require.Len(ctx, fsReport.DestroyList, 1)
|
||||
require.Equal(ctx, fsReport.DestroyList[0], pruner.SnapshotReport{
|
||||
Name: "1",
|
||||
Replicated: true,
|
||||
Date: fsReport.DestroyList[0].Date,
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func PrunerNoKeepNotReplicatedNoKeepStepHoldConvertsAnyStepHoldToBookmark(ctx *platformtest.Context) {
|
||||
|
||||
platformtest.Run(ctx, platformtest.PanicErr, ctx.RootDataset, `
|
||||
DESTROYROOT
|
||||
CREATEROOT
|
||||
+ "foo bar"
|
||||
+ "foo bar@1"
|
||||
+ "foo bar@2"
|
||||
+ "foo bar@3"
|
||||
+ "foo bar@4"
|
||||
+ "foo bar@5"
|
||||
`)
|
||||
|
||||
c, err := config.ParseConfigBytes([]byte(fmt.Sprintf(`
|
||||
jobs:
|
||||
- name: prunetest
|
||||
type: push
|
||||
filesystems: {
|
||||
"%s/foo bar<": true
|
||||
}
|
||||
connect:
|
||||
type: tcp
|
||||
address: 255.255.255.255:255
|
||||
snapshotting:
|
||||
type: manual
|
||||
pruning:
|
||||
keep_sender:
|
||||
- type: last_n
|
||||
count: 1
|
||||
keep_receiver:
|
||||
- type: last_n
|
||||
count: 2
|
||||
`, ctx.RootDataset)))
|
||||
require.NoError(ctx, err)
|
||||
|
||||
pushJob := c.Jobs[0].Ret.(*config.PushJob)
|
||||
|
||||
dummyHistVec := prometheus.NewHistogramVec(prometheus.HistogramOpts{
|
||||
Namespace: "foo",
|
||||
Subsystem: "foo",
|
||||
Name: "foo",
|
||||
Help: "foo",
|
||||
}, []string{"foo"})
|
||||
|
||||
prunerFactory, err := pruner.NewPrunerFactory(pushJob.Pruning, dummyHistVec)
|
||||
require.NoError(ctx, err)
|
||||
|
||||
senderJid := endpoint.MustMakeJobID("sender-job")
|
||||
|
||||
fsfilter, err := filters.DatasetMapFilterFromConfig(pushJob.Filesystems)
|
||||
require.NoError(ctx, err)
|
||||
|
||||
sender := endpoint.NewSender(endpoint.SenderConfig{
|
||||
FSF: fsfilter,
|
||||
Encrypt: &zfs.NilBool{
|
||||
B: false,
|
||||
},
|
||||
JobID: senderJid,
|
||||
})
|
||||
|
||||
fs := ctx.RootDataset + "/foo bar"
|
||||
|
||||
// create a replication cursor to make pruning work at all
|
||||
_, err = endpoint.CreateReplicationCursor(ctx, fs, fsversion(ctx, fs, "@2"), senderJid)
|
||||
require.NoError(ctx, err)
|
||||
|
||||
// create step holds for the incremental @2->@3
|
||||
endpoint.HoldStep(ctx, fs, fsversion(ctx, fs, "@2"), senderJid)
|
||||
endpoint.HoldStep(ctx, fs, fsversion(ctx, fs, "@3"), senderJid)
|
||||
// create step holds for another job
|
||||
otherJid := endpoint.MustMakeJobID("other-job")
|
||||
endpoint.HoldStep(ctx, fs, fsversion(ctx, fs, "@2"), otherJid)
|
||||
endpoint.HoldStep(ctx, fs, fsversion(ctx, fs, "@3"), otherJid)
|
||||
|
||||
p := prunerFactory.BuildSenderPruner(ctx, sender, sender)
|
||||
|
||||
p.Prune()
|
||||
|
||||
report := p.Report()
|
||||
|
||||
reportJSON, err := json.MarshalIndent(report, "", " ")
|
||||
require.NoError(ctx, err)
|
||||
ctx.Logf("%s\n", string(reportJSON))
|
||||
|
||||
require.Equal(ctx, pruner.Done.String(), report.State)
|
||||
require.Len(ctx, report.Completed, 1)
|
||||
fsReport := report.Completed[0]
|
||||
require.Equal(ctx, fs, fsReport.Filesystem)
|
||||
require.Empty(ctx, fsReport.SkipReason)
|
||||
require.Empty(ctx, fsReport.LastError)
|
||||
expectDestroyList := []pruner.SnapshotReport{
|
||||
{
|
||||
Name: "1",
|
||||
Replicated: true,
|
||||
},
|
||||
{
|
||||
Name: "2",
|
||||
Replicated: true,
|
||||
},
|
||||
{
|
||||
Name: "3",
|
||||
Replicated: true,
|
||||
},
|
||||
{
|
||||
Name: "4",
|
||||
Replicated: true,
|
||||
},
|
||||
}
|
||||
for _, d := range fsReport.DestroyList {
|
||||
d.Date = time.Time{}
|
||||
}
|
||||
require.Subset(ctx, fsReport.DestroyList, expectDestroyList)
|
||||
}
|
||||
|
||||
|
||||
|
||||
func PrunerNoKeepNotReplicatedButKeepStepHold(ctx *platformtest.Context) {
|
||||
|
||||
platformtest.Run(ctx, platformtest.PanicErr, ctx.RootDataset, `
|
||||
DESTROYROOT
|
||||
CREATEROOT
|
||||
+ "foo bar"
|
||||
+ "foo bar@1"
|
||||
+ "foo bar@2"
|
||||
+ "foo bar@3"
|
||||
+ "foo bar@4"
|
||||
+ "foo bar@5"
|
||||
`)
|
||||
|
||||
c, err := config.ParseConfigBytes([]byte(fmt.Sprintf(`
|
||||
jobs:
|
||||
- name: prunetest
|
||||
type: push
|
||||
filesystems: {
|
||||
"%s/foo bar<": true
|
||||
}
|
||||
connect:
|
||||
type: tcp
|
||||
address: 255.255.255.255:255
|
||||
snapshotting:
|
||||
type: manual
|
||||
pruning:
|
||||
keep_sender:
|
||||
- type: step_holds
|
||||
- type: last_n
|
||||
count: 1
|
||||
keep_receiver:
|
||||
- type: last_n
|
||||
count: 2
|
||||
`, ctx.RootDataset)))
|
||||
require.NoError(ctx, err)
|
||||
|
||||
pushJob := c.Jobs[0].Ret.(*config.PushJob)
|
||||
|
||||
dummyHistVec := prometheus.NewHistogramVec(prometheus.HistogramOpts{
|
||||
Namespace: "foo",
|
||||
Subsystem: "foo",
|
||||
Name: "foo",
|
||||
Help: "foo",
|
||||
}, []string{"foo"})
|
||||
|
||||
prunerFactory, err := pruner.NewPrunerFactory(pushJob.Pruning, dummyHistVec)
|
||||
require.NoError(ctx, err)
|
||||
|
||||
senderJid := endpoint.MustMakeJobID("sender-job")
|
||||
|
||||
fsfilter, err := filters.DatasetMapFilterFromConfig(pushJob.Filesystems)
|
||||
require.NoError(ctx, err)
|
||||
|
||||
sender := endpoint.NewSender(endpoint.SenderConfig{
|
||||
FSF: fsfilter,
|
||||
Encrypt: &zfs.NilBool{
|
||||
B: false,
|
||||
},
|
||||
JobID: senderJid,
|
||||
})
|
||||
|
||||
fs := ctx.RootDataset + "/foo bar"
|
||||
|
||||
// create a replication cursor to make pruning work at all
|
||||
_, err = endpoint.CreateReplicationCursor(ctx, fs, fsversion(ctx, fs, "@2"), senderJid)
|
||||
require.NoError(ctx, err)
|
||||
|
||||
// create step holds for the incremental @2->@3
|
||||
endpoint.HoldStep(ctx, fs, fsversion(ctx, fs, "@2"), senderJid)
|
||||
endpoint.HoldStep(ctx, fs, fsversion(ctx, fs, "@3"), senderJid)
|
||||
// create step holds for another job
|
||||
otherJid := endpoint.MustMakeJobID("other-job")
|
||||
endpoint.HoldStep(ctx, fs, fsversion(ctx, fs, "@2"), otherJid)
|
||||
endpoint.HoldStep(ctx, fs, fsversion(ctx, fs, "@3"), otherJid)
|
||||
|
||||
p := prunerFactory.BuildSenderPruner(ctx, sender, sender)
|
||||
|
||||
p.Prune()
|
||||
|
||||
report := p.Report()
|
||||
|
||||
reportJSON, err := json.MarshalIndent(report, "", " ")
|
||||
require.NoError(ctx, err)
|
||||
ctx.Logf("%s\n", string(reportJSON))
|
||||
|
||||
require.Equal(ctx, pruner.Done.String(), report.State)
|
||||
require.Len(ctx, report.Completed, 1)
|
||||
fsReport := report.Completed[0]
|
||||
require.Equal(ctx, fs, fsReport.Filesystem)
|
||||
require.Empty(ctx, fsReport.SkipReason)
|
||||
require.Empty(ctx, fsReport.LastError)
|
||||
expectDestroyList := []pruner.SnapshotReport{
|
||||
{
|
||||
Name: "1",
|
||||
Replicated: true,
|
||||
},
|
||||
{
|
||||
Name: "4",
|
||||
Replicated: true,
|
||||
},
|
||||
}
|
||||
for _, d := range fsReport.DestroyList {
|
||||
d.Date = time.Time{}
|
||||
}
|
||||
require.Subset(ctx, fsReport.DestroyList, expectDestroyList)
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/kr/pretty"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/zrepl/zrepl/config"
|
||||
"github.com/zrepl/zrepl/daemon/filters"
|
||||
"github.com/zrepl/zrepl/daemon/pruner.v2"
|
||||
"github.com/zrepl/zrepl/endpoint"
|
||||
"github.com/zrepl/zrepl/platformtest"
|
||||
"github.com/zrepl/zrepl/pruning"
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
|
||||
func Pruner2NotReplicated(ctx *platformtest.Context) {
|
||||
|
||||
platformtest.Run(ctx, platformtest.PanicErr, ctx.RootDataset, `
|
||||
DESTROYROOT
|
||||
CREATEROOT
|
||||
+ "foo bar"
|
||||
+ "foo bar@1"
|
||||
+ "foo bar@2"
|
||||
+ "foo bar@3"
|
||||
+ "foo bar@4"
|
||||
+ "foo bar@5"
|
||||
`)
|
||||
|
||||
fs := ctx.RootDataset + "/foo bar"
|
||||
senderJid := endpoint.MustMakeJobID("sender-job")
|
||||
otherJid1 := endpoint.MustMakeJobID("other-job-1")
|
||||
otherJid2 := endpoint.MustMakeJobID("other-job-2")
|
||||
|
||||
|
||||
// create step holds for the incremental @2->@3
|
||||
endpoint.HoldStep(ctx, fs, fsversion(ctx, fs, "@2"), senderJid)
|
||||
endpoint.HoldStep(ctx, fs, fsversion(ctx, fs, "@3"), senderJid)
|
||||
// create step holds for other-job-1 @2 -> @3
|
||||
endpoint.HoldStep(ctx, fs, fsversion(ctx, fs, "@2"), otherJid1)
|
||||
endpoint.HoldStep(ctx, fs, fsversion(ctx, fs, "@3"), otherJid1)
|
||||
// create step hold for other-job-2 @1 (will be pruned)
|
||||
endpoint.HoldStep(ctx, fs, fsversion(ctx, fs, "@1"), otherJid2)
|
||||
|
||||
c, err := config.ParseConfigBytes([]byte(fmt.Sprintf(`
|
||||
jobs:
|
||||
- name: prunetest
|
||||
type: push
|
||||
filesystems: {
|
||||
"%s/foo bar": true
|
||||
}
|
||||
connect:
|
||||
type: tcp
|
||||
address: 255.255.255.255:255
|
||||
snapshotting:
|
||||
type: manual
|
||||
pruning:
|
||||
keep_sender:
|
||||
#- type: not_replicated
|
||||
- type: step_holds
|
||||
- type: last_n
|
||||
count: 1
|
||||
keep_receiver:
|
||||
- type: last_n
|
||||
count: 2
|
||||
`, ctx.RootDataset)))
|
||||
require.NoError(ctx, err)
|
||||
|
||||
pushJob := c.Jobs[0].Ret.(*config.PushJob)
|
||||
|
||||
require.NoError(ctx, err)
|
||||
|
||||
fsfilter, err := filters.DatasetMapFilterFromConfig(pushJob.Filesystems)
|
||||
require.NoError(ctx, err)
|
||||
|
||||
matchedFilesystems, err := zfs.ZFSListMapping(ctx, fsfilter)
|
||||
ctx.Logf("%s", pretty.Sprint(matchedFilesystems))
|
||||
require.NoError(ctx, err)
|
||||
require.Len(ctx, matchedFilesystems, 1)
|
||||
|
||||
sideSender := pruner.NewSideSender(senderJid)
|
||||
|
||||
keepRules, err := pruning.RulesFromConfig(senderJid, pushJob.Pruning.KeepSender)
|
||||
require.NoError(ctx, err)
|
||||
p := pruner.NewPruner(fsfilter, senderJid, sideSender, keepRules)
|
||||
|
||||
runDone := make(chan *pruner.Report)
|
||||
go func() {
|
||||
runDone <- p.Run(ctx)
|
||||
}()
|
||||
|
||||
var report *pruner.Report
|
||||
// concurrency stress
|
||||
out:
|
||||
for {
|
||||
select {
|
||||
case <-time.After(10 * time.Millisecond):
|
||||
p.Report(ctx)
|
||||
case report = <-runDone:
|
||||
break out
|
||||
}
|
||||
}
|
||||
ctx.Logf("%s\n", pretty.Sprint(report))
|
||||
|
||||
reportJSON, err := json.MarshalIndent(report, "", " ")
|
||||
require.NoError(ctx, err)
|
||||
ctx.Logf("%s\n", string(reportJSON))
|
||||
|
||||
ctx.FailNow()
|
||||
// fs := ctx.RootDataset + "/foo bar"
|
||||
|
||||
// // create a replication cursor to make pruning work at all
|
||||
// _, err = endpoint.CreateReplicationCursor(ctx, fs, fsversion(ctx, fs, "@2"), senderJid)
|
||||
// require.NoError(ctx, err)
|
||||
|
||||
// p := prunerFactory.BuildSenderPruner(ctx, sender, sender)
|
||||
|
||||
// p.Prune()
|
||||
|
||||
// report := p.Report()
|
||||
|
||||
// require.Equal(ctx, pruner.Done.String(), report.State)
|
||||
// require.Len(ctx, report.Completed, 1)
|
||||
// fsReport := report.Completed[0]
|
||||
// require.Equal(ctx, fs, fsReport.Filesystem)
|
||||
// require.Empty(ctx, fsReport.SkipReason)
|
||||
// require.Empty(ctx, fsReport.LastError)
|
||||
// require.Len(ctx, fsReport.DestroyList, 1)
|
||||
// require.Equal(ctx, fsReport.DestroyList[0], pruner.SnapshotReport{
|
||||
// Name: "1",
|
||||
// Replicated: true,
|
||||
// Date: fsReport.DestroyList[0].Date,
|
||||
// })
|
||||
|
||||
}
|
||||
@@ -4,9 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"sort"
|
||||
|
||||
"github.com/kr/pretty"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -28,13 +26,10 @@ import (
|
||||
// of a new sender and receiver instance and one blocking invocation
|
||||
// of the replication engine without encryption
|
||||
type replicationInvocation struct {
|
||||
sjid, rjid endpoint.JobID
|
||||
sfs string
|
||||
sfilter *filters.DatasetMapFilter
|
||||
rfsRoot string
|
||||
interceptSender func(e *endpoint.Sender) logic.Sender
|
||||
interceptReceiver func(e *endpoint.Receiver) logic.Receiver
|
||||
guarantee pdu.ReplicationConfigProtection
|
||||
sjid, rjid endpoint.JobID
|
||||
sfs string
|
||||
rfsRoot string
|
||||
interceptSender func(e *endpoint.Sender) logic.Sender
|
||||
}
|
||||
|
||||
func (i replicationInvocation) Do(ctx *platformtest.Context) *report.Report {
|
||||
@@ -42,33 +37,23 @@ func (i replicationInvocation) Do(ctx *platformtest.Context) *report.Report {
|
||||
if i.interceptSender == nil {
|
||||
i.interceptSender = func(e *endpoint.Sender) logic.Sender { return e }
|
||||
}
|
||||
if i.interceptReceiver == nil {
|
||||
i.interceptReceiver = func(e *endpoint.Receiver) logic.Receiver { return e }
|
||||
}
|
||||
|
||||
if i.sfs != "" && i.sfilter != nil || i.sfs == "" && i.sfilter == nil {
|
||||
panic("either sfs or sfilter must be set")
|
||||
}
|
||||
if i.sfilter == nil {
|
||||
i.sfilter = filters.NewDatasetMapFilter(1, true)
|
||||
err := i.sfilter.Add(i.sfs, "ok")
|
||||
require.NoError(ctx, err)
|
||||
}
|
||||
sfilter := filters.NewDatasetMapFilter(1, true)
|
||||
err := sfilter.Add(i.sfs, "ok")
|
||||
require.NoError(ctx, err)
|
||||
sender := i.interceptSender(endpoint.NewSender(endpoint.SenderConfig{
|
||||
FSF: i.sfilter.AsFilter(),
|
||||
FSF: sfilter.AsFilter(),
|
||||
Encrypt: &zfs.NilBool{B: false},
|
||||
JobID: i.sjid,
|
||||
}))
|
||||
receiver := i.interceptReceiver(endpoint.NewReceiver(endpoint.ReceiverConfig{
|
||||
receiver := endpoint.NewReceiver(endpoint.ReceiverConfig{
|
||||
JobID: i.rjid,
|
||||
AppendClientIdentity: false,
|
||||
RootWithoutClientComponent: mustDatasetPath(i.rfsRoot),
|
||||
}))
|
||||
UpdateLastReceivedHold: true,
|
||||
})
|
||||
plannerPolicy := logic.PlannerPolicy{
|
||||
EncryptedSend: logic.TriFromBool(false),
|
||||
ReplicationConfig: pdu.ReplicationConfig{
|
||||
Protection: &i.guarantee,
|
||||
},
|
||||
}
|
||||
|
||||
report, wait := replication.Do(
|
||||
@@ -101,11 +86,10 @@ func ReplicationIncrementalIsPossibleIfCommonSnapshotIsDestroyed(ctx *platformte
|
||||
snap1 := fsversion(ctx, sfs, "@1")
|
||||
|
||||
rep := replicationInvocation{
|
||||
sjid: sjid,
|
||||
rjid: rjid,
|
||||
sfs: sfs,
|
||||
rfsRoot: rfsRoot,
|
||||
guarantee: *pdu.ReplicationConfigProtectionWithKind(pdu.ReplicationGuaranteeKind_GuaranteeResumability),
|
||||
sjid: sjid,
|
||||
rjid: rjid,
|
||||
sfs: sfs,
|
||||
rfsRoot: rfsRoot,
|
||||
}
|
||||
rfs := rep.ReceiveSideFilesystem()
|
||||
|
||||
@@ -165,11 +149,10 @@ func implReplicationIncrementalCleansUpStaleAbstractions(ctx *platformtest.Conte
|
||||
rfsRoot := ctx.RootDataset + "/receiver"
|
||||
|
||||
rep := replicationInvocation{
|
||||
sjid: sjid,
|
||||
rjid: rjid,
|
||||
sfs: sfs,
|
||||
rfsRoot: rfsRoot,
|
||||
guarantee: *pdu.ReplicationConfigProtectionWithKind(pdu.ReplicationGuaranteeKind_GuaranteeResumability),
|
||||
sjid: sjid,
|
||||
rjid: rjid,
|
||||
sfs: sfs,
|
||||
rfsRoot: rfsRoot,
|
||||
}
|
||||
rfs := rep.ReceiveSideFilesystem()
|
||||
|
||||
@@ -195,7 +178,7 @@ func implReplicationIncrementalCleansUpStaleAbstractions(ctx *platformtest.Conte
|
||||
require.NoError(ctx, err)
|
||||
require.Contains(ctx, holds, expectRjidHoldTag)
|
||||
|
||||
// create artifical stale replication cursors & step holds
|
||||
// create artifical stale replication cursors
|
||||
createArtificalStaleAbstractions := func(jobId endpoint.JobID) []endpoint.Abstraction {
|
||||
snap2Cursor, err := endpoint.CreateReplicationCursor(ctx, sfs, snap2, jobId) // no shadow
|
||||
require.NoError(ctx, err)
|
||||
@@ -219,7 +202,7 @@ func implReplicationIncrementalCleansUpStaleAbstractions(ctx *platformtest.Conte
|
||||
snap5 := fsversion(ctx, sfs, "@5")
|
||||
|
||||
if invalidateCacheBeforeSecondReplication {
|
||||
endpoint.AbstractionsCacheInvalidate(sfs)
|
||||
endpoint.SendAbstractionsCacheInvalidate(sfs)
|
||||
}
|
||||
|
||||
// do another replication
|
||||
@@ -339,59 +322,7 @@ func (s *PartialSender) Send(ctx context.Context, r *pdu.SendReq) (r1 *pdu.SendR
|
||||
return r1, r2, r3
|
||||
}
|
||||
|
||||
func ReplicationIsResumableFullSend__both_GuaranteeResumability(ctx *platformtest.Context) {
|
||||
|
||||
setup := replicationIsResumableFullSendSetup{
|
||||
protection: pdu.ReplicationConfigProtection{
|
||||
Initial: pdu.ReplicationGuaranteeKind_GuaranteeResumability,
|
||||
Incremental: pdu.ReplicationGuaranteeKind_GuaranteeResumability,
|
||||
},
|
||||
expectDatasetIsBusyErrorWhenDestroySnapshotWhilePartiallyReplicated: true,
|
||||
expectAllThreeSnapshotsToThreeBePresentAfterLoop: true,
|
||||
expectNoSnapshotsOnReceiverAfterLoop: false,
|
||||
}
|
||||
|
||||
implReplicationIsResumableFullSend(ctx, setup)
|
||||
}
|
||||
|
||||
func ReplicationIsResumableFullSend__initial_GuaranteeResumability_incremental_GuaranteeIncrementalReplication(ctx *platformtest.Context) {
|
||||
|
||||
setup := replicationIsResumableFullSendSetup{
|
||||
protection: pdu.ReplicationConfigProtection{
|
||||
Initial: pdu.ReplicationGuaranteeKind_GuaranteeResumability,
|
||||
Incremental: pdu.ReplicationGuaranteeKind_GuaranteeIncrementalReplication,
|
||||
},
|
||||
expectDatasetIsBusyErrorWhenDestroySnapshotWhilePartiallyReplicated: true,
|
||||
expectAllThreeSnapshotsToThreeBePresentAfterLoop: true,
|
||||
expectNoSnapshotsOnReceiverAfterLoop: false,
|
||||
}
|
||||
|
||||
implReplicationIsResumableFullSend(ctx, setup)
|
||||
}
|
||||
|
||||
func ReplicationIsResumableFullSend__initial_GuaranteeIncrementalReplication_incremental_GuaranteeIncrementalReplication(ctx *platformtest.Context) {
|
||||
|
||||
setup := replicationIsResumableFullSendSetup{
|
||||
protection: pdu.ReplicationConfigProtection{
|
||||
Initial: pdu.ReplicationGuaranteeKind_GuaranteeIncrementalReplication,
|
||||
Incremental: pdu.ReplicationGuaranteeKind_GuaranteeIncrementalReplication,
|
||||
},
|
||||
expectDatasetIsBusyErrorWhenDestroySnapshotWhilePartiallyReplicated: false,
|
||||
expectAllThreeSnapshotsToThreeBePresentAfterLoop: false,
|
||||
expectNoSnapshotsOnReceiverAfterLoop: true,
|
||||
}
|
||||
|
||||
implReplicationIsResumableFullSend(ctx, setup)
|
||||
}
|
||||
|
||||
type replicationIsResumableFullSendSetup struct {
|
||||
protection pdu.ReplicationConfigProtection
|
||||
expectDatasetIsBusyErrorWhenDestroySnapshotWhilePartiallyReplicated bool
|
||||
expectAllThreeSnapshotsToThreeBePresentAfterLoop bool
|
||||
expectNoSnapshotsOnReceiverAfterLoop bool
|
||||
}
|
||||
|
||||
func implReplicationIsResumableFullSend(ctx *platformtest.Context, setup replicationIsResumableFullSendSetup) {
|
||||
func ReplicationIsResumableFullSend(ctx *platformtest.Context) {
|
||||
|
||||
platformtest.Run(ctx, platformtest.PanicErr, ctx.RootDataset, `
|
||||
CREATEROOT
|
||||
@@ -422,9 +353,7 @@ func implReplicationIsResumableFullSend(ctx *platformtest.Context, setup replica
|
||||
interceptSender: func(e *endpoint.Sender) logic.Sender {
|
||||
return &PartialSender{Sender: e, failAfterByteCount: 1 << 20}
|
||||
},
|
||||
guarantee: setup.protection,
|
||||
}
|
||||
|
||||
rfs := rep.ReceiveSideFilesystem()
|
||||
|
||||
for i := 2; i < 10; i++ {
|
||||
@@ -438,11 +367,8 @@ func implReplicationIsResumableFullSend(ctx *platformtest.Context, setup replica
|
||||
// and we wrote dummy data 1<<22 bytes, thus at least
|
||||
// for the first 4 times this should not be possible
|
||||
// due to step holds
|
||||
if setup.expectDatasetIsBusyErrorWhenDestroySnapshotWhilePartiallyReplicated {
|
||||
ctx.Logf("i=%v", i)
|
||||
require.Error(ctx, err)
|
||||
require.Contains(ctx, err.Error(), "dataset is busy")
|
||||
}
|
||||
require.Error(ctx, err)
|
||||
require.Contains(ctx, err.Error(), "dataset is busy")
|
||||
}
|
||||
|
||||
// and create some additional snapshots that could
|
||||
@@ -461,456 +387,10 @@ func implReplicationIsResumableFullSend(ctx *platformtest.Context, setup replica
|
||||
}
|
||||
}
|
||||
|
||||
if setup.expectAllThreeSnapshotsToThreeBePresentAfterLoop {
|
||||
// make sure all the filesystem versions we created
|
||||
// were replicated by the replication loop
|
||||
_ = fsversion(ctx, rfs, "@1")
|
||||
_ = fsversion(ctx, rfs, "@2")
|
||||
_ = fsversion(ctx, rfs, "@3")
|
||||
}
|
||||
|
||||
if setup.expectNoSnapshotsOnReceiverAfterLoop {
|
||||
versions, err := zfs.ZFSListFilesystemVersions(ctx, mustDatasetPath(rfs), zfs.ListFilesystemVersionsOptions{})
|
||||
require.NoError(ctx, err)
|
||||
require.Empty(ctx, versions)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func ReplicationIncrementalDestroysStepHoldsIffIncrementalStepHoldsAreDisabledButStepHoldsExist(ctx *platformtest.Context) {
|
||||
|
||||
platformtest.Run(ctx, platformtest.PanicErr, ctx.RootDataset, `
|
||||
CREATEROOT
|
||||
+ "sender"
|
||||
+ "receiver"
|
||||
R zfs create -p "${ROOTDS}/receiver/${ROOTDS}"
|
||||
`)
|
||||
|
||||
sjid := endpoint.MustMakeJobID("sender-job")
|
||||
rjid := endpoint.MustMakeJobID("receiver-job")
|
||||
|
||||
sfs := ctx.RootDataset + "/sender"
|
||||
rfsRoot := ctx.RootDataset + "/receiver"
|
||||
|
||||
// fully replicate snapshots @1
|
||||
{
|
||||
mustSnapshot(ctx, sfs+"@1")
|
||||
rep := replicationInvocation{
|
||||
sjid: sjid,
|
||||
rjid: rjid,
|
||||
sfs: sfs,
|
||||
rfsRoot: rfsRoot,
|
||||
guarantee: *pdu.ReplicationConfigProtectionWithKind(pdu.ReplicationGuaranteeKind_GuaranteeResumability),
|
||||
}
|
||||
rfs := rep.ReceiveSideFilesystem()
|
||||
report := rep.Do(ctx)
|
||||
ctx.Logf("\n%s", pretty.Sprint(report))
|
||||
// assert this worked (not the main subject of the test)
|
||||
_ = fsversion(ctx, rfs, "@1")
|
||||
}
|
||||
|
||||
// create a large snapshot @2
|
||||
{
|
||||
sfsmp, err := zfs.ZFSGetMountpoint(ctx, sfs)
|
||||
require.NoError(ctx, err)
|
||||
require.True(ctx, sfsmp.Mounted)
|
||||
writeDummyData(path.Join(sfsmp.Mountpoint, "dummy.data"), 1<<22)
|
||||
mustSnapshot(ctx, sfs+"@2")
|
||||
}
|
||||
snap2sfs := fsversion(ctx, sfs, "@2")
|
||||
|
||||
// partially replicate snapshots @2 with step holds enabled
|
||||
// to effect a step-holds situation
|
||||
{
|
||||
rep := replicationInvocation{
|
||||
sjid: sjid,
|
||||
rjid: rjid,
|
||||
sfs: sfs,
|
||||
rfsRoot: rfsRoot,
|
||||
guarantee: *pdu.ReplicationConfigProtectionWithKind(pdu.ReplicationGuaranteeKind_GuaranteeResumability), // !
|
||||
interceptSender: func(e *endpoint.Sender) logic.Sender {
|
||||
return &PartialSender{Sender: e, failAfterByteCount: 1 << 20}
|
||||
},
|
||||
}
|
||||
rfs := rep.ReceiveSideFilesystem()
|
||||
report := rep.Do(ctx)
|
||||
ctx.Logf("\n%s", pretty.Sprint(report))
|
||||
// assert this partial receive worked
|
||||
_, err := zfs.ZFSGetFilesystemVersion(ctx, rfs+"@2")
|
||||
ctx.Logf("%T %s", err, err)
|
||||
_, notFullyReceived := err.(*zfs.DatasetDoesNotExist)
|
||||
require.True(ctx, notFullyReceived)
|
||||
// assert step holds are in place
|
||||
abs, absErrs, err := endpoint.ListAbstractions(ctx, endpoint.ListZFSHoldsAndBookmarksQuery{
|
||||
FS: endpoint.ListZFSHoldsAndBookmarksQueryFilesystemFilter{
|
||||
FS: &sfs,
|
||||
},
|
||||
Concurrency: 1,
|
||||
JobID: &sjid,
|
||||
What: endpoint.AbstractionTypeSet{endpoint.AbstractionStepHold: true},
|
||||
})
|
||||
require.NoError(ctx, err)
|
||||
require.Empty(ctx, absErrs)
|
||||
require.Len(ctx, abs, 2)
|
||||
sort.Slice(abs, func(i, j int) bool {
|
||||
return abs[i].GetCreateTXG() < abs[j].GetCreateTXG()
|
||||
})
|
||||
require.True(ctx, zfs.FilesystemVersionEqualIdentity(abs[0].GetFilesystemVersion(), fsversion(ctx, sfs, "@1")))
|
||||
require.True(ctx, zfs.FilesystemVersionEqualIdentity(abs[1].GetFilesystemVersion(), fsversion(ctx, sfs, "@2")))
|
||||
}
|
||||
|
||||
//
|
||||
// end of test setup
|
||||
//
|
||||
|
||||
// retry replication with incremental step holds disabled (set to bookmarks-only in this case)
|
||||
// - replication should not fail due to holds-related stuff
|
||||
// - replication should fail intermittently due to partial sender being fully read
|
||||
// - the partial sender is 1/4th the length of the stream, thus expect
|
||||
// successful replication after 5 more attempts
|
||||
rep := replicationInvocation{
|
||||
sjid: sjid,
|
||||
rjid: rjid,
|
||||
sfs: sfs,
|
||||
rfsRoot: rfsRoot,
|
||||
guarantee: *pdu.ReplicationConfigProtectionWithKind(pdu.ReplicationGuaranteeKind_GuaranteeIncrementalReplication), // !
|
||||
interceptSender: func(e *endpoint.Sender) logic.Sender {
|
||||
return &PartialSender{Sender: e, failAfterByteCount: 1 << 20}
|
||||
},
|
||||
}
|
||||
rfs := rep.ReceiveSideFilesystem()
|
||||
for i := 0; ; i++ {
|
||||
require.True(ctx, i < 5)
|
||||
report := rep.Do(ctx)
|
||||
ctx.Logf("retry run=%v\n%s", i, pretty.Sprint(report))
|
||||
_, err := zfs.ZFSGetFilesystemVersion(ctx, rfs+"@2")
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// assert replication worked
|
||||
fsversion(ctx, rfs, "@2")
|
||||
|
||||
// assert no step holds exist
|
||||
abs, absErrs, err := endpoint.ListAbstractions(ctx, endpoint.ListZFSHoldsAndBookmarksQuery{
|
||||
FS: endpoint.ListZFSHoldsAndBookmarksQueryFilesystemFilter{
|
||||
FS: &sfs,
|
||||
},
|
||||
Concurrency: 1,
|
||||
JobID: &sjid,
|
||||
What: endpoint.AbstractionTypeSet{endpoint.AbstractionStepHold: true},
|
||||
})
|
||||
require.NoError(ctx, err)
|
||||
require.Empty(ctx, absErrs)
|
||||
require.Len(ctx, abs, 0)
|
||||
|
||||
// assert that the replication cursor bookmark exists
|
||||
abs, absErrs, err = endpoint.ListAbstractions(ctx, endpoint.ListZFSHoldsAndBookmarksQuery{
|
||||
FS: endpoint.ListZFSHoldsAndBookmarksQueryFilesystemFilter{
|
||||
FS: &sfs,
|
||||
},
|
||||
Concurrency: 1,
|
||||
JobID: &sjid,
|
||||
What: endpoint.AbstractionTypeSet{endpoint.AbstractionReplicationCursorBookmarkV2: true},
|
||||
})
|
||||
require.NoError(ctx, err)
|
||||
require.Empty(ctx, absErrs)
|
||||
require.Len(ctx, abs, 1)
|
||||
require.True(ctx, zfs.FilesystemVersionEqualIdentity(abs[0].GetFilesystemVersion(), snap2sfs))
|
||||
}
|
||||
|
||||
func ReplicationStepCompletedLostBehavior__GuaranteeResumability(ctx *platformtest.Context) {
|
||||
scenario := replicationStepCompletedLostBehavior_impl(ctx, pdu.ReplicationGuaranteeKind_GuaranteeResumability)
|
||||
|
||||
require.Error(ctx, scenario.deleteSfs1Err, "protected by holds")
|
||||
require.Contains(ctx, scenario.deleteSfs1Err.Error(), "dataset is busy")
|
||||
|
||||
require.Error(ctx, scenario.deleteSfs2Err, "protected by holds")
|
||||
require.Contains(ctx, scenario.deleteSfs2Err.Error(), "dataset is busy")
|
||||
|
||||
require.Nil(ctx, scenario.finalReport.Error())
|
||||
_ = fsversion(ctx, scenario.rfs, "@3") // @3 ade it to the other side
|
||||
}
|
||||
|
||||
func ReplicationStepCompletedLostBehavior__GuaranteeIncrementalReplication(ctx *platformtest.Context) {
|
||||
scenario := replicationStepCompletedLostBehavior_impl(ctx, pdu.ReplicationGuaranteeKind_GuaranteeIncrementalReplication)
|
||||
|
||||
require.NoError(ctx, scenario.deleteSfs1Err, "not protected by holds")
|
||||
require.NoError(ctx, scenario.deleteSfs2Err, "not protected by holds")
|
||||
|
||||
// step bookmarks should protect against loss of StepCompleted message
|
||||
require.Nil(ctx, scenario.finalReport.Error())
|
||||
_ = fsversion(ctx, scenario.rfs, "@3") // @3 ade it to the other side
|
||||
}
|
||||
|
||||
type FailSendCompletedSender struct {
|
||||
*endpoint.Sender
|
||||
}
|
||||
|
||||
var _ logic.Sender = (*FailSendCompletedSender)(nil)
|
||||
|
||||
func (p *FailSendCompletedSender) SendCompleted(ctx context.Context, r *pdu.SendCompletedReq) (*pdu.SendCompletedRes, error) {
|
||||
return nil, fmt.Errorf("[mock] SendCompleted not delivered to actual endpoint")
|
||||
}
|
||||
|
||||
type replicationStepCompletedLost_scenario struct {
|
||||
rfs string
|
||||
deleteSfs1Err, deleteSfs2Err error
|
||||
finalReport *report.FilesystemReport
|
||||
}
|
||||
|
||||
func replicationStepCompletedLostBehavior_impl(ctx *platformtest.Context, guaranteeKind pdu.ReplicationGuaranteeKind) *replicationStepCompletedLost_scenario {
|
||||
|
||||
platformtest.Run(ctx, platformtest.PanicErr, ctx.RootDataset, `
|
||||
CREATEROOT
|
||||
+ "sender"
|
||||
+ "receiver"
|
||||
R zfs create -p "${ROOTDS}/receiver/${ROOTDS}"
|
||||
`)
|
||||
|
||||
sjid := endpoint.MustMakeJobID("sender-job")
|
||||
rjid := endpoint.MustMakeJobID("receiver-job")
|
||||
|
||||
sfs := ctx.RootDataset + "/sender"
|
||||
rfsRoot := ctx.RootDataset + "/receiver"
|
||||
|
||||
// fully replicate snapshots @1
|
||||
{
|
||||
mustSnapshot(ctx, sfs+"@1")
|
||||
rep := replicationInvocation{
|
||||
sjid: sjid,
|
||||
rjid: rjid,
|
||||
sfs: sfs,
|
||||
rfsRoot: rfsRoot,
|
||||
guarantee: *pdu.ReplicationConfigProtectionWithKind(guaranteeKind),
|
||||
}
|
||||
rfs := rep.ReceiveSideFilesystem()
|
||||
report := rep.Do(ctx)
|
||||
ctx.Logf("\n%s", pretty.Sprint(report))
|
||||
// assert this worked (not the main subject of the test)
|
||||
_ = fsversion(ctx, rfs, "@1")
|
||||
}
|
||||
|
||||
// create a second snapshot @2
|
||||
mustSnapshot(ctx, sfs+"@2")
|
||||
|
||||
// fake loss of stepcompleted message
|
||||
rep := replicationInvocation{
|
||||
sjid: sjid,
|
||||
rjid: rjid,
|
||||
sfs: sfs,
|
||||
rfsRoot: rfsRoot,
|
||||
guarantee: *pdu.ReplicationConfigProtectionWithKind(guaranteeKind),
|
||||
interceptSender: func(e *endpoint.Sender) logic.Sender {
|
||||
return &FailSendCompletedSender{e}
|
||||
},
|
||||
}
|
||||
rfs := rep.ReceiveSideFilesystem()
|
||||
report := rep.Do(ctx)
|
||||
ctx.Logf("\n%s", pretty.Sprint(report))
|
||||
|
||||
// assert the replication worked
|
||||
// make sure all the filesystem versions we created
|
||||
// were replicated by the replication loop
|
||||
_ = fsversion(ctx, rfs, "@1")
|
||||
_ = fsversion(ctx, rfs, "@2")
|
||||
// and that we hold it using a last-received-hold
|
||||
abs, absErrs, err := endpoint.ListAbstractions(ctx, endpoint.ListZFSHoldsAndBookmarksQuery{
|
||||
FS: endpoint.ListZFSHoldsAndBookmarksQueryFilesystemFilter{
|
||||
FS: &rfs,
|
||||
},
|
||||
Concurrency: 1,
|
||||
JobID: &rjid,
|
||||
What: endpoint.AbstractionTypeSet{endpoint.AbstractionLastReceivedHold: true},
|
||||
})
|
||||
require.NoError(ctx, err)
|
||||
require.Empty(ctx, absErrs)
|
||||
require.Len(ctx, abs, 1)
|
||||
require.True(ctx, zfs.FilesystemVersionEqualIdentity(abs[0].GetFilesystemVersion(), fsversion(ctx, rfs, "@2")))
|
||||
|
||||
// now try to delete @2 on the sender, this should work because don't have step holds on it
|
||||
deleteSfs2Err := zfs.ZFSDestroy(ctx, sfs+"@2")
|
||||
// defer check to caller
|
||||
|
||||
// and create a new snapshot on the sender
|
||||
mustSnapshot(ctx, sfs+"@3")
|
||||
|
||||
// now we have: sender @1, @3
|
||||
// recver @1, @2
|
||||
|
||||
// delete @1 on both sides to demonstrate that, if we didn't have bookmarks, we would be out of sync
|
||||
deleteSfs1Err := zfs.ZFSDestroy(ctx, sfs+"@1")
|
||||
// defer check to caller
|
||||
err = zfs.ZFSDestroy(ctx, rfs+"@1")
|
||||
require.NoError(ctx, err)
|
||||
|
||||
// attempt replication and return the filesystem report report
|
||||
{
|
||||
rep := replicationInvocation{
|
||||
sjid: sjid,
|
||||
rjid: rjid,
|
||||
sfs: sfs,
|
||||
rfsRoot: rfsRoot,
|
||||
guarantee: *pdu.ReplicationConfigProtectionWithKind(guaranteeKind),
|
||||
}
|
||||
report := rep.Do(ctx)
|
||||
ctx.Logf("expecting failure:\n%s", pretty.Sprint(report))
|
||||
require.Len(ctx, report.Attempts, 1)
|
||||
require.Len(ctx, report.Attempts[0].Filesystems, 1)
|
||||
return &replicationStepCompletedLost_scenario{
|
||||
rfs: rfs,
|
||||
deleteSfs1Err: deleteSfs1Err,
|
||||
deleteSfs2Err: deleteSfs2Err,
|
||||
finalReport: report.Attempts[0].Filesystems[0],
|
||||
}
|
||||
}
|
||||
_ = fsversion(ctx, rfs, "@3")
|
||||
|
||||
}
|
||||
|
||||
type ErroringReceiver struct {
|
||||
recvErr error
|
||||
*endpoint.Receiver
|
||||
}
|
||||
|
||||
func (r *ErroringReceiver) Receive(ctx context.Context, req *pdu.ReceiveReq, stream io.ReadCloser) (*pdu.ReceiveRes, error) {
|
||||
return nil, r.recvErr
|
||||
}
|
||||
|
||||
type NeverEndingSender struct {
|
||||
*endpoint.Sender
|
||||
}
|
||||
|
||||
func (s *NeverEndingSender) Send(ctx context.Context, req *pdu.SendReq) (r *pdu.SendRes, stream io.ReadCloser, _ error) {
|
||||
stream = nil
|
||||
r = &pdu.SendRes{
|
||||
UsedResumeToken: false,
|
||||
ExpectedSize: 1 << 30,
|
||||
}
|
||||
if req.DryRun {
|
||||
return r, stream, nil
|
||||
}
|
||||
dz, err := os.Open("/dev/zero")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return r, dz, nil
|
||||
}
|
||||
|
||||
func ReplicationReceiverErrorWhileStillSending(ctx *platformtest.Context) {
|
||||
|
||||
platformtest.Run(ctx, platformtest.PanicErr, ctx.RootDataset, `
|
||||
CREATEROOT
|
||||
+ "sender"
|
||||
+ "sender@1"
|
||||
+ "receiver"
|
||||
R zfs create -p "${ROOTDS}/receiver/${ROOTDS}"
|
||||
`)
|
||||
|
||||
sjid := endpoint.MustMakeJobID("sender-job")
|
||||
rjid := endpoint.MustMakeJobID("receiver-job")
|
||||
|
||||
sfs := ctx.RootDataset + "/sender"
|
||||
rfsRoot := ctx.RootDataset + "/receiver"
|
||||
|
||||
mockRecvErr := fmt.Errorf("YiezahK3thie8ahKiel5sah2uugei2ize1yi8feivuu7musoat")
|
||||
|
||||
rep := replicationInvocation{
|
||||
sjid: sjid,
|
||||
rjid: rjid,
|
||||
sfs: sfs,
|
||||
rfsRoot: rfsRoot,
|
||||
guarantee: *pdu.ReplicationConfigProtectionWithKind(pdu.ReplicationGuaranteeKind_GuaranteeNothing),
|
||||
interceptReceiver: func(r *endpoint.Receiver) logic.Receiver {
|
||||
return &ErroringReceiver{recvErr: mockRecvErr, Receiver: r}
|
||||
},
|
||||
interceptSender: func(s *endpoint.Sender) logic.Sender {
|
||||
return &NeverEndingSender{s}
|
||||
},
|
||||
}
|
||||
|
||||
// first replication
|
||||
report := rep.Do(ctx)
|
||||
ctx.Logf("\n%s", pretty.Sprint(report))
|
||||
|
||||
require.Len(ctx, report.Attempts, 1)
|
||||
attempt := report.Attempts[0]
|
||||
require.Nil(ctx, attempt.PlanError)
|
||||
require.Len(ctx, attempt.Filesystems, 1)
|
||||
afs := attempt.Filesystems[0]
|
||||
require.Nil(ctx, afs.PlanError)
|
||||
require.Len(ctx, afs.Steps, 1)
|
||||
require.Nil(ctx, afs.PlanError)
|
||||
require.NotNil(ctx, afs.StepError)
|
||||
require.Contains(ctx, afs.StepError.Err, mockRecvErr.Error())
|
||||
}
|
||||
|
||||
func ReplicationFailingInitialParentProhibitsChildReplication(ctx *platformtest.Context) {
|
||||
|
||||
platformtest.Run(ctx, platformtest.PanicErr, ctx.RootDataset, `
|
||||
CREATEROOT
|
||||
+ "sender"
|
||||
+ "sender/a"
|
||||
+ "sender/a/child"
|
||||
+ "sender/aa"
|
||||
+ "receiver"
|
||||
R zfs create -p "${ROOTDS}/receiver/${ROOTDS}"
|
||||
R zfs snapshot -r ${ROOTDS}/sender@initial
|
||||
`)
|
||||
|
||||
sjid := endpoint.MustMakeJobID("sender-job")
|
||||
rjid := endpoint.MustMakeJobID("receiver-job")
|
||||
|
||||
fsA := ctx.RootDataset + "/sender/a"
|
||||
fsAChild := ctx.RootDataset + "/sender/a/child"
|
||||
fsAA := ctx.RootDataset + "/sender/aa"
|
||||
|
||||
sfilter := filters.NewDatasetMapFilter(3, true)
|
||||
mustAddToSFilter := func(fs string) {
|
||||
err := sfilter.Add(fs, "ok")
|
||||
require.NoError(ctx, err)
|
||||
}
|
||||
mustAddToSFilter(fsA)
|
||||
mustAddToSFilter(fsAChild)
|
||||
mustAddToSFilter(fsAA)
|
||||
rfsRoot := ctx.RootDataset + "/receiver"
|
||||
|
||||
mockRecvErr := fmt.Errorf("yifae4ohPhaquaes0hohghiep9oufie4roo7quoWooluaj2ee8")
|
||||
|
||||
rep := replicationInvocation{
|
||||
sjid: sjid,
|
||||
rjid: rjid,
|
||||
sfilter: sfilter,
|
||||
rfsRoot: rfsRoot,
|
||||
guarantee: *pdu.ReplicationConfigProtectionWithKind(pdu.ReplicationGuaranteeKind_GuaranteeNothing),
|
||||
interceptReceiver: func(r *endpoint.Receiver) logic.Receiver {
|
||||
return &ErroringReceiver{recvErr: mockRecvErr, Receiver: r}
|
||||
},
|
||||
}
|
||||
|
||||
r := rep.Do(ctx)
|
||||
ctx.Logf("\n%s", pretty.Sprint(r))
|
||||
|
||||
require.Len(ctx, r.Attempts, 1)
|
||||
attempt := r.Attempts[0]
|
||||
require.Nil(ctx, attempt.PlanError)
|
||||
require.Len(ctx, attempt.Filesystems, 3)
|
||||
|
||||
fsByName := make(map[string]*report.FilesystemReport, len(attempt.Filesystems))
|
||||
for _, fs := range attempt.Filesystems {
|
||||
fsByName[fs.Info.Name] = fs
|
||||
}
|
||||
|
||||
require.Contains(ctx, fsByName, fsA)
|
||||
require.Contains(ctx, fsByName, fsAA)
|
||||
require.Contains(ctx, fsByName, fsAA)
|
||||
|
||||
checkFS := func(fs string, expectErrMsg string) {
|
||||
rep := fsByName[fs]
|
||||
require.Len(ctx, rep.Steps, 1)
|
||||
require.Nil(ctx, rep.PlanError)
|
||||
require.NotNil(ctx, rep.StepError)
|
||||
require.Contains(ctx, rep.StepError.Err, expectErrMsg)
|
||||
}
|
||||
|
||||
checkFS(fsA, mockRecvErr.Error())
|
||||
checkFS(fsAChild, "parent(s) failed during initial replication")
|
||||
checkFS(fsAA, mockRecvErr.Error()) // fsAA is not treated as a child of fsA
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@ func CreateReplicationCursor(ctx *platformtest.Context) {
|
||||
R zfs bookmark "${ROOTDS}/foo bar@2 with space" "${ROOTDS}/foo bar#2 with space"
|
||||
+ "foo bar@3 with space"
|
||||
R zfs bookmark "${ROOTDS}/foo bar@3 with space" "${ROOTDS}/foo bar#3 with space"
|
||||
- "foo bar@3 with space"
|
||||
`)
|
||||
|
||||
jobid := endpoint.MustMakeJobID("zreplplatformtest")
|
||||
@@ -43,7 +42,6 @@ func CreateReplicationCursor(ctx *platformtest.Context) {
|
||||
|
||||
snap := fsversion(ctx, fs, "@1 with space")
|
||||
book := fsversion(ctx, fs, "#1 with space")
|
||||
book3 := fsversion(ctx, fs, "#3 with space")
|
||||
|
||||
// create first cursor
|
||||
cursorOfSnap, err := endpoint.CreateReplicationCursor(ctx, fs, snap, jobid)
|
||||
@@ -51,11 +49,8 @@ func CreateReplicationCursor(ctx *platformtest.Context) {
|
||||
// check CreateReplicationCursor is idempotent (for snapshot target)
|
||||
cursorOfSnapIdemp, err := endpoint.CreateReplicationCursor(ctx, fs, snap, jobid)
|
||||
checkCreateCursor(err, cursorOfSnap, snap)
|
||||
// check CreateReplicationCursor is idempotent (for bookmark target of snapshot)
|
||||
cursorOfBook, err := endpoint.CreateReplicationCursor(ctx, fs, book, jobid)
|
||||
checkCreateCursor(err, cursorOfBook, snap)
|
||||
// ... for target = non-cursor bookmark
|
||||
_, err = endpoint.CreateReplicationCursor(ctx, fs, book3, jobid)
|
||||
_, err = endpoint.CreateReplicationCursor(ctx, fs, book, jobid)
|
||||
assert.Equal(ctx, zfs.ErrBookmarkCloningNotSupported, err)
|
||||
// ... for target = replication cursor bookmark to be created
|
||||
cursorOfCursor, err := endpoint.CreateReplicationCursor(ctx, fs, cursorOfSnapIdemp.GetFilesystemVersion(), jobid)
|
||||
|
||||
@@ -17,10 +17,12 @@ func UndestroyableSnapshotParsing(t *platformtest.Context) {
|
||||
+ "foo bar@1 2 3"
|
||||
+ "foo bar@4 5 6"
|
||||
+ "foo bar@7 8 9"
|
||||
+ "foo bar@10 11 12"
|
||||
R zfs hold zrepl_platformtest "${ROOTDS}/foo bar@4 5 6"
|
||||
R zfs hold zrepl_platformtest "${ROOTDS}/foo bar@7 8 9"
|
||||
`)
|
||||
|
||||
err := zfs.ZFSDestroy(t, fmt.Sprintf("%s/foo bar@1 2 3,4 5 6,7 8 9", t.RootDataset))
|
||||
err := zfs.ZFSDestroy(t, fmt.Sprintf("%s/foo bar@1 2 3,4 5 6,7 8 9,10 11 12", t.RootDataset))
|
||||
if err == nil {
|
||||
panic("expecting destroy error due to hold")
|
||||
}
|
||||
@@ -30,8 +32,9 @@ func UndestroyableSnapshotParsing(t *platformtest.Context) {
|
||||
if dse.Filesystem != fmt.Sprintf("%s/foo bar", t.RootDataset) {
|
||||
panic(dse.Filesystem)
|
||||
}
|
||||
require.Equal(t, []string{"4 5 6"}, dse.Undestroyable)
|
||||
require.Equal(t, []string{"dataset is busy"}, dse.Reason)
|
||||
expectUndestroyable := []string{"4 5 6", "7 8 9"}
|
||||
require.Len(t, dse.Undestroyable, len(expectUndestroyable))
|
||||
require.Subset(t, dse.Undestroyable, expectUndestroyable)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+16
-9
@@ -66,24 +66,26 @@ type retentionGridAdaptor struct {
|
||||
Snapshot
|
||||
}
|
||||
|
||||
func (a retentionGridAdaptor) Date() time.Time { return a.Snapshot.GetCreation() }
|
||||
|
||||
func (a retentionGridAdaptor) LessThan(b retentiongrid.Entry) bool {
|
||||
return a.Date().Before(b.Date())
|
||||
}
|
||||
|
||||
// Prune filters snapshots with the retention grid.
|
||||
func (p *KeepGrid) KeepRule(snaps []Snapshot) (destroyList []Snapshot) {
|
||||
func (p *KeepGrid) KeepRule(snaps []Snapshot) PruneSnapshotsResult {
|
||||
|
||||
snaps = filterSnapList(snaps, func(snapshot Snapshot) bool {
|
||||
return p.re.MatchString(snapshot.Name())
|
||||
reCandidates := partitionSnapList(snaps, func(snapshot Snapshot) bool {
|
||||
return p.re.MatchString(snapshot.GetName())
|
||||
})
|
||||
if len(snaps) == 0 {
|
||||
return nil
|
||||
if len(reCandidates.Remove) == 0 {
|
||||
return reCandidates
|
||||
}
|
||||
|
||||
// Build adaptors for retention grid
|
||||
adaptors := make([]retentiongrid.Entry, 0)
|
||||
for i := range snaps {
|
||||
adaptors = append(adaptors, retentionGridAdaptor{snaps[i]})
|
||||
adaptors = append(adaptors, retentionGridAdaptor{reCandidates.Remove[i]})
|
||||
}
|
||||
|
||||
// determine 'now' edge
|
||||
@@ -93,12 +95,17 @@ func (p *KeepGrid) KeepRule(snaps []Snapshot) (destroyList []Snapshot) {
|
||||
now := adaptors[len(adaptors)-1].Date()
|
||||
|
||||
// Evaluate retention grid
|
||||
_, removea := p.retentionGrid.FitEntries(now, adaptors)
|
||||
keepa, removea := p.retentionGrid.FitEntries(now, adaptors)
|
||||
|
||||
// Revert adaptors
|
||||
destroyList = make([]Snapshot, len(removea))
|
||||
destroyList := make([]Snapshot, len(removea))
|
||||
for i := range removea {
|
||||
destroyList[i] = removea[i].(retentionGridAdaptor).Snapshot
|
||||
}
|
||||
return destroyList
|
||||
for _, a := range keepa {
|
||||
reCandidates.Keep = append(reCandidates.Keep, a.(retentionGridAdaptor))
|
||||
}
|
||||
reCandidates.Remove = destroyList
|
||||
|
||||
return reCandidates
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
package pruning
|
||||
|
||||
func filterSnapList(snaps []Snapshot, predicate func(Snapshot) bool) []Snapshot {
|
||||
r := make([]Snapshot, 0, len(snaps))
|
||||
func partitionSnapList(snaps []Snapshot, remove func(Snapshot) bool) (r PruneSnapshotsResult) {
|
||||
r.Keep = make([]Snapshot, 0, len(snaps))
|
||||
r.Remove = make([]Snapshot, 0, len(snaps))
|
||||
for i := range snaps {
|
||||
if predicate(snaps[i]) {
|
||||
r = append(r, snaps[i])
|
||||
if remove(snaps[i]) {
|
||||
r.Remove = append(r.Remove, snaps[i])
|
||||
} else {
|
||||
r.Keep = append(r.Keep, snaps[i])
|
||||
}
|
||||
}
|
||||
return r
|
||||
|
||||
@@ -17,17 +17,17 @@ func NewKeepLastN(n int) (*KeepLastN, error) {
|
||||
return &KeepLastN{n}, nil
|
||||
}
|
||||
|
||||
func (k KeepLastN) KeepRule(snaps []Snapshot) (destroyList []Snapshot) {
|
||||
func (k KeepLastN) KeepRule(snaps []Snapshot) PruneSnapshotsResult {
|
||||
|
||||
if k.n > len(snaps) {
|
||||
return []Snapshot{}
|
||||
return PruneSnapshotsResult{Keep: snaps}
|
||||
}
|
||||
|
||||
res := shallowCopySnapList(snaps)
|
||||
|
||||
sort.Slice(res, func(i, j int) bool {
|
||||
return res[i].Date().After(res[j].Date())
|
||||
return res[i].GetCreateTXG() > res[j].GetCreateTXG()
|
||||
})
|
||||
|
||||
return res[k.n:]
|
||||
return PruneSnapshotsResult{Remove: res[k.n:], Keep: res[:k.n]}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@ package pruning
|
||||
|
||||
type KeepNotReplicated struct{}
|
||||
|
||||
func (*KeepNotReplicated) KeepRule(snaps []Snapshot) (destroyList []Snapshot) {
|
||||
return filterSnapList(snaps, func(snapshot Snapshot) bool {
|
||||
func (*KeepNotReplicated) KeepRule(snaps []Snapshot) PruneSnapshotsResult {
|
||||
return partitionSnapList(snaps, func(snapshot Snapshot) bool {
|
||||
return snapshot.Replicated()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -27,12 +27,12 @@ func MustKeepRegex(expr string, negate bool) *KeepRegex {
|
||||
return k
|
||||
}
|
||||
|
||||
func (k *KeepRegex) KeepRule(snaps []Snapshot) []Snapshot {
|
||||
return filterSnapList(snaps, func(s Snapshot) bool {
|
||||
func (k *KeepRegex) KeepRule(snaps []Snapshot) PruneSnapshotsResult {
|
||||
return partitionSnapList(snaps, func(s Snapshot) bool {
|
||||
if k.negate {
|
||||
return k.expr.FindStringIndex(s.Name()) != nil
|
||||
return k.expr.FindStringIndex(s.GetName()) != nil
|
||||
} else {
|
||||
return k.expr.FindStringIndex(s.Name()) == nil
|
||||
return k.expr.FindStringIndex(s.GetName()) == nil
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package pruning
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
"github.com/zrepl/zrepl/endpoint"
|
||||
)
|
||||
|
||||
type KeepStepHolds struct {
|
||||
keepJobIDs map[endpoint.JobID]bool
|
||||
}
|
||||
|
||||
var _ KeepRule = (*KeepStepHolds)(nil)
|
||||
|
||||
func NewKeepStepHolds(mainJobId endpoint.JobID, additionalJobIdsStrings []string) (_ *KeepStepHolds, err error) {
|
||||
additionalJobIds := make(map[endpoint.JobID]bool, len(additionalJobIdsStrings))
|
||||
|
||||
mainJobId.MustValidate()
|
||||
additionalJobIds[mainJobId] = true
|
||||
|
||||
for i := range additionalJobIdsStrings {
|
||||
ajid, err := endpoint.MakeJobID(additionalJobIdsStrings[i])
|
||||
if err != nil {
|
||||
return nil, errors.WithMessagef(err, "cannot parse job id %q: %s", additionalJobIdsStrings[i])
|
||||
}
|
||||
if additionalJobIds[ajid] == true {
|
||||
return nil, errors.Errorf("duplicate job id %q", ajid)
|
||||
}
|
||||
}
|
||||
return &KeepStepHolds{additionalJobIds}, nil
|
||||
}
|
||||
|
||||
func (h *KeepStepHolds) KeepRule(snaps []Snapshot) PruneSnapshotsResult {
|
||||
return partitionSnapList(snaps, func(s Snapshot) bool {
|
||||
holdingJobIDs := make(map[endpoint.JobID]bool)
|
||||
for _, h := range s.StepHolds() {
|
||||
holdingJobIDs[h.GetJobID()] = true
|
||||
}
|
||||
oneOrMoreOfOurJobIDsHoldsSnap := false
|
||||
for kjid := range h.keepJobIDs {
|
||||
oneOrMoreOfOurJobIDsHoldsSnap = oneOrMoreOfOurJobIDsHoldsSnap || holdingJobIDs[kjid]
|
||||
}
|
||||
return !oneOrMoreOfOurJobIDsHoldsSnap
|
||||
})
|
||||
}
|
||||
+65
-17
@@ -7,47 +7,93 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/zrepl/zrepl/config"
|
||||
"github.com/zrepl/zrepl/endpoint"
|
||||
)
|
||||
|
||||
type KeepRule interface {
|
||||
KeepRule(snaps []Snapshot) (destroyList []Snapshot)
|
||||
KeepRule(snaps []Snapshot) PruneSnapshotsResult
|
||||
}
|
||||
|
||||
type Snapshot interface {
|
||||
Name() string
|
||||
GetName() string
|
||||
Replicated() bool
|
||||
Date() time.Time
|
||||
GetCreation() time.Time
|
||||
GetCreateTXG() uint64
|
||||
StepHolds() []StepHold
|
||||
}
|
||||
|
||||
// The returned snapshot list is guaranteed to only contains elements of input parameter snaps
|
||||
func PruneSnapshots(snaps []Snapshot, keepRules []KeepRule) []Snapshot {
|
||||
type StepHold interface {
|
||||
GetJobID() endpoint.JobID
|
||||
}
|
||||
|
||||
type PruneSnapshotsResult struct {
|
||||
Remove, Keep []Snapshot
|
||||
}
|
||||
|
||||
// The returned snapshot results are a partition of the snaps argument.
|
||||
// That means than len(Remove) + len(Keep) == len(snaps)
|
||||
func PruneSnapshots(snapsI []Snapshot, keepRules []KeepRule) PruneSnapshotsResult {
|
||||
|
||||
if len(keepRules) == 0 {
|
||||
return []Snapshot{}
|
||||
return PruneSnapshotsResult{Remove: nil, Keep: snapsI}
|
||||
}
|
||||
|
||||
type snapshot struct {
|
||||
Snapshot
|
||||
keepCount, removeCount int
|
||||
}
|
||||
|
||||
// project down to snapshot
|
||||
snaps := make([]Snapshot, len(snapsI))
|
||||
for i := range snaps {
|
||||
snaps[i] = &snapshot{snapsI[i], 0, 0}
|
||||
}
|
||||
|
||||
remCount := make(map[Snapshot]int, len(snaps))
|
||||
for _, r := range keepRules {
|
||||
ruleRems := r.KeepRule(snaps)
|
||||
for _, ruleRem := range ruleRems {
|
||||
remCount[ruleRem]++
|
||||
|
||||
ruleImplCheckSet := make(map[Snapshot]int, len(snaps))
|
||||
for _, s := range snaps {
|
||||
ruleImplCheckSet[s] = ruleImplCheckSet[s] + 1
|
||||
}
|
||||
|
||||
ruleResults := r.KeepRule(snaps)
|
||||
|
||||
for _, s := range snaps {
|
||||
ruleImplCheckSet[s] = ruleImplCheckSet[s] - 1
|
||||
}
|
||||
for _, n := range ruleImplCheckSet {
|
||||
if n != 0 {
|
||||
panic(fmt.Sprintf("incorrect rule implementation: %T", r))
|
||||
}
|
||||
}
|
||||
|
||||
for _, s := range ruleResults.Remove {
|
||||
s.(*snapshot).removeCount++
|
||||
}
|
||||
for _, s := range ruleResults.Keep {
|
||||
s.(*snapshot).keepCount++
|
||||
}
|
||||
}
|
||||
|
||||
remove := make([]Snapshot, 0, len(snaps))
|
||||
for snap, rc := range remCount {
|
||||
if rc == len(keepRules) {
|
||||
remove = append(remove, snap)
|
||||
keep := make([]Snapshot, 0, len(snaps))
|
||||
for _, sI := range snaps {
|
||||
s := sI.(*snapshot)
|
||||
if s.removeCount == len(keepRules) {
|
||||
// all keep rules agree to remove the snap
|
||||
remove = append(remove, s.Snapshot)
|
||||
} else {
|
||||
keep = append(keep, s.Snapshot)
|
||||
}
|
||||
}
|
||||
|
||||
return remove
|
||||
return PruneSnapshotsResult{Remove: remove, Keep: keep}
|
||||
}
|
||||
|
||||
func RulesFromConfig(in []config.PruningEnum) (rules []KeepRule, err error) {
|
||||
func RulesFromConfig(mainJobId endpoint.JobID, in []config.PruningEnum) (rules []KeepRule, err error) {
|
||||
rules = make([]KeepRule, len(in))
|
||||
for i := range in {
|
||||
rules[i], err = RuleFromConfig(in[i])
|
||||
rules[i], err = RuleFromConfig(mainJobId, in[i])
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "cannot build rule #%d", i)
|
||||
}
|
||||
@@ -55,7 +101,7 @@ func RulesFromConfig(in []config.PruningEnum) (rules []KeepRule, err error) {
|
||||
return rules, nil
|
||||
}
|
||||
|
||||
func RuleFromConfig(in config.PruningEnum) (KeepRule, error) {
|
||||
func RuleFromConfig(mainJobId endpoint.JobID, in config.PruningEnum) (KeepRule, error) {
|
||||
switch v := in.Ret.(type) {
|
||||
case *config.PruneKeepNotReplicated:
|
||||
return NewKeepNotReplicated(), nil
|
||||
@@ -65,6 +111,8 @@ func RuleFromConfig(in config.PruningEnum) (KeepRule, error) {
|
||||
return NewKeepRegex(v.Regex, v.Negate)
|
||||
case *config.PruneGrid:
|
||||
return NewKeepGrid(v)
|
||||
case *config.PruneKeepStepHolds:
|
||||
return NewKeepStepHolds(mainJobId, v.AdditionalJobIds)
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown keep rule type %T", v)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package driver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"sort"
|
||||
@@ -9,9 +10,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/zrepl/zrepl/daemon/logging/trace"
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
|
||||
@@ -375,28 +374,11 @@ func (a *attempt) doGlobalPlanning(ctx context.Context, prev *attempt) map[*fs]*
|
||||
// invariant: prevs contains an entry for each unambiguous correspondence
|
||||
|
||||
// build up parent-child relationship (FIXME (O(n^2), but who's going to have that many filesystems...))
|
||||
mustDatasetPathOrPlanFail := func(fs string) *zfs.DatasetPath {
|
||||
dp, err := zfs.NewDatasetPath(fs)
|
||||
if err != nil {
|
||||
now := time.Now()
|
||||
a.planErr = newTimedError(errors.Wrapf(err, "%q", fs), now)
|
||||
a.fss = nil
|
||||
a.finishedAt = now
|
||||
return nil
|
||||
}
|
||||
return dp
|
||||
}
|
||||
for _, f1 := range a.fss {
|
||||
fs1 := mustDatasetPathOrPlanFail(f1.fs.ReportInfo().Name)
|
||||
if fs1 == nil {
|
||||
return nil
|
||||
}
|
||||
fs1 := f1.fs.ReportInfo().Name
|
||||
for _, f2 := range a.fss {
|
||||
fs2 := mustDatasetPathOrPlanFail(f2.fs.ReportInfo().Name)
|
||||
if fs2 == nil {
|
||||
return nil
|
||||
}
|
||||
if fs1.HasPrefix(fs2) && !fs1.Equal(fs2) {
|
||||
fs2 := f2.fs.ReportInfo().Name
|
||||
if strings.HasPrefix(fs1, fs2) && fs1 != fs2 {
|
||||
f1.initialRepOrd.parents = append(f1.initialRepOrd.parents, f2)
|
||||
f2.initialRepOrd.children = append(f2.initialRepOrd.children, f1)
|
||||
}
|
||||
|
||||
+177
-220
@@ -46,36 +46,36 @@ func (x Tri) String() string {
|
||||
return proto.EnumName(Tri_name, int32(x))
|
||||
}
|
||||
func (Tri) EnumDescriptor() ([]byte, []int) {
|
||||
return fileDescriptor_pdu_616c27178643eca4, []int{0}
|
||||
return fileDescriptor_pdu_2d84e8d7d278a80d, []int{0}
|
||||
}
|
||||
|
||||
type ReplicationGuaranteeKind int32
|
||||
type SendAbstraction_SendAbstractionType int32
|
||||
|
||||
const (
|
||||
ReplicationGuaranteeKind_GuaranteeInvalid ReplicationGuaranteeKind = 0
|
||||
ReplicationGuaranteeKind_GuaranteeResumability ReplicationGuaranteeKind = 1
|
||||
ReplicationGuaranteeKind_GuaranteeIncrementalReplication ReplicationGuaranteeKind = 2
|
||||
ReplicationGuaranteeKind_GuaranteeNothing ReplicationGuaranteeKind = 3
|
||||
SendAbstraction_Undefined SendAbstraction_SendAbstractionType = 0
|
||||
SendAbstraction_ReplicationCursorV2 SendAbstraction_SendAbstractionType = 1
|
||||
SendAbstraction_StepHold SendAbstraction_SendAbstractionType = 2
|
||||
SendAbstraction_StepBookmark SendAbstraction_SendAbstractionType = 3
|
||||
)
|
||||
|
||||
var ReplicationGuaranteeKind_name = map[int32]string{
|
||||
0: "GuaranteeInvalid",
|
||||
1: "GuaranteeResumability",
|
||||
2: "GuaranteeIncrementalReplication",
|
||||
3: "GuaranteeNothing",
|
||||
var SendAbstraction_SendAbstractionType_name = map[int32]string{
|
||||
0: "Undefined",
|
||||
1: "ReplicationCursorV2",
|
||||
2: "StepHold",
|
||||
3: "StepBookmark",
|
||||
}
|
||||
var ReplicationGuaranteeKind_value = map[string]int32{
|
||||
"GuaranteeInvalid": 0,
|
||||
"GuaranteeResumability": 1,
|
||||
"GuaranteeIncrementalReplication": 2,
|
||||
"GuaranteeNothing": 3,
|
||||
var SendAbstraction_SendAbstractionType_value = map[string]int32{
|
||||
"Undefined": 0,
|
||||
"ReplicationCursorV2": 1,
|
||||
"StepHold": 2,
|
||||
"StepBookmark": 3,
|
||||
}
|
||||
|
||||
func (x ReplicationGuaranteeKind) String() string {
|
||||
return proto.EnumName(ReplicationGuaranteeKind_name, int32(x))
|
||||
func (x SendAbstraction_SendAbstractionType) String() string {
|
||||
return proto.EnumName(SendAbstraction_SendAbstractionType_name, int32(x))
|
||||
}
|
||||
func (ReplicationGuaranteeKind) EnumDescriptor() ([]byte, []int) {
|
||||
return fileDescriptor_pdu_616c27178643eca4, []int{1}
|
||||
func (SendAbstraction_SendAbstractionType) EnumDescriptor() ([]byte, []int) {
|
||||
return fileDescriptor_pdu_2d84e8d7d278a80d, []int{5, 0}
|
||||
}
|
||||
|
||||
type FilesystemVersion_VersionType int32
|
||||
@@ -98,7 +98,7 @@ func (x FilesystemVersion_VersionType) String() string {
|
||||
return proto.EnumName(FilesystemVersion_VersionType_name, int32(x))
|
||||
}
|
||||
func (FilesystemVersion_VersionType) EnumDescriptor() ([]byte, []int) {
|
||||
return fileDescriptor_pdu_616c27178643eca4, []int{5, 0}
|
||||
return fileDescriptor_pdu_2d84e8d7d278a80d, []int{6, 0}
|
||||
}
|
||||
|
||||
type ListFilesystemReq struct {
|
||||
@@ -111,7 +111,7 @@ func (m *ListFilesystemReq) Reset() { *m = ListFilesystemReq{} }
|
||||
func (m *ListFilesystemReq) String() string { return proto.CompactTextString(m) }
|
||||
func (*ListFilesystemReq) ProtoMessage() {}
|
||||
func (*ListFilesystemReq) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_pdu_616c27178643eca4, []int{0}
|
||||
return fileDescriptor_pdu_2d84e8d7d278a80d, []int{0}
|
||||
}
|
||||
func (m *ListFilesystemReq) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_ListFilesystemReq.Unmarshal(m, b)
|
||||
@@ -142,7 +142,7 @@ func (m *ListFilesystemRes) Reset() { *m = ListFilesystemRes{} }
|
||||
func (m *ListFilesystemRes) String() string { return proto.CompactTextString(m) }
|
||||
func (*ListFilesystemRes) ProtoMessage() {}
|
||||
func (*ListFilesystemRes) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_pdu_616c27178643eca4, []int{1}
|
||||
return fileDescriptor_pdu_2d84e8d7d278a80d, []int{1}
|
||||
}
|
||||
func (m *ListFilesystemRes) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_ListFilesystemRes.Unmarshal(m, b)
|
||||
@@ -183,7 +183,7 @@ func (m *Filesystem) Reset() { *m = Filesystem{} }
|
||||
func (m *Filesystem) String() string { return proto.CompactTextString(m) }
|
||||
func (*Filesystem) ProtoMessage() {}
|
||||
func (*Filesystem) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_pdu_616c27178643eca4, []int{2}
|
||||
return fileDescriptor_pdu_2d84e8d7d278a80d, []int{2}
|
||||
}
|
||||
func (m *Filesystem) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_Filesystem.Unmarshal(m, b)
|
||||
@@ -242,7 +242,7 @@ func (m *ListFilesystemVersionsReq) Reset() { *m = ListFilesystemVersion
|
||||
func (m *ListFilesystemVersionsReq) String() string { return proto.CompactTextString(m) }
|
||||
func (*ListFilesystemVersionsReq) ProtoMessage() {}
|
||||
func (*ListFilesystemVersionsReq) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_pdu_616c27178643eca4, []int{3}
|
||||
return fileDescriptor_pdu_2d84e8d7d278a80d, []int{3}
|
||||
}
|
||||
func (m *ListFilesystemVersionsReq) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_ListFilesystemVersionsReq.Unmarshal(m, b)
|
||||
@@ -271,6 +271,7 @@ func (m *ListFilesystemVersionsReq) GetFilesystem() string {
|
||||
|
||||
type ListFilesystemVersionsRes struct {
|
||||
Versions []*FilesystemVersion `protobuf:"bytes,1,rep,name=Versions,proto3" json:"Versions,omitempty"`
|
||||
SendAbstractions []*SendAbstraction `protobuf:"bytes,2,rep,name=SendAbstractions,proto3" json:"SendAbstractions,omitempty"`
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
XXX_sizecache int32 `json:"-"`
|
||||
@@ -280,7 +281,7 @@ func (m *ListFilesystemVersionsRes) Reset() { *m = ListFilesystemVersion
|
||||
func (m *ListFilesystemVersionsRes) String() string { return proto.CompactTextString(m) }
|
||||
func (*ListFilesystemVersionsRes) ProtoMessage() {}
|
||||
func (*ListFilesystemVersionsRes) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_pdu_616c27178643eca4, []int{4}
|
||||
return fileDescriptor_pdu_2d84e8d7d278a80d, []int{4}
|
||||
}
|
||||
func (m *ListFilesystemVersionsRes) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_ListFilesystemVersionsRes.Unmarshal(m, b)
|
||||
@@ -307,6 +308,67 @@ func (m *ListFilesystemVersionsRes) GetVersions() []*FilesystemVersion {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *ListFilesystemVersionsRes) GetSendAbstractions() []*SendAbstraction {
|
||||
if m != nil {
|
||||
return m.SendAbstractions
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type SendAbstraction struct {
|
||||
Type SendAbstraction_SendAbstractionType `protobuf:"varint,1,opt,name=Type,proto3,enum=SendAbstraction_SendAbstractionType" json:"Type,omitempty"`
|
||||
JobID string `protobuf:"bytes,2,opt,name=JobID,proto3" json:"JobID,omitempty"`
|
||||
Version *FilesystemVersion `protobuf:"bytes,3,opt,name=Version,proto3" json:"Version,omitempty"`
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
XXX_sizecache int32 `json:"-"`
|
||||
}
|
||||
|
||||
func (m *SendAbstraction) Reset() { *m = SendAbstraction{} }
|
||||
func (m *SendAbstraction) String() string { return proto.CompactTextString(m) }
|
||||
func (*SendAbstraction) ProtoMessage() {}
|
||||
func (*SendAbstraction) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_pdu_2d84e8d7d278a80d, []int{5}
|
||||
}
|
||||
func (m *SendAbstraction) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_SendAbstraction.Unmarshal(m, b)
|
||||
}
|
||||
func (m *SendAbstraction) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
|
||||
return xxx_messageInfo_SendAbstraction.Marshal(b, m, deterministic)
|
||||
}
|
||||
func (dst *SendAbstraction) XXX_Merge(src proto.Message) {
|
||||
xxx_messageInfo_SendAbstraction.Merge(dst, src)
|
||||
}
|
||||
func (m *SendAbstraction) XXX_Size() int {
|
||||
return xxx_messageInfo_SendAbstraction.Size(m)
|
||||
}
|
||||
func (m *SendAbstraction) XXX_DiscardUnknown() {
|
||||
xxx_messageInfo_SendAbstraction.DiscardUnknown(m)
|
||||
}
|
||||
|
||||
var xxx_messageInfo_SendAbstraction proto.InternalMessageInfo
|
||||
|
||||
func (m *SendAbstraction) GetType() SendAbstraction_SendAbstractionType {
|
||||
if m != nil {
|
||||
return m.Type
|
||||
}
|
||||
return SendAbstraction_Undefined
|
||||
}
|
||||
|
||||
func (m *SendAbstraction) GetJobID() string {
|
||||
if m != nil {
|
||||
return m.JobID
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *SendAbstraction) GetVersion() *FilesystemVersion {
|
||||
if m != nil {
|
||||
return m.Version
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type FilesystemVersion struct {
|
||||
Type FilesystemVersion_VersionType `protobuf:"varint,1,opt,name=Type,proto3,enum=FilesystemVersion_VersionType" json:"Type,omitempty"`
|
||||
Name string `protobuf:"bytes,2,opt,name=Name,proto3" json:"Name,omitempty"`
|
||||
@@ -322,7 +384,7 @@ func (m *FilesystemVersion) Reset() { *m = FilesystemVersion{} }
|
||||
func (m *FilesystemVersion) String() string { return proto.CompactTextString(m) }
|
||||
func (*FilesystemVersion) ProtoMessage() {}
|
||||
func (*FilesystemVersion) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_pdu_616c27178643eca4, []int{5}
|
||||
return fileDescriptor_pdu_2d84e8d7d278a80d, []int{6}
|
||||
}
|
||||
func (m *FilesystemVersion) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_FilesystemVersion.Unmarshal(m, b)
|
||||
@@ -388,20 +450,19 @@ type SendReq struct {
|
||||
// SHOULD clear the resume token on their side and use From and To instead If
|
||||
// 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.
|
||||
ResumeToken string `protobuf:"bytes,4,opt,name=ResumeToken,proto3" json:"ResumeToken,omitempty"`
|
||||
Encrypted Tri `protobuf:"varint,5,opt,name=Encrypted,proto3,enum=Tri" json:"Encrypted,omitempty"`
|
||||
DryRun bool `protobuf:"varint,6,opt,name=DryRun,proto3" json:"DryRun,omitempty"`
|
||||
ReplicationConfig *ReplicationConfig `protobuf:"bytes,7,opt,name=ReplicationConfig,proto3" json:"ReplicationConfig,omitempty"`
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
XXX_sizecache int32 `json:"-"`
|
||||
ResumeToken string `protobuf:"bytes,4,opt,name=ResumeToken,proto3" json:"ResumeToken,omitempty"`
|
||||
Encrypted Tri `protobuf:"varint,5,opt,name=Encrypted,proto3,enum=Tri" json:"Encrypted,omitempty"`
|
||||
DryRun bool `protobuf:"varint,6,opt,name=DryRun,proto3" json:"DryRun,omitempty"`
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
XXX_sizecache int32 `json:"-"`
|
||||
}
|
||||
|
||||
func (m *SendReq) Reset() { *m = SendReq{} }
|
||||
func (m *SendReq) String() string { return proto.CompactTextString(m) }
|
||||
func (*SendReq) ProtoMessage() {}
|
||||
func (*SendReq) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_pdu_616c27178643eca4, []int{6}
|
||||
return fileDescriptor_pdu_2d84e8d7d278a80d, []int{7}
|
||||
}
|
||||
func (m *SendReq) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_SendReq.Unmarshal(m, b)
|
||||
@@ -463,97 +524,6 @@ func (m *SendReq) GetDryRun() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *SendReq) GetReplicationConfig() *ReplicationConfig {
|
||||
if m != nil {
|
||||
return m.ReplicationConfig
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type ReplicationConfig struct {
|
||||
Protection *ReplicationConfigProtection `protobuf:"bytes,1,opt,name=protection,proto3" json:"protection,omitempty"`
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
XXX_sizecache int32 `json:"-"`
|
||||
}
|
||||
|
||||
func (m *ReplicationConfig) Reset() { *m = ReplicationConfig{} }
|
||||
func (m *ReplicationConfig) String() string { return proto.CompactTextString(m) }
|
||||
func (*ReplicationConfig) ProtoMessage() {}
|
||||
func (*ReplicationConfig) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_pdu_616c27178643eca4, []int{7}
|
||||
}
|
||||
func (m *ReplicationConfig) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_ReplicationConfig.Unmarshal(m, b)
|
||||
}
|
||||
func (m *ReplicationConfig) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
|
||||
return xxx_messageInfo_ReplicationConfig.Marshal(b, m, deterministic)
|
||||
}
|
||||
func (dst *ReplicationConfig) XXX_Merge(src proto.Message) {
|
||||
xxx_messageInfo_ReplicationConfig.Merge(dst, src)
|
||||
}
|
||||
func (m *ReplicationConfig) XXX_Size() int {
|
||||
return xxx_messageInfo_ReplicationConfig.Size(m)
|
||||
}
|
||||
func (m *ReplicationConfig) XXX_DiscardUnknown() {
|
||||
xxx_messageInfo_ReplicationConfig.DiscardUnknown(m)
|
||||
}
|
||||
|
||||
var xxx_messageInfo_ReplicationConfig proto.InternalMessageInfo
|
||||
|
||||
func (m *ReplicationConfig) GetProtection() *ReplicationConfigProtection {
|
||||
if m != nil {
|
||||
return m.Protection
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type ReplicationConfigProtection struct {
|
||||
Initial ReplicationGuaranteeKind `protobuf:"varint,1,opt,name=Initial,proto3,enum=ReplicationGuaranteeKind" json:"Initial,omitempty"`
|
||||
Incremental ReplicationGuaranteeKind `protobuf:"varint,2,opt,name=Incremental,proto3,enum=ReplicationGuaranteeKind" json:"Incremental,omitempty"`
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
XXX_sizecache int32 `json:"-"`
|
||||
}
|
||||
|
||||
func (m *ReplicationConfigProtection) Reset() { *m = ReplicationConfigProtection{} }
|
||||
func (m *ReplicationConfigProtection) String() string { return proto.CompactTextString(m) }
|
||||
func (*ReplicationConfigProtection) ProtoMessage() {}
|
||||
func (*ReplicationConfigProtection) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_pdu_616c27178643eca4, []int{8}
|
||||
}
|
||||
func (m *ReplicationConfigProtection) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_ReplicationConfigProtection.Unmarshal(m, b)
|
||||
}
|
||||
func (m *ReplicationConfigProtection) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
|
||||
return xxx_messageInfo_ReplicationConfigProtection.Marshal(b, m, deterministic)
|
||||
}
|
||||
func (dst *ReplicationConfigProtection) XXX_Merge(src proto.Message) {
|
||||
xxx_messageInfo_ReplicationConfigProtection.Merge(dst, src)
|
||||
}
|
||||
func (m *ReplicationConfigProtection) XXX_Size() int {
|
||||
return xxx_messageInfo_ReplicationConfigProtection.Size(m)
|
||||
}
|
||||
func (m *ReplicationConfigProtection) XXX_DiscardUnknown() {
|
||||
xxx_messageInfo_ReplicationConfigProtection.DiscardUnknown(m)
|
||||
}
|
||||
|
||||
var xxx_messageInfo_ReplicationConfigProtection proto.InternalMessageInfo
|
||||
|
||||
func (m *ReplicationConfigProtection) GetInitial() ReplicationGuaranteeKind {
|
||||
if m != nil {
|
||||
return m.Initial
|
||||
}
|
||||
return ReplicationGuaranteeKind_GuaranteeInvalid
|
||||
}
|
||||
|
||||
func (m *ReplicationConfigProtection) GetIncremental() ReplicationGuaranteeKind {
|
||||
if m != nil {
|
||||
return m.Incremental
|
||||
}
|
||||
return ReplicationGuaranteeKind_GuaranteeInvalid
|
||||
}
|
||||
|
||||
type Property struct {
|
||||
Name string `protobuf:"bytes,1,opt,name=Name,proto3" json:"Name,omitempty"`
|
||||
Value string `protobuf:"bytes,2,opt,name=Value,proto3" json:"Value,omitempty"`
|
||||
@@ -566,7 +536,7 @@ func (m *Property) Reset() { *m = Property{} }
|
||||
func (m *Property) String() string { return proto.CompactTextString(m) }
|
||||
func (*Property) ProtoMessage() {}
|
||||
func (*Property) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_pdu_616c27178643eca4, []int{9}
|
||||
return fileDescriptor_pdu_2d84e8d7d278a80d, []int{8}
|
||||
}
|
||||
func (m *Property) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_Property.Unmarshal(m, b)
|
||||
@@ -617,7 +587,7 @@ func (m *SendRes) Reset() { *m = SendRes{} }
|
||||
func (m *SendRes) String() string { return proto.CompactTextString(m) }
|
||||
func (*SendRes) ProtoMessage() {}
|
||||
func (*SendRes) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_pdu_616c27178643eca4, []int{10}
|
||||
return fileDescriptor_pdu_2d84e8d7d278a80d, []int{9}
|
||||
}
|
||||
func (m *SendRes) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_SendRes.Unmarshal(m, b)
|
||||
@@ -669,7 +639,7 @@ func (m *SendCompletedReq) Reset() { *m = SendCompletedReq{} }
|
||||
func (m *SendCompletedReq) String() string { return proto.CompactTextString(m) }
|
||||
func (*SendCompletedReq) ProtoMessage() {}
|
||||
func (*SendCompletedReq) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_pdu_616c27178643eca4, []int{11}
|
||||
return fileDescriptor_pdu_2d84e8d7d278a80d, []int{10}
|
||||
}
|
||||
func (m *SendCompletedReq) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_SendCompletedReq.Unmarshal(m, b)
|
||||
@@ -706,7 +676,7 @@ func (m *SendCompletedRes) Reset() { *m = SendCompletedRes{} }
|
||||
func (m *SendCompletedRes) String() string { return proto.CompactTextString(m) }
|
||||
func (*SendCompletedRes) ProtoMessage() {}
|
||||
func (*SendCompletedRes) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_pdu_616c27178643eca4, []int{12}
|
||||
return fileDescriptor_pdu_2d84e8d7d278a80d, []int{11}
|
||||
}
|
||||
func (m *SendCompletedRes) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_SendCompletedRes.Unmarshal(m, b)
|
||||
@@ -731,18 +701,17 @@ type ReceiveReq struct {
|
||||
To *FilesystemVersion `protobuf:"bytes,2,opt,name=To,proto3" json:"To,omitempty"`
|
||||
// If true, the receiver should clear the resume token before performing the
|
||||
// zfs recv of the stream in the request
|
||||
ClearResumeToken bool `protobuf:"varint,3,opt,name=ClearResumeToken,proto3" json:"ClearResumeToken,omitempty"`
|
||||
ReplicationConfig *ReplicationConfig `protobuf:"bytes,4,opt,name=ReplicationConfig,proto3" json:"ReplicationConfig,omitempty"`
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
XXX_sizecache int32 `json:"-"`
|
||||
ClearResumeToken bool `protobuf:"varint,3,opt,name=ClearResumeToken,proto3" json:"ClearResumeToken,omitempty"`
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
XXX_sizecache int32 `json:"-"`
|
||||
}
|
||||
|
||||
func (m *ReceiveReq) Reset() { *m = ReceiveReq{} }
|
||||
func (m *ReceiveReq) String() string { return proto.CompactTextString(m) }
|
||||
func (*ReceiveReq) ProtoMessage() {}
|
||||
func (*ReceiveReq) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_pdu_616c27178643eca4, []int{13}
|
||||
return fileDescriptor_pdu_2d84e8d7d278a80d, []int{12}
|
||||
}
|
||||
func (m *ReceiveReq) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_ReceiveReq.Unmarshal(m, b)
|
||||
@@ -783,13 +752,6 @@ func (m *ReceiveReq) GetClearResumeToken() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *ReceiveReq) GetReplicationConfig() *ReplicationConfig {
|
||||
if m != nil {
|
||||
return m.ReplicationConfig
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type ReceiveRes struct {
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
@@ -800,7 +762,7 @@ func (m *ReceiveRes) Reset() { *m = ReceiveRes{} }
|
||||
func (m *ReceiveRes) String() string { return proto.CompactTextString(m) }
|
||||
func (*ReceiveRes) ProtoMessage() {}
|
||||
func (*ReceiveRes) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_pdu_616c27178643eca4, []int{14}
|
||||
return fileDescriptor_pdu_2d84e8d7d278a80d, []int{13}
|
||||
}
|
||||
func (m *ReceiveRes) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_ReceiveRes.Unmarshal(m, b)
|
||||
@@ -833,7 +795,7 @@ func (m *DestroySnapshotsReq) Reset() { *m = DestroySnapshotsReq{} }
|
||||
func (m *DestroySnapshotsReq) String() string { return proto.CompactTextString(m) }
|
||||
func (*DestroySnapshotsReq) ProtoMessage() {}
|
||||
func (*DestroySnapshotsReq) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_pdu_616c27178643eca4, []int{15}
|
||||
return fileDescriptor_pdu_2d84e8d7d278a80d, []int{14}
|
||||
}
|
||||
func (m *DestroySnapshotsReq) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_DestroySnapshotsReq.Unmarshal(m, b)
|
||||
@@ -879,7 +841,7 @@ func (m *DestroySnapshotRes) Reset() { *m = DestroySnapshotRes{} }
|
||||
func (m *DestroySnapshotRes) String() string { return proto.CompactTextString(m) }
|
||||
func (*DestroySnapshotRes) ProtoMessage() {}
|
||||
func (*DestroySnapshotRes) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_pdu_616c27178643eca4, []int{16}
|
||||
return fileDescriptor_pdu_2d84e8d7d278a80d, []int{15}
|
||||
}
|
||||
func (m *DestroySnapshotRes) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_DestroySnapshotRes.Unmarshal(m, b)
|
||||
@@ -924,7 +886,7 @@ func (m *DestroySnapshotsRes) Reset() { *m = DestroySnapshotsRes{} }
|
||||
func (m *DestroySnapshotsRes) String() string { return proto.CompactTextString(m) }
|
||||
func (*DestroySnapshotsRes) ProtoMessage() {}
|
||||
func (*DestroySnapshotsRes) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_pdu_616c27178643eca4, []int{17}
|
||||
return fileDescriptor_pdu_2d84e8d7d278a80d, []int{16}
|
||||
}
|
||||
func (m *DestroySnapshotsRes) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_DestroySnapshotsRes.Unmarshal(m, b)
|
||||
@@ -962,7 +924,7 @@ func (m *ReplicationCursorReq) Reset() { *m = ReplicationCursorReq{} }
|
||||
func (m *ReplicationCursorReq) String() string { return proto.CompactTextString(m) }
|
||||
func (*ReplicationCursorReq) ProtoMessage() {}
|
||||
func (*ReplicationCursorReq) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_pdu_616c27178643eca4, []int{18}
|
||||
return fileDescriptor_pdu_2d84e8d7d278a80d, []int{17}
|
||||
}
|
||||
func (m *ReplicationCursorReq) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_ReplicationCursorReq.Unmarshal(m, b)
|
||||
@@ -1003,7 +965,7 @@ func (m *ReplicationCursorRes) Reset() { *m = ReplicationCursorRes{} }
|
||||
func (m *ReplicationCursorRes) String() string { return proto.CompactTextString(m) }
|
||||
func (*ReplicationCursorRes) ProtoMessage() {}
|
||||
func (*ReplicationCursorRes) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_pdu_616c27178643eca4, []int{19}
|
||||
return fileDescriptor_pdu_2d84e8d7d278a80d, []int{18}
|
||||
}
|
||||
func (m *ReplicationCursorRes) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_ReplicationCursorRes.Unmarshal(m, b)
|
||||
@@ -1139,7 +1101,7 @@ func (m *PingReq) Reset() { *m = PingReq{} }
|
||||
func (m *PingReq) String() string { return proto.CompactTextString(m) }
|
||||
func (*PingReq) ProtoMessage() {}
|
||||
func (*PingReq) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_pdu_616c27178643eca4, []int{20}
|
||||
return fileDescriptor_pdu_2d84e8d7d278a80d, []int{19}
|
||||
}
|
||||
func (m *PingReq) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_PingReq.Unmarshal(m, b)
|
||||
@@ -1178,7 +1140,7 @@ func (m *PingRes) Reset() { *m = PingRes{} }
|
||||
func (m *PingRes) String() string { return proto.CompactTextString(m) }
|
||||
func (*PingRes) ProtoMessage() {}
|
||||
func (*PingRes) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_pdu_616c27178643eca4, []int{21}
|
||||
return fileDescriptor_pdu_2d84e8d7d278a80d, []int{20}
|
||||
}
|
||||
func (m *PingRes) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_PingRes.Unmarshal(m, b)
|
||||
@@ -1211,10 +1173,9 @@ func init() {
|
||||
proto.RegisterType((*Filesystem)(nil), "Filesystem")
|
||||
proto.RegisterType((*ListFilesystemVersionsReq)(nil), "ListFilesystemVersionsReq")
|
||||
proto.RegisterType((*ListFilesystemVersionsRes)(nil), "ListFilesystemVersionsRes")
|
||||
proto.RegisterType((*SendAbstraction)(nil), "SendAbstraction")
|
||||
proto.RegisterType((*FilesystemVersion)(nil), "FilesystemVersion")
|
||||
proto.RegisterType((*SendReq)(nil), "SendReq")
|
||||
proto.RegisterType((*ReplicationConfig)(nil), "ReplicationConfig")
|
||||
proto.RegisterType((*ReplicationConfigProtection)(nil), "ReplicationConfigProtection")
|
||||
proto.RegisterType((*Property)(nil), "Property")
|
||||
proto.RegisterType((*SendRes)(nil), "SendRes")
|
||||
proto.RegisterType((*SendCompletedReq)(nil), "SendCompletedReq")
|
||||
@@ -1229,7 +1190,7 @@ func init() {
|
||||
proto.RegisterType((*PingReq)(nil), "PingReq")
|
||||
proto.RegisterType((*PingRes)(nil), "PingRes")
|
||||
proto.RegisterEnum("Tri", Tri_name, Tri_value)
|
||||
proto.RegisterEnum("ReplicationGuaranteeKind", ReplicationGuaranteeKind_name, ReplicationGuaranteeKind_value)
|
||||
proto.RegisterEnum("SendAbstraction_SendAbstractionType", SendAbstraction_SendAbstractionType_name, SendAbstraction_SendAbstractionType_value)
|
||||
proto.RegisterEnum("FilesystemVersion_VersionType", FilesystemVersion_VersionType_name, FilesystemVersion_VersionType_value)
|
||||
}
|
||||
|
||||
@@ -1470,71 +1431,67 @@ var _Replication_serviceDesc = grpc.ServiceDesc{
|
||||
Metadata: "pdu.proto",
|
||||
}
|
||||
|
||||
func init() { proto.RegisterFile("pdu.proto", fileDescriptor_pdu_616c27178643eca4) }
|
||||
func init() { proto.RegisterFile("pdu.proto", fileDescriptor_pdu_2d84e8d7d278a80d) }
|
||||
|
||||
var fileDescriptor_pdu_616c27178643eca4 = []byte{
|
||||
// 995 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x56, 0xcf, 0x6e, 0xdb, 0xc6,
|
||||
0x13, 0x36, 0x25, 0xda, 0xa2, 0x46, 0xce, 0x2f, 0xf4, 0xd8, 0x09, 0x68, 0xfd, 0xd2, 0xd4, 0xd8,
|
||||
0x14, 0x85, 0x63, 0xa0, 0x44, 0xe1, 0xb4, 0x05, 0x8a, 0x14, 0x41, 0xeb, 0xbf, 0x31, 0xd2, 0xba,
|
||||
0xea, 0x5a, 0x0d, 0x8a, 0xdc, 0x18, 0x69, 0x2a, 0x2f, 0x4c, 0x71, 0xe9, 0x5d, 0x2a, 0x88, 0x7a,
|
||||
0xec, 0xa1, 0x87, 0x5e, 0x7a, 0xea, 0xeb, 0xf4, 0x29, 0xfa, 0x20, 0x7d, 0x84, 0x82, 0x6b, 0x92,
|
||||
0xa2, 0x44, 0xca, 0x70, 0x4f, 0xda, 0xf9, 0xe6, 0xdb, 0xd9, 0xd9, 0xd1, 0x37, 0xb3, 0x84, 0x76,
|
||||
0x3c, 0x9c, 0xf8, 0xb1, 0x92, 0x89, 0x64, 0x9b, 0xb0, 0xf1, 0xad, 0xd0, 0xc9, 0x89, 0x08, 0x49,
|
||||
0x4f, 0x75, 0x42, 0x63, 0x4e, 0xd7, 0xec, 0xa0, 0x0a, 0x6a, 0xfc, 0x04, 0x3a, 0x33, 0x40, 0x7b,
|
||||
0xd6, 0x4e, 0x73, 0xb7, 0xb3, 0xdf, 0xf1, 0x4b, 0xa4, 0xb2, 0x9f, 0xfd, 0x6e, 0x01, 0xcc, 0x6c,
|
||||
0x44, 0xb0, 0x7b, 0x41, 0x72, 0xe9, 0x59, 0x3b, 0xd6, 0x6e, 0x9b, 0x9b, 0x35, 0xee, 0x40, 0x87,
|
||||
0x93, 0x9e, 0x8c, 0xa9, 0x2f, 0xaf, 0x28, 0xf2, 0x1a, 0xc6, 0x55, 0x86, 0xf0, 0x23, 0xb8, 0x77,
|
||||
0xa6, 0x7b, 0x61, 0x30, 0xa0, 0x4b, 0x19, 0x0e, 0x49, 0x79, 0xcd, 0x1d, 0x6b, 0xd7, 0xe1, 0xf3,
|
||||
0x60, 0x1a, 0xe7, 0x4c, 0x1f, 0x47, 0x03, 0x35, 0x8d, 0x13, 0x1a, 0x7a, 0xb6, 0xe1, 0x94, 0x21,
|
||||
0xf6, 0x1c, 0xb6, 0xe7, 0x2f, 0xf4, 0x9a, 0x94, 0x16, 0x32, 0xd2, 0x9c, 0xae, 0xf1, 0x71, 0x39,
|
||||
0xd1, 0x2c, 0xc1, 0x12, 0xc2, 0x5e, 0x2d, 0xdf, 0xac, 0xd1, 0x07, 0x27, 0x37, 0xb3, 0x92, 0xa0,
|
||||
0x5f, 0x61, 0xf2, 0x82, 0xc3, 0xfe, 0xb6, 0x60, 0xa3, 0xe2, 0xc7, 0x7d, 0xb0, 0xfb, 0xd3, 0x98,
|
||||
0xcc, 0xe1, 0xff, 0xdb, 0x7f, 0x5c, 0x8d, 0xe0, 0x67, 0xbf, 0x29, 0x8b, 0x1b, 0x6e, 0x5a, 0xd1,
|
||||
0xf3, 0x60, 0x4c, 0x59, 0xd9, 0xcc, 0x3a, 0xc5, 0x4e, 0x27, 0x62, 0x68, 0xca, 0x64, 0x73, 0xb3,
|
||||
0xc6, 0x47, 0xd0, 0x3e, 0x54, 0x14, 0x24, 0xd4, 0xff, 0xe9, 0xd4, 0xd4, 0xc6, 0xe6, 0x33, 0x00,
|
||||
0xbb, 0xe0, 0x18, 0x43, 0xc8, 0xc8, 0x5b, 0x35, 0x91, 0x0a, 0x9b, 0x3d, 0x85, 0x4e, 0xe9, 0x58,
|
||||
0x5c, 0x07, 0xe7, 0x22, 0x0a, 0x62, 0x7d, 0x29, 0x13, 0x77, 0x25, 0xb5, 0x0e, 0xa4, 0xbc, 0x1a,
|
||||
0x07, 0xea, 0xca, 0xb5, 0xd8, 0x9f, 0x0d, 0x68, 0x5d, 0x50, 0x34, 0xbc, 0x43, 0x3d, 0xf1, 0x63,
|
||||
0xb0, 0x4f, 0x94, 0x1c, 0x9b, 0xc4, 0xeb, 0xcb, 0x65, 0xfc, 0xc8, 0xa0, 0xd1, 0x97, 0xe6, 0x2a,
|
||||
0xf5, 0xac, 0x46, 0x5f, 0x2e, 0x4a, 0xc8, 0xae, 0x4a, 0x88, 0x41, 0x7b, 0x26, 0x8d, 0x55, 0x53,
|
||||
0x5f, 0xdb, 0xef, 0x2b, 0xc1, 0x67, 0x30, 0x3e, 0x84, 0xb5, 0x23, 0x35, 0xe5, 0x93, 0xc8, 0x5b,
|
||||
0x33, 0xda, 0xc9, 0x2c, 0xfc, 0x1a, 0x36, 0x38, 0xc5, 0xa1, 0x18, 0x98, 0x7a, 0x1c, 0xca, 0xe8,
|
||||
0x67, 0x31, 0xf2, 0x5a, 0x59, 0x42, 0x15, 0x0f, 0xaf, 0x92, 0xd9, 0x0f, 0x35, 0x11, 0xf0, 0x2b,
|
||||
0x80, 0xb4, 0xf9, 0x68, 0x60, 0xaa, 0x6e, 0x99, 0x78, 0x8f, 0xaa, 0xf1, 0x7a, 0x05, 0x87, 0x97,
|
||||
0xf8, 0xec, 0x0f, 0x0b, 0xfe, 0x7f, 0x0b, 0x17, 0x9f, 0x41, 0xeb, 0x2c, 0x12, 0x89, 0x08, 0xc2,
|
||||
0x4c, 0x4e, 0xdb, 0xe5, 0xd0, 0xa7, 0x93, 0x40, 0x05, 0x51, 0x42, 0xf4, 0x4a, 0x44, 0x43, 0x9e,
|
||||
0x33, 0xf1, 0x39, 0x74, 0xce, 0xa2, 0x81, 0xa2, 0x31, 0x45, 0x49, 0x10, 0x9a, 0xbf, 0xe6, 0xd6,
|
||||
0x8d, 0x65, 0x36, 0xfb, 0x0c, 0x9c, 0x9e, 0x92, 0x31, 0xa9, 0x64, 0x5a, 0xa8, 0xd2, 0x2a, 0xa9,
|
||||
0x72, 0x0b, 0x56, 0x5f, 0x07, 0xe1, 0x24, 0x97, 0xea, 0x8d, 0xc1, 0x7e, 0xb5, 0x72, 0xc9, 0x68,
|
||||
0xdc, 0x85, 0xfb, 0x3f, 0x6a, 0x1a, 0x2e, 0x4e, 0x03, 0x87, 0x2f, 0xc2, 0xc8, 0x60, 0xfd, 0xf8,
|
||||
0x7d, 0x4c, 0x83, 0x84, 0x86, 0x17, 0xe2, 0x17, 0x32, 0xf2, 0x68, 0xf2, 0x39, 0x0c, 0x9f, 0x02,
|
||||
0x64, 0xf9, 0x08, 0xd2, 0x9e, 0x6d, 0xba, 0xb2, 0xed, 0xe7, 0x29, 0xf2, 0x92, 0x93, 0xbd, 0x00,
|
||||
0x37, 0xcd, 0xe1, 0x50, 0x8e, 0xe3, 0x90, 0x12, 0x32, 0xfa, 0xdd, 0x83, 0xce, 0xf7, 0x4a, 0x8c,
|
||||
0x44, 0x14, 0x84, 0x9c, 0xae, 0x33, 0x99, 0x3a, 0x7e, 0x26, 0x6f, 0x5e, 0x76, 0x32, 0xac, 0xec,
|
||||
0xd7, 0xec, 0x2f, 0x0b, 0x80, 0xd3, 0x80, 0xc4, 0x3b, 0xba, 0x4b, 0x3b, 0xdc, 0xc8, 0xbc, 0x71,
|
||||
0xab, 0xcc, 0xf7, 0xc0, 0x3d, 0x0c, 0x29, 0x50, 0xe5, 0x02, 0xdd, 0x8c, 0xc2, 0x0a, 0x5e, 0x2f,
|
||||
0x5a, 0xfb, 0xbf, 0x88, 0x76, 0xbd, 0x94, 0xbf, 0x66, 0x23, 0xd8, 0x3c, 0x22, 0x9d, 0x28, 0x39,
|
||||
0xcd, 0xbb, 0xff, 0x2e, 0x53, 0x13, 0x3f, 0x85, 0x76, 0xc1, 0xf7, 0x1a, 0x4b, 0x27, 0xe3, 0x8c,
|
||||
0xc4, 0xde, 0x00, 0x2e, 0x1c, 0x94, 0x0d, 0xd8, 0xdc, 0xcc, 0x5a, 0xa5, 0x76, 0xc0, 0xe6, 0x9c,
|
||||
0x54, 0x6c, 0xc7, 0x4a, 0x49, 0x95, 0x8b, 0xcd, 0x18, 0xec, 0xa8, 0xee, 0x12, 0xe9, 0x9b, 0xd6,
|
||||
0x4a, 0x4b, 0x17, 0x26, 0xf9, 0xf0, 0xde, 0xf4, 0xab, 0x29, 0xf0, 0x9c, 0xc3, 0xbe, 0x80, 0xad,
|
||||
0x72, 0xb5, 0x26, 0x4a, 0x4b, 0x75, 0x97, 0x17, 0xa4, 0x5f, 0xbb, 0x4f, 0xe3, 0x56, 0x36, 0xae,
|
||||
0xd3, 0x1d, 0xf6, 0xcb, 0x95, 0x62, 0x60, 0x3b, 0xe7, 0x32, 0xa1, 0xf7, 0x42, 0x27, 0x37, 0x5d,
|
||||
0xf0, 0x72, 0x85, 0x17, 0xc8, 0x81, 0x03, 0x6b, 0x37, 0xe9, 0xb0, 0x27, 0xd0, 0xea, 0x89, 0x68,
|
||||
0x94, 0x26, 0xe0, 0x41, 0xeb, 0x3b, 0xd2, 0x3a, 0x18, 0xe5, 0x8d, 0x97, 0x9b, 0xec, 0x83, 0x9c,
|
||||
0xa4, 0xd3, 0xd6, 0x3c, 0x1e, 0x5c, 0xca, 0xbc, 0x35, 0xd3, 0xf5, 0xde, 0x2e, 0x34, 0xfb, 0x4a,
|
||||
0xa4, 0xc3, 0xfc, 0x48, 0x46, 0xc9, 0x61, 0xa0, 0xc8, 0x5d, 0xc1, 0x36, 0xac, 0x9e, 0x04, 0xa1,
|
||||
0x26, 0xd7, 0x42, 0x07, 0xec, 0xbe, 0x9a, 0x90, 0xdb, 0xd8, 0xfb, 0xcd, 0x02, 0x6f, 0xd9, 0x38,
|
||||
0xc0, 0x2d, 0x70, 0x0b, 0xe0, 0x2c, 0x7a, 0x17, 0x84, 0x62, 0xe8, 0xae, 0xe0, 0x36, 0x3c, 0x28,
|
||||
0x50, 0xa3, 0xd0, 0xe0, 0xad, 0x08, 0x45, 0x32, 0x75, 0x2d, 0x7c, 0x02, 0x1f, 0x96, 0x36, 0x14,
|
||||
0xa3, 0xa4, 0x74, 0x80, 0xdb, 0x98, 0x8b, 0x7a, 0x2e, 0x93, 0x4b, 0x11, 0x8d, 0xdc, 0xe6, 0xfe,
|
||||
0x3f, 0x8d, 0x74, 0xe6, 0x17, 0x3c, 0xec, 0x82, 0x9d, 0xde, 0x10, 0x1d, 0x3f, 0xab, 0x46, 0x37,
|
||||
0x5f, 0x69, 0xfc, 0x12, 0xee, 0xcf, 0x3f, 0xdd, 0x1a, 0xd1, 0xaf, 0x7c, 0xef, 0x74, 0xab, 0x98,
|
||||
0xc6, 0x1e, 0x3c, 0xac, 0x7f, 0xf5, 0xb1, 0xeb, 0x2f, 0xfd, 0x96, 0xe8, 0x2e, 0xf7, 0x69, 0x7c,
|
||||
0x01, 0xee, 0xa2, 0x06, 0x71, 0xcb, 0xaf, 0xe9, 0xad, 0x6e, 0x1d, 0xaa, 0xf1, 0x9b, 0xf9, 0xc6,
|
||||
0x36, 0x2a, 0xc2, 0x07, 0x7e, 0x9d, 0x22, 0xbb, 0xb5, 0xb0, 0xc6, 0xcf, 0xe1, 0xde, 0xdc, 0xb8,
|
||||
0xc2, 0x0d, 0x7f, 0x71, 0xfc, 0x75, 0x2b, 0x90, 0x3e, 0x58, 0x7d, 0xd3, 0x8c, 0x87, 0x93, 0xb7,
|
||||
0x6b, 0xe6, 0x93, 0xf1, 0xd9, 0xbf, 0x01, 0x00, 0x00, 0xff, 0xff, 0x7e, 0xa4, 0xfb, 0xca, 0x3f,
|
||||
0x0a, 0x00, 0x00,
|
||||
var fileDescriptor_pdu_2d84e8d7d278a80d = []byte{
|
||||
// 940 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x56, 0x6d, 0x6f, 0xe3, 0xc4,
|
||||
0x13, 0xaf, 0x13, 0xa7, 0x75, 0x26, 0xed, 0xbf, 0xee, 0x24, 0xff, 0x23, 0x44, 0x70, 0xaa, 0x96,
|
||||
0x13, 0xca, 0x55, 0x60, 0xa1, 0xf0, 0x20, 0x10, 0xe8, 0xa4, 0x6b, 0xd2, 0x5e, 0x8b, 0xe0, 0x88,
|
||||
0xb6, 0xb9, 0x0a, 0x9d, 0xc4, 0x0b, 0x37, 0x1e, 0x5a, 0xab, 0x8e, 0xd7, 0xdd, 0x75, 0xd0, 0x05,
|
||||
0xf1, 0x8a, 0x77, 0xf0, 0xf5, 0xe0, 0x73, 0x20, 0x3e, 0x02, 0xf2, 0xc6, 0x4e, 0x1c, 0xdb, 0x3d,
|
||||
0xf5, 0x55, 0x76, 0x7e, 0x33, 0xe3, 0x9d, 0xc7, 0xdf, 0x06, 0x9a, 0x91, 0x37, 0x77, 0x22, 0x29,
|
||||
0x62, 0xc1, 0xda, 0x70, 0xf0, 0x9d, 0xaf, 0xe2, 0x53, 0x3f, 0x20, 0xb5, 0x50, 0x31, 0xcd, 0x38,
|
||||
0xdd, 0xb1, 0xe3, 0x32, 0xa8, 0xf0, 0x63, 0x68, 0xad, 0x01, 0xd5, 0x35, 0x0e, 0xeb, 0xfd, 0xd6,
|
||||
0xa0, 0xe5, 0xe4, 0x8c, 0xf2, 0x7a, 0xf6, 0xa7, 0x01, 0xb0, 0x96, 0x11, 0xc1, 0x1c, 0xbb, 0xf1,
|
||||
0x4d, 0xd7, 0x38, 0x34, 0xfa, 0x4d, 0xae, 0xcf, 0x78, 0x08, 0x2d, 0x4e, 0x6a, 0x3e, 0xa3, 0x89,
|
||||
0xb8, 0xa5, 0xb0, 0x5b, 0xd3, 0xaa, 0x3c, 0x84, 0x4f, 0x60, 0xef, 0x5c, 0x8d, 0x03, 0x77, 0x4a,
|
||||
0x37, 0x22, 0xf0, 0x48, 0x76, 0xeb, 0x87, 0x46, 0xdf, 0xe2, 0x9b, 0x60, 0xf2, 0x9d, 0x73, 0x75,
|
||||
0x12, 0x4e, 0xe5, 0x22, 0x8a, 0xc9, 0xeb, 0x9a, 0xda, 0x26, 0x0f, 0xb1, 0xaf, 0xe1, 0xdd, 0xcd,
|
||||
0x84, 0x2e, 0x49, 0x2a, 0x5f, 0x84, 0x8a, 0xd3, 0x1d, 0x3e, 0xce, 0x07, 0x9a, 0x06, 0x98, 0x43,
|
||||
0xd8, 0x1f, 0xc6, 0xfd, 0xde, 0x0a, 0x1d, 0xb0, 0x32, 0x31, 0xad, 0x09, 0x3a, 0x25, 0x4b, 0xbe,
|
||||
0xb2, 0xc1, 0x6f, 0xc0, 0xbe, 0xa0, 0xd0, 0x7b, 0x7e, 0xa5, 0x62, 0xe9, 0x4e, 0x63, 0xed, 0x57,
|
||||
0xd3, 0x7e, 0xb6, 0x53, 0x50, 0xf0, 0x92, 0x25, 0xfb, 0xc7, 0x80, 0xfd, 0x02, 0x88, 0x5f, 0x82,
|
||||
0x39, 0x59, 0x44, 0xa4, 0x23, 0xff, 0xdf, 0xe0, 0x49, 0xf1, 0x2b, 0x45, 0x39, 0xb1, 0xe5, 0xda,
|
||||
0x03, 0x3b, 0xd0, 0xf8, 0x56, 0x5c, 0x9d, 0x8f, 0xd2, 0xd2, 0x2f, 0x05, 0xfc, 0x08, 0x76, 0xd2,
|
||||
0x68, 0x75, 0xb9, 0xab, 0x13, 0xca, 0x4c, 0xd8, 0x4f, 0xd0, 0xae, 0xb8, 0x00, 0xf7, 0xa0, 0xf9,
|
||||
0x2a, 0xf4, 0xe8, 0x67, 0x3f, 0x24, 0xcf, 0xde, 0xc2, 0x77, 0xa0, 0xcd, 0x29, 0x0a, 0xfc, 0xa9,
|
||||
0x9b, 0x58, 0x0c, 0xe7, 0x52, 0x09, 0x79, 0x39, 0xb0, 0x0d, 0xdc, 0x05, 0xeb, 0x22, 0xa6, 0xe8,
|
||||
0x4c, 0x04, 0x9e, 0x5d, 0x43, 0x1b, 0x76, 0x13, 0xe9, 0x58, 0x88, 0xdb, 0x99, 0x2b, 0x6f, 0xed,
|
||||
0x3a, 0xfb, 0xdb, 0x80, 0x83, 0xd2, 0xed, 0x38, 0xd8, 0x48, 0xf9, 0x71, 0x39, 0x3e, 0x27, 0xfd,
|
||||
0xcd, 0x25, 0x8b, 0x60, 0xbe, 0x74, 0x67, 0x94, 0xe6, 0xaa, 0xcf, 0x09, 0xf6, 0x62, 0xee, 0x7b,
|
||||
0x3a, 0x4f, 0x93, 0xeb, 0x33, 0xbe, 0x07, 0xcd, 0xa1, 0x24, 0x37, 0xa6, 0xc9, 0x8f, 0x2f, 0xf4,
|
||||
0x2c, 0x99, 0x7c, 0x0d, 0x60, 0x0f, 0x2c, 0x2d, 0x24, 0xd5, 0x69, 0xe8, 0x2f, 0xad, 0x64, 0xf6,
|
||||
0x14, 0x5a, 0xb9, 0x6b, 0x75, 0x6a, 0xa1, 0x1b, 0xa9, 0x1b, 0x11, 0xdb, 0x5b, 0x89, 0xb4, 0x4a,
|
||||
0xcb, 0x60, 0x7f, 0x19, 0xb0, 0x93, 0x94, 0xed, 0x01, 0xf3, 0x87, 0x1f, 0x82, 0x79, 0x2a, 0xc5,
|
||||
0x4c, 0x07, 0x5e, 0xdd, 0x0c, 0xad, 0x47, 0x06, 0xb5, 0x89, 0x78, 0x4b, 0xcb, 0x6a, 0x13, 0x51,
|
||||
0x5c, 0x39, 0xb3, 0xbc, 0x72, 0x0c, 0x9a, 0xeb, 0x55, 0x6a, 0xe8, 0xfa, 0x9a, 0xce, 0x44, 0xfa,
|
||||
0x7c, 0x0d, 0xe3, 0x23, 0xd8, 0x1e, 0xc9, 0x05, 0x9f, 0x87, 0xdd, 0x6d, 0xbd, 0x6b, 0xa9, 0xc4,
|
||||
0x3e, 0x03, 0x6b, 0x2c, 0x45, 0x44, 0x32, 0x5e, 0xac, 0xca, 0x6d, 0xe4, 0xca, 0xdd, 0x81, 0xc6,
|
||||
0xa5, 0x1b, 0xcc, 0xb3, 0x1e, 0x2c, 0x05, 0xf6, 0xfb, 0xaa, 0x16, 0x0a, 0xfb, 0xb0, 0xff, 0x4a,
|
||||
0x91, 0x57, 0xa4, 0x05, 0x8b, 0x17, 0x61, 0x64, 0xb0, 0x7b, 0xf2, 0x26, 0xa2, 0x69, 0x4c, 0xde,
|
||||
0x85, 0xff, 0x2b, 0xe9, 0xbc, 0xeb, 0x7c, 0x03, 0xc3, 0xa7, 0x00, 0x69, 0x3c, 0x3e, 0xa9, 0xae,
|
||||
0xa9, 0xb7, 0xac, 0xe9, 0x64, 0x21, 0xf2, 0x9c, 0x92, 0x3d, 0x5b, 0xae, 0xe5, 0x50, 0xcc, 0xa2,
|
||||
0x80, 0x62, 0xd2, 0x8d, 0x39, 0x82, 0xd6, 0x0f, 0xd2, 0xbf, 0xf6, 0x43, 0x37, 0xe0, 0x74, 0x97,
|
||||
0xd6, 0xdf, 0x72, 0xd2, 0xbe, 0xf1, 0xbc, 0x92, 0x61, 0xc9, 0x5f, 0xb1, 0xdf, 0x00, 0x38, 0x4d,
|
||||
0xc9, 0xff, 0x85, 0x1e, 0xd2, 0xe6, 0x65, 0xfb, 0x6a, 0x6f, 0x6d, 0xdf, 0x11, 0xd8, 0xc3, 0x80,
|
||||
0x5c, 0x99, 0xaf, 0xcf, 0x92, 0x12, 0x4b, 0x38, 0xdb, 0xcd, 0xdd, 0xae, 0xd8, 0x35, 0xb4, 0x47,
|
||||
0xa4, 0x62, 0x29, 0x16, 0xd9, 0x4c, 0x3e, 0x84, 0xfb, 0xf0, 0x13, 0x68, 0xae, 0xec, 0x53, 0x9a,
|
||||
0xaa, 0x8a, 0x6d, 0x6d, 0xc4, 0x5e, 0x03, 0x16, 0x2e, 0x4a, 0x59, 0x32, 0x13, 0xf5, 0x2d, 0xf7,
|
||||
0xb0, 0x64, 0x66, 0x93, 0x4c, 0xca, 0x89, 0x94, 0x42, 0x66, 0x93, 0xa2, 0x05, 0x36, 0xaa, 0x4a,
|
||||
0x22, 0x79, 0x99, 0x76, 0x92, 0xc4, 0x83, 0x38, 0x63, 0xe0, 0xb6, 0x53, 0x0e, 0x81, 0x67, 0x36,
|
||||
0xec, 0x0b, 0xe8, 0x94, 0xb8, 0xe8, 0x21, 0xef, 0xc0, 0xa4, 0xd2, 0x4f, 0x61, 0x27, 0x25, 0x91,
|
||||
0xc4, 0xc3, 0x3c, 0xdb, 0x5a, 0xd1, 0x88, 0xf5, 0x52, 0xc4, 0xf4, 0xc6, 0x57, 0xf1, 0x72, 0x84,
|
||||
0xcf, 0xb6, 0xf8, 0x0a, 0x39, 0xb6, 0x60, 0x7b, 0x19, 0x0e, 0xfb, 0x00, 0x76, 0xc6, 0x7e, 0x78,
|
||||
0x9d, 0x04, 0xd0, 0x85, 0x9d, 0xef, 0x49, 0x29, 0xf7, 0x3a, 0xdb, 0x9a, 0x4c, 0x64, 0xef, 0x67,
|
||||
0x46, 0x2a, 0xd9, 0xab, 0x93, 0xe9, 0x8d, 0xc8, 0xf6, 0x2a, 0x39, 0x1f, 0xf5, 0xa1, 0x3e, 0x91,
|
||||
0x7e, 0x42, 0x31, 0x23, 0x11, 0xc6, 0x43, 0x57, 0x92, 0xbd, 0x85, 0x4d, 0x68, 0x9c, 0xba, 0x81,
|
||||
0x22, 0xdb, 0x40, 0x0b, 0xcc, 0x89, 0x9c, 0x93, 0x5d, 0x1b, 0xfc, 0x5b, 0x4b, 0x08, 0x60, 0x95,
|
||||
0x04, 0xf6, 0xc0, 0x4c, 0x3e, 0x8c, 0x96, 0x93, 0x06, 0xd1, 0xcb, 0x4e, 0x0a, 0xbf, 0x82, 0xfd,
|
||||
0xcd, 0x67, 0x4f, 0x21, 0x3a, 0xa5, 0x3f, 0x0b, 0xbd, 0x32, 0xa6, 0x70, 0x0c, 0x8f, 0xaa, 0x5f,
|
||||
0x4c, 0xec, 0x39, 0xf7, 0x3e, 0xc4, 0xbd, 0xfb, 0x75, 0x0a, 0x9f, 0x81, 0x5d, 0x6c, 0x3d, 0x76,
|
||||
0x9c, 0x8a, 0x91, 0xee, 0x55, 0xa1, 0x0a, 0x9f, 0xc3, 0x41, 0xa9, 0x79, 0xf8, 0x7f, 0xa7, 0x6a,
|
||||
0x10, 0x7a, 0x95, 0xb0, 0xc2, 0xcf, 0x61, 0x6f, 0x63, 0xc5, 0xf1, 0xc0, 0x29, 0x52, 0x46, 0xaf,
|
||||
0x04, 0xa9, 0xe3, 0xc6, 0xeb, 0x7a, 0xe4, 0xcd, 0xaf, 0xb6, 0xf5, 0xff, 0xad, 0x4f, 0xff, 0x0b,
|
||||
0x00, 0x00, 0xff, 0xff, 0x0a, 0xa0, 0x3f, 0xc2, 0x7c, 0x09, 0x00, 0x00,
|
||||
}
|
||||
|
||||
@@ -25,7 +25,22 @@ message Filesystem {
|
||||
|
||||
message ListFilesystemVersionsReq { string Filesystem = 1; }
|
||||
|
||||
message ListFilesystemVersionsRes { repeated FilesystemVersion Versions = 1; }
|
||||
message ListFilesystemVersionsRes {
|
||||
repeated FilesystemVersion Versions = 1;
|
||||
repeated SendAbstraction SendAbstractions = 2;
|
||||
}
|
||||
|
||||
message SendAbstraction {
|
||||
enum SendAbstractionType {
|
||||
Undefined = 0;
|
||||
ReplicationCursorV2 = 1;
|
||||
StepHold = 2;
|
||||
StepBookmark = 3;
|
||||
};
|
||||
SendAbstractionType Type = 1;
|
||||
string JobID = 2;
|
||||
FilesystemVersion Version = 3;
|
||||
}
|
||||
|
||||
message FilesystemVersion {
|
||||
enum VersionType {
|
||||
@@ -61,25 +76,6 @@ message SendReq {
|
||||
Tri Encrypted = 5;
|
||||
|
||||
bool DryRun = 6;
|
||||
|
||||
ReplicationConfig ReplicationConfig = 7;
|
||||
}
|
||||
|
||||
message ReplicationConfig {
|
||||
ReplicationConfigProtection protection = 1;
|
||||
}
|
||||
|
||||
|
||||
message ReplicationConfigProtection {
|
||||
ReplicationGuaranteeKind Initial = 1;
|
||||
ReplicationGuaranteeKind Incremental = 2;
|
||||
}
|
||||
|
||||
enum ReplicationGuaranteeKind {
|
||||
GuaranteeInvalid = 0;
|
||||
GuaranteeResumability = 1;
|
||||
GuaranteeIncrementalReplication = 2;
|
||||
GuaranteeNothing = 3;
|
||||
}
|
||||
|
||||
message Property {
|
||||
@@ -99,9 +95,7 @@ message SendRes {
|
||||
repeated Property Properties = 4;
|
||||
}
|
||||
|
||||
message SendCompletedReq {
|
||||
SendReq OriginalReq = 2;
|
||||
}
|
||||
message SendCompletedReq { SendReq OriginalReq = 2; }
|
||||
|
||||
message SendCompletedRes {}
|
||||
|
||||
@@ -112,14 +106,12 @@ message ReceiveReq {
|
||||
// If true, the receiver should clear the resume token before performing the
|
||||
// zfs recv of the stream in the request
|
||||
bool ClearResumeToken = 3;
|
||||
|
||||
ReplicationConfig ReplicationConfig = 4;
|
||||
}
|
||||
|
||||
message ReceiveRes {}
|
||||
|
||||
message DestroySnapshotsReq {
|
||||
string Filesystem = 1;
|
||||
string Filesystem = 1;
|
||||
// Path to filesystem, snapshot or bookmark to be destroyed
|
||||
repeated FilesystemVersion Snapshots = 2;
|
||||
}
|
||||
|
||||
@@ -83,10 +83,3 @@ func (v *FilesystemVersion) ZFSFilesystemVersion() (*zfs.FilesystemVersion, erro
|
||||
Creation: ct,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func ReplicationConfigProtectionWithKind(both ReplicationGuaranteeKind) *ReplicationConfigProtection {
|
||||
return &ReplicationConfigProtection{
|
||||
Initial: both,
|
||||
Incremental: both,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +52,10 @@ type Receiver interface {
|
||||
Receive(ctx context.Context, req *pdu.ReceiveReq, receive io.ReadCloser) (*pdu.ReceiveRes, error)
|
||||
}
|
||||
|
||||
type PlannerPolicy struct {
|
||||
EncryptedSend tri // all sends must be encrypted (send -w, and encryption!=off)
|
||||
}
|
||||
|
||||
type Planner struct {
|
||||
sender Sender
|
||||
receiver Receiver
|
||||
@@ -550,25 +554,19 @@ func (s *Step) updateSizeEstimate(ctx context.Context) error {
|
||||
log.WithError(err).Error("dry run send request failed")
|
||||
return err
|
||||
}
|
||||
if sres == nil {
|
||||
err := fmt.Errorf("dry run send request returned nil send result")
|
||||
log.Error(err.Error())
|
||||
return err
|
||||
}
|
||||
s.expectedSize = sres.GetExpectedSize()
|
||||
s.expectedSize = sres.ExpectedSize
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Step) buildSendRequest(dryRun bool) (sr *pdu.SendReq) {
|
||||
fs := s.parent.Path
|
||||
sr = &pdu.SendReq{
|
||||
Filesystem: fs,
|
||||
From: s.from, // may be nil
|
||||
To: s.to,
|
||||
Encrypted: s.encrypt.ToPDU(),
|
||||
ResumeToken: s.resumeToken,
|
||||
DryRun: dryRun,
|
||||
ReplicationConfig: &s.parent.policy.ReplicationConfig,
|
||||
Filesystem: fs,
|
||||
From: s.from, // may be nil
|
||||
To: s.to,
|
||||
Encrypted: s.encrypt.ToPDU(),
|
||||
ResumeToken: s.resumeToken,
|
||||
DryRun: dryRun,
|
||||
}
|
||||
return sr
|
||||
}
|
||||
@@ -586,11 +584,6 @@ func (s *Step) doReplication(ctx context.Context) error {
|
||||
log.WithError(err).Error("send request failed")
|
||||
return err
|
||||
}
|
||||
if sres == nil {
|
||||
err := fmt.Errorf("send request returned nil send result")
|
||||
log.Error(err.Error())
|
||||
return err
|
||||
}
|
||||
if stream == nil {
|
||||
err := errors.New("send request did not return a stream, broken endpoint implementation")
|
||||
return err
|
||||
@@ -610,10 +603,9 @@ func (s *Step) doReplication(ctx context.Context) error {
|
||||
}()
|
||||
|
||||
rr := &pdu.ReceiveReq{
|
||||
Filesystem: fs,
|
||||
To: sr.GetTo(),
|
||||
ClearResumeToken: !sres.UsedResumeToken,
|
||||
ReplicationConfig: &s.parent.policy.ReplicationConfig,
|
||||
Filesystem: fs,
|
||||
To: sr.GetTo(),
|
||||
ClearResumeToken: !sres.UsedResumeToken,
|
||||
}
|
||||
log.Debug("initiate receive request")
|
||||
_, err = s.receiver.Receive(ctx, rr, byteCountingStream)
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
package logic
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
"github.com/zrepl/zrepl/config"
|
||||
"github.com/zrepl/zrepl/replication/logic/pdu"
|
||||
)
|
||||
|
||||
type PlannerPolicy struct {
|
||||
EncryptedSend tri // all sends must be encrypted (send -w, and encryption!=off)
|
||||
ReplicationConfig pdu.ReplicationConfig
|
||||
}
|
||||
|
||||
func ReplicationConfigFromConfig(in *config.Replication) (*pdu.ReplicationConfig, error) {
|
||||
initial, err := pduReplicationGuaranteeKindFromConfig(in.Protection.Initial)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "field 'initial'")
|
||||
}
|
||||
incremental, err := pduReplicationGuaranteeKindFromConfig(in.Protection.Incremental)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "field 'incremental'")
|
||||
}
|
||||
return &pdu.ReplicationConfig{
|
||||
Protection: &pdu.ReplicationConfigProtection{
|
||||
Initial: initial,
|
||||
Incremental: incremental,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func pduReplicationGuaranteeKindFromConfig(in string) (k pdu.ReplicationGuaranteeKind, _ error) {
|
||||
switch in {
|
||||
case "guarantee_nothing":
|
||||
return pdu.ReplicationGuaranteeKind_GuaranteeNothing, nil
|
||||
case "guarantee_incremental":
|
||||
return pdu.ReplicationGuaranteeKind_GuaranteeIncrementalReplication, nil
|
||||
case "guarantee_resumability":
|
||||
return pdu.ReplicationGuaranteeKind_GuaranteeResumability, nil
|
||||
default:
|
||||
return k, errors.Errorf("%q is not in guarantee_{nothing,incremental,resumability}", in)
|
||||
}
|
||||
}
|
||||
@@ -162,30 +162,3 @@ func (f *FilesystemReport) NextStep() *StepReport {
|
||||
func (f *StepReport) IsIncremental() bool {
|
||||
return f.Info.From != ""
|
||||
}
|
||||
|
||||
// Returns, for the latest replication attempt,
|
||||
// 0 if there have not been any replication attempts,
|
||||
// -1 if the replication failed while enumerating file systems
|
||||
// N if N filesystems could not not be replicated successfully
|
||||
func (r *Report) GetFailedFilesystemsCountInLatestAttempt() int {
|
||||
|
||||
if len(r.Attempts) == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
a := r.Attempts[len(r.Attempts)-1]
|
||||
switch a.State {
|
||||
case AttemptPlanningError:
|
||||
return -1
|
||||
case AttemptFanOutError:
|
||||
var count int
|
||||
for _, f := range a.Filesystems {
|
||||
if f.Error() != nil {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ type ProtocolError struct {
|
||||
}
|
||||
|
||||
func (e *ProtocolError) Error() string {
|
||||
return fmt.Sprintf("protocol error: %s", e.cause)
|
||||
return fmt.Sprintf("protocol error: %s", e)
|
||||
}
|
||||
|
||||
func (c *Client) recv(ctx context.Context, conn *stream.Conn, res proto.Message) error {
|
||||
|
||||
@@ -239,10 +239,6 @@ func (s *Server) serveConnRequest(ctx context.Context, endpoint string, c *strea
|
||||
|
||||
if sendStream != nil {
|
||||
err := c.SendStream(ctx, sendStream, ZFSStream)
|
||||
closeErr := sendStream.Close()
|
||||
if closeErr != nil {
|
||||
s.log.WithError(err).Error("cannot close send stream")
|
||||
}
|
||||
if err != nil {
|
||||
s.log.WithError(err).Error("cannot write send stream")
|
||||
}
|
||||
|
||||
@@ -152,7 +152,7 @@ func (m *HandshakeMessage) DecodeReader(r io.Reader, maxLen int) error {
|
||||
|
||||
func DoHandshakeCurrentVersion(conn net.Conn, deadline time.Time) *HandshakeError {
|
||||
// current protocol version is hardcoded here
|
||||
return DoHandshakeVersion(conn, deadline, 5)
|
||||
return DoHandshakeVersion(conn, deadline, 4)
|
||||
}
|
||||
|
||||
const HandshakeMessageMaxLen = 16 * 4096
|
||||
|
||||
@@ -23,10 +23,6 @@ func TLSConnecterFromConfig(in *config.TLSConnect) (*TLSConnecter, error) {
|
||||
Timeout: in.DialTimeout,
|
||||
}
|
||||
|
||||
if fakeCertificateLoading {
|
||||
return &TLSConnecter{in.Address, dialer, nil}, nil
|
||||
}
|
||||
|
||||
ca, err := tlsconf.ParseCAFile(in.Ca)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "cannot parse ca file")
|
||||
|
||||
@@ -25,10 +25,6 @@ func TLSListenerFactoryFromConfig(c *config.Global, in *config.TLSServe) (transp
|
||||
return nil, errors.New("fields 'ca', 'cert' and 'key'must be specified")
|
||||
}
|
||||
|
||||
if fakeCertificateLoading {
|
||||
return func() (transport.AuthenticatedListener, error) { return nil, nil }, nil
|
||||
}
|
||||
|
||||
clientCA, err := tlsconf.ParseCAFile(in.Ca)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "cannot parse ca file")
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
package tls
|
||||
|
||||
import "testing"
|
||||
|
||||
var fakeCertificateLoading bool
|
||||
|
||||
func FakeCertificateLoading(t *testing.T) {
|
||||
t.Logf("faking certificate loading")
|
||||
fakeCertificateLoading = true
|
||||
}
|
||||
+39
-79
@@ -12,100 +12,84 @@ import (
|
||||
|
||||
var cache sync.Map
|
||||
|
||||
func Reset() {
|
||||
cache.Range(func(key, _ interface{}) bool {
|
||||
cache.Delete(key)
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
func Duration(varname string, def time.Duration) (d time.Duration) {
|
||||
var err error
|
||||
func Duration(varname string, def time.Duration) time.Duration {
|
||||
if v, ok := cache.Load(varname); ok {
|
||||
return v.(time.Duration)
|
||||
}
|
||||
e := os.Getenv(varname)
|
||||
if e == "" {
|
||||
d = def
|
||||
} else {
|
||||
d, err = time.ParseDuration(e)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return def
|
||||
}
|
||||
d, err := time.ParseDuration(e)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
cache.Store(varname, d)
|
||||
return d
|
||||
}
|
||||
|
||||
func Int(varname string, def int) (d int) {
|
||||
func Int(varname string, def int) int {
|
||||
if v, ok := cache.Load(varname); ok {
|
||||
return v.(int)
|
||||
}
|
||||
e := os.Getenv(varname)
|
||||
if e == "" {
|
||||
d = def
|
||||
} else {
|
||||
d64, err := strconv.ParseInt(e, 10, strconv.IntSize)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
d = int(d64)
|
||||
return def
|
||||
}
|
||||
d64, err := strconv.ParseInt(e, 10, strconv.IntSize)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
d := int(d64)
|
||||
cache.Store(varname, d)
|
||||
return d
|
||||
}
|
||||
|
||||
func Int64(varname string, def int64) (d int64) {
|
||||
var err error
|
||||
func Int64(varname string, def int64) int64 {
|
||||
if v, ok := cache.Load(varname); ok {
|
||||
return v.(int64)
|
||||
}
|
||||
e := os.Getenv(varname)
|
||||
if e == "" {
|
||||
d = def
|
||||
} else {
|
||||
d, err = strconv.ParseInt(e, 10, 64)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return def
|
||||
}
|
||||
d, err := strconv.ParseInt(e, 10, 64)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
cache.Store(varname, d)
|
||||
return d
|
||||
}
|
||||
|
||||
func Bool(varname string, def bool) (d bool) {
|
||||
var err error
|
||||
func Bool(varname string, def bool) bool {
|
||||
if v, ok := cache.Load(varname); ok {
|
||||
return v.(bool)
|
||||
}
|
||||
e := os.Getenv(varname)
|
||||
if e == "" {
|
||||
d = def
|
||||
} else {
|
||||
d, err = strconv.ParseBool(e)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return def
|
||||
}
|
||||
d, err := strconv.ParseBool(e)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
cache.Store(varname, d)
|
||||
return d
|
||||
}
|
||||
|
||||
func String(varname string, def string) (d string) {
|
||||
func String(varname string, def string) string {
|
||||
if v, ok := cache.Load(varname); ok {
|
||||
return v.(string)
|
||||
}
|
||||
e := os.Getenv(varname)
|
||||
if e == "" {
|
||||
d = def
|
||||
} else {
|
||||
d = e
|
||||
return def
|
||||
}
|
||||
cache.Store(varname, d)
|
||||
return d
|
||||
cache.Store(varname, e)
|
||||
return e
|
||||
}
|
||||
|
||||
func Var(varname string, def flag.Value) (d interface{}) {
|
||||
func Var(varname string, def flag.Value) interface{} {
|
||||
|
||||
// use def's type to instantiate a new object of that same type
|
||||
// and call flag.Value.Set() on it
|
||||
@@ -116,43 +100,19 @@ func Var(varname string, def flag.Value) (d interface{}) {
|
||||
defElemType := defType.Elem()
|
||||
|
||||
if v, ok := cache.Load(varname); ok {
|
||||
return v
|
||||
return v.(string)
|
||||
}
|
||||
|
||||
e := os.Getenv(varname)
|
||||
if e == "" {
|
||||
d = def
|
||||
} else {
|
||||
newInstance := reflect.New(defElemType)
|
||||
if err := newInstance.Interface().(flag.Value).Set(e); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
d = newInstance.Interface()
|
||||
return def
|
||||
}
|
||||
|
||||
cache.Store(varname, d)
|
||||
return d
|
||||
}
|
||||
newInstance := reflect.New(defElemType)
|
||||
if err := newInstance.Interface().(flag.Value).Set(e); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
type Report struct {
|
||||
Entries []EntryReport
|
||||
}
|
||||
|
||||
type EntryReport struct {
|
||||
Var string
|
||||
Value string
|
||||
ValueGoType string
|
||||
}
|
||||
|
||||
func GetReport() *Report {
|
||||
var r Report
|
||||
cache.Range(func(key, value interface{}) bool {
|
||||
r.Entries = append(r.Entries, EntryReport{
|
||||
Var: key.(string),
|
||||
Value: fmt.Sprintf("%v", value),
|
||||
ValueGoType: fmt.Sprintf("%T", value),
|
||||
})
|
||||
return true
|
||||
})
|
||||
return &r
|
||||
res := newInstance.Interface()
|
||||
cache.Store(varname, res)
|
||||
return res
|
||||
}
|
||||
|
||||
@@ -32,27 +32,19 @@ func (m *ExampleVarType) Set(s string) error {
|
||||
|
||||
const EnvVarName = "ZREPL_ENVCONST_UNIT_TEST_VAR"
|
||||
|
||||
func TestVarDefaultValue(t *testing.T) {
|
||||
envconst.Reset()
|
||||
func TestVar(t *testing.T) {
|
||||
_, set := os.LookupEnv(EnvVarName)
|
||||
require.False(t, set)
|
||||
defer os.Unsetenv(EnvVarName)
|
||||
|
||||
val := envconst.Var(EnvVarName, &Var1)
|
||||
if &Var1 != val {
|
||||
t.Errorf("default value should be same address")
|
||||
t.Errorf("default value shut be same address")
|
||||
}
|
||||
}
|
||||
|
||||
func TestVarOverriddenValue(t *testing.T) {
|
||||
envconst.Reset()
|
||||
_, set := os.LookupEnv(EnvVarName)
|
||||
require.False(t, set)
|
||||
defer os.Unsetenv(EnvVarName)
|
||||
|
||||
err := os.Setenv(EnvVarName, "var2")
|
||||
require.NoError(t, err)
|
||||
|
||||
val := envconst.Var(EnvVarName, &Var1)
|
||||
val = envconst.Var(EnvVarName, &Var1)
|
||||
require.Equal(t, &Var2, val, "only structural identity is required for non-default vars")
|
||||
}
|
||||
|
||||
@@ -3,8 +3,6 @@ package version
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -31,21 +29,3 @@ func (i *ZreplVersionInformation) String() string {
|
||||
return fmt.Sprintf("zrepl version=%s GOOS=%s GOARCH=%s Compiler=%s",
|
||||
i.Version, i.RuntimeGOOS, i.RuntimeGOARCH, i.RUNTIMECompiler)
|
||||
}
|
||||
|
||||
var prometheusMetric = prometheus.NewUntypedFunc(
|
||||
prometheus.UntypedOpts{
|
||||
Namespace: "zrepl",
|
||||
Subsystem: "version",
|
||||
Name: "daemon",
|
||||
Help: "zrepl daemon version",
|
||||
ConstLabels: map[string]string{
|
||||
"raw": zreplVersion,
|
||||
"version_info": NewZreplVersionInformation().String(),
|
||||
},
|
||||
},
|
||||
func() float64 { return 1 },
|
||||
)
|
||||
|
||||
func PrometheusRegister(r prometheus.Registerer) {
|
||||
r.MustRegister(prometheusMetric)
|
||||
}
|
||||
|
||||
@@ -33,10 +33,9 @@ type DatasetPathVisit struct {
|
||||
Path *DatasetPath
|
||||
// If true, the dataset referenced by Path was not in the list of datasets to traverse
|
||||
FilledIn bool
|
||||
Parent *DatasetPathVisit
|
||||
}
|
||||
|
||||
type DatasetPathsVisitor func(v *DatasetPathVisit) (visitChildTree bool)
|
||||
type DatasetPathsVisitor func(v DatasetPathVisit) (visitChildTree bool)
|
||||
|
||||
// Traverse a list of DatasetPaths top down, i.e. given a set of datasets with same
|
||||
// path prefix, those with shorter prefix are traversed first.
|
||||
@@ -45,11 +44,7 @@ type DatasetPathsVisitor func(v *DatasetPathVisit) (visitChildTree bool)
|
||||
func (f *DatasetPathForest) WalkTopDown(visitor DatasetPathsVisitor) {
|
||||
|
||||
for _, r := range f.roots {
|
||||
r.WalkTopDown(&DatasetPathVisit{
|
||||
Path: &DatasetPath{nil},
|
||||
FilledIn: true,
|
||||
Parent: nil,
|
||||
}, visitor)
|
||||
r.WalkTopDown([]string{}, visitor)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -93,21 +88,19 @@ func (t *datasetPathTree) Add(p []string) bool {
|
||||
|
||||
}
|
||||
|
||||
func (t *datasetPathTree) WalkTopDown(parent *DatasetPathVisit, visitor DatasetPathsVisitor) {
|
||||
func (t *datasetPathTree) WalkTopDown(parent []string, visitor DatasetPathsVisitor) {
|
||||
|
||||
thisVisitPath := parent.Path.Copy()
|
||||
thisVisitPath.Extend(&DatasetPath{[]string{t.Component}})
|
||||
this := append(parent, t.Component)
|
||||
|
||||
thisVisit := &DatasetPathVisit{
|
||||
thisVisitPath,
|
||||
thisVisit := DatasetPathVisit{
|
||||
&DatasetPath{this},
|
||||
t.FilledIn,
|
||||
parent,
|
||||
}
|
||||
visitChildTree := visitor(thisVisit)
|
||||
|
||||
if visitChildTree {
|
||||
for _, c := range t.Children {
|
||||
c.WalkTopDown(thisVisit, visitor)
|
||||
c.WalkTopDown(this, visitor)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,8 +28,8 @@ func makeVisitRecorder() (v DatasetPathsVisitor, rec *visitRecorder) {
|
||||
rec = &visitRecorder{
|
||||
visits: make([]DatasetPathVisit, 0),
|
||||
}
|
||||
v = func(v *DatasetPathVisit) bool {
|
||||
rec.visits = append(rec.visits, *v)
|
||||
v = func(v DatasetPathVisit) bool {
|
||||
rec.visits = append(rec.visits, v)
|
||||
return true
|
||||
}
|
||||
return
|
||||
@@ -43,27 +43,6 @@ func buildForest(paths []*DatasetPath) (f *DatasetPathForest) {
|
||||
return
|
||||
}
|
||||
|
||||
type expectedDatasetPathVisit struct {
|
||||
expectPath *DatasetPath
|
||||
expectFillIn bool
|
||||
}
|
||||
|
||||
func (e *expectedDatasetPathVisit) AssertEqual(t *testing.T, actual DatasetPathVisit) {
|
||||
assert.Equal(t, e.expectPath, actual.Path)
|
||||
assert.Equal(t, e.expectFillIn, actual.FilledIn)
|
||||
if actual.Parent != nil {
|
||||
assert.Equal(t, actual.Parent.Path.Length()+1, e.expectPath.Length())
|
||||
assert.True(t, e.expectPath.HasPrefix(actual.Parent.Path))
|
||||
}
|
||||
}
|
||||
|
||||
func expectedDatasetPathVisits(t *testing.T, expected []expectedDatasetPathVisit, actual []DatasetPathVisit) {
|
||||
assert.Equal(t, len(expected), len(actual))
|
||||
for i := range expected {
|
||||
expected[i].AssertEqual(t, actual[i])
|
||||
}
|
||||
}
|
||||
|
||||
func TestDatasetPathForestWalkTopDown(t *testing.T) {
|
||||
|
||||
paths := []*DatasetPath{
|
||||
@@ -77,7 +56,7 @@ func TestDatasetPathForestWalkTopDown(t *testing.T) {
|
||||
|
||||
buildForest(paths).WalkTopDown(v)
|
||||
|
||||
expectedVisits := []expectedDatasetPathVisit{
|
||||
expectedVisits := []DatasetPathVisit{
|
||||
{toDatasetPath("pool1"), false},
|
||||
{toDatasetPath("pool1/foo"), true},
|
||||
{toDatasetPath("pool1/foo/bar"), false},
|
||||
@@ -86,7 +65,8 @@ func TestDatasetPathForestWalkTopDown(t *testing.T) {
|
||||
{toDatasetPath("pool2/test"), true},
|
||||
{toDatasetPath("pool2/test/bar"), false},
|
||||
}
|
||||
expectedDatasetPathVisits(t, expectedVisits, rec.visits)
|
||||
assert.Equal(t, expectedVisits, rec.visits)
|
||||
|
||||
}
|
||||
|
||||
func TestDatasetPathWalkTopDownWorksUnordered(t *testing.T) {
|
||||
@@ -102,7 +82,7 @@ func TestDatasetPathWalkTopDownWorksUnordered(t *testing.T) {
|
||||
|
||||
buildForest(paths).WalkTopDown(v)
|
||||
|
||||
expectedVisits := []expectedDatasetPathVisit{
|
||||
expectedVisits := []DatasetPathVisit{
|
||||
{toDatasetPath("pool1"), false},
|
||||
{toDatasetPath("pool1/foo"), true},
|
||||
{toDatasetPath("pool1/foo/bar"), false},
|
||||
@@ -111,6 +91,6 @@ func TestDatasetPathWalkTopDownWorksUnordered(t *testing.T) {
|
||||
{toDatasetPath("pool1/bang/baz"), false},
|
||||
}
|
||||
|
||||
expectedDatasetPathVisits(t, expectedVisits, rec.visits)
|
||||
assert.Equal(t, expectedVisits, rec.visits)
|
||||
|
||||
}
|
||||
|
||||
+5
-15
@@ -6,7 +6,6 @@ import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/zrepl/zrepl/zfs/zfscmd"
|
||||
)
|
||||
|
||||
@@ -79,23 +78,14 @@ func ZFSGetFilesystemPlaceholderState(ctx context.Context, p *DatasetPath) (stat
|
||||
return state, nil
|
||||
}
|
||||
|
||||
func ZFSCreatePlaceholderFilesystem(ctx context.Context, fs *DatasetPath, parent *DatasetPath) (err error) {
|
||||
if fs.Length() == 1 {
|
||||
return fmt.Errorf("cannot create %q: pools cannot be created with zfs create", fs.ToString())
|
||||
func ZFSCreatePlaceholderFilesystem(ctx context.Context, p *DatasetPath) (err error) {
|
||||
if p.Length() == 1 {
|
||||
return fmt.Errorf("cannot create %q: pools cannot be created with zfs create", p.ToString())
|
||||
}
|
||||
|
||||
cmdline := []string{
|
||||
"create",
|
||||
cmd := zfscmd.CommandContext(ctx, ZFS_BINARY, "create",
|
||||
"-o", fmt.Sprintf("%s=%s", PlaceholderPropertyName, placeholderPropertyOn),
|
||||
"-o", "mountpoint=none",
|
||||
}
|
||||
if parentEncrypted, err := ZFSGetEncryptionEnabled(ctx, parent.ToString()); err != nil {
|
||||
return errors.Wrap(err, "cannot determine encryption support")
|
||||
} else if parentEncrypted {
|
||||
cmdline = append(cmdline, "-o", "encryption=off")
|
||||
}
|
||||
cmdline = append(cmdline, fs.ToString())
|
||||
cmd := zfscmd.CommandContext(ctx, ZFS_BINARY, cmdline...)
|
||||
p.ToString())
|
||||
|
||||
stdio, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
|
||||
@@ -115,6 +115,8 @@ func (v FilesystemVersion) RelName() string {
|
||||
}
|
||||
func (v FilesystemVersion) String() string { return v.RelName() }
|
||||
|
||||
func (v FilesystemVersion) GetCreation() time.Time { return v.Creation }
|
||||
|
||||
// Only takes into account those attributes of FilesystemVersion that
|
||||
// are immutable over time in ZFS.
|
||||
func FilesystemVersionEqualIdentity(a, b FilesystemVersion) bool {
|
||||
|
||||
+44
-41
@@ -860,9 +860,9 @@ type DrySendInfo struct {
|
||||
var (
|
||||
// keep same number of capture groups for unmarshalInfoLine homogeneity
|
||||
|
||||
sendDryRunInfoLineRegexFull = regexp.MustCompile(`^(?P<type>full)\t()(?P<to>[^\t]+@[^\t]+)(\t(?P<size>[0-9]+))?$`)
|
||||
sendDryRunInfoLineRegexFull = regexp.MustCompile(`^(full)\t()([^\t]+@[^\t]+)\t([0-9]+)$`)
|
||||
// cannot enforce '[#@]' in incremental source, see test cases
|
||||
sendDryRunInfoLineRegexIncremental = regexp.MustCompile(`^(?P<type>incremental)\t(?P<from>[^\t]+)\t(?P<to>[^\t]+@[^\t]+)(\t(?P<size>[0-9]+))?$`)
|
||||
sendDryRunInfoLineRegexIncremental = regexp.MustCompile(`^(incremental)\t([^\t]+)\t([^\t]+@[^\t]+)\t([0-9]+)$`)
|
||||
)
|
||||
|
||||
// see test cases for example output
|
||||
@@ -890,44 +890,30 @@ func (s *DrySendInfo) unmarshalInfoLine(l string) (regexMatched bool, err error)
|
||||
|
||||
mFull := sendDryRunInfoLineRegexFull.FindStringSubmatch(l)
|
||||
mInc := sendDryRunInfoLineRegexIncremental.FindStringSubmatch(l)
|
||||
var matchingExpr *regexp.Regexp
|
||||
var m []string
|
||||
if mFull == nil && mInc == nil {
|
||||
return false, nil
|
||||
} else if mFull != nil && mInc != nil {
|
||||
panic(fmt.Sprintf("ambiguous ZFS dry send output: %q", l))
|
||||
} else if mFull != nil {
|
||||
matchingExpr, m = sendDryRunInfoLineRegexFull, mFull
|
||||
m = mFull
|
||||
} else if mInc != nil {
|
||||
matchingExpr, m = sendDryRunInfoLineRegexIncremental, mInc
|
||||
m = mInc
|
||||
}
|
||||
|
||||
fields := make(map[string]string, matchingExpr.NumSubexp())
|
||||
for i, name := range matchingExpr.SubexpNames() {
|
||||
if i != 0 {
|
||||
fields[name] = m[i]
|
||||
}
|
||||
}
|
||||
|
||||
s.Type, err = DrySendTypeFromString(fields["type"])
|
||||
s.Type, err = DrySendTypeFromString(m[1])
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
|
||||
s.From = fields["from"]
|
||||
s.To = fields["to"]
|
||||
s.From = m[2]
|
||||
s.To = m[3]
|
||||
toFS, _, _, err := DecomposeVersionString(s.To)
|
||||
if err != nil {
|
||||
return true, fmt.Errorf("'to' is not a valid filesystem version: %s", err)
|
||||
}
|
||||
s.Filesystem = toFS
|
||||
|
||||
if fields["size"] == "" {
|
||||
// workaround for OpenZFS 0.7 prior to https://github.com/openzfs/zfs/commit/835db58592d7d947e5818eb7281882e2a46073e0#diff-66bd524398bcd2ac70d90925ab6d8073L1245
|
||||
// see https://github.com/zrepl/zrepl/issues/289
|
||||
fields["size"] = "0"
|
||||
}
|
||||
s.SizeEstimate, err = strconv.ParseInt(fields["size"], 10, 64)
|
||||
s.SizeEstimate, err = strconv.ParseInt(m[4], 10, 64)
|
||||
if err != nil {
|
||||
return true, fmt.Errorf("cannot not parse size: %s", err)
|
||||
}
|
||||
@@ -1514,6 +1500,30 @@ func tryParseDestroySnapshotsError(arg string, stderr []byte) *DestroySnapshotsE
|
||||
}
|
||||
}
|
||||
|
||||
type ErrDestroySnapshotDatasetIsBusy struct {
|
||||
*DestroySnapshotsError
|
||||
Name string
|
||||
}
|
||||
|
||||
var _ error = (*ErrDestroySnapshotDatasetIsBusy)(nil)
|
||||
|
||||
func tryErrDestroySnapshotDatasetIsBusy(arg string, stderr []byte) *ErrDestroySnapshotDatasetIsBusy {
|
||||
dsne := tryParseDestroySnapshotsError(arg, stderr)
|
||||
if dsne == nil {
|
||||
return nil
|
||||
}
|
||||
if len(dsne.Reason) != 1 {
|
||||
return nil
|
||||
}
|
||||
if dsne.Reason[0] == "dataset is busy" {
|
||||
return &ErrDestroySnapshotDatasetIsBusy{
|
||||
DestroySnapshotsError: dsne,
|
||||
Name: dsne.Undestroyable[0],
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ZFSDestroy(ctx context.Context, arg string) (err error) {
|
||||
|
||||
var dstype, filesystem string
|
||||
@@ -1547,6 +1557,8 @@ func ZFSDestroy(ctx context.Context, arg string) (err error) {
|
||||
err = &DatasetDoesNotExist{arg}
|
||||
} else if dsNotExistErr := tryDatasetDoesNotExist(filesystem, stdio); dsNotExistErr != nil {
|
||||
err = dsNotExistErr
|
||||
} else if dsBusy := tryErrDestroySnapshotDatasetIsBusy(arg, stdio); dsBusy != nil {
|
||||
err = dsBusy
|
||||
} else if dserr := tryParseDestroySnapshotsError(arg, stdio); dserr != nil {
|
||||
err = dserr
|
||||
}
|
||||
@@ -1607,11 +1619,10 @@ var ErrBookmarkCloningNotSupported = fmt.Errorf("bookmark cloning feature is not
|
||||
|
||||
// idempotently create bookmark of the given version v
|
||||
//
|
||||
// if `v` is a bookmark, returns ErrBookmarkCloningNotSupported
|
||||
// unless a bookmark with the name `bookmark` exists and has the same idenitty (zfs.FilesystemVersionEqualIdentity)
|
||||
//
|
||||
// v must be validated by the caller
|
||||
//
|
||||
// does not destroy an existing bookmark, returns
|
||||
//
|
||||
func ZFSBookmark(ctx context.Context, fs string, v FilesystemVersion, bookmark string) (bm FilesystemVersion, err error) {
|
||||
|
||||
bm = FilesystemVersion{
|
||||
@@ -1627,21 +1638,7 @@ func ZFSBookmark(ctx context.Context, fs string, v FilesystemVersion, bookmark s
|
||||
promTimer := prometheus.NewTimer(prom.ZFSBookmarkDuration.WithLabelValues(fs))
|
||||
defer promTimer.ObserveDuration()
|
||||
|
||||
bookmarkname := fmt.Sprintf("%s#%s", fs, bookmark)
|
||||
if err := EntityNamecheck(bookmarkname, EntityTypeBookmark); err != nil {
|
||||
return bm, err
|
||||
}
|
||||
|
||||
if v.IsBookmark() {
|
||||
existingBm, err := ZFSGetFilesystemVersion(ctx, bookmarkname)
|
||||
if _, ok := err.(*DatasetDoesNotExist); ok {
|
||||
return bm, ErrBookmarkCloningNotSupported
|
||||
} else if err != nil {
|
||||
return bm, errors.Wrap(err, "bookmark: idempotency check for bookmark cloning")
|
||||
}
|
||||
if FilesystemVersionEqualIdentity(bm, existingBm) {
|
||||
return existingBm, nil
|
||||
}
|
||||
if !v.IsSnapshot() {
|
||||
return bm, ErrBookmarkCloningNotSupported // TODO This is work in progress: https://github.com/zfsonlinux/zfs/pull/9571
|
||||
}
|
||||
|
||||
@@ -1649,6 +1646,12 @@ func ZFSBookmark(ctx context.Context, fs string, v FilesystemVersion, bookmark s
|
||||
if err := EntityNamecheck(snapname, EntityTypeSnapshot); err != nil {
|
||||
return bm, err
|
||||
}
|
||||
bookmarkname := fmt.Sprintf("%s#%s", fs, bookmark)
|
||||
if err := EntityNamecheck(bookmarkname, EntityTypeBookmark); err != nil {
|
||||
return bm, err
|
||||
}
|
||||
|
||||
debug("bookmark: %q %q", snapname, bookmarkname)
|
||||
|
||||
cmd := zfscmd.CommandContext(ctx, ZFS_BINARY, "bookmark", snapname, bookmarkname)
|
||||
stdio, err := cmd.CombinedOutput()
|
||||
@@ -1660,7 +1663,7 @@ func ZFSBookmark(ctx context.Context, fs string, v FilesystemVersion, bookmark s
|
||||
// check if this was idempotent
|
||||
bookGuid, err := ZFSGetGUID(ctx, fs, "#"+bookmark)
|
||||
if err != nil {
|
||||
return bm, errors.Wrap(err, "bookmark: idempotency check for bookmark creation") // guid error expressive enough
|
||||
return bm, errors.Wrap(err, "bookmark idempotency check") // guid error expressive enough
|
||||
}
|
||||
|
||||
if v.Guid == bookGuid {
|
||||
|
||||
+1
-34
@@ -136,18 +136,6 @@ size 10511856
|
||||
fullNoToken := `
|
||||
full zroot/test/a@3 10518512
|
||||
size 10518512
|
||||
`
|
||||
|
||||
// zero-length incremental send on ZoL 0.7.12
|
||||
// (it omits the size field as well as the size line if size is 0)
|
||||
// see https://github.com/zrepl/zrepl/issues/289
|
||||
// fixed in https://github.com/openzfs/zfs/commit/835db58592d7d947e5818eb7281882e2a46073e0#diff-66bd524398bcd2ac70d90925ab6d8073L1245
|
||||
incZeroSized_0_7_12 := `
|
||||
incremental p1 with/ spaces d1@1 with space p1 with/ spaces d1@2 with space
|
||||
`
|
||||
|
||||
fullZeroSized_0_7_12 := `
|
||||
full p1 with/ spaces d1@2 with space
|
||||
`
|
||||
|
||||
fullWithSpaces := "\nfull\tpool1/otherjob/ds with spaces@blaffoo\t12912\nsize\t12912\n"
|
||||
@@ -255,25 +243,6 @@ full p1 with/ spaces d1@2 with space
|
||||
SizeEstimate: 624,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "incrementalZeroSizedOpenZFS_pre0.7.12", in: incZeroSized_0_7_12,
|
||||
exp: &DrySendInfo{
|
||||
Type: DrySendTypeIncremental,
|
||||
Filesystem: "p1 with/ spaces d1",
|
||||
From: "p1 with/ spaces d1@1 with space",
|
||||
To: "p1 with/ spaces d1@2 with space",
|
||||
SizeEstimate: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "fullZeroSizedOpenZFS_pre0.7.12", in: fullZeroSized_0_7_12,
|
||||
exp: &DrySendInfo{
|
||||
Type: DrySendTypeFull,
|
||||
Filesystem: "p1 with/ spaces d1",
|
||||
To: "p1 with/ spaces d1@2 with space",
|
||||
SizeEstimate: 0,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tcs {
|
||||
@@ -282,12 +251,10 @@ full p1 with/ spaces d1@2 with space
|
||||
in := tc.in[1:] // strip first newline
|
||||
var si DrySendInfo
|
||||
err := si.unmarshalZFSOutput([]byte(in))
|
||||
t.Logf("%#v", &si)
|
||||
t.Logf("err=%T %s", err, err)
|
||||
|
||||
if tc.expErr {
|
||||
assert.Error(t, err)
|
||||
}
|
||||
t.Logf("%#v", &si)
|
||||
if tc.exp != nil {
|
||||
assert.Equal(t, tc.exp, &si)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user