Compare commits

..

1 Commits

Author SHA1 Message Date
Christian Schwarz 3cf48852e9 issue 611: add debug logging that periodically dumps remaining time, from daemon's perspective 2022-07-11 01:36:28 +02:00
121 changed files with 3668 additions and 7481 deletions
+177 -102
View File
@@ -1,8 +1,5 @@
version: 2.1 version: 2.1
orbs:
# NB: 1.7.2 is not the Go version, but the Orb version
# https://circleci.com/developer/orbs/orb/circleci/go#usage-go-modules-cache
go: circleci/go@1.7.2
commands: commands:
setup-home-local-bin: setup-home-local-bin:
steps: steps:
@@ -27,12 +24,18 @@ commands:
apt-update-and-install-common-deps: apt-update-and-install-common-deps:
steps: steps:
- run: sudo apt-get update - run: sudo apt update && sudo apt install gawk make
- run: sudo apt-get install -y gawk make
# CircleCI doesn't update its cimg/go images. restore-cache-gomod:
# So, need to update manually to get up-to-date trust chains. steps:
# The need for this was required for cimg/go:1.12, but let's future proof this here and now. - restore_cache:
- run: sudo apt-get install -y git ca-certificates key: go-mod-v4-{{ checksum "go.sum" }}
save-cache-gomod:
steps:
- save_cache:
key: go-mod-v4-{{ checksum "go.sum" }}
paths:
- "/go/pkg/mod"
install-godep: install-godep:
steps: steps:
@@ -47,48 +50,94 @@ commands:
- invoke-lazy-sh: - invoke-lazy-sh:
subcommand: docdep subcommand: docdep
docs-publish-sh: download-and-install-minio-client:
steps:
- setup-home-local-bin
- restore_cache:
key: minio-client-v2
- run:
shell: /bin/bash -eo pipefail
command: |
if which mc; then exit 0; fi
sudo curl -sSL https://dl.min.io/client/mc/release/linux-amd64/archive/mc.RELEASE.2020-08-20T00-23-01Z \
-o "$HOME/.local/bin/mc"
sudo chmod +x "$HOME/.local/bin/mc"
- save_cache:
key: minio-client-v2
paths:
- "$HOME/.local/bin/mc"
upload-minio:
parameters: parameters:
push: src:
type: boolean type: string
dst:
type: string
steps: steps:
- checkout
- run: - run:
shell: /bin/bash -eo pipefail
when: always
command: | command: |
git config --global user.email "zreplbot@cschwarz.com" if [ -n "$CIRCLE_PR_NUMBER" ]; then # CIRCLE_PR_NUMBER is guaranteed to be only present in forked PRs (external)
git config --global user.name "zrepl-github-io-ci" echo "Forked PR detected. Sry, can't trust you with credentials to external artifact store, use CircleCI's instead."
exit 0
fi
set -u # from now on
# if we're pushing, we need to add the deploy key mc config host add --api s3v4 zrepl-minio https://minio.cschwarz.com ${MINIO_ACCESS_KEY} ${MINIO_SECRET_KEY}
# which is stored as "Additional SSH Keys" in the CircleCI project settings.
# We can't use the CircleCI-manage deploy key because we're pushing # keep in sync with set-github-minio-status
# to a different repo than the one we're building. jobprefix=zrepl-ci-artifacts/${CIRCLE_SHA1}-pipeline-<<pipeline.number>>/${CIRCLE_JOB}
- when:
condition: << parameters.push >> # Upload artifacts
mkdir -p ./artifacts
mc cp -r <<parameters.src>> "zrepl-minio/$jobprefix/<<parameters.dst>>"
set-github-minio-status:
parameters:
context:
type: string
description:
type: string
minio-dst:
type: string
steps: steps:
# https://circleci.com/docs/2.0/add-ssh-key/#adding-multiple-keys-with-blank-hostnames
- run: ssh-add -D
# the default circleci ssh config only additional ssh keys for Host !github.com
- run: - run:
shell: /bin/bash -eo pipefail
command: | command: |
cat > ~/.ssh/config \<<EOF if [ -n "$CIRCLE_PR_NUMBER" ]; then # CIRCLE_PR_NUMBER is guaranteed to be only present in forked PRs (external)
Host * echo "Forked PR detected. Sry, can't trust you with credentials to external artifact store, use CircleCI's instead."
IdentityFile /home/circleci/.ssh/id_rsa_458e62c517f6c480e40452126ce47421 exit 0
EOF fi
- add_ssh_keys: set -u # from now on
fingerprints:
# deploy key for zrepl.github.io
- "45:8e:62:c5:17:f6:c4:80:e4:04:52:12:6c:e4:74:21"
# caller must install-docdep # keep in sync with with upload-minio command
- when: jobprefix=zrepl-ci-artifacts/${CIRCLE_SHA1}-pipeline-<<pipeline.number>>/${CIRCLE_JOB}
condition: << parameters.push >> # Push Artifact Link to GitHub
REPO="zrepl/zrepl"
COMMIT="${CIRCLE_SHA1}"
JOB_NAME="${CIRCLE_JOB}"
CONTEXT="<<parameters.context>>"
DESCRIPTION="<<parameters.description>>"
TARGETURL=https://minio.cschwarz.com/minio/"$jobprefix"/"<<parameters.minio-dst>>"
curl "https://api.github.com/repos/$REPO/statuses/$COMMIT" \
-H "Content-Type: application/json" \
-H "Authorization: token $GITHUB_COMMIT_STATUS_TOKEN" \
-X POST \
-d '{"context":"'"$CONTEXT"'", "state": "success", "description":"'"$DESCRIPTION"'", "target_url":"'"$TARGETURL"'"}'
trigger-pipeline:
parameters:
body_no_shell_subst:
type: string
steps: steps:
- run: bash -x docs/publish.sh -c -a -P - run: |
- when: curl -X POST https://circleci.com/api/v2/project/github/zrepl/zrepl/pipeline \
condition: -H 'Content-Type: application/json' \
not: << parameters.push >> -H 'Accept: application/json' \
steps: -H "Circle-Token: $ZREPL_BOT_CIRCLE_TOKEN" \
- run: bash -x docs/publish.sh -c -a --data '<<parameters.body_no_shell_subst>>'
parameters: parameters:
do_ci: do_ci:
@@ -101,7 +150,7 @@ parameters:
release_docker_baseimage_tag: release_docker_baseimage_tag:
type: string type: string
default: "1.21" default: "1.17"
workflows: workflows:
version: 2 version: 2
@@ -111,19 +160,19 @@ workflows:
jobs: jobs:
- quickcheck-docs - quickcheck-docs
- quickcheck-go: &quickcheck-go-smoketest - quickcheck-go: &quickcheck-go-smoketest
name: quickcheck-go-amd64-linux-1.21 name: quickcheck-go-amd64-linux-1.17
goversion: &latest-go-release "1.21" goversion: &latest-go-release "1.17"
goos: linux goos: linux
goarch: amd64 goarch: amd64
- test-go-on-latest-go-release: - test-go-on-latest-go-release:
goversion: *latest-go-release goversion: *latest-go-release
- quickcheck-go: - quickcheck-go:
requires: requires:
- quickcheck-go-amd64-linux-1.21 #quickcheck-go-smoketest.name - quickcheck-go-amd64-linux-1.17 #quickcheck-go-smoketest.name
matrix: &quickcheck-go-matrix matrix: &quickcheck-go-matrix
alias: quickcheck-go-matrix alias: quickcheck-go-matrix
parameters: parameters:
goversion: [*latest-go-release, "1.20"] goversion: [*latest-go-release, "1.12"]
goos: ["linux", "freebsd"] goos: ["linux", "freebsd"]
goarch: ["amd64", "arm64"] goarch: ["amd64", "arm64"]
exclude: exclude:
@@ -131,14 +180,10 @@ workflows:
- goversion: *latest-go-release - goversion: *latest-go-release
goos: linux goos: linux
goarch: amd64 goarch: amd64
- platformtest: # not supported by Go 1.12
matrix: - goversion: "1.12"
parameters: goos: freebsd
goversion: [*latest-go-release] goarch: arm64
goos: ["linux"]
goarch: ["amd64"]
requires:
- quickcheck-go-<< matrix.goarch >>-<< matrix.goos >>-<< matrix.goversion >>
release: release:
when: << pipeline.parameters.do_release >> when: << pipeline.parameters.do_release >>
@@ -156,7 +201,20 @@ workflows:
- release-deb - release-deb
- release-rpm - release-rpm
publish-zrepl.github.io: periodic:
triggers:
- schedule:
cron: "00 17 * * *"
filters:
branches:
only:
- master
- stable
- problame/circleci-build
jobs:
- periodic-full-pipeline-run
zrepl.github.io:
jobs: jobs:
- publish-zrepl-github-io: - publish-zrepl-github-io:
filters: filters:
@@ -167,15 +225,20 @@ workflows:
jobs: jobs:
quickcheck-docs: quickcheck-docs:
docker: docker:
- image: cimg/base:2023.09 - image: cimg/base:2020.08
steps: steps:
- checkout - checkout
- install-docdep - install-docdep
# do the current docs build
- run: make docs - run: make docs
# does the publish.sh script still work?
- docs-publish-sh: - download-and-install-minio-client
push: false - upload-minio:
src: artifacts
dst: ""
- set-github-minio-status:
context: artifacts/${CIRCLE_JOB}
description: artifacts of CI job ${CIRCLE_JOB}
minio-dst: ""
quickcheck-go: quickcheck-go:
parameters: parameters:
@@ -186,7 +249,7 @@ jobs:
goarch: goarch:
type: string type: string
docker: docker:
- image: cimg/go:<<parameters.goversion>> - image: circleci/golang:<<parameters.goversion>>
environment: environment:
GOOS: <<parameters.goos>> GOOS: <<parameters.goos>>
GOARCH: <<parameters.goarch>> GOARCH: <<parameters.goarch>>
@@ -194,62 +257,41 @@ jobs:
steps: steps:
- checkout - checkout
- go/load-cache: - restore-cache-gomod
key: quickcheck-<<parameters.goversion>>
- install-godep
- run: go mod download - run: go mod download
- run: cd build && go mod download - run: cd build && go mod download
- go/save-cache: - save-cache-gomod
key: quickcheck-<<parameters.goversion>>
- install-godep
- run: make formatcheck - run: make formatcheck
- run: make generate-platform-test-list - run: make generate-platform-test-list
- run: make zrepl-bin test-platform-bin - run: make zrepl-bin test-platform-bin
- run: make vet - run: make vet
- run: make lint - run: make lint
- download-and-install-minio-client
- run: rm -f artifacts/generate-platform-test-list - run: rm -f artifacts/generate-platform-test-list
- store_artifacts: - store_artifacts:
path: artifacts path: artifacts
- persist_to_workspace: - upload-minio:
root: . src: artifacts
paths: [.] dst: ""
- set-github-minio-status:
platformtest: context: artifacts/${CIRCLE_JOB}
parameters: description: artifacts of CI job ${CIRCLE_JOB}
goversion: minio-dst: ""
type: string
goos:
type: string
goarch:
type: string
machine:
image: ubuntu-2204:current
resource_class: medium
environment:
GOOS: <<parameters.goos>>
GOARCH: <<parameters.goarch>>
steps:
- attach_workspace:
at: .
- run: sudo apt-get update
- run: sudo apt-get install -y zfsutils-linux
- run: sudo zfs version
- run: sudo make test-platform GOOS="$GOOS" GOARCH="$GOARCH"
test-go-on-latest-go-release: test-go-on-latest-go-release:
parameters: parameters:
goversion: goversion:
type: string type: string
docker: docker:
- image: cimg/go:<<parameters.goversion>> - image: circleci/golang:<<parameters.goversion>>
steps: steps:
- checkout - checkout
- go/load-cache: - restore-cache-gomod
key: make-test-go
- run: make test-go - run: make test-go
- go/save-cache: # don't save-cache-gomod here, test-go doesn't pull all the dependencies
key: make-test-go
release-build: release-build:
machine: machine:
@@ -290,15 +332,48 @@ jobs:
steps: steps:
- attach_workspace: - attach_workspace:
at: . at: .
- run: make wrapup-and-checksum
- store_artifacts: - store_artifacts:
path: artifacts path: artifacts
- download-and-install-minio-client
- upload-minio:
src: artifacts
dst: ""
- set-github-minio-status:
context: artifacts/release
description: CI-generated release artifacts
minio-dst: ""
periodic-full-pipeline-run:
docker:
- image: cimg/base:2020.08
steps:
- trigger-pipeline:
body_no_shell_subst: '{"branch":"<<pipeline.git.branch>>", "parameters": { "do_ci": true, "do_release": true }}'
publish-zrepl-github-io: publish-zrepl-github-io:
docker: docker:
- image: cimg/base:2023.09 - image: cimg/python:3.7
steps: steps:
- checkout - checkout
- install-docdep - invoke-lazy-sh:
- docs-publish-sh: subcommand: docdep
push: true - run:
command: |
git config --global user.email "zreplbot@cschwarz.com"
git config --global user.name "zrepl-github-io-ci"
# https://circleci.com/docs/2.0/add-ssh-key/#adding-multiple-keys-with-blank-hostnames
- run: ssh-add -D
# the default circleci ssh config only additional ssh keys for Host !github.com
- run:
command: |
cat > ~/.ssh/config \<<EOF
Host *
IdentityFile /home/circleci/.ssh/id_rsa_458e62c517f6c480e40452126ce47421
EOF
- add_ssh_keys:
fingerprints:
# deploy key for zrepl.github.io
- "45:8e:62:c5:17:f6:c4:80:e4:04:52:12:6c:e4:74:21"
- run: bash -x docs/publish.sh -c -a
-84
View File
@@ -1,84 +0,0 @@
import argparse
from pathlib import Path
import re
import requests
import time
import os
import argparse
from pathlib import Path
circle_token = os.environ.get('CIRCLE_TOKEN')
if not circle_token:
raise ValueError('CIRCLE_TOKEN environment variable not set')
parser = argparse.ArgumentParser(description='Download artifacts from CircleCI')
parser.add_argument('build_num', type=str, help='Build number')
parser.add_argument('dst', type=Path, help='Destination directory')
parser.add_argument('--prefix', type=str, default='', help='Filter for prefix')
parser.add_argument('--match', type=str, default='.*', help='Only include paths matching the given regex')
args = parser.parse_args()
res = requests.get(
f"https://circleci.com/api/v1.1/project/github/zrepl/zrepl/{args.build_num}/artifacts",
headers={
"Circle-Token": circle_token,
},
)
res.raise_for_status()
# https://circleci.com/docs/api/v1/index.html#artifacts-of-a-job
# [ {
# "path" : "raw-test-output/go-test-report.xml",
# "pretty_path" : "raw-test-output/go-test-report.xml",
# "node_index" : 0,
# "url" : "https://24-88881093-gh.circle-artifacts.com/0/raw-test-output/go-test-report.xml"
# }, {
# "path" : "raw-test-output/go-test.out",
# "pretty_path" : "raw-test-output/go-test.out",
# "node_index" : 0,
# "url" : "https://24-88881093-gh.circle-artifacts.com/0/raw-test-output/go-test.out"
# } ]
res = res.json()
for artifact in res:
if not artifact["pretty_path"].startswith(args.prefix):
continue
if not re.match(args.match, artifact["pretty_path"]):
continue
stripped = artifact["pretty_path"][len(args.prefix):]
print(f"Downloading {artifact['pretty_path']} to {args.dst / stripped}")
artifact_rel = Path(stripped)
artifact_dst = args.dst / artifact_rel
artifact_dst.parent.mkdir(parents=True, exist_ok=True)
res = requests.get(
artifact["url"],
headers={
"Circle-Token": circle_token,
},
stream=True,
)
res.raise_for_status()
total_size = int(res.headers.get("Content-Length", 0))
block_size = 128 * 1024
with open(artifact_dst, "wb") as f:
progress = 0
start_time = time.time()
for chunk in res.iter_content(chunk_size=block_size):
f.write(chunk)
progress += len(chunk)
percent = progress / total_size * 100
elapsed_time = time.time() - start_time
if elapsed_time >= 5:
print(f"Downloaded {progress}/{total_size} bytes ({percent:.2f}%)", end="\r")
start_time = time.time()
print(f"Downloaded {progress}/{total_size} bytes ({percent:.2f}%)")
print("Download complete!")
print("All files downloaded")
-6
View File
@@ -7,10 +7,4 @@ issues:
- path: _test\.go - path: _test\.go
linters: linters:
- errcheck - errcheck
# Disable staticcheck 'Empty body in an if or else branch' as it's useful
# to put a comment into an empty else-clause that explains why whatever
# is done in the if-caluse is not necessary if the condition is false.
- linters:
- staticcheck
text: "SA9003:"
+45 -65
View File
@@ -1,5 +1,5 @@
.PHONY: generate build test vet cover release docs docs-clean clean format lint platformtest .PHONY: generate build test vet cover release docs docs-clean clean format lint platformtest
.PHONY: release release-noarch .PHONY: release bins-all release-noarch
.DEFAULT_GOAL := zrepl-bin .DEFAULT_GOAL := zrepl-bin
ARTIFACTDIR := artifacts ARTIFACTDIR := artifacts
@@ -14,15 +14,13 @@ ifndef _ZREPL_VERSION
endif endif
endif endif
ZREPL_PACKAGE_RELEASE := 1
GO := go GO := go
GOOS ?= $(shell bash -c 'source <($(GO) env) && echo "$$GOOS"') GOOS ?= $(shell bash -c 'source <($(GO) env) && echo "$$GOOS"')
GOARCH ?= $(shell bash -c 'source <($(GO) env) && echo "$$GOARCH"') GOARCH ?= $(shell bash -c 'source <($(GO) env) && echo "$$GOARCH"')
GOARM ?= $(shell bash -c 'source <($(GO) env) && echo "$$GOARM"') GOARM ?= $(shell bash -c 'source <($(GO) env) && echo "$$GOARM"')
GOHOSTOS ?= $(shell bash -c 'source <($(GO) env) && echo "$$GOHOSTOS"') GOHOSTOS ?= $(shell bash -c 'source <($(GO) env) && echo "$$GOHOSTOS"')
GOHOSTARCH ?= $(shell bash -c 'source <($(GO) env) && echo "$$GOHOSTARCH"') GOHOSTARCH ?= $(shell bash -c 'source <($(GO) env) && echo "$$GOHOSTARCH"')
GO_ENV_VARS := GO111MODULE=on CGO_ENABLED=0 GO_ENV_VARS := GO111MODULE=on
GO_LDFLAGS := "-X github.com/zrepl/zrepl/version.zreplVersion=$(_ZREPL_VERSION)" GO_LDFLAGS := "-X github.com/zrepl/zrepl/version.zreplVersion=$(_ZREPL_VERSION)"
GO_MOD_READONLY := -mod=readonly GO_MOD_READONLY := -mod=readonly
GO_EXTRA_BUILDFLAGS := GO_EXTRA_BUILDFLAGS :=
@@ -30,7 +28,7 @@ GO_BUILDFLAGS := $(GO_MOD_READONLY) $(GO_EXTRA_BUILDFLAGS)
GO_BUILD := $(GO_ENV_VARS) $(GO) build $(GO_BUILDFLAGS) -ldflags $(GO_LDFLAGS) GO_BUILD := $(GO_ENV_VARS) $(GO) build $(GO_BUILDFLAGS) -ldflags $(GO_LDFLAGS)
GOLANGCI_LINT := golangci-lint GOLANGCI_LINT := golangci-lint
GOCOVMERGE := gocovmerge GOCOVMERGE := gocovmerge
RELEASE_DOCKER_BASEIMAGE_TAG ?= 1.21 RELEASE_DOCKER_BASEIMAGE_TAG ?= 1.17
RELEASE_DOCKER_BASEIMAGE ?= golang:$(RELEASE_DOCKER_BASEIMAGE_TAG) RELEASE_DOCKER_BASEIMAGE ?= golang:$(RELEASE_DOCKER_BASEIMAGE_TAG)
ifneq ($(GOARM),) ifneq ($(GOARM),)
@@ -52,20 +50,21 @@ printvars:
release: clean release: clean
# no cross-platform support for target test # no cross-platform support for target test
$(MAKE) test-go $(MAKE) test-go
$(MAKE) _run_make_foreach_target_tuple RUN_MAKE_FOREACH_TARGET_TUPLE_ARG="vet" $(MAKE) bins-all
$(MAKE) _run_make_foreach_target_tuple RUN_MAKE_FOREACH_TARGET_TUPLE_ARG="lint"
$(MAKE) _run_make_foreach_target_tuple RUN_MAKE_FOREACH_TARGET_TUPLE_ARG="zrepl-bin"
$(MAKE) _run_make_foreach_target_tuple RUN_MAKE_FOREACH_TARGET_TUPLE_ARG="test-platform-bin"
$(MAKE) noarch $(MAKE) noarch
$(MAKE) wrapup-and-checksum
$(MAKE) check-git-clean
ifeq (SIGN, 1)
$(make) sign
endif
@echo "ZREPL RELEASE ARTIFACTS AVAILABLE IN artifacts/release"
release-docker: $(ARTIFACTDIR) release-docker: $(ARTIFACTDIR)
sed 's/FROM.*!SUBSTITUTED_BY_MAKEFILE/FROM $(RELEASE_DOCKER_BASEIMAGE)/' build.Dockerfile > artifacts/release-docker.Dockerfile sed 's/FROM.*!SUBSTITUTED_BY_MAKEFILE/FROM $(RELEASE_DOCKER_BASEIMAGE)/' build.Dockerfile > artifacts/release-docker.Dockerfile
docker build -t zrepl_release --pull -f artifacts/release-docker.Dockerfile . docker build -t zrepl_release --pull -f artifacts/release-docker.Dockerfile .
docker run --rm -i -v $(CURDIR):/src -u $$(id -u):$$(id -g) \ docker run --rm -i -v $(CURDIR):/src -u $$(id -u):$$(id -g) \
zrepl_release \ zrepl_release \
make release \ make release GOOS=$(GOOS) GOARCH=$(GOARCH) GOARM=$(GOARM)
GOOS=$(GOOS) GOARCH=$(GOARCH) GOARM=$(GOARM) \
ZREPL_VERSION=$(ZREPL_VERSION) ZREPL_PACKAGE_RELEASE=$(ZREPL_PACKAGE_RELEASE)
debs-docker: debs-docker:
$(MAKE) _debs_or_rpms_docker _DEB_OR_RPM=deb $(MAKE) _debs_or_rpms_docker _DEB_OR_RPM=deb
@@ -82,14 +81,9 @@ rpm: $(ARTIFACTDIR) # artifacts/_zrepl.zsh_completion artifacts/bash_completion
$(eval _ZREPL_RPM_TOPDIR_ABS := $(CURDIR)/$(ARTIFACTDIR)/rpmbuild) $(eval _ZREPL_RPM_TOPDIR_ABS := $(CURDIR)/$(ARTIFACTDIR)/rpmbuild)
rm -rf "$(_ZREPL_RPM_TOPDIR_ABS)" rm -rf "$(_ZREPL_RPM_TOPDIR_ABS)"
mkdir "$(_ZREPL_RPM_TOPDIR_ABS)" mkdir "$(_ZREPL_RPM_TOPDIR_ABS)"
for d in BUILD BUILDROOT RPMS SOURCES SPECS SRPMS; do \ mkdir -p "$(_ZREPL_RPM_TOPDIR_ABS)"/{SPECS,RPMS,BUILD,BUILDROOT}
mkdir -p "$(_ZREPL_RPM_TOPDIR_ABS)/$$d"; \ sed "s/^Version:.*/Version: $(_ZREPL_RPM_VERSION)/g" \
done packaging/rpm/zrepl.spec > $(_ZREPL_RPM_TOPDIR_ABS)/SPECS/zrepl.spec
sed \
-e "s/^Version:.*/Version: $(_ZREPL_RPM_VERSION)/g" \
-e "s/^Release:.*/Release: $(ZREPL_PACKAGE_RELEASE)/g" \
packaging/rpm/zrepl.spec \
> $(_ZREPL_RPM_TOPDIR_ABS)/SPECS/zrepl.spec
# see /usr/lib/rpm/platform # see /usr/lib/rpm/platform
ifeq ($(GOARCH),amd64) ifeq ($(GOARCH),amd64)
@@ -116,16 +110,13 @@ rpm-docker:
docker build -t zrepl_rpm_pkg --pull -f packaging/rpm/Dockerfile . docker build -t zrepl_rpm_pkg --pull -f packaging/rpm/Dockerfile .
docker run --rm -i -v $(CURDIR):/build/src -u $$(id -u):$$(id -g) \ docker run --rm -i -v $(CURDIR):/build/src -u $$(id -u):$$(id -g) \
zrepl_rpm_pkg \ zrepl_rpm_pkg \
make rpm \ make rpm GOOS=$(GOOS) GOARCH=$(GOARCH) GOARM=$(GOARM)
GOOS=$(GOOS) GOARCH=$(GOARCH) GOARM=$(GOARM) \
ZREPL_VERSION=$(ZREPL_VERSION) ZREPL_PACKAGE_RELEASE=$(ZREPL_PACKAGE_RELEASE)
deb: $(ARTIFACTDIR) # artifacts/_zrepl.zsh_completion artifacts/bash_completion docs zrepl-bin deb: $(ARTIFACTDIR) # artifacts/_zrepl.zsh_completion artifacts/bash_completion docs zrepl-bin
cp packaging/deb/debian/changelog.template packaging/deb/debian/changelog cp packaging/deb/debian/changelog.template packaging/deb/debian/changelog
sed -i 's/DATE_DASH_R_OUTPUT/$(shell date -R)/' packaging/deb/debian/changelog sed -i 's/DATE_DASH_R_OUTPUT/$(shell date -R)/' packaging/deb/debian/changelog
VERSION="$(subst -,.,$(_ZREPL_VERSION))-$(ZREPL_PACKAGE_RELEASE)"; \ VERSION="$(subst -,.,$(_ZREPL_VERSION))"; \
export VERSION="$${VERSION#v}"; \ export VERSION="$${VERSION#v}"; \
sed -i 's/VERSION/'"$$VERSION"'/' packaging/deb/debian/changelog sed -i 's/VERSION/'"$$VERSION"'/' packaging/deb/debian/changelog
@@ -143,19 +134,11 @@ endif
deb-docker: deb-docker:
docker build -t zrepl_debian_pkg --pull -f packaging/deb/Dockerfile . docker build -t zrepl_debian_pkg --pull -f packaging/deb/Dockerfile .
# Use a small open file limit to make fakeroot work. If we don't
# specify it, docker daemon will use its file limit. I don't know
# what changed (Docker, its systemd service, its Go version). But I
# observed fakeroot iterating close(i) up to i > 1000000, which costs
# a good amount of CPU time and makes the build slow.
docker run --rm -i -v $(CURDIR):/build/src -u $$(id -u):$$(id -g) \ docker run --rm -i -v $(CURDIR):/build/src -u $$(id -u):$$(id -g) \
--ulimit nofile=1024:1024 \
zrepl_debian_pkg \ zrepl_debian_pkg \
make deb \ make deb GOOS=$(GOOS) GOARCH=$(GOARCH) GOARM=$(GOARM)
GOOS=$(GOOS) GOARCH=$(GOARCH) GOARM=$(GOARM) \
ZREPL_VERSION=$(ZREPL_VERSION) ZREPL_PACKAGE_RELEASE=$(ZREPL_PACKAGE_RELEASE)
# expects `release`, `deb` & `rpm` targets to have run before # expects `release` target to have run before
NOARCH_TARBALL := $(ARTIFACTDIR)/zrepl-noarch.tar NOARCH_TARBALL := $(ARTIFACTDIR)/zrepl-noarch.tar
wrapup-and-checksum: wrapup-and-checksum:
rm -f $(NOARCH_TARBALL) rm -f $(NOARCH_TARBALL)
@@ -172,7 +155,7 @@ wrapup-and-checksum:
config/samples config/samples
rm -rf "$(ARTIFACTDIR)/release" rm -rf "$(ARTIFACTDIR)/release"
mkdir -p "$(ARTIFACTDIR)/release" mkdir -p "$(ARTIFACTDIR)/release"
cp -l $(ARTIFACTDIR)/zrepl* \ cp -l $(ARTIFACTDIR)/zrepl-* \
$(ARTIFACTDIR)/platformtest-* \ $(ARTIFACTDIR)/platformtest-* \
"$(ARTIFACTDIR)/release" "$(ARTIFACTDIR)/release"
cd "$(ARTIFACTDIR)/release" && sha512sum $$(ls | sort) > sha512sum.txt cd "$(ARTIFACTDIR)/release" && sha512sum $$(ls | sort) > sha512sum.txt
@@ -192,42 +175,39 @@ check-git-clean:
tag-release: tag-release:
test -n "$(ZREPL_TAG_VERSION)" || exit 1 test -n "$(ZREPL_TAG_VERSION)" || exit 1
git tag -u '328A6627FA98061D!' -m "$(ZREPL_TAG_VERSION)" "$(ZREPL_TAG_VERSION)" git tag -u E27CA5FC -m "$(ZREPL_TAG_VERSION)" "$(ZREPL_TAG_VERSION)"
sign: sign:
gpg -u '328A6627FA98061D!' \ gpg -u "89BC 5D89 C845 568B F578 B306 CDBD 8EC8 E27C A5FC" \
--armor \ --armor \
--detach-sign $(ARTIFACTDIR)/release/sha512sum.txt --detach-sign $(ARTIFACTDIR)/release/sha512sum.txt
clean: docs-clean clean: docs-clean
rm -rf "$(ARTIFACTDIR)" rm -rf "$(ARTIFACTDIR)"
download-circleci-release: ##################### BINARIES #####################
rm -rf "$(ARTIFACTDIR)" .PHONY: bins-all lint test-go test-platform cover-merge cover-html vet zrepl-bin test-platform-bin generate-platform-test-list
mkdir -p "$(ARTIFACTDIR)/release"
python3 .circleci/download_artifacts.py --prefix 'artifacts/release/' "$(BUILD_NUM)" "$(ARTIFACTDIR)/release"
##################### MULTI-ARCH HELPERS ##################### BINS_ALL_TARGETS := zrepl-bin test-platform-bin vet lint
GO_SUPPORTS_ILLUMOS := $(shell $(GO) version | gawk -F '.' '/^go version /{split($$0, comps, " "); split(comps[3], v, "."); if (v[1] == "go1" && v[2] >= 13) { print "illumos"; } else { print "noillumos"; }}')
_run_make_foreach_target_tuple: bins-all:
if [ "$(RUN_MAKE_FOREACH_TARGET_TUPLE_ARG)" = "" ]; then \ $(MAKE) $(BINS_ALL_TARGETS) GOOS=freebsd GOARCH=amd64
echo "RUN_MAKE_FOREACH_TARGET_TUPLE_ARG must be set"; \ $(MAKE) $(BINS_ALL_TARGETS) GOOS=freebsd GOARCH=386
exit 1; \ $(MAKE) $(BINS_ALL_TARGETS) GOOS=freebsd GOARCH=arm GOARM=7
fi $(MAKE) $(BINS_ALL_TARGETS) GOOS=freebsd GOARCH=arm64
$(MAKE) $(RUN_MAKE_FOREACH_TARGET_TUPLE_ARG) GOOS=freebsd GOARCH=amd64 $(MAKE) $(BINS_ALL_TARGETS) GOOS=linux GOARCH=amd64
$(MAKE) $(RUN_MAKE_FOREACH_TARGET_TUPLE_ARG) GOOS=freebsd GOARCH=386 $(MAKE) $(BINS_ALL_TARGETS) GOOS=linux GOARCH=arm64
$(MAKE) $(RUN_MAKE_FOREACH_TARGET_TUPLE_ARG) GOOS=freebsd GOARCH=arm GOARM=7 $(MAKE) $(BINS_ALL_TARGETS) GOOS=linux GOARCH=arm GOARM=7
$(MAKE) $(RUN_MAKE_FOREACH_TARGET_TUPLE_ARG) GOOS=freebsd GOARCH=arm64 $(MAKE) $(BINS_ALL_TARGETS) GOOS=linux GOARCH=386
$(MAKE) $(RUN_MAKE_FOREACH_TARGET_TUPLE_ARG) GOOS=linux GOARCH=amd64 $(MAKE) $(BINS_ALL_TARGETS) GOOS=darwin GOARCH=amd64
$(MAKE) $(RUN_MAKE_FOREACH_TARGET_TUPLE_ARG) GOOS=linux GOARCH=arm64 $(MAKE) $(BINS_ALL_TARGETS) GOOS=solaris GOARCH=amd64
$(MAKE) $(RUN_MAKE_FOREACH_TARGET_TUPLE_ARG) GOOS=linux GOARCH=arm GOARM=7 ifeq ($(GO_SUPPORTS_ILLUMOS), illumos)
$(MAKE) $(RUN_MAKE_FOREACH_TARGET_TUPLE_ARG) GOOS=linux GOARCH=386 $(MAKE) $(BINS_ALL_TARGETS) GOOS=illumos GOARCH=amd64
$(MAKE) $(RUN_MAKE_FOREACH_TARGET_TUPLE_ARG) GOOS=darwin GOARCH=amd64 else ifeq ($(GO_SUPPORTS_ILLUMOS), noillumos)
$(MAKE) $(RUN_MAKE_FOREACH_TARGET_TUPLE_ARG) GOOS=solaris GOARCH=amd64 @echo "SKIPPING ILLUMOS BUILD BECAUSE GO VERSION DOESN'T SUPPORT IT"
$(MAKE) $(RUN_MAKE_FOREACH_TARGET_TUPLE_ARG) GOOS=illumos GOARCH=amd64 else
@echo "CANNOT DETERMINE WHETHER GO VERSION SUPPORTS GOOS=illumos"; exit 1
##################### REGULAR TARGETS ##################### endif
.PHONY: lint test-go test-platform cover-merge cover-html vet zrepl-bin test-platform-bin generate-platform-test-list
lint: lint:
$(GO_ENV_VARS) $(GOLANGCI_LINT) run ./... $(GO_ENV_VARS) $(GOLANGCI_LINT) run ./...
@@ -353,12 +333,12 @@ $(ARTIFACTDIR)/go_env.txt:
docs: $(ARTIFACTDIR)/docs docs: $(ARTIFACTDIR)/docs
# https://www.sphinx-doc.org/en/master/man/sphinx-build.html # https://www.sphinx-doc.org/en/master/man/sphinx-build.html
$(MAKE) -C docs \ make -C docs \
html \ html \
BUILDDIR=../artifacts/docs \ BUILDDIR=../artifacts/docs \
SPHINXOPTS="-W --keep-going -n" SPHINXOPTS="-W --keep-going -n"
docs-clean: docs-clean:
$(MAKE) -C docs \ make -C docs \
clean \ clean \
BUILDDIR=../artifacts/docs BUILDDIR=../artifacts/docs
-6
View File
@@ -2,18 +2,12 @@ FROM !SUBSTITUTED_BY_MAKEFILE
RUN apt-get update && apt-get install -y \ RUN apt-get update && apt-get install -y \
python3-pip \ python3-pip \
python3-venv \
unzip \ unzip \
gawk gawk
ADD build.installprotoc.bash ./ ADD build.installprotoc.bash ./
RUN bash build.installprotoc.bash RUN bash build.installprotoc.bash
# setup venv
ENV VIRTUAL_ENV=/opt/venv
RUN python3 -m venv $VIRTUAL_ENV
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
ADD lazy.sh /tmp/lazy.sh ADD lazy.sh /tmp/lazy.sh
ADD docs/requirements.txt /tmp/requirements.txt ADD docs/requirements.txt /tmp/requirements.txt
ENV ZREPL_LAZY_DOCS_REQPATH=/tmp/requirements.txt ENV ZREPL_LAZY_DOCS_REQPATH=/tmp/requirements.txt
+8 -32
View File
@@ -4,37 +4,13 @@ go 1.12
require ( require (
github.com/alvaroloes/enumer v1.1.1 github.com/alvaroloes/enumer v1.1.1
github.com/breml/bidichk v0.2.6 // indirect github.com/golangci/golangci-lint v1.35.2
github.com/breml/errchkjson v0.3.5 // indirect github.com/golangci/misspell v0.3.4 // indirect
github.com/chavacava/garif v0.1.0 // indirect github.com/golangci/revgrep v0.0.0-20180812185044-276a5c0a1039 // indirect
github.com/daixiang0/gci v0.11.1 // indirect github.com/spf13/afero v1.2.2 // indirect
github.com/golangci/golangci-lint v1.54.2 github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/golangci/revgrep v0.5.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/jgautheron/goconst v1.6.0 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/mgechev/revive v1.3.3 // indirect
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
github.com/polyfloyd/go-errorlint v1.4.5 // indirect
github.com/prometheus/client_golang v1.16.0 // indirect
github.com/prometheus/common v0.44.0 // indirect
github.com/prometheus/procfs v0.11.1 // indirect
github.com/rivo/uniseg v0.4.4 // indirect
github.com/ryanrolds/sqlclosecheck v0.5.1 // indirect
github.com/spf13/viper v1.16.0 // indirect
github.com/stretchr/objx v0.5.1 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/tetafro/godot v1.4.15 // indirect
github.com/wadey/gocovmerge v0.0.0-20160331181800-b5bfa59ec0ad github.com/wadey/gocovmerge v0.0.0-20160331181800-b5bfa59ec0ad
github.com/xen0n/gosmopolitan v1.2.2 // indirect golang.org/x/tools v0.0.0-20210105210202-9ed45478a130
gitlab.com/bosi/decorder v0.4.1 // indirect google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0 // indirect
go.uber.org/multierr v1.11.0 // indirect google.golang.org/protobuf v1.25.0
go.uber.org/zap v1.25.0 // indirect
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
golang.org/x/exp/typeparams v0.0.0-20230905200255-921286631fa9 // indirect
golang.org/x/tools v0.13.0
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0
google.golang.org/protobuf v1.31.0
mvdan.cc/unparam v0.0.0-20230815095028-f7c6fb1088f0 // indirect
) )
+299 -2166
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -129,7 +129,7 @@ func interactive(c Client, flag statusFlags) error {
FSFilter: func(_ string) bool { return true }, FSFilter: func(_ string) bool { return true },
DetailViewWidth: 100, DetailViewWidth: 100,
DetailViewWrap: false, DetailViewWrap: false,
ShortKeybindingOverview: "[::b]Q[::-] quit [::b]<TAB>[::-] switch panes [::b]W[::-] wrap lines [::b]Shift+M[::-] toggle navbar [::b]Shift+S[::-] signal job [::b]</>[::-] filter filesystems", ShortKeybindingOverview: "[::b]Q[::-] quit [::b]<TAB>[::-] switch panes [::b]Shift+M[::-] toggle navbar [::b]Shift+S[::-] signal job [::b]</>[::-] filter filesystems",
} }
paramsMtx := &sync.Mutex{} paramsMtx := &sync.Mutex{}
var redraw func() var redraw func()
+9 -42
View File
@@ -273,9 +273,9 @@ func printFilesystemStatus(t *stringbuilder.B, rep *report.FilesystemReport, max
attribs = append(attribs, "resumed") attribs = append(attribs, "resumed")
} }
if len(attribs) > 0 { attribs = append(attribs, fmt.Sprintf("encrypted=%s", nextStep.Info.Encrypted))
next += fmt.Sprintf(" (%s)", strings.Join(attribs, ", ")) next += fmt.Sprintf(" (%s)", strings.Join(attribs, ", "))
}
} else { } else {
next = "" // individual FSes may still be in planning state next = "" // individual FSes may still be in planning state
} }
@@ -374,7 +374,6 @@ func renderReplicationReport(t *stringbuilder.B, rep *report.Report, history *by
eta = time.Duration((float64(expected)-float64(replicated))/float64(rate)) * time.Second eta = time.Duration((float64(expected)-float64(replicated))/float64(rate)) * time.Second
} }
if !latest.State.IsTerminal() {
t.Write("Progress: ") t.Write("Progress: ")
t.DrawBar(50, replicated, expected, changeCount) t.DrawBar(50, replicated, expected, changeCount)
t.Write(fmt.Sprintf(" %s / %s @ %s/s", ByteCountBinaryUint(replicated), ByteCountBinaryUint(expected), ByteCountBinary(rate))) t.Write(fmt.Sprintf(" %s / %s @ %s/s", ByteCountBinaryUint(replicated), ByteCountBinaryUint(expected), ByteCountBinary(rate)))
@@ -382,7 +381,6 @@ func renderReplicationReport(t *stringbuilder.B, rep *report.Report, history *by
t.Write(fmt.Sprintf(" (%s remaining)", humanizeDuration(eta))) t.Write(fmt.Sprintf(" (%s remaining)", humanizeDuration(eta)))
} }
t.Newline() t.Newline()
}
if containsInvalidSizeEstimates { if containsInvalidSizeEstimates {
t.Write("NOTE: not all steps could be size-estimated, total estimate is likely imprecise!") t.Write("NOTE: not all steps could be size-estimated, total estimate is likely imprecise!")
t.Newline() t.Newline()
@@ -495,7 +493,6 @@ func renderPrunerReport(t *stringbuilder.B, r *pruner.Report, fsfilter FilterFun
} }
// global progress bar // global progress bar
if !state.IsTerminal() {
progress := int(math.Round(80 * float64(completedDestroyCount) / float64(totalDestroyCount))) progress := int(math.Round(80 * float64(completedDestroyCount) / float64(totalDestroyCount)))
t.Write("Progress: ") t.Write("Progress: ")
t.Write("[") t.Write("[")
@@ -505,7 +502,6 @@ func renderPrunerReport(t *stringbuilder.B, r *pruner.Report, fsfilter FilterFun
t.Write("]") t.Write("]")
t.Printf(" %d/%d snapshots", completedDestroyCount, totalDestroyCount) t.Printf(" %d/%d snapshots", completedDestroyCount, totalDestroyCount)
t.Newline() t.Newline()
}
sort.SliceStable(all, func(i, j int) bool { sort.SliceStable(all, func(i, j int) bool {
return strings.Compare(all[i].Filesystem, all[j].Filesystem) == -1 return strings.Compare(all[i].Filesystem, all[j].Filesystem) == -1
@@ -551,20 +547,10 @@ func renderPrunerReport(t *stringbuilder.B, r *pruner.Report, fsfilter FilterFun
func renderSnapperReport(t *stringbuilder.B, r *snapper.Report, fsfilter FilterFunc) { func renderSnapperReport(t *stringbuilder.B, r *snapper.Report, fsfilter FilterFunc) {
if r == nil { if r == nil {
t.Printf("<no snapshotting report available>\n") t.Printf("<snapshot type does not have a report>\n")
return return
} }
t.Printf("Type: %s\n", r.Type)
if r.Periodic != nil {
renderSnapperReportPeriodic(t, r.Periodic, fsfilter)
} else if r.Cron != nil {
renderSnapperReportCron(t, r.Cron, fsfilter)
} else {
t.Printf("<no details available>")
}
}
func renderSnapperReportPeriodic(t *stringbuilder.B, r *snapper.PeriodicReport, fsfilter FilterFunc) {
t.Printf("Status: %s", r.State) t.Printf("Status: %s", r.State)
t.Newline() t.Newline()
@@ -575,25 +561,8 @@ func renderSnapperReportPeriodic(t *stringbuilder.B, r *snapper.PeriodicReport,
t.Printf("Sleep until: %s\n", r.SleepUntil) t.Printf("Sleep until: %s\n", r.SleepUntil)
} }
renderSnapperPlanReportFilesystem(t, r.Progress, fsfilter) sort.Slice(r.Progress, func(i, j int) bool {
} return strings.Compare(r.Progress[i].Path, r.Progress[j].Path) == -1
func renderSnapperReportCron(t *stringbuilder.B, r *snapper.CronReport, fsfilter FilterFunc) {
t.Printf("State: %s\n", r.State)
now := time.Now()
if r.WakeupTime.After(now) {
t.Printf("Sleep until: %s (%s remaining)\n", r.WakeupTime, r.WakeupTime.Sub(now).Round(time.Second))
} else {
t.Printf("Started: %s (lasting %s)\n", r.WakeupTime, now.Sub(r.WakeupTime).Round(time.Second))
}
renderSnapperPlanReportFilesystem(t, r.Progress, fsfilter)
}
func renderSnapperPlanReportFilesystem(t *stringbuilder.B, fss []*snapper.ReportFilesystem, fsfilter FilterFunc) {
sort.Slice(fss, func(i, j int) bool {
return strings.Compare(fss[i].Path, fss[j].Path) == -1
}) })
dur := func(d time.Duration) string { dur := func(d time.Duration) string {
@@ -606,8 +575,8 @@ func renderSnapperPlanReportFilesystem(t *stringbuilder.B, fss []*snapper.Report
var widths struct { var widths struct {
path, state, duration int path, state, duration int
} }
rows := make([]*row, 0, len(fss)) rows := make([]*row, 0, len(r.Progress))
for _, fs := range fss { for _, fs := range r.Progress {
if !fsfilter(fs.Path) { if !fsfilter(fs.Path) {
continue continue
} }
@@ -650,11 +619,9 @@ func renderSnapperPlanReportFilesystem(t *stringbuilder.B, fss []*snapper.Report
t.Printf("%s %s %s", path, state, duration) t.Printf("%s %s %s", path, state, duration)
t.PrintfDrawIndentedAndWrappedIfMultiline(" %s", r.remainder) t.PrintfDrawIndentedAndWrappedIfMultiline(" %s", r.remainder)
if r.hookReport != "" { if r.hookReport != "" {
t.AddIndent(1) t.PrintfDrawIndentedAndWrappedIfMultiline("%s", r.hookReport)
t.Newline()
t.Printf("%s", r.hookReport)
t.AddIndent(-1)
} }
t.Newline() t.Newline()
} }
} }
+1 -1
View File
@@ -57,7 +57,7 @@ func (f *zabsFilterFlags) registerZabsFilterFlags(s *pflag.FlagSet, verb string)
for v := range endpoint.AbstractionTypesAll { for v := range endpoint.AbstractionTypesAll {
variants = append(variants, string(v)) variants = append(variants, string(v))
} }
sort.Strings(variants) variants = sort.StringSlice(variants)
variantsJoined := strings.Join(variants, "|") variantsJoined := strings.Join(variants, "|")
s.Var(&f.Types, "type", fmt.Sprintf("only %s holds of the specified type [default: all] [comma-separated list of %s]", verb, variantsJoined)) s.Var(&f.Types, "type", fmt.Sprintf("only %s holds of the specified type [default: all] [comma-separated list of %s]", verb, variantsJoined))
+1 -2
View File
@@ -44,11 +44,10 @@ func doZabsList(ctx context.Context, sc *cli.Subcommand, args []string) error {
return errors.Wrap(err, "invalid filter specification on command line") return errors.Wrap(err, "invalid filter specification on command line")
} }
abstractions, errors, drainDone, err := endpoint.ListAbstractionsStreamed(ctx, q) abstractions, errors, err := endpoint.ListAbstractionsStreamed(ctx, q)
if err != nil { if err != nil {
return err // context clear by invocation of command return err // context clear by invocation of command
} }
defer drainDone()
var line chainlock.L var line chainlock.L
var wg sync.WaitGroup var wg sync.WaitGroup
+57 -65
View File
@@ -6,10 +6,11 @@ import (
"log/syslog" "log/syslog"
"os" "os"
"reflect" "reflect"
"regexp"
"strconv"
"time" "time"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/robfig/cron/v3"
"github.com/zrepl/yaml-config" "github.com/zrepl/yaml-config"
"github.com/zrepl/zrepl/util/datasizeunit" "github.com/zrepl/zrepl/util/datasizeunit"
@@ -65,6 +66,7 @@ type ActiveJob struct {
Name string `yaml:"name"` Name string `yaml:"name"`
Connect ConnectEnum `yaml:"connect"` Connect ConnectEnum `yaml:"connect"`
Pruning PruningSenderReceiver `yaml:"pruning"` Pruning PruningSenderReceiver `yaml:"pruning"`
Debug JobDebugSettings `yaml:"debug,optional"`
Replication *Replication `yaml:"replication,optional,fromdefaults"` Replication *Replication `yaml:"replication,optional,fromdefaults"`
ConflictResolution *ConflictResolution `yaml:"conflict_resolution,optional,fromdefaults"` ConflictResolution *ConflictResolution `yaml:"conflict_resolution,optional,fromdefaults"`
} }
@@ -77,12 +79,14 @@ type PassiveJob struct {
Type string `yaml:"type"` Type string `yaml:"type"`
Name string `yaml:"name"` Name string `yaml:"name"`
Serve ServeEnum `yaml:"serve"` Serve ServeEnum `yaml:"serve"`
Debug JobDebugSettings `yaml:"debug,optional"`
} }
type SnapJob struct { type SnapJob struct {
Type string `yaml:"type"` Type string `yaml:"type"`
Name string `yaml:"name"` Name string `yaml:"name"`
Pruning PruningLocal `yaml:"pruning"` Pruning PruningLocal `yaml:"pruning"`
Debug JobDebugSettings `yaml:"debug,optional"`
Snapshotting SnapshottingEnum `yaml:"snapshotting"` Snapshotting SnapshottingEnum `yaml:"snapshotting"`
Filesystems FilesystemsFilter `yaml:"filesystems"` Filesystems FilesystemsFilter `yaml:"filesystems"`
} }
@@ -121,7 +125,6 @@ type BandwidthLimit struct {
} }
type Replication struct { type Replication struct {
Triggers []*ReplicationTriggerEnum
Protection *ReplicationOptionsProtection `yaml:"protection,optional,fromdefaults"` Protection *ReplicationOptionsProtection `yaml:"protection,optional,fromdefaults"`
Concurrency *ReplicationOptionsConcurrency `yaml:"concurrency,optional,fromdefaults"` Concurrency *ReplicationOptionsConcurrency `yaml:"concurrency,optional,fromdefaults"`
} }
@@ -136,32 +139,6 @@ type ReplicationOptionsConcurrency struct {
SizeEstimates int `yaml:"size_estimates,optional,default=4"` SizeEstimates int `yaml:"size_estimates,optional,default=4"`
} }
type ReplicationTriggerEnum struct {
Ret interface{}
}
func (t *ReplicationTriggerEnum) UnmarshalYAML(u func(interface{}, bool) error) (err error) {
t.Ret, err = enumUnmarshal(u, map[string]interface{}{
"manual": &ReplicationTriggerManual{},
"periodic": &ReplicationTriggerPeriodic{},
})
return
}
type ReplicationTriggerManual struct {
Type string `yaml:"type"`
}
type ReplicationTriggerPeriodic struct {
Type string `yaml:"type"`
Interval *PositiveDuration `yaml:"interval"`
}
type ReplicationTriggerCron struct {
Type string `yaml:"type"`
Cron CronSpec `yaml:"cron"`
}
type PropertyRecvOptions struct { type PropertyRecvOptions struct {
Inherit []zfsprop.Property `yaml:"inherit,optional"` Inherit []zfsprop.Property `yaml:"inherit,optional"`
Override map[zfsprop.Property]string `yaml:"override,optional"` Override map[zfsprop.Property]string `yaml:"override,optional"`
@@ -184,6 +161,7 @@ func (j *PushJob) GetSendOptions() *SendOptions { return j.Send }
type PullJob struct { type PullJob struct {
ActiveJob `yaml:",inline"` ActiveJob `yaml:",inline"`
RootFS string `yaml:"root_fs"` RootFS string `yaml:"root_fs"`
Interval PositiveDurationOrManual `yaml:"interval"`
Recv *RecvOptions `yaml:"recv,fromdefaults,optional"` Recv *RecvOptions `yaml:"recv,fromdefaults,optional"`
} }
@@ -211,10 +189,13 @@ func (i *PositiveDurationOrManual) UnmarshalYAML(u func(interface{}, bool) error
return fmt.Errorf("value must not be empty") return fmt.Errorf("value must not be empty")
default: default:
i.Manual = false i.Manual = false
i.Interval, err = parsePositiveDuration(s) i.Interval, err = time.ParseDuration(s)
if err != nil { if err != nil {
return err return err
} }
if i.Interval <= 0 {
return fmt.Errorf("value must be a positive duration, got %q", s)
}
} }
return nil return nil
} }
@@ -248,42 +229,8 @@ type SnapshottingEnum struct {
type SnapshottingPeriodic struct { type SnapshottingPeriodic struct {
Type string `yaml:"type"` Type string `yaml:"type"`
Prefix string `yaml:"prefix"` Prefix string `yaml:"prefix"`
Interval *PositiveDuration `yaml:"interval"` Interval time.Duration `yaml:"interval,positive"`
Hooks HookList `yaml:"hooks,optional"` Hooks HookList `yaml:"hooks,optional"`
TimestampFormat string `yaml:"timestamp_format,optional,default=dense"`
}
type CronSpec struct {
Schedule cron.Schedule
}
var _ yaml.Unmarshaler = &CronSpec{}
func (s *CronSpec) UnmarshalYAML(unmarshal func(v interface{}, not_strict bool) error) error {
var specString string
if err := unmarshal(&specString, false); err != nil {
return err
}
// Use standard cron format.
// Disable the various "descriptors" (@daily, etc)
// They are just aliases to "top of hour", "midnight", etc.
parser := cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.SecondOptional)
sched, err := parser.Parse(specString)
if err != nil {
return errors.Wrap(err, "cron syntax invalid")
}
s.Schedule = sched
return nil
}
type SnapshottingCron struct {
Type string `yaml:"type"`
Prefix string `yaml:"prefix"`
Cron CronSpec `yaml:"cron"`
Hooks HookList `yaml:"hooks,optional"`
TimestampFormat string `yaml:"timestamp_format,optional,default=dense"`
} }
type SnapshottingManual struct { type SnapshottingManual struct {
@@ -503,6 +450,14 @@ type GlobalStdinServer struct {
SockDir string `yaml:"sockdir,default=/var/run/zrepl/stdinserver"` SockDir string `yaml:"sockdir,default=/var/run/zrepl/stdinserver"`
} }
type JobDebugSettings struct {
Conn *struct {
ReadDump string `yaml:"read_dump"`
WriteDump string `yaml:"write_dump"`
} `yaml:"conn,optional"`
RPCLog bool `yaml:"rpc_log,optional,default=false"`
}
type HookList []HookEnum type HookList []HookEnum
type HookEnum struct { type HookEnum struct {
@@ -601,7 +556,6 @@ func (t *SnapshottingEnum) UnmarshalYAML(u func(interface{}, bool) error) (err e
t.Ret, err = enumUnmarshal(u, map[string]interface{}{ t.Ret, err = enumUnmarshal(u, map[string]interface{}{
"periodic": &SnapshottingPeriodic{}, "periodic": &SnapshottingPeriodic{},
"manual": &SnapshottingManual{}, "manual": &SnapshottingManual{},
"cron": &SnapshottingCron{},
}) })
return return
} }
@@ -727,3 +681,41 @@ func ParseConfigBytes(bytes []byte) (*Config, error) {
} }
return c, nil return c, nil
} }
var durationStringRegex *regexp.Regexp = regexp.MustCompile(`^\s*(\d+)\s*(s|m|h|d|w)\s*$`)
func parsePositiveDuration(e string) (d time.Duration, err error) {
comps := durationStringRegex.FindStringSubmatch(e)
if len(comps) != 3 {
err = fmt.Errorf("does not match regex: %s %#v", e, comps)
return
}
durationFactor, err := strconv.ParseInt(comps[1], 10, 64)
if err != nil {
return 0, err
}
if durationFactor <= 0 {
return 0, errors.New("duration must be positive integer")
}
var durationUnit time.Duration
switch comps[2] {
case "s":
durationUnit = time.Second
case "m":
durationUnit = time.Minute
case "h":
durationUnit = time.Hour
case "d":
durationUnit = 24 * time.Hour
case "w":
durationUnit = 24 * 7 * time.Hour
default:
err = fmt.Errorf("contains unknown time unit '%s'", comps[2])
return
}
d = time.Duration(durationFactor) * durationUnit
return
}
-106
View File
@@ -1,106 +0,0 @@
package config
import (
"errors"
"fmt"
"regexp"
"strconv"
"time"
"github.com/kr/pretty"
"github.com/zrepl/yaml-config"
)
type Duration struct{ d time.Duration }
func (d Duration) Duration() time.Duration { return d.d }
var _ yaml.Unmarshaler = &Duration{}
func (d *Duration) UnmarshalYAML(unmarshal func(v interface{}, not_strict bool) error) error {
var s string
err := unmarshal(&s, false)
if err != nil {
return err
}
d.d, err = parseDuration(s)
if err != nil {
d.d = 0
return &yaml.TypeError{Errors: []string{fmt.Sprintf("cannot parse value %q: %s", s, err)}}
}
return nil
}
type PositiveDuration struct{ d Duration }
var _ yaml.Unmarshaler = &PositiveDuration{}
func (d PositiveDuration) Duration() time.Duration { return d.d.Duration() }
func (d *PositiveDuration) UnmarshalYAML(unmarshal func(v interface{}, not_strict bool) error) error {
err := d.d.UnmarshalYAML(unmarshal)
if err != nil {
return err
}
if d.d.Duration() <= 0 {
return fmt.Errorf("duration must be positive, got %s", d.d.Duration())
}
return nil
}
func parsePositiveDuration(e string) (time.Duration, error) {
d, err := parseDuration(e)
if err != nil {
return d, err
}
if d <= 0 {
return 0, errors.New("duration must be positive integer")
}
return d, err
}
var durationStringRegex *regexp.Regexp = regexp.MustCompile(`^\s*([\+-]?\d+)\s*(|s|m|h|d|w)\s*$`)
func parseDuration(e string) (d time.Duration, err error) {
comps := durationStringRegex.FindStringSubmatch(e)
if comps == nil {
err = fmt.Errorf("must match %s", durationStringRegex)
return
}
if len(comps) != 3 {
panic(pretty.Sprint(comps))
}
durationFactor, err := strconv.ParseInt(comps[1], 10, 64)
if err != nil {
return 0, err
}
var durationUnit time.Duration
switch comps[2] {
case "":
if durationFactor != 0 {
err = fmt.Errorf("missing time unit")
return
} else {
// It's the case where user specified '0'.
// We want to allow this, just like time.ParseDuration.
}
case "s":
durationUnit = time.Second
case "m":
durationUnit = time.Minute
case "h":
durationUnit = time.Hour
case "d":
durationUnit = 24 * time.Hour
case "w":
durationUnit = 24 * 7 * time.Hour
default:
err = fmt.Errorf("contains unknown time unit '%s'", comps[2])
return
}
d = time.Duration(durationFactor) * durationUnit
return
}
+1 -87
View File
@@ -35,23 +35,8 @@ jobs:
snapshotting: snapshotting:
type: periodic type: periodic
prefix: zrepl_ prefix: zrepl_
timestamp_format: dense
interval: 10m interval: 10m
` `
cron := `
snapshotting:
type: cron
prefix: zrepl_
timestamp_format: human
cron: "10 * * * *"
`
periodicDaily := `
snapshotting:
type: periodic
prefix: zrepl_
interval: 1d
`
hooks := ` hooks := `
snapshotting: snapshotting:
@@ -89,27 +74,10 @@ jobs:
c = testValidConfig(t, fillSnapshotting(periodic)) c = testValidConfig(t, fillSnapshotting(periodic))
snp := c.Jobs[0].Ret.(*PushJob).Snapshotting.Ret.(*SnapshottingPeriodic) snp := c.Jobs[0].Ret.(*PushJob).Snapshotting.Ret.(*SnapshottingPeriodic)
assert.Equal(t, "periodic", snp.Type) assert.Equal(t, "periodic", snp.Type)
assert.Equal(t, 10*time.Minute, snp.Interval.Duration()) assert.Equal(t, 10*time.Minute, snp.Interval)
assert.Equal(t, "zrepl_", snp.Prefix) assert.Equal(t, "zrepl_", snp.Prefix)
}) })
t.Run("periodicDaily", func(t *testing.T) {
c = testValidConfig(t, fillSnapshotting(periodicDaily))
snp := c.Jobs[0].Ret.(*PushJob).Snapshotting.Ret.(*SnapshottingPeriodic)
assert.Equal(t, "periodic", snp.Type)
assert.Equal(t, 24*time.Hour, snp.Interval.Duration())
assert.Equal(t, "zrepl_", snp.Prefix)
assert.Equal(t, "dense", snp.TimestampFormat)
})
t.Run("cron", func(t *testing.T) {
c = testValidConfig(t, fillSnapshotting(cron))
snp := c.Jobs[0].Ret.(*PushJob).Snapshotting.Ret.(*SnapshottingCron)
assert.Equal(t, "cron", snp.Type)
assert.Equal(t, "zrepl_", snp.Prefix)
assert.Equal(t, "human", snp.TimestampFormat)
})
t.Run("hooks", func(t *testing.T) { t.Run("hooks", func(t *testing.T) {
c = testValidConfig(t, fillSnapshotting(hooks)) c = testValidConfig(t, fillSnapshotting(hooks))
hs := c.Jobs[0].Ret.(*PushJob).Snapshotting.Ret.(*SnapshottingPeriodic).Hooks hs := c.Jobs[0].Ret.(*PushJob).Snapshotting.Ret.(*SnapshottingPeriodic).Hooks
@@ -120,57 +88,3 @@ jobs:
}) })
} }
func TestSnapshottingTimestampDefaults(t *testing.T) {
tmpl := `
jobs:
- name: foo
type: push
connect:
type: local
listener_name: foo
client_identity: bar
filesystems: {"<": true}
%s
pruning:
keep_sender:
- type: last_n
count: 10
keep_receiver:
- type: last_n
count: 10
`
periodic := `
snapshotting:
type: periodic
prefix: zrepl_
interval: 10m
`
cron := `
snapshotting:
type: cron
prefix: zrepl_
cron: "10 * * * *"
`
fillSnapshotting := func(s string) string { return fmt.Sprintf(tmpl, s) }
var c *Config
t.Run("periodic", func(t *testing.T) {
c = testValidConfig(t, fillSnapshotting(periodic))
snp := c.Jobs[0].Ret.(*PushJob).Snapshotting.Ret.(*SnapshottingPeriodic)
assert.Equal(t, "periodic", snp.Type)
assert.Equal(t, 10*time.Minute, snp.Interval.Duration())
assert.Equal(t, "zrepl_", snp.Prefix)
assert.Equal(t, "dense", snp.TimestampFormat) // default was set correctly
})
t.Run("cron", func(t *testing.T) {
c = testValidConfig(t, fillSnapshotting(cron))
snp := c.Jobs[0].Ret.(*PushJob).Snapshotting.Ret.(*SnapshottingCron)
assert.Equal(t, "cron", snp.Type)
assert.Equal(t, "zrepl_", snp.Prefix)
assert.Equal(t, "dense", snp.TimestampFormat) // default was set correctly
})
}
+1 -53
View File
@@ -13,7 +13,6 @@ import (
"github.com/kr/pretty" "github.com/kr/pretty"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/zrepl/yaml-config"
) )
func TestSampleConfigsAreParsedWithoutErrors(t *testing.T) { func TestSampleConfigsAreParsedWithoutErrors(t *testing.T) {
@@ -44,8 +43,7 @@ func TestSampleConfigsAreParsedWithoutErrors(t *testing.T) {
} }
// template must be a template/text template with a single '{{ . }}' as placeholder for val // template must be a template/text template with a single '{{ . }}' as placeholder for val
// //nolint[:deadcode,unused]
//nolint:deadcode,unused
func testValidConfigTemplate(t *testing.T, tmpl string, val string) *Config { func testValidConfigTemplate(t *testing.T, tmpl string, val string) *Config {
tmp, err := template.New("master").Parse(tmpl) tmp, err := template.New("master").Parse(tmpl)
if err != nil { if err != nil {
@@ -88,53 +86,3 @@ func TestTrimSpaceEachLineAndPad(t *testing.T) {
` `
assert.Equal(t, " \n foo\n bar baz\n \n", trimSpaceEachLineAndPad(foo, " ")) assert.Equal(t, " \n foo\n bar baz\n \n", trimSpaceEachLineAndPad(foo, " "))
} }
func TestCronSpec(t *testing.T) {
expectAccept := []string{
`"* * * * *"`,
`"0-10 * * * *"`,
`"* 0-5,8,12 * * *"`,
}
expectFail := []string{
`* * * *`,
``,
`23`,
`"@reboot"`,
`"@every 1h30m"`,
`"@daily"`,
`* * * * * *`,
}
for _, input := range expectAccept {
t.Run(input, func(t *testing.T) {
s := fmt.Sprintf("spec: %s\n", input)
var v struct {
Spec CronSpec
}
v.Spec.Schedule = nil
t.Logf("input:\n%s", s)
err := yaml.UnmarshalStrict([]byte(s), &v)
t.Logf("error: %T %s", err, err)
require.NoError(t, err)
require.NotNil(t, v.Spec.Schedule)
})
}
for _, input := range expectFail {
t.Run(input, func(t *testing.T) {
s := fmt.Sprintf("spec: %s\n", input)
var v struct {
Spec CronSpec
}
v.Spec.Schedule = nil
t.Logf("input: %q", s)
err := yaml.UnmarshalStrict([]byte(s), &v)
t.Logf("error: %T %s", err, err)
require.Error(t, err)
require.Nil(t, v.Spec.Schedule)
})
}
}
-14
View File
@@ -1,14 +0,0 @@
jobs:
- name: snapjob
type: snap
filesystems: {
"tank<": true,
}
snapshotting:
type: cron
prefix: zrepl_snapjob_
cron: "*/5 * * * *"
pruning:
keep:
- type: last_n
count: 60
-8
View File
@@ -160,14 +160,6 @@ func (m DatasetMapFilter) Filter(p *zfs.DatasetPath) (pass bool, err error) {
return return
} }
func (m DatasetMapFilter) UserSpecifiedDatasets() (datasets zfs.UserSpecifiedDatasetsSet) {
datasets = make(zfs.UserSpecifiedDatasetsSet)
for i := range m.entries {
datasets[m.entries[i].path.ToString()] = true
}
return
}
// Construct a new filter-only DatasetMapFilter from a mapping // Construct a new filter-only DatasetMapFilter from a mapping
// The new filter allows exactly those paths that were not forbidden by the mapping. // The new filter allows exactly those paths that were not forbidden by the mapping.
func (m DatasetMapFilter) InvertedFilter() (inv *DatasetMapFilter, err error) { func (m DatasetMapFilter) InvertedFilter() (inv *DatasetMapFilter, err error) {
+3 -2
View File
@@ -5,7 +5,7 @@
// //
// This package also provides all supported hook type implementations and abstractions around them. // This package also provides all supported hook type implementations and abstractions around them.
// //
// # Use For Other Kinds Of ExpectStepReports // Use For Other Kinds Of ExpectStepReports
// //
// This package REQUIRES REFACTORING before it can be used for other activities than snapshots, e.g. pre- and post-replication: // This package REQUIRES REFACTORING before it can be used for other activities than snapshots, e.g. pre- and post-replication:
// //
@@ -15,7 +15,7 @@
// The hook implementations should move out of this package. // The hook implementations should move out of this package.
// However, there is a lot of tight coupling which to untangle isn't worth it ATM. // However, there is a lot of tight coupling which to untangle isn't worth it ATM.
// //
// # How This Package Is Used By Package Snapper // How This Package Is Used By Package Snapper
// //
// Deserialize a config.List using ListFromConfig(). // Deserialize a config.List using ListFromConfig().
// Then it MUST filter the list to only contain hooks for a particular filesystem using // Then it MUST filter the list to only contain hooks for a particular filesystem using
@@ -30,4 +30,5 @@
// Command hooks make it available in the environment variable ZREPL_DRYRUN. // Command hooks make it available in the environment variable ZREPL_DRYRUN.
// //
// Plan.Report() can be called while Plan.Run() is executing to give an overview of plan execution progress (future use in "zrepl status"). // Plan.Report() can be called while Plan.Run() is executing to give an overview of plan execution progress (future use in "zrepl status").
//
package hooks package hooks
+1 -8
View File
@@ -93,14 +93,7 @@ func (r *CommandHookReport) String() string {
cmdLine.WriteString(fmt.Sprintf("%s'%s'", sep, a)) cmdLine.WriteString(fmt.Sprintf("%s'%s'", sep, a))
} }
var msg string return fmt.Sprintf("command hook invocation: \"%s\"", cmdLine.String()) // no %q to make copy-pastable
if r.Err == nil {
msg = "command hook"
} else {
msg = fmt.Sprintf("command hook failed with %q", r.Err)
}
return fmt.Sprintf("%s: \"%s\"", msg, cmdLine.String()) // no %q to make copy-pastable
} }
func (r *CommandHookReport) Error() string { func (r *CommandHookReport) Error() string {
if r.Err == nil { if r.Err == nil {
@@ -17,7 +17,6 @@ import (
"github.com/zrepl/zrepl/zfs" "github.com/zrepl/zrepl/zfs"
) )
// Hook to implement the following recommmendation from MySQL docs
// https://dev.mysql.com/doc/mysql-backup-excerpt/5.7/en/backup-methods.html // https://dev.mysql.com/doc/mysql-backup-excerpt/5.7/en/backup-methods.html
// //
// Making Backups Using a File System Snapshot: // Making Backups Using a File System Snapshot:
@@ -31,8 +30,6 @@ import (
// Unmount the snapshot. // Unmount the snapshot.
// //
// Similar snapshot capabilities may be available in other file systems, such as LVM or ZFS. // Similar snapshot capabilities may be available in other file systems, such as LVM or ZFS.
//
type MySQLLockTables struct { type MySQLLockTables struct {
errIsFatal bool errIsFatal bool
connector sqldriver.Connector connector sqldriver.Connector
+5 -5
View File
@@ -161,7 +161,7 @@ jobs:
ExpectedEdge: hooks.Pre, ExpectedEdge: hooks.Pre,
ExpectStatus: hooks.StepErr, ExpectStatus: hooks.StepErr,
OutputTest: containsTest(fmt.Sprintf("TEST ERROR pre_testing %s@%s", testFSName, testSnapshotName)), OutputTest: containsTest(fmt.Sprintf("TEST ERROR pre_testing %s@%s", testFSName, testSnapshotName)),
ErrorTest: regexpTest("^command hook failed.*exit status 1$"), ErrorTest: regexpTest("^command hook invocation.*exit status 1$"),
}, },
expectStep{ExpectedEdge: hooks.Callback, ExpectStatus: hooks.StepOk}, expectStep{ExpectedEdge: hooks.Callback, ExpectStatus: hooks.StepOk},
expectStep{ expectStep{
@@ -185,7 +185,7 @@ jobs:
ExpectedEdge: hooks.Pre, ExpectedEdge: hooks.Pre,
ExpectStatus: hooks.StepErr, ExpectStatus: hooks.StepErr,
OutputTest: containsTest(fmt.Sprintf("TEST ERROR pre_testing %s@%s", testFSName, testSnapshotName)), OutputTest: containsTest(fmt.Sprintf("TEST ERROR pre_testing %s@%s", testFSName, testSnapshotName)),
ErrorTest: regexpTest("^command hook failed.*exit status 1$"), ErrorTest: regexpTest("^command hook invocation.*exit status 1$"),
}, },
expectStep{ expectStep{
ExpectedEdge: hooks.Pre, ExpectedEdge: hooks.Pre,
@@ -234,7 +234,7 @@ jobs:
ExpectedEdge: hooks.Post, ExpectedEdge: hooks.Post,
ExpectStatus: hooks.StepErr, ExpectStatus: hooks.StepErr,
OutputTest: containsTest(fmt.Sprintf("TEST ERROR post_testing %s@%s", testFSName, testSnapshotName)), OutputTest: containsTest(fmt.Sprintf("TEST ERROR post_testing %s@%s", testFSName, testSnapshotName)),
ErrorTest: regexpTest("^command hook failed.*exit status 1$"), ErrorTest: regexpTest("^command hook invocation.*exit status 1$"),
}, },
}, },
}, },
@@ -267,7 +267,7 @@ jobs:
ExpectedEdge: hooks.Pre, ExpectedEdge: hooks.Pre,
ExpectStatus: hooks.StepErr, ExpectStatus: hooks.StepErr,
OutputTest: containsTest(fmt.Sprintf("TEST ERROR pre_testing %s@%s", testFSName, testSnapshotName)), OutputTest: containsTest(fmt.Sprintf("TEST ERROR pre_testing %s@%s", testFSName, testSnapshotName)),
ErrorTest: regexpTest("^command hook failed.*exit status 1$"), ErrorTest: regexpTest("^command hook invocation.*exit status 1$"),
}, },
expectStep{ExpectedEdge: hooks.Callback, ExpectStatus: hooks.StepOk}, expectStep{ExpectedEdge: hooks.Callback, ExpectStatus: hooks.StepOk},
expectStep{ expectStep{
@@ -295,7 +295,7 @@ jobs:
ExpectedEdge: hooks.Pre, ExpectedEdge: hooks.Pre,
ExpectStatus: hooks.StepErr, ExpectStatus: hooks.StepErr,
OutputTest: containsTest(fmt.Sprintf("TEST ERROR pre_testing %s@%s", testFSName, testSnapshotName)), OutputTest: containsTest(fmt.Sprintf("TEST ERROR pre_testing %s@%s", testFSName, testSnapshotName)),
ErrorTest: regexpTest("^command hook failed.*exit status 1$"), ErrorTest: regexpTest("^command hook invocation.*exit status 1$"),
}, },
expectStep{ExpectedEdge: hooks.Callback, ExpectStatus: hooks.StepOk}, expectStep{ExpectedEdge: hooks.Callback, ExpectStatus: hooks.StepOk},
expectStep{ expectStep{
+22 -32
View File
@@ -15,7 +15,6 @@ import (
"github.com/zrepl/zrepl/config" "github.com/zrepl/zrepl/config"
"github.com/zrepl/zrepl/daemon/job/reset" "github.com/zrepl/zrepl/daemon/job/reset"
"github.com/zrepl/zrepl/daemon/job/trigger"
"github.com/zrepl/zrepl/daemon/job/wakeup" "github.com/zrepl/zrepl/daemon/job/wakeup"
"github.com/zrepl/zrepl/daemon/pruner" "github.com/zrepl/zrepl/daemon/pruner"
"github.com/zrepl/zrepl/daemon/snapper" "github.com/zrepl/zrepl/daemon/snapper"
@@ -45,8 +44,6 @@ type ActiveSide struct {
promReplicationErrors prometheus.Gauge promReplicationErrors prometheus.Gauge
promLastSuccessful prometheus.Gauge promLastSuccessful prometheus.Gauge
triggers *trigger.Triggers
tasksMtx sync.Mutex tasksMtx sync.Mutex
tasks activeSideTasks tasks activeSideTasks
} }
@@ -93,7 +90,7 @@ type activeMode interface {
SenderReceiver() (logic.Sender, logic.Receiver) SenderReceiver() (logic.Sender, logic.Receiver)
Type() Type Type() Type
PlannerPolicy() logic.PlannerPolicy PlannerPolicy() logic.PlannerPolicy
RunPeriodic(ctx context.Context, wakeReplication *trigger.Manual) RunPeriodic(ctx context.Context, wakeUpCommon chan<- struct{})
SnapperReport() *snapper.Report SnapperReport() *snapper.Report
ResetConnectBackoff() ResetConnectBackoff()
} }
@@ -104,7 +101,7 @@ type modePush struct {
receiver *rpc.Client receiver *rpc.Client
senderConfig *endpoint.SenderConfig senderConfig *endpoint.SenderConfig
plannerPolicy *logic.PlannerPolicy plannerPolicy *logic.PlannerPolicy
snapper snapper.Snapper snapper *snapper.PeriodicOrManual
} }
func (m *modePush) ConnectEndpoints(ctx context.Context, connecter transport.Connecter) { func (m *modePush) ConnectEndpoints(ctx context.Context, connecter transport.Connecter) {
@@ -135,13 +132,12 @@ func (m *modePush) Type() Type { return TypePush }
func (m *modePush) PlannerPolicy() logic.PlannerPolicy { return *m.plannerPolicy } func (m *modePush) PlannerPolicy() logic.PlannerPolicy { return *m.plannerPolicy }
func (m *modePush) RunPeriodic(ctx context.Context, trigger *trigger.Manual) { func (m *modePush) RunPeriodic(ctx context.Context, wakeUpCommon chan<- struct{}) {
m.snapper.Run(ctx, trigger) m.snapper.Run(ctx, wakeUpCommon)
} }
func (m *modePush) SnapperReport() *snapper.Report { func (m *modePush) SnapperReport() *snapper.Report {
r := m.snapper.Report() return m.snapper.Report()
return &r
} }
func (m *modePush) ResetConnectBackoff() { func (m *modePush) ResetConnectBackoff() {
@@ -172,6 +168,7 @@ func modePushFromConfig(g *config.Global, in *config.PushJob, jobID endpoint.Job
} }
m.plannerPolicy = &logic.PlannerPolicy{ m.plannerPolicy = &logic.PlannerPolicy{
EncryptedSend: logic.TriFromBool(in.Send.Encrypted),
ConflictResolution: conflictResolution, ConflictResolution: conflictResolution,
ReplicationConfig: replicationConfig, ReplicationConfig: replicationConfig,
SizeEstimationConcurrency: in.Replication.Concurrency.SizeEstimates, SizeEstimationConcurrency: in.Replication.Concurrency.SizeEstimates,
@@ -224,7 +221,7 @@ func (*modePull) Type() Type { return TypePull }
func (m *modePull) PlannerPolicy() logic.PlannerPolicy { return *m.plannerPolicy } func (m *modePull) PlannerPolicy() logic.PlannerPolicy { return *m.plannerPolicy }
func (m *modePull) RunPeriodic(ctx context.Context, wakeReplication *trigger.Manual) { func (m *modePull) RunPeriodic(ctx context.Context, wakeUpCommon chan<- struct{}) {
if m.interval.Manual { if m.interval.Manual {
GetLogger(ctx).Info("manual pull configured, periodic pull disabled") GetLogger(ctx).Info("manual pull configured, periodic pull disabled")
// "waiting for wakeups" is printed in common ActiveSide.do // "waiting for wakeups" is printed in common ActiveSide.do
@@ -235,7 +232,14 @@ func (m *modePull) RunPeriodic(ctx context.Context, wakeReplication *trigger.Man
for { for {
select { select {
case <-t.C: case <-t.C:
wakeReplication.Fire() select {
case wakeUpCommon <- struct{}{}:
default:
GetLogger(ctx).
WithField("pull_interval", m.interval).
Warn("pull job took longer than pull interval")
wakeUpCommon <- struct{}{} // block anyways, to queue up the wakeup
}
case <-ctx.Done(): case <-ctx.Done():
return return
} }
@@ -269,6 +273,7 @@ func modePullFromConfig(g *config.Global, in *config.PullJob, jobID endpoint.Job
} }
m.plannerPolicy = &logic.PlannerPolicy{ m.plannerPolicy = &logic.PlannerPolicy{
EncryptedSend: logic.DontCare,
ConflictResolution: conflictResolution, ConflictResolution: conflictResolution,
ReplicationConfig: replicationConfig, ReplicationConfig: replicationConfig,
SizeEstimationConcurrency: in.Replication.Concurrency.SizeEstimates, SizeEstimationConcurrency: in.Replication.Concurrency.SizeEstimates,
@@ -366,11 +371,6 @@ func activeSide(g *config.Global, in *config.ActiveJob, configJob interface{}, p
return nil, errors.Wrap(err, "cannot build replication driver config") return nil, errors.Wrap(err, "cannot build replication driver config")
} }
j.triggers, err = trigger.FromConfig(in.Replication.Triggers)
if err != nil {
return nil, errors.Wrap(err, "cannot build triggers")
}
return j, nil return j, nil
} }
@@ -445,17 +445,12 @@ func (j *ActiveSide) Run(ctx context.Context) {
defer log.Info("job exiting") defer log.Info("job exiting")
periodicDone := make(chan struct{})
ctx, cancel := context.WithCancel(ctx) ctx, cancel := context.WithCancel(ctx)
defer cancel() defer cancel()
periodicCtx, endTask := trace.WithTask(ctx, "periodic")
periodCtx, endTask := trace.WithTask(ctx, "periodic")
defer endTask()
go j.mode.RunPeriodic(periodCtx, periodicTrigger)
wakeupTrigger := wakeup.Trigger(ctx)
triggered, endTask := j.triggers.Spawn(ctx, []*trigger.Trigger{periodicTrigger, wakeupTrigger})
defer endTask() defer endTask()
go j.mode.RunPeriodic(periodicCtx, periodicDone)
invocationCount := 0 invocationCount := 0
outer: outer:
@@ -465,15 +460,10 @@ outer:
case <-ctx.Done(): case <-ctx.Done():
log.WithError(ctx.Err()).Info("context") log.WithError(ctx.Err()).Info("context")
break outer break outer
case trigger := <-triggered:
log := case <-wakeup.Wait(ctx):
log.WithField("trigger_id", trigger.ID())
log.Info("triggered")
switch trigger {
case wakeupTrigger:
log.Info("trigger is wakeup command, resetting connection backoff")
j.mode.ResetConnectBackoff() j.mode.ResetConnectBackoff()
} case <-periodicDone:
} }
invocationCount++ invocationCount++
invocationCtx, endSpan := trace.WithSpan(ctx, fmt.Sprintf("invocation-%d", invocationCount)) invocationCtx, endSpan := trace.WithSpan(ctx, fmt.Sprintf("invocation-%d", invocationCount))
+15
View File
@@ -24,6 +24,21 @@ func JobsFromConfig(c *config.Config, parseFlags config.ParseFlags) ([]Job, erro
js[i] = j js[i] = j
} }
// receiving-side root filesystems must not overlap
{
rfss := make([]string, 0, len(js))
for _, j := range js {
jrfs, ok := j.OwnedDatasetSubtreeRoot()
if !ok {
continue
}
rfss = append(rfss, jrfs.ToString())
}
if err := validateReceivingSidesDoNotOverlap(rfss); err != nil {
return nil, err
}
}
return js, nil return js, nil
} }
+2 -3
View File
@@ -58,7 +58,7 @@ func modeSinkFromConfig(g *config.Global, in *config.SinkJob, jobID endpoint.Job
type modeSource struct { type modeSource struct {
senderConfig *endpoint.SenderConfig senderConfig *endpoint.SenderConfig
snapper snapper.Snapper snapper *snapper.PeriodicOrManual
} }
func modeSourceFromConfig(g *config.Global, in *config.SourceJob, jobID endpoint.JobID) (m *modeSource, err error) { func modeSourceFromConfig(g *config.Global, in *config.SourceJob, jobID endpoint.JobID) (m *modeSource, err error) {
@@ -88,8 +88,7 @@ func (m *modeSource) RunPeriodic(ctx context.Context) {
} }
func (m *modeSource) SnapperReport() *snapper.Report { func (m *modeSource) SnapperReport() *snapper.Report {
r := m.snapper.Report() return m.snapper.Report()
return &r
} }
func passiveSideFromConfig(g *config.Global, in *config.PassiveJob, configJob interface{}, parseFlags config.ParseFlags) (s *PassiveSide, err error) { func passiveSideFromConfig(g *config.Global, in *config.PassiveJob, configJob interface{}, parseFlags config.ParseFlags) (s *PassiveSide, err error) {
+7 -13
View File
@@ -15,7 +15,6 @@ import (
"github.com/zrepl/zrepl/config" "github.com/zrepl/zrepl/config"
"github.com/zrepl/zrepl/daemon/filters" "github.com/zrepl/zrepl/daemon/filters"
"github.com/zrepl/zrepl/daemon/job/trigger"
"github.com/zrepl/zrepl/daemon/job/wakeup" "github.com/zrepl/zrepl/daemon/job/wakeup"
"github.com/zrepl/zrepl/daemon/pruner" "github.com/zrepl/zrepl/daemon/pruner"
"github.com/zrepl/zrepl/daemon/snapper" "github.com/zrepl/zrepl/daemon/snapper"
@@ -27,7 +26,7 @@ import (
type SnapJob struct { type SnapJob struct {
name endpoint.JobID name endpoint.JobID
fsfilter zfs.DatasetFilter fsfilter zfs.DatasetFilter
snapper snapper.Snapper snapper *snapper.PeriodicOrManual
prunerFactory *pruner.LocalPrunerFactory prunerFactory *pruner.LocalPrunerFactory
@@ -87,8 +86,7 @@ func (j *SnapJob) Status() *Status {
s.Pruning = j.pruner.Report() s.Pruning = j.pruner.Report()
} }
j.prunerMtx.Unlock() j.prunerMtx.Unlock()
r := j.snapper.Report() s.Snapshotting = j.snapper.Report()
s.Snapshotting = &r
return &Status{Type: t, JobSpecific: s} return &Status{Type: t, JobSpecific: s}
} }
@@ -105,18 +103,12 @@ func (j *SnapJob) Run(ctx context.Context) {
defer log.Info("job exiting") defer log.Info("job exiting")
wakeupTrigger := wakeup.Trigger(ctx) periodicDone := make(chan struct{})
snapshottingTrigger := trigger.New("periodic")
ctx, cancel := context.WithCancel(ctx) ctx, cancel := context.WithCancel(ctx)
defer cancel() defer cancel()
periodicCtx, endTask := trace.WithTask(ctx, "snapshotting") periodicCtx, endTask := trace.WithTask(ctx, "snapshotting")
defer endTask() defer endTask()
go j.snapper.Run(periodicCtx, snapshottingTrigger) go j.snapper.Run(periodicCtx, periodicDone)
triggers := trigger.Empty()
triggered, endTask := triggers.Spawn(ctx, []trigger.Trigger{snapshottingTrigger, wakeupTrigger})
defer endTask()
invocationCount := 0 invocationCount := 0
outer: outer:
@@ -126,7 +118,9 @@ outer:
case <-ctx.Done(): case <-ctx.Done():
log.WithError(ctx.Err()).Info("context") log.WithError(ctx.Err()).Info("context")
break outer break outer
case <-triggered:
case <-wakeup.Wait(ctx):
case <-periodicDone:
} }
invocationCount++ invocationCount++
-23
View File
@@ -1,23 +0,0 @@
package trigger
import (
"context"
"github.com/robfig/cron/v3"
)
type Cron struct {
spec cron.Schedule
}
var _ Trigger = &Cron{}
func NewCron(spec cron.Schedule) *Cron {
return &Cron{spec: spec}
}
func (t *Cron) ID() string { return "cron" }
func (t *Cron) run(ctx context.Context, signal chan<- struct{}) {
panic("unimpl: extract from cron snapper")
}
-30
View File
@@ -1,30 +0,0 @@
package trigger
import (
"fmt"
"github.com/zrepl/zrepl/config"
)
func FromConfig(in []*config.ReplicationTriggerEnum) (*Triggers, error) {
triggers := make([]Trigger, len(in))
for i, e := range in {
var t Trigger = nil
switch te := e.Ret.(type) {
case *config.ReplicationTriggerManual:
// not a trigger
t = NewManual("manual")
case *config.ReplicationTriggerPeriodic:
t = NewPeriodic(te.Interval.Duration())
case *config.ReplicationTriggerCron:
t = NewCron(te.Cron.Schedule)
default:
return nil, fmt.Errorf("unknown trigger type %T", te)
}
triggers[i] = t
}
return &Triggers{
spawned: false,
triggers: triggers,
}, nil
}
-12
View File
@@ -1,12 +0,0 @@
package trigger
import (
"context"
"github.com/zrepl/zrepl/logger"
)
func getLogger(ctx context.Context) logger.Logger {
panic("unimpl")
}
-33
View File
@@ -1,33 +0,0 @@
package trigger
import "context"
type Manual struct {
id string
signal chan<- struct{}
}
var _ Trigger = &Manual{}
func NewManual(id string) *Manual {
return &Manual{
id: id,
signal: nil,
}
}
func (t *Manual) ID() string {
return t.id
}
func (t *Manual) run(ctx context.Context, signal chan<- struct{}) {
if t.signal != nil {
panic("run must only be called once")
}
t.signal = signal
}
// Panics if called before the trigger has been spanwed as part of a `Triggers`.
func (t *Manual) Fire() {
t.signal <- struct{}{}
}
-33
View File
@@ -1,33 +0,0 @@
package trigger
import (
"context"
"time"
)
type Periodic struct {
interval time.Duration
}
var _ Trigger = &Periodic{}
func NewPeriodic(interval time.Duration) *Periodic {
return &Periodic{
interval: interval,
}
}
func (p *Periodic) ID() string { return "periodic" }
func (p *Periodic) run(ctx context.Context, signal chan<- struct{}) {
t := time.NewTicker(p.interval)
defer t.Stop()
for {
select {
case <-t.C:
signal <- struct{}{}
case <-ctx.Done():
return
}
}
}
-91
View File
@@ -1,91 +0,0 @@
package trigger
import (
"context"
"github.com/zrepl/zrepl/daemon/logging/trace"
)
type Triggers struct {
spawned bool
triggers []Trigger
}
type Trigger interface {
ID() string
run(context.Context, chan<- struct{})
}
func Empty() *Triggers {
return &Triggers{
spawned: false,
triggers: nil,
}
}
func (t *Triggers) Spawn(ctx context.Context, additionalTriggers []Trigger) (chan Trigger, trace.DoneFunc) {
if t.spawned {
panic("must only spawn once")
}
t.spawned = true
t.triggers = append(t.triggers, additionalTriggers...)
sink := make(chan Trigger)
endTask := t.spawn(ctx, sink)
return sink, endTask
}
type triggering struct {
trigger Trigger
handled chan struct{}
}
func (t *Triggers) spawn(ctx context.Context, sink chan Trigger) trace.DoneFunc {
ctx, endTask := trace.WithTask(ctx, "triggers")
ctx, add, wait := trace.WithTaskGroup(ctx, "trigger-tasks")
triggered := make(chan triggering, len(t.triggers))
for _, t := range t.triggers {
t := t
signal := make(chan struct{})
go add(func(ctx context.Context) {
t.run(ctx, signal)
})
go func() {
for {
select {
case <-ctx.Done():
return
case <-signal:
handled := make(chan struct{})
select {
case triggered <- triggering{trigger: t, handled: handled}:
default:
panic("this funtion ensures that there's always room in the channel")
}
select {
case <-handled:
case <-ctx.Done():
return
}
}
}
}()
}
go func() {
defer wait()
for {
select {
case <-ctx.Done():
return
case triggering := <-triggered:
select {
case sink <- triggering.trigger:
default:
getLogger(ctx).
WithField("trigger_id", triggering.trigger.ID()).
Warn("dropping triggering because job is busy")
}
close(triggering.handled)
}
}
}()
return endTask
}
-6
View File
@@ -3,8 +3,6 @@ package wakeup
import ( import (
"context" "context"
"errors" "errors"
"github.com/zrepl/zrepl/daemon/job/trigger"
) )
type contextKey int type contextKey int
@@ -19,10 +17,6 @@ func Wait(ctx context.Context) <-chan struct{} {
return wc return wc
} }
func Trigger(ctx context.Context) trigger.Trigger {
panic("unimpl")
}
type Func func() error type Func func() error
var AlreadyWokenUp = errors.New("already woken up") var AlreadyWokenUp = errors.New("already woken up")
-1
View File
@@ -63,7 +63,6 @@ type Subsystem string
const ( const (
SubsysMeta Subsystem = "meta" SubsysMeta Subsystem = "meta"
SubsysJob Subsystem = "job" SubsysJob Subsystem = "job"
SubsysTrigger Subsystem = "trigger"
SubsysReplication Subsystem = "repl" SubsysReplication Subsystem = "repl"
SubsysEndpoint Subsystem = "endpoint" SubsysEndpoint Subsystem = "endpoint"
SubsysPruning Subsystem = "pruning" SubsysPruning Subsystem = "pruning"
+7 -4
View File
@@ -1,6 +1,6 @@
// package trace provides activity tracing via ctx through Tasks and Spans // package trace provides activity tracing via ctx through Tasks and Spans
// //
// # Basic Concepts // Basic Concepts
// //
// Tracing can be used to identify where a piece of code spends its time. // Tracing can be used to identify where a piece of code spends its time.
// //
@@ -10,9 +10,11 @@
// to tech-savvy users (albeit not developers). // to tech-savvy users (albeit not developers).
// //
// This package provides the concept of Tasks and Spans to express what activity is happening within an application: // This package provides the concept of Tasks and Spans to express what activity is happening within an application:
//
// - Neither task nor span is really tangible but instead contained within the context.Context tree // - Neither task nor span is really tangible but instead contained within the context.Context tree
// - Tasks represent concurrent activity (i.e. goroutines). // - Tasks represent concurrent activity (i.e. goroutines).
// - Spans represent a semantic stack trace within a task. // - Spans represent a semantic stack trace within a task.
//
// As a consequence, whenever a context is propagated across goroutine boundary, you need to create a child task: // As a consequence, whenever a context is propagated across goroutine boundary, you need to create a child task:
// //
// go func(ctx context.Context) { // go func(ctx context.Context) {
@@ -37,7 +39,6 @@
// } // }
// //
// In combination: // In combination:
//
// ctx, endTask = WithTask(ctx, "copy-dirs") // ctx, endTask = WithTask(ctx, "copy-dirs")
// defer endTask() // defer endTask()
// for i := range dirs { // for i := range dirs {
@@ -64,7 +65,8 @@
// //
// Recovering from endSpan() or endTask() panics will corrupt the trace stack and lead to corrupt tracefile output. // Recovering from endSpan() or endTask() panics will corrupt the trace stack and lead to corrupt tracefile output.
// //
// # Best Practices For Naming Tasks And Spans //
// Best Practices For Naming Tasks And Spans
// //
// Tasks should always have string constants as names, and must not contain the `#` character. WHy? // Tasks should always have string constants as names, and must not contain the `#` character. WHy?
// First, the visualization by chrome://tracing draws a horizontal bar for each task in the trace. // First, the visualization by chrome://tracing draws a horizontal bar for each task in the trace.
@@ -72,7 +74,8 @@
// Note that the `#NUM` suffix will be reused if a task has ended, in order to avoid an // Note that the `#NUM` suffix will be reused if a task has ended, in order to avoid an
// infinite number of horizontal bars in the visualization. // infinite number of horizontal bars in the visualization.
// //
// # Chrome-compatible Tracefile Support //
// Chrome-compatible Tracefile Support
// //
// The activity trace generated by usage of WithTask and WithSpan can be rendered to a JSON output file // The activity trace generated by usage of WithTask and WithSpan can be rendered to a JSON output file
// that can be loaded into chrome://tracing . // that can be loaded into chrome://tracing .
@@ -11,6 +11,8 @@ import (
// use like this: // use like this:
// //
// defer WithSpanFromStackUpdateCtx(&existingCtx)() // defer WithSpanFromStackUpdateCtx(&existingCtx)()
//
//
func WithSpanFromStackUpdateCtx(ctx *context.Context) DoneFunc { func WithSpanFromStackUpdateCtx(ctx *context.Context) DoneFunc {
childSpanCtx, end := WithSpan(*ctx, getMyCallerOrPanic()) childSpanCtx, end := WithSpan(*ctx, getMyCallerOrPanic())
*ctx = childSpanCtx *ctx = childSpanCtx
-10
View File
@@ -199,16 +199,6 @@ const (
Done Done
) )
// Returns true in case the State is a terminal state(PlanErr, ExecErr, Done)
func (s State) IsTerminal() bool {
switch s {
case PlanErr, ExecErr, Done:
return true
default:
return false
}
}
type updater func(func(*Pruner)) type updater func(func(*Pruner))
func (p *Pruner) Prune() { func (p *Pruner) Prune() {
-147
View File
@@ -1,147 +0,0 @@
package snapper
import (
"context"
"fmt"
"sync"
"time"
"github.com/pkg/errors"
"github.com/zrepl/zrepl/config"
"github.com/zrepl/zrepl/daemon/hooks"
"github.com/zrepl/zrepl/daemon/job/trigger"
"github.com/zrepl/zrepl/util/suspendresumesafetimer"
"github.com/zrepl/zrepl/zfs"
)
func cronFromConfig(fsf zfs.DatasetFilter, in config.SnapshottingCron) (*Cron, error) {
hooksList, err := hooks.ListFromConfig(&in.Hooks)
if err != nil {
return nil, errors.Wrap(err, "hook config error")
}
planArgs := planArgs{
prefix: in.Prefix,
timestampFormat: in.TimestampFormat,
hooks: hooksList,
}
return &Cron{config: in, fsf: fsf, planArgs: planArgs}, nil
}
type Cron struct {
config config.SnapshottingCron
fsf zfs.DatasetFilter
planArgs planArgs
mtx sync.RWMutex
running bool
wakeupTime time.Time // zero value means uninit
lastError error
lastPlan *plan
wakeupWhileRunningCount int
}
func (s *Cron) Run(ctx context.Context, snapshotsTaken *trigger.Manual) {
for {
now := time.Now()
s.mtx.Lock()
s.wakeupTime = s.config.Cron.Schedule.Next(now)
s.mtx.Unlock()
ctxDone := suspendresumesafetimer.SleepUntil(ctx, s.wakeupTime)
if ctxDone != nil {
return
}
getLogger(ctx).Debug("cron timer fired")
s.mtx.Lock()
if s.running {
getLogger(ctx).Warn("snapshotting triggered according to cron rules but previous snapshotting is not done; not taking a snapshot this time")
s.wakeupWhileRunningCount++
s.mtx.Unlock()
continue
}
s.lastError = nil
s.lastPlan = nil
s.wakeupWhileRunningCount = 0
s.running = true
s.mtx.Unlock()
go func() {
err := s.do(ctx)
s.mtx.Lock()
s.lastError = err
s.running = false
s.mtx.Unlock()
snapshotsTaken.Fire()
}()
}
}
func (s *Cron) do(ctx context.Context) error {
fss, err := zfs.ZFSListMapping(ctx, s.fsf)
if err != nil {
return errors.Wrap(err, "cannot list filesystems")
}
p := makePlan(s.planArgs, fss)
s.mtx.Lock()
s.lastPlan = p
s.lastError = nil
s.mtx.Unlock()
ok := p.execute(ctx, false)
if !ok {
return errors.New("one or more snapshots could not be created, check logs for details")
} else {
return nil
}
}
type CronState string
const (
CronStateRunning CronState = "running"
CronStateWaiting CronState = "waiting"
)
type CronReport struct {
State CronState
WakeupTime time.Time
Errors []string
Progress []*ReportFilesystem
}
func (s *Cron) Report() Report {
s.mtx.Lock()
defer s.mtx.Unlock()
r := CronReport{}
r.WakeupTime = s.wakeupTime
if s.running {
r.State = CronStateRunning
} else {
r.State = CronStateWaiting
}
if s.lastError != nil {
r.Errors = append(r.Errors, s.lastError.Error())
}
if s.wakeupWhileRunningCount > 0 {
r.Errors = append(r.Errors, fmt.Sprintf("cron frequency is too high; snapshots were not taken %d times", s.wakeupWhileRunningCount))
}
r.Progress = nil
if s.lastPlan != nil {
r.Progress = s.lastPlan.report()
}
return Report{Type: TypeCron, Cron: &r}
}
-68
View File
@@ -1,68 +0,0 @@
package snapper
import (
"fmt"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zrepl/yaml-config"
"github.com/zrepl/zrepl/config"
)
func TestCronLibraryWorks(t *testing.T) {
type testCase struct {
spec string
in time.Time
expect time.Time
}
dhm := func(day, hour, minutes int) time.Time {
return time.Date(2022, 7, day, hour, minutes, 0, 0, time.UTC)
}
hm := func(hour, minutes int) time.Time {
return dhm(23, hour, minutes)
}
tcs := []testCase{
{"0-10 * * * *", dhm(17, 1, 10), dhm(17, 2, 0)},
{"0-10 * * * *", dhm(17, 23, 10), dhm(18, 0, 0)},
{"0-10 * * * *", hm(1, 9), hm(1, 10)},
{"0-10 * * * *", hm(1, 9), hm(1, 10)},
{"1,3,5 * * * *", hm(1, 1), hm(1, 3)},
{"1,3,5 * * * *", hm(1, 2), hm(1, 3)},
{"1,3,5 * * * *", hm(1, 3), hm(1, 5)},
{"1,3,5 * * * *", hm(1, 5), hm(2, 1)},
{"* 0-5,8,12 * * *", hm(0, 0), hm(0, 1)},
{"* 0-5,8,12 * * *", hm(4, 59), hm(5, 0)},
{"* 0-5,8,12 * * *", hm(5, 0), hm(5, 1)},
{"* 0-5,8,12 * * *", hm(5, 59), hm(8, 0)},
{"* 0-5,8,12 * * *", hm(8, 59), hm(12, 0)},
// https://github.com/zrepl/zrepl/pull/614#issuecomment-1188358989
{"53 17,18,19 * * *", dhm(23, 17, 52), dhm(23, 17, 53)},
{"53 17,18,19 * * *", dhm(23, 17, 53), dhm(23, 18, 53)},
{"53 17,18,19 * * *", dhm(23, 18, 53), dhm(23, 19, 53)},
{"53 17,18,19 * * *", dhm(23, 19, 53), dhm(24 /* ! */, 17, 53)},
}
for i, tc := range tcs {
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
var s struct {
Cron config.CronSpec `yaml:"cron"`
}
inp := fmt.Sprintf("cron: %q", tc.spec)
fmt.Println("spec is ", inp)
err := yaml.UnmarshalStrict([]byte(inp), &s)
require.NoError(t, err)
actual := s.Cron.Schedule.Next(tc.in)
assert.Equal(t, tc.expect, actual)
})
}
}
-268
View File
@@ -1,268 +0,0 @@
package snapper
import (
"context"
"fmt"
"sort"
"strconv"
"strings"
"time"
"github.com/zrepl/zrepl/daemon/hooks"
"github.com/zrepl/zrepl/daemon/logging"
"github.com/zrepl/zrepl/util/chainlock"
"github.com/zrepl/zrepl/zfs"
)
type planArgs struct {
prefix string
timestampFormat string
hooks *hooks.List
}
type plan struct {
mtx chainlock.L
args planArgs
snaps map[*zfs.DatasetPath]*snapProgress
}
func makePlan(args planArgs, fss []*zfs.DatasetPath) *plan {
snaps := make(map[*zfs.DatasetPath]*snapProgress, len(fss))
for _, fs := range fss {
snaps[fs] = &snapProgress{state: SnapPending}
}
return &plan{snaps: snaps, args: args}
}
//go:generate stringer -type=SnapState
type SnapState uint
const (
SnapPending SnapState = 1 << iota
SnapStarted
SnapDone
SnapError
)
// All fields protected by Snapper.mtx
type snapProgress struct {
state SnapState
// SnapStarted, SnapDone, SnapError
name string
startAt time.Time
hookPlan *hooks.Plan
// SnapDone
doneAt time.Time
// SnapErr TODO disambiguate state
runResults hooks.PlanReport
}
func (plan *plan) formatNow(format string) string {
now := time.Now().UTC()
switch strings.ToLower(format) {
case "dense":
format = "20060102_150405_000"
case "human":
format = "2006-01-02_15:04:05"
case "iso-8601":
format = "2006-01-02T15:04:05.000Z"
case "unix-seconds":
return strconv.FormatInt(now.Unix(), 10)
}
return now.Format(format)
}
func (plan *plan) execute(ctx context.Context, dryRun bool) (ok bool) {
hookMatchCount := make(map[hooks.Hook]int, len(*plan.args.hooks))
for _, h := range *plan.args.hooks {
hookMatchCount[h] = 0
}
anyFsHadErr := false
// TODO channel programs -> allow a little jitter?
for fs, progress := range plan.snaps {
suffix := plan.formatNow(plan.args.timestampFormat)
snapname := fmt.Sprintf("%s%s", plan.args.prefix, suffix)
ctx := logging.WithInjectedField(ctx, "fs", fs.ToString())
ctx = logging.WithInjectedField(ctx, "snap", snapname)
hookEnvExtra := hooks.Env{
hooks.EnvFS: fs.ToString(),
hooks.EnvSnapshot: snapname,
}
jobCallback := hooks.NewCallbackHookForFilesystem("snapshot", fs, func(ctx context.Context) (err error) {
l := getLogger(ctx)
l.Debug("create snapshot")
err = zfs.ZFSSnapshot(ctx, fs, snapname, false) // TODO propagate context to ZFSSnapshot
if err != nil {
l.WithError(err).Error("cannot create snapshot")
}
return
})
fsHadErr := false
var hookPlanReport hooks.PlanReport
var hookPlan *hooks.Plan
{
filteredHooks, err := plan.args.hooks.CopyFilteredForFilesystem(fs)
if err != nil {
getLogger(ctx).WithError(err).Error("unexpected filter error")
fsHadErr = true
goto updateFSState
}
// account for running hooks
for _, h := range filteredHooks {
hookMatchCount[h] = hookMatchCount[h] + 1
}
var planErr error
hookPlan, planErr = hooks.NewPlan(&filteredHooks, hooks.PhaseSnapshot, jobCallback, hookEnvExtra)
if planErr != nil {
fsHadErr = true
getLogger(ctx).WithError(planErr).Error("cannot create job hook plan")
goto updateFSState
}
}
plan.mtx.HoldWhile(func() {
progress.name = snapname
progress.startAt = time.Now()
progress.hookPlan = hookPlan
progress.state = SnapStarted
})
{
getLogger(ctx).WithField("report", hookPlan.Report().String()).Debug("begin run job plan")
hookPlan.Run(ctx, dryRun)
hookPlanReport = hookPlan.Report()
fsHadErr = hookPlanReport.HadError() // not just fatal errors
if fsHadErr {
getLogger(ctx).WithField("report", hookPlanReport.String()).Error("end run job plan with error")
} else {
getLogger(ctx).WithField("report", hookPlanReport.String()).Info("end run job plan successful")
}
}
updateFSState:
anyFsHadErr = anyFsHadErr || fsHadErr
plan.mtx.HoldWhile(func() {
progress.doneAt = time.Now()
progress.state = SnapDone
if fsHadErr {
progress.state = SnapError
}
progress.runResults = hookPlanReport
})
}
for h, mc := range hookMatchCount {
if mc == 0 {
hookIdx := -1
for idx, ah := range *plan.args.hooks {
if ah == h {
hookIdx = idx
break
}
}
getLogger(ctx).WithField("hook", h.String()).WithField("hook_number", hookIdx+1).Warn("hook did not match any snapshotted filesystems")
}
}
return !anyFsHadErr
}
type ReportFilesystem struct {
Path string
State SnapState
// Valid in SnapStarted and later
SnapName string
StartAt time.Time
Hooks string
HooksHadError bool
// Valid in SnapDone | SnapError
DoneAt time.Time
}
func (plan *plan) report() []*ReportFilesystem {
plan.mtx.Lock()
defer plan.mtx.Unlock()
pReps := make([]*ReportFilesystem, 0, len(plan.snaps))
for fs, p := range plan.snaps {
var hooksStr string
var hooksHadError bool
if p.hookPlan != nil {
hooksStr, hooksHadError = p.report()
}
pReps = append(pReps, &ReportFilesystem{
Path: fs.ToString(),
State: p.state,
SnapName: p.name,
StartAt: p.startAt,
DoneAt: p.doneAt,
Hooks: hooksStr,
HooksHadError: hooksHadError,
})
}
sort.Slice(pReps, func(i, j int) bool {
return strings.Compare(pReps[i].Path, pReps[j].Path) == -1
})
return pReps
}
func (p *snapProgress) report() (hooksStr string, hooksHadError bool) {
hr := p.hookPlan.Report()
// FIXME: technically this belongs into client
// but we can't serialize hooks.Step ATM
rightPad := func(str string, length int, pad string) string {
if len(str) > length {
return str[:length]
}
return str + strings.Repeat(pad, length-len(str))
}
hooksHadError = hr.HadError()
rows := make([][]string, len(hr))
const numCols = 4
lens := make([]int, numCols)
for i, e := range hr {
rows[i] = make([]string, numCols)
rows[i][0] = fmt.Sprintf("%d", i+1)
rows[i][1] = e.Status.String()
runTime := "..."
if e.Status != hooks.StepPending {
runTime = e.End.Sub(e.Begin).Round(time.Millisecond).String()
}
rows[i][2] = runTime
rows[i][3] = ""
if e.Report != nil {
rows[i][3] = e.Report.String()
}
for j, col := range lens {
if len(rows[i][j]) > col {
lens[j] = len(rows[i][j])
}
}
}
rowsFlat := make([]string, len(hr))
for i, r := range rows {
colsPadded := make([]string, len(r))
for j, c := range r[:len(r)-1] {
colsPadded[j] = rightPad(c, lens[j], " ")
}
colsPadded[len(r)-1] = r[len(r)-1]
rowsFlat[i] = strings.Join(colsPadded, " ")
}
hooksStr = strings.Join(rowsFlat, "\n")
return hooksStr, hooksHadError
}
-17
View File
@@ -1,17 +0,0 @@
package snapper
import (
"context"
"github.com/zrepl/zrepl/daemon/job/trigger"
)
type manual struct{}
func (s *manual) Run(ctx context.Context, snapshotsTaken *trigger.Manual) {
// nothing to do
}
func (s *manual) Report() Report {
return Report{Type: TypeManual, Manual: &struct{}{}}
}
-389
View File
@@ -1,389 +0,0 @@
package snapper
import (
"context"
"fmt"
"sort"
"sync"
"time"
"github.com/pkg/errors"
"github.com/zrepl/zrepl/daemon/job/trigger"
"github.com/zrepl/zrepl/daemon/logging/trace"
"github.com/zrepl/zrepl/config"
"github.com/zrepl/zrepl/daemon/hooks"
"github.com/zrepl/zrepl/daemon/logging"
"github.com/zrepl/zrepl/util/envconst"
"github.com/zrepl/zrepl/util/suspendresumesafetimer"
"github.com/zrepl/zrepl/zfs"
)
func periodicFromConfig(g *config.Global, fsf zfs.DatasetFilter, in *config.SnapshottingPeriodic) (*Periodic, error) {
if in.Prefix == "" {
return nil, errors.New("prefix must not be empty")
}
if in.Interval.Duration() <= 0 {
return nil, errors.New("interval must be positive")
}
hookList, err := hooks.ListFromConfig(&in.Hooks)
if err != nil {
return nil, errors.Wrap(err, "hook config error")
}
args := periodicArgs{
interval: in.Interval.Duration(),
fsf: fsf,
planArgs: planArgs{
prefix: in.Prefix,
timestampFormat: in.TimestampFormat,
hooks: hookList,
},
// ctx and log is set in Run()
}
return &Periodic{state: SyncUp, args: args}, nil
}
type periodicArgs struct {
ctx context.Context
interval time.Duration
fsf zfs.DatasetFilter
planArgs planArgs
snapshotsTaken *trigger.Manual
dryRun bool
}
type Periodic struct {
args periodicArgs
mtx sync.Mutex
state State
// set in state Plan, used in Waiting
lastInvocation time.Time
// valid for state Snapshotting
plan *plan
// valid for state SyncUp and Waiting
sleepUntil time.Time
// valid for state Err
err error
}
//go:generate stringer -type=State
type State uint
const (
SyncUp State = 1 << iota
SyncUpErrWait
Planning
Snapshotting
Waiting
ErrorWait
Stopped
)
func (s State) sf() state {
m := map[State]state{
SyncUp: periodicStateSyncUp,
SyncUpErrWait: periodicStateWait,
Planning: periodicStatePlan,
Snapshotting: periodicStateSnapshot,
Waiting: periodicStateWait,
ErrorWait: periodicStateWait,
Stopped: nil,
}
return m[s]
}
type updater func(u func(*Periodic)) State
type state func(a periodicArgs, u updater) state
func (s *Periodic) Run(ctx context.Context, snapshotsTaken *trigger.Manual) {
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
getLogger(ctx).Debug("start")
defer getLogger(ctx).Debug("stop")
s.args.snapshotsTaken = snapshotsTaken
s.args.ctx = ctx
s.args.dryRun = false // for future expansion
u := func(u func(*Periodic)) State {
s.mtx.Lock()
defer s.mtx.Unlock()
if u != nil {
u(s)
}
return s.state
}
var st state = periodicStateSyncUp
for st != nil {
pre := u(nil)
st = st(s.args, u)
post := u(nil)
getLogger(ctx).
WithField("transition", fmt.Sprintf("%s=>%s", pre, post)).
Debug("state transition")
}
}
func onErr(err error, u updater) state {
return u(func(s *Periodic) {
s.err = err
preState := s.state
switch s.state {
case SyncUp:
s.state = SyncUpErrWait
case Planning:
fallthrough
case Snapshotting:
s.state = ErrorWait
}
getLogger(s.args.ctx).WithError(err).WithField("pre_state", preState).WithField("post_state", s.state).Error("snapshotting error")
}).sf()
}
func onMainCtxDone(ctx context.Context, u updater) state {
return u(func(s *Periodic) {
s.err = ctx.Err()
s.state = Stopped
}).sf()
}
func periodicStateSyncUp(a periodicArgs, u updater) state {
u(func(snapper *Periodic) {
snapper.lastInvocation = time.Now()
})
fss, err := listFSes(a.ctx, a.fsf)
if err != nil {
return onErr(err, u)
}
syncPoint, err := findSyncPoint(a.ctx, fss, a.planArgs.prefix, a.interval)
if err != nil {
return onErr(err, u)
}
u(func(s *Periodic) {
s.sleepUntil = syncPoint
})
ctxDone := suspendresumesafetimer.SleepUntil(a.ctx, syncPoint)
if ctxDone != nil {
return onMainCtxDone(a.ctx, u)
}
return u(func(s *Periodic) {
s.state = Planning
}).sf()
}
func periodicStatePlan(a periodicArgs, u updater) state {
u(func(snapper *Periodic) {
snapper.lastInvocation = time.Now()
})
fss, err := listFSes(a.ctx, a.fsf)
if err != nil {
return onErr(err, u)
}
p := makePlan(a.planArgs, fss)
return u(func(s *Periodic) {
s.state = Snapshotting
s.plan = p
s.err = nil
}).sf()
}
func periodicStateSnapshot(a periodicArgs, u updater) state {
var plan *plan
u(func(snapper *Periodic) {
plan = snapper.plan
})
ok := plan.execute(a.ctx, false)
a.snapshotsTaken.Fire()
return u(func(snapper *Periodic) {
if !ok {
snapper.state = ErrorWait
snapper.err = errors.New("one or more snapshots could not be created, check logs for details")
} else {
snapper.state = Waiting
snapper.err = nil
}
}).sf()
}
func periodicStateWait(a periodicArgs, u updater) state {
var sleepUntil time.Time
u(func(snapper *Periodic) {
lastTick := snapper.lastInvocation
snapper.sleepUntil = lastTick.Add(a.interval)
sleepUntil = snapper.sleepUntil
log := getLogger(a.ctx).WithField("sleep_until", sleepUntil).WithField("duration", a.interval)
logFunc := log.Debug
if snapper.state == ErrorWait || snapper.state == SyncUpErrWait {
logFunc = log.Error
}
logFunc("enter wait-state after error")
})
ctxDone := suspendresumesafetimer.SleepUntil(a.ctx, sleepUntil)
if ctxDone != nil {
return onMainCtxDone(a.ctx, u)
}
return u(func(snapper *Periodic) {
snapper.state = Planning
}).sf()
}
func listFSes(ctx context.Context, mf zfs.DatasetFilter) (fss []*zfs.DatasetPath, err error) {
return zfs.ZFSListMapping(ctx, mf)
}
var syncUpWarnNoSnapshotUntilSyncupMinDuration = envconst.Duration("ZREPL_SNAPPER_SYNCUP_WARN_MIN_DURATION", 1*time.Second)
// see docs/snapshotting.rst
func findSyncPoint(ctx context.Context, fss []*zfs.DatasetPath, prefix string, interval time.Duration) (syncPoint time.Time, err error) {
const (
prioHasVersions int = iota
prioNoVersions
)
type snapTime struct {
ds *zfs.DatasetPath
prio int // lower is higher
time time.Time
}
if len(fss) == 0 {
return time.Now(), nil
}
snaptimes := make([]snapTime, 0, len(fss))
hardErrs := 0
now := time.Now()
getLogger(ctx).Debug("examine filesystem state to find sync point")
for _, d := range fss {
ctx := logging.WithInjectedField(ctx, "fs", d.ToString())
syncPoint, err := findSyncPointFSNextOptimalSnapshotTime(ctx, now, interval, prefix, d)
if err == findSyncPointFSNoFilesystemVersionsErr {
snaptimes = append(snaptimes, snapTime{
ds: d,
prio: prioNoVersions,
time: now,
})
} else if err != nil {
hardErrs++
getLogger(ctx).WithError(err).Error("cannot determine optimal sync point for this filesystem")
} else {
getLogger(ctx).WithField("syncPoint", syncPoint).Debug("found optimal sync point for this filesystem")
snaptimes = append(snaptimes, snapTime{
ds: d,
prio: prioHasVersions,
time: syncPoint,
})
}
}
if hardErrs == len(fss) {
return time.Time{}, fmt.Errorf("hard errors in determining sync point for every matching filesystem")
}
if len(snaptimes) == 0 {
panic("implementation error: loop must either inc hardErrs or add result to snaptimes")
}
// sort ascending by (prio,time)
// => those filesystems with versions win over those without any
sort.Slice(snaptimes, func(i, j int) bool {
if snaptimes[i].prio == snaptimes[j].prio {
return snaptimes[i].time.Before(snaptimes[j].time)
}
return snaptimes[i].prio < snaptimes[j].prio
})
winnerSyncPoint := snaptimes[0].time
l := getLogger(ctx).WithField("syncPoint", winnerSyncPoint.String())
l.Info("determined sync point")
if winnerSyncPoint.Sub(now) > syncUpWarnNoSnapshotUntilSyncupMinDuration {
for _, st := range snaptimes {
if st.prio == prioNoVersions {
l.WithField("fs", st.ds.ToString()).Warn("filesystem will not be snapshotted until sync point")
}
}
}
return snaptimes[0].time, nil
}
var findSyncPointFSNoFilesystemVersionsErr = fmt.Errorf("no filesystem versions")
func findSyncPointFSNextOptimalSnapshotTime(ctx context.Context, now time.Time, interval time.Duration, prefix string, d *zfs.DatasetPath) (time.Time, error) {
fsvs, err := zfs.ZFSListFilesystemVersions(ctx, d, zfs.ListFilesystemVersionsOptions{
Types: zfs.Snapshots,
ShortnamePrefix: prefix,
})
if err != nil {
return time.Time{}, errors.Wrap(err, "list filesystem versions")
}
if len(fsvs) <= 0 {
return time.Time{}, findSyncPointFSNoFilesystemVersionsErr
}
// Sort versions by creation
sort.SliceStable(fsvs, func(i, j int) bool {
return fsvs[i].CreateTXG < fsvs[j].CreateTXG
})
latest := fsvs[len(fsvs)-1]
getLogger(ctx).WithField("creation", latest.Creation).Debug("found latest snapshot")
since := now.Sub(latest.Creation)
if since < 0 {
return time.Time{}, fmt.Errorf("snapshot %q is from the future: creation=%q now=%q", latest.ToAbsPath(d), latest.Creation, now)
}
return latest.Creation.Add(interval), nil
}
type PeriodicReport struct {
State State
// valid in state SyncUp and Waiting
SleepUntil time.Time
// valid in state Err
Error string
// valid in state Snapshotting
Progress []*ReportFilesystem
}
func (s *Periodic) Report() Report {
s.mtx.Lock()
defer s.mtx.Unlock()
var progress []*ReportFilesystem = nil
if s.plan != nil {
progress = s.plan.report()
}
r := &PeriodicReport{
State: s.state,
SleepUntil: s.sleepUntil,
Error: errOrEmptyString(s.err),
Progress: progress,
}
return Report{Type: TypePeriodic, Periodic: r}
}
+486 -22
View File
@@ -3,41 +3,505 @@ package snapper
import ( import (
"context" "context"
"fmt" "fmt"
"sort"
"sync"
"time"
"github.com/pkg/errors"
"github.com/zrepl/zrepl/daemon/logging/trace"
"github.com/zrepl/zrepl/config" "github.com/zrepl/zrepl/config"
"github.com/zrepl/zrepl/daemon/job/trigger" "github.com/zrepl/zrepl/daemon/hooks"
"github.com/zrepl/zrepl/daemon/logging"
"github.com/zrepl/zrepl/logger"
"github.com/zrepl/zrepl/util/envconst"
"github.com/zrepl/zrepl/zfs" "github.com/zrepl/zrepl/zfs"
) )
type Type string //go:generate stringer -type=SnapState
type SnapState uint
const ( const (
TypePeriodic Type = "periodic" SnapPending SnapState = 1 << iota
TypeCron Type = "cron" SnapStarted
TypeManual Type = "manual" SnapDone
SnapError
) )
type Snapper interface { // All fields protected by Snapper.mtx
Run(ctx context.Context, snapshotsTaken *trigger.Manual) type snapProgress struct {
Report() Report state SnapState
// SnapStarted, SnapDone, SnapError
name string
startAt time.Time
hookPlan *hooks.Plan
// SnapDone
doneAt time.Time
// SnapErr TODO disambiguate state
runResults hooks.PlanReport
} }
type Report struct { type args struct {
Type Type ctx context.Context
Periodic *PeriodicReport prefix string
Cron *CronReport interval time.Duration
Manual *struct{} fsf zfs.DatasetFilter
snapshotsTaken chan<- struct{}
hooks *hooks.List
dryRun bool
} }
func FromConfig(g *config.Global, fsf zfs.DatasetFilter, in config.SnapshottingEnum) (Snapper, error) { type Snapper struct {
switch v := in.Ret.(type) { args args
case *config.SnapshottingPeriodic:
return periodicFromConfig(g, fsf, v) mtx sync.Mutex
case *config.SnapshottingCron: state State
return cronFromConfig(fsf, *v)
case *config.SnapshottingManual: // set in state Plan, used in Waiting
return &manual{}, nil lastInvocation time.Time
// valid for state Snapshotting
plan map[*zfs.DatasetPath]*snapProgress
// valid for state SyncUp and Waiting
sleepUntil time.Time
// valid for state Err
err error
}
//go:generate stringer -type=State
type State uint
const (
SyncUp State = 1 << iota
SyncUpErrWait
Planning
Snapshotting
Waiting
ErrorWait
Stopped
)
func (s State) sf() state {
m := map[State]state{
SyncUp: syncUp,
SyncUpErrWait: wait,
Planning: plan,
Snapshotting: snapshot,
Waiting: wait,
ErrorWait: wait,
Stopped: nil,
}
return m[s]
}
type updater func(u func(*Snapper)) State
type state func(a args, u updater) state
type Logger = logger.Logger
func getLogger(ctx context.Context) Logger {
return logging.GetLogger(ctx, logging.SubsysSnapshot)
}
func PeriodicFromConfig(g *config.Global, fsf zfs.DatasetFilter, in *config.SnapshottingPeriodic) (*Snapper, error) {
if in.Prefix == "" {
return nil, errors.New("prefix must not be empty")
}
if in.Interval <= 0 {
return nil, errors.New("interval must be positive")
}
hookList, err := hooks.ListFromConfig(&in.Hooks)
if err != nil {
return nil, errors.Wrap(err, "hook config error")
}
args := args{
prefix: in.Prefix,
interval: in.Interval,
fsf: fsf,
hooks: hookList,
// ctx and log is set in Run()
}
return &Snapper{state: SyncUp, args: args}, nil
}
func (s *Snapper) Run(ctx context.Context, snapshotsTaken chan<- struct{}) {
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
getLogger(ctx).Debug("start")
defer getLogger(ctx).Debug("stop")
s.args.snapshotsTaken = snapshotsTaken
s.args.ctx = ctx
s.args.dryRun = false // for future expansion
u := func(u func(*Snapper)) State {
s.mtx.Lock()
defer s.mtx.Unlock()
if u != nil {
u(s)
}
return s.state
}
var st state = syncUp
for st != nil {
pre := u(nil)
st = st(s.args, u)
post := u(nil)
getLogger(ctx).
WithField("transition", fmt.Sprintf("%s=>%s", pre, post)).
Debug("state transition")
}
}
func onErr(err error, u updater) state {
return u(func(s *Snapper) {
s.err = err
preState := s.state
switch s.state {
case SyncUp:
s.state = SyncUpErrWait
case Planning:
fallthrough
case Snapshotting:
s.state = ErrorWait
}
getLogger(s.args.ctx).WithError(err).WithField("pre_state", preState).WithField("post_state", s.state).Error("snapshotting error")
}).sf()
}
func onMainCtxDone(ctx context.Context, u updater) state {
return u(func(s *Snapper) {
s.err = ctx.Err()
s.state = Stopped
}).sf()
}
func syncUp(a args, u updater) state {
u(func(snapper *Snapper) {
snapper.lastInvocation = time.Now()
})
fss, err := listFSes(a.ctx, a.fsf)
if err != nil {
return onErr(err, u)
}
syncPoint, err := findSyncPoint(a.ctx, fss, a.prefix, a.interval)
if err != nil {
return onErr(err, u)
}
u(func(s *Snapper) {
s.sleepUntil = syncPoint
})
return waitUntilThenTransitionToPlanning(a, u, syncPoint)
}
func plan(a args, u updater) state {
u(func(snapper *Snapper) {
snapper.lastInvocation = time.Now()
})
fss, err := listFSes(a.ctx, a.fsf)
if err != nil {
return onErr(err, u)
}
plan := make(map[*zfs.DatasetPath]*snapProgress, len(fss))
for _, fs := range fss {
plan[fs] = &snapProgress{state: SnapPending}
}
return u(func(s *Snapper) {
s.state = Snapshotting
s.plan = plan
s.err = nil
}).sf()
}
func snapshot(a args, u updater) state {
var plan map[*zfs.DatasetPath]*snapProgress
u(func(snapper *Snapper) {
plan = snapper.plan
})
hookMatchCount := make(map[hooks.Hook]int, len(*a.hooks))
for _, h := range *a.hooks {
hookMatchCount[h] = 0
}
anyFsHadErr := false
// TODO channel programs -> allow a little jitter?
for fs, progress := range plan {
suffix := time.Now().In(time.UTC).Format("20060102_150405_000")
snapname := fmt.Sprintf("%s%s", a.prefix, suffix)
ctx := logging.WithInjectedField(a.ctx, "fs", fs.ToString())
ctx = logging.WithInjectedField(ctx, "snap", snapname)
hookEnvExtra := hooks.Env{
hooks.EnvFS: fs.ToString(),
hooks.EnvSnapshot: snapname,
}
jobCallback := hooks.NewCallbackHookForFilesystem("snapshot", fs, func(ctx context.Context) (err error) {
l := getLogger(ctx)
l.Debug("create snapshot")
err = zfs.ZFSSnapshot(ctx, fs, snapname, false) // TODO propagate context to ZFSSnapshot
if err != nil {
l.WithError(err).Error("cannot create snapshot")
}
return
})
fsHadErr := false
var planReport hooks.PlanReport
var plan *hooks.Plan
{
filteredHooks, err := a.hooks.CopyFilteredForFilesystem(fs)
if err != nil {
getLogger(ctx).WithError(err).Error("unexpected filter error")
fsHadErr = true
goto updateFSState
}
// account for running hooks
for _, h := range filteredHooks {
hookMatchCount[h] = hookMatchCount[h] + 1
}
var planErr error
plan, planErr = hooks.NewPlan(&filteredHooks, hooks.PhaseSnapshot, jobCallback, hookEnvExtra)
if planErr != nil {
fsHadErr = true
getLogger(ctx).WithError(planErr).Error("cannot create job hook plan")
goto updateFSState
}
}
u(func(snapper *Snapper) {
progress.name = snapname
progress.startAt = time.Now()
progress.hookPlan = plan
progress.state = SnapStarted
})
{
getLogger(ctx).WithField("report", plan.Report().String()).Debug("begin run job plan")
plan.Run(ctx, a.dryRun)
planReport = plan.Report()
fsHadErr = planReport.HadError() // not just fatal errors
if fsHadErr {
getLogger(ctx).WithField("report", planReport.String()).Error("end run job plan with error")
} else {
getLogger(ctx).WithField("report", planReport.String()).Info("end run job plan successful")
}
}
updateFSState:
anyFsHadErr = anyFsHadErr || fsHadErr
u(func(snapper *Snapper) {
progress.doneAt = time.Now()
progress.state = SnapDone
if fsHadErr {
progress.state = SnapError
}
progress.runResults = planReport
})
}
select {
case a.snapshotsTaken <- struct{}{}:
default: default:
return nil, fmt.Errorf("unknown snapshotting type %T", v) if a.snapshotsTaken != nil {
getLogger(a.ctx).Warn("callback channel is full, discarding snapshot update event")
}
}
for h, mc := range hookMatchCount {
if mc == 0 {
hookIdx := -1
for idx, ah := range *a.hooks {
if ah == h {
hookIdx = idx
break
}
}
getLogger(a.ctx).WithField("hook", h.String()).WithField("hook_number", hookIdx+1).Warn("hook did not match any snapshotted filesystems")
}
}
return u(func(snapper *Snapper) {
if anyFsHadErr {
snapper.state = ErrorWait
snapper.err = errors.New("one or more snapshots could not be created, check logs for details")
} else {
snapper.state = Waiting
snapper.err = nil
}
}).sf()
}
func wait(a args, u updater) state {
var sleepUntil time.Time
u(func(snapper *Snapper) {
lastTick := snapper.lastInvocation
snapper.sleepUntil = lastTick.Add(a.interval)
sleepUntil = snapper.sleepUntil
log := getLogger(a.ctx).WithField("sleep_until", sleepUntil).WithField("duration", a.interval)
logFunc := log.Debug
if snapper.state == ErrorWait || snapper.state == SyncUpErrWait {
logFunc = log.Error
}
logFunc("enter wait-state after error")
})
return waitUntilThenTransitionToPlanning(a, u, sleepUntil)
}
func waitUntilThenTransitionToPlanning(a args, u updater, sleepUntil time.Time) state {
ticker := time.NewTicker(333 * time.Millisecond)
defer ticker.Stop()
lastLog := time.Now()
for {
select {
case <-ticker.C:
now := time.Now()
if now.Before(sleepUntil) {
// rate limit loggin to once per minute
if (now.Sub(lastLog)) > envconst.Duration("ZREPL_SNAPPER_DEBUG_LOG_ISSUE_611", 1*time.Minute) {
getLogger(a.ctx).WithField("sleep_until", sleepUntil).WithField("remaining", sleepUntil.Sub(now)).Error("DEBUG-LOG-ZREPL-ISSUE-611")
lastLog = now
}
continue // continue with ticker
}
return u(func(snapper *Snapper) {
snapper.state = Planning
}).sf()
case <-a.ctx.Done():
return onMainCtxDone(a.ctx, u)
}
} }
} }
func listFSes(ctx context.Context, mf zfs.DatasetFilter) (fss []*zfs.DatasetPath, err error) {
return zfs.ZFSListMapping(ctx, mf)
}
var syncUpWarnNoSnapshotUntilSyncupMinDuration = envconst.Duration("ZREPL_SNAPPER_SYNCUP_WARN_MIN_DURATION", 1*time.Second)
// see docs/snapshotting.rst
func findSyncPoint(ctx context.Context, fss []*zfs.DatasetPath, prefix string, interval time.Duration) (syncPoint time.Time, err error) {
const (
prioHasVersions int = iota
prioNoVersions
)
type snapTime struct {
ds *zfs.DatasetPath
prio int // lower is higher
time time.Time
}
if len(fss) == 0 {
return time.Now(), nil
}
snaptimes := make([]snapTime, 0, len(fss))
hardErrs := 0
now := time.Now()
getLogger(ctx).Debug("examine filesystem state to find sync point")
for _, d := range fss {
ctx := logging.WithInjectedField(ctx, "fs", d.ToString())
syncPoint, err := findSyncPointFSNextOptimalSnapshotTime(ctx, now, interval, prefix, d)
if err == findSyncPointFSNoFilesystemVersionsErr {
snaptimes = append(snaptimes, snapTime{
ds: d,
prio: prioNoVersions,
time: now,
})
} else if err != nil {
hardErrs++
getLogger(ctx).WithError(err).Error("cannot determine optimal sync point for this filesystem")
} else {
getLogger(ctx).WithField("syncPoint", syncPoint).Debug("found optimal sync point for this filesystem")
snaptimes = append(snaptimes, snapTime{
ds: d,
prio: prioHasVersions,
time: syncPoint,
})
}
}
if hardErrs == len(fss) {
return time.Time{}, fmt.Errorf("hard errors in determining sync point for every matching filesystem")
}
if len(snaptimes) == 0 {
panic("implementation error: loop must either inc hardErrs or add result to snaptimes")
}
// sort ascending by (prio,time)
// => those filesystems with versions win over those without any
sort.Slice(snaptimes, func(i, j int) bool {
if snaptimes[i].prio == snaptimes[j].prio {
return snaptimes[i].time.Before(snaptimes[j].time)
}
return snaptimes[i].prio < snaptimes[j].prio
})
winnerSyncPoint := snaptimes[0].time
l := getLogger(ctx).WithField("syncPoint", winnerSyncPoint.String())
l.Info("determined sync point")
if winnerSyncPoint.Sub(now) > syncUpWarnNoSnapshotUntilSyncupMinDuration {
for _, st := range snaptimes {
if st.prio == prioNoVersions {
l.WithField("fs", st.ds.ToString()).Warn("filesystem will not be snapshotted until sync point")
}
}
}
return snaptimes[0].time, nil
}
var findSyncPointFSNoFilesystemVersionsErr = fmt.Errorf("no filesystem versions")
func findSyncPointFSNextOptimalSnapshotTime(ctx context.Context, now time.Time, interval time.Duration, prefix string, d *zfs.DatasetPath) (time.Time, error) {
fsvs, err := zfs.ZFSListFilesystemVersions(ctx, d, zfs.ListFilesystemVersionsOptions{
Types: zfs.Snapshots,
ShortnamePrefix: prefix,
})
if err != nil {
return time.Time{}, errors.Wrap(err, "list filesystem versions")
}
if len(fsvs) <= 0 {
return time.Time{}, findSyncPointFSNoFilesystemVersionsErr
}
// Sort versions by creation
sort.SliceStable(fsvs, func(i, j int) bool {
return fsvs[i].CreateTXG < fsvs[j].CreateTXG
})
latest := fsvs[len(fsvs)-1]
getLogger(ctx).WithField("creation", latest.Creation).Debug("found latest snapshot")
since := now.Sub(latest.Creation)
if since < 0 {
return time.Time{}, fmt.Errorf("snapshot %q is from the future: creation=%q now=%q", latest.ToAbsPath(d), latest.Creation, now)
}
return latest.Creation.Add(interval), nil
}
+48
View File
@@ -0,0 +1,48 @@
package snapper
import (
"context"
"fmt"
"github.com/zrepl/zrepl/config"
"github.com/zrepl/zrepl/zfs"
)
// FIXME: properly abstract snapshotting:
// - split up things that trigger snapshotting from the mechanism
// - timer-based trigger (periodic)
// - call from control socket (manual)
// - mixed modes?
// - support a `zrepl snapshot JOBNAME` subcommand for config.SnapshottingManual
type PeriodicOrManual struct {
s *Snapper
}
func (s *PeriodicOrManual) Run(ctx context.Context, wakeUpCommon chan<- struct{}) {
if s.s != nil {
s.s.Run(ctx, wakeUpCommon)
}
}
// Returns nil if manual
func (s *PeriodicOrManual) Report() *Report {
if s.s != nil {
return s.s.Report()
}
return nil
}
func FromConfig(g *config.Global, fsf zfs.DatasetFilter, in config.SnapshottingEnum) (*PeriodicOrManual, error) {
switch v := in.Ret.(type) {
case *config.SnapshottingPeriodic:
snapper, err := PeriodicFromConfig(g, fsf, v)
if err != nil {
return nil, err
}
return &PeriodicOrManual{snapper}, nil
case *config.SnapshottingManual:
return &PeriodicOrManual{}, nil
default:
return nil, fmt.Errorf("unknown snapshotting type %T", v)
}
}
+118
View File
@@ -0,0 +1,118 @@
package snapper
import (
"fmt"
"sort"
"strings"
"time"
"github.com/zrepl/zrepl/daemon/hooks"
)
type Report struct {
State State
// valid in state SyncUp and Waiting
SleepUntil time.Time
// valid in state Err
Error string
// valid in state Snapshotting
Progress []*ReportFilesystem
}
type ReportFilesystem struct {
Path string
State SnapState
// Valid in SnapStarted and later
SnapName string
StartAt time.Time
Hooks string
HooksHadError bool
// Valid in SnapDone | SnapError
DoneAt time.Time
}
func errOrEmptyString(e error) string {
if e != nil {
return e.Error()
}
return ""
}
func (s *Snapper) Report() *Report {
s.mtx.Lock()
defer s.mtx.Unlock()
pReps := make([]*ReportFilesystem, 0, len(s.plan))
for fs, p := range s.plan {
var hooksStr string
var hooksHadError bool
if p.hookPlan != nil {
hr := p.hookPlan.Report()
// FIXME: technically this belongs into client
// but we can't serialize hooks.Step ATM
rightPad := func(str string, length int, pad string) string {
if len(str) > length {
return str[:length]
}
return str + strings.Repeat(pad, length-len(str))
}
hooksHadError = hr.HadError()
rows := make([][]string, len(hr))
const numCols = 4
lens := make([]int, numCols)
for i, e := range hr {
rows[i] = make([]string, numCols)
rows[i][0] = fmt.Sprintf("%d", i+1)
rows[i][1] = e.Status.String()
runTime := "..."
if e.Status != hooks.StepPending {
runTime = e.End.Sub(e.Begin).Round(time.Millisecond).String()
}
rows[i][2] = runTime
rows[i][3] = ""
if e.Report != nil {
rows[i][3] = e.Report.String()
}
for j, col := range lens {
if len(rows[i][j]) > col {
lens[j] = len(rows[i][j])
}
}
}
rowsFlat := make([]string, len(hr))
for i, r := range rows {
colsPadded := make([]string, len(r))
for j, c := range r[:len(r)-1] {
colsPadded[j] = rightPad(c, lens[j], " ")
}
colsPadded[len(r)-1] = r[len(r)-1]
rowsFlat[i] = strings.Join(colsPadded, " ")
}
hooksStr = strings.Join(rowsFlat, "\n")
}
pReps = append(pReps, &ReportFilesystem{
Path: fs.ToString(),
State: p.state,
SnapName: p.name,
StartAt: p.startAt,
DoneAt: p.doneAt,
Hooks: hooksStr,
HooksHadError: hooksHadError,
})
}
sort.Slice(pReps, func(i, j int) bool {
return strings.Compare(pReps[i].Path, pReps[j].Path) == -1
})
r := &Report{
State: s.state,
SleepUntil: s.sleepUntil,
Error: errOrEmptyString(s.err),
Progress: pReps,
}
return r
}
-21
View File
@@ -1,21 +0,0 @@
package snapper
import (
"context"
"github.com/zrepl/zrepl/daemon/logging"
"github.com/zrepl/zrepl/logger"
)
type Logger = logger.Logger
func getLogger(ctx context.Context) Logger {
return logging.GetLogger(ctx, logging.SubsysSnapshot)
}
func errOrEmptyString(e error) string {
if e != nil {
return e.Error()
}
return ""
}
File diff suppressed because it is too large Load Diff
-23
View File
@@ -1,23 +0,0 @@
#!/sbin/openrc-run
command='/usr/local/bin/zrepl'
command_args='daemon'
command_background='true'
pidfile="/run/${RC_SVCNAME}.pid"
output_log="/var/log/${RC_SVCNAME}.log"
error_log="/var/log/${RC_SVCNAME}.log"
zrepl_runtime_dir='/var/run/zrepl'
start() {
mkdir -p "$zrepl_runtime_dir/stdinserver"
chmod -R 0700 "$zrepl_runtime_dir"
default_start
}
stop() {
rm -rf "$zrepl_runtime_dir"
default_stop
}
# vi: noet sw=8 sts=0
+1 -5
View File
@@ -9,9 +9,6 @@ ExecStart=/usr/local/bin/zrepl --config /etc/zrepl/zrepl.yml daemon
RuntimeDirectory=zrepl zrepl/stdinserver RuntimeDirectory=zrepl zrepl/stdinserver
RuntimeDirectoryMode=0700 RuntimeDirectoryMode=0700
# Make Go produce coredumps
Environment=GOTRACEBACK='crash'
ProtectSystem=strict ProtectSystem=strict
#PrivateDevices=yes # TODO ZFS needs access to /dev/zfs, could we limit this? #PrivateDevices=yes # TODO ZFS needs access to /dev/zfs, could we limit this?
ProtectKernelTunables=yes ProtectKernelTunables=yes
@@ -30,8 +27,7 @@ ProtectHome=read-only
# SystemCallFilter # SystemCallFilter
# ~@privileged doesn't work with Ubuntu 18.04 ssh # ~@privileged doesn't work with Ubuntu 18.04 ssh
SystemCallFilter=~ @mount @cpu-emulation @keyring @module @obsolete @raw-io @debug @clock @resources SystemCallFilter=~ @mount @cpu-emulation @keyring @module @obsolete @raw-io @debug @clock @resources
# Go1.19 added automatic RLIMIT_NOFILE changes, so, we need to allow that
SystemCallFilter= setrlimit
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target
+2 -2
View File
@@ -10,11 +10,11 @@ BUILDDIR = _build
# Put it first so that "make" without argument is like "make help". # Put it first so that "make" without argument is like "make help".
help: help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" -c sphinxconf $(SPHINXOPTS) $(O)
.PHONY: help Makefile .PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new # Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile %: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" -c sphinxconf $(SPHINXOPTS) $(O)
-43
View File
@@ -1,43 +0,0 @@
/* https://github.com/sphinx-contrib/sphinxcontrib-versioning/blob/0b56210959c6b21bbb730072e81876491b4e1371/sphinxcontrib/versioning/_static/banner.css */
.scv-banner {
padding: 3px;
border-radius: 2px;
font-size: 80%;
text-align: center;
color: white;
background: #d40 linear-gradient(-45deg,
rgba(255, 255, 255, 0.2) 0%,
rgba(255, 255, 255, 0.2) 25%,
transparent 25%,
transparent 50%,
rgba(255, 255, 255, 0.2) 50%,
rgba(255, 255, 255, 0.2) 75%,
transparent 75%,
transparent
);
background-size: 28px 28px;
}
.scv-banner > a {
color: white;
}
.scv-sphinx_rtd_theme {
background-color: #2980B9;
}
.scv-bizstyle {
background-color: #336699;
}
.scv-classic {
text-align: center !important;
}
.scv-traditional {
text-align: center !important;
}
-17
View File
@@ -1,17 +0,0 @@
{% extends "!page.html" %}
{% block body %}
{% if current_version and latest_version and current_version != latest_version %}
<p class="scv-banner scv-sphinx_rtd_theme">
<strong>
{% if current_version.is_released %}
You're reading an old version of this documentation.
If you want up-to-date information, please have a look at <a href="{{ vpathto(latest_version.name) }}">{{latest_version.name}}</a>.
{% else %}
You're reading the documentation for a development version.
For the latest released version, please have a look at <a href="{{ vpathto(latest_version.name) }}">{{latest_version.name}}</a>.
{% endif %}
</strong>
</p>
{% endif %}
{{ super() }}
{% endblock %}%
-27
View File
@@ -1,27 +0,0 @@
{%- if current_version %}
<div class="rst-versions" data-toggle="rst-versions" role="note" aria-label="versions">
<span class="rst-current-version" data-toggle="rst-current-version">
<span class="fa fa-book"> Other Versions</span>
v: {{ current_version.name }}
<span class="fa fa-caret-down"></span>
</span>
<div class="rst-other-versions">
{%- if versions.tags %}
<dl>
<dt>Tags</dt>
{%- for item in versions.tags %}
<dd><a href="{{ item.url }}">{{ item.name }}</a></dd>
{%- endfor %}
</dl>
{%- endif %}
{%- if versions.branches %}
<dl>
<dt>Branches</dt>
{%- for item in versions.branches %}
<dd><a href="{{ item.url }}">{{ item.name }}</a></dd>
{%- endfor %}
</dl>
{%- endif %}
</div>
</div>
{%- endif %}
+13 -76
View File
@@ -16,90 +16,19 @@ Changelog
The changelog summarizes bugfixes that are deemed relevant for users and package maintainers. The changelog summarizes bugfixes that are deemed relevant for users and package maintainers.
Developers should consult the git commit log or GitHub issue tracker. Developers should consult the git commit log or GitHub issue tracker.
Next Release 0.6 (Unreleased)
------------ ----------------
The plan for the next release is to revisit how zrepl does snapshot management. * `Feature Wishlist on GitHub <https://github.com/zrepl/zrepl/discussions/547>`_
High-level goals:
- Make it easy to decouple snapshot management (snapshotting, pruning) from replication. * |feature| Add ``ZREPL_DESTROY_MAX_BATCH_SIZE`` env var (default 0=unlimited).
- Ability to include/exclude snapshots from replication. * |break| |feature| convert Prometheus metric ``zrepl_version_daemon`` to ``zrepl_start_time`` metric
This is useful for aforementioned decoupling, e.g., separate snapshot prefixes for local & remote replication.
Also, it makes explicit that by default, zrepl replicates all snapshots, and that
replication has no concept of "zrepl-created snapshots", which is a common misconception.
- Use of ``zfs snapshot`` comma syntax or channel programs to take snapshots of multiple
datasets atomically.
- Provide an alternative to the ``grid`` pruning policy.
Most likely something based on hourly/daily/weekly/monthly "trains" plus a count.
- Ability to prune at the granularity of the **group** of snapshots created at a given
time, as opposed to the individual snapshots within a dataset.
Maybe this will be addressed by the alternative to the ``grid`` pruning policy,
as it will likely be more predictable.
Those changes will likely come with some breakage in the config.
However, I want to avoid breaking **use cases** that are satisfied by the current design.
There will be beta/RC releases to give users a chance to evaluate.
0.6.1
-----
* |feature| add metric to detect filesystems rules that don't match any local dataset (thanks, `@gmekicaxcient <https://github.com/gmekicaxcient>`_).
* |bugfix| ``zrepl status``: hide progress bar once all filesystems reach terminal state (thanks, `@0x3333 <https://github.com/0x3333>`_).
* |bugfix| handling of tenative cursor presence if protection strategy doesn't use it (:issue:`714`).
* |docs| address setup with two or more external disks (thanks, `@se-jaeger <https://github.com/se-jaeger>`_).
* |docs| document ``replication`` and ``conflict_resolution`` options (thanks, `@InsanePrawn <https://github.com/InsanePrawn>`_).
* |docs| docs: talks: add note on keep_bookmarks option (thanks, `@skirmess <https://github.com/skirmess>`_).
* |maint| dist: add openrc service file (thanks, `@gramosg <https://github.com/gramosg>`_).
* |maint| grafana: update dashboard to Grafana 9.3.6.
* |maint| run platform tests as part of CI.
* |maint| build: upgrade to Go 1.21 and update golangci-lint; minimum Go version for builds is now 1.20
.. 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.6
---
* |feature| :ref:`Schedule-based snapshotting<job-snapshotting--cron>` using ``cron`` syntax instead of an interval.
* |feature| Configurable initial replication policy.
When a filesystem is first replicated to a receiver, this control whether just the newest
snapshot will be replicated vs. all existing snapshots. Learn more :ref:`in the docs <conflict_resolution-initial_replication>`.
* |feature| Configurable timestamp format for snapshot names via :ref:`timestamp_format<job-snapshotting-timestamp_format>`
(Thanks, `@ydylla <https://github.com/ydylla>`_).
* |feature| Add ``ZREPL_DESTROY_MAX_BATCH_SIZE`` env var (default 0=unlimited)
(Thanks, `@3nprob <https://github.com/3nprob>`_).
* |feature| Add ``zrepl configcheck --skip-cert-check`` flag (Thanks, `@cole-h <https://github.com/cole-h>`_).
* |bugfix| Fix resuming from interrupted replications that use ``send.raw`` on unencrypted datasets.
* The send options introduced in zrepl 0.4 allow users to specify additional zfs send flags for zrepl to use.
Before this fix, when setting ``send.raw=true`` on a job that replicates unencrypted datasets,
zrepl would not allow an interrupted replication to resume.
The reason were overly cautious checks to support the ``send.encrypted`` option.
* This bugfix removes these checks from the replication planner.
This makes ``send.encrypted`` a sender-side-only concern, much like all other ``send.*`` flags.
* However, this means that the ``zrepl status`` UI no longer indicates whether a replication step uses encrypted sends or not.
The setting is still effective though.
* |break| convert Prometheus metric ``zrepl_version_daemon`` to ``zrepl_start_time`` metric
* The metric still reports the zrepl version in a label. * The metric still reports the zrepl version in a label.
But the metric *value* is now the Unix timestamp at the time the daemon was started. But the metric *value* is now the Unix timestamp at the time the daemon was started.
The Grafana dashboard in :repomasterlink:`dist/grafana` has been updated. The Grafana dashboard in :repomasterlink:`dist/grafana` has been updated.
* |bugfix| transient zrepl status error: ``Post "http://unix/status": EOF`` * |bugfix| transient zrepl status error: ``Post "http://unix/status": EOF``
* |bugfix| don't treat receive-side bookmarks as a replication conflict.
This facilitates chaining of replication jobs. See :issue:`490`.
* |bugfix| workaround for Go/gRPC problem on Illumos where zrepl would
crash when using the ``local`` transport type (:issue:`598`).
* |bugfix| fix active child tasks panic that cold occur during replication plannig (:issue:`193abbe`)
* |bugfix| ``zrepl status`` off-by-one error in display of completed step count (:commit:`ce6701f`)
* |bugfix| Allow using day & week units for ``snapshotting.interval`` (:commit:`ffb1d89`)
* |docs| ``docs/overview`` improvements (Thanks, `@jtagcat <https://github.com/jtagcat>`_).
* |maint| Update to Go 1.19.
0.5 0.5
--- ---
@@ -132,6 +61,14 @@ Note to all users: please read up on the following OpenZFS bugs, as you might be
Finally, I'd like to point you to the `GitHub discussion <https://github.com/zrepl/zrepl/discussions/547>`_ about which bugfixes and features should be prioritized in zrepl 0.6 and beyond! Finally, I'd like to point you to the `GitHub discussion <https://github.com/zrepl/zrepl/discussions/547>`_ about which bugfixes and features should be prioritized in zrepl 0.6 and beyond!
.. NOTE::
| zrepl is a spare-time project primarily developed by `Christian Schwarz <https://cschwarz.com>`_.
| You can support maintenance and feature development through one of the following services:
| |Donate via Patreon| |Donate via GitHub Sponsors| |Donate via Liberapay| |Donate via PayPal|
| Note that PayPal processing fees are relatively high for small donations.
| For SEPA wire transfer and **commercial support**, please `contact Christian directly <https://cschwarz.com>`_.
0.4.0 0.4.0
----- -----
-187
View File
@@ -1,187 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# zrepl documentation build configuration file, created by
# sphinx-quickstart on Wed Nov 8 22:28:10 2017.
#
# This file is execfile()d with the current directory set to its
# containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
# import os
# import sys
# sys.path.insert(0, os.path.abspath('.'))
# -- General configuration ------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#
# needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = ['sphinx.ext.todo',
'sphinx.ext.githubpages',
'sphinx.ext.extlinks',
"sphinx_multiversion",
]
# suppress_warnings = ['image.nonlocal_uri']
# Add any paths that contain templates here, relative to this directory.
templates_path = ['./_templates']
# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
#
# source_suffix = ['.rst', '.md']
source_suffix = '.rst'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = 'zrepl'
copyright = '2017-2023, Christian Schwarz'
author = 'Christian Schwarz'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
#version = set by sphinxcontrib-versioning
# The full version, including alpha/beta/rc tags.
#release = version
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = 'en'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This patterns also effect to html_static_path and html_extra_path
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = True
# -- Options for HTML output ----------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = 'sphinx_rtd_theme'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#
# html_theme_options = {}
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
html_css_files = [
'banner.css',
]
html_logo = '_static/zrepl.svg'
html_context = {
# https://github.com/rtfd/sphinx_rtd_theme/issues/205
# Add 'Edit on Github' link instead of 'View page source'
"display_github": True,
"github_user": "zrepl",
"github_repo": "zrepl",
"github_version": "master",
"conf_py_path": "/docs/",
"source_suffix": source_suffix,
}
# -- Options for HTMLHelp output ------------------------------------------
# Output file base name for HTML help builder.
htmlhelp_basename = 'zrepldoc'
# -- Options for LaTeX output ---------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#
# 'preamble': '',
# Latex figure (float) alignment
#
# 'figure_align': 'htbp',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
(master_doc, 'zrepl.tex', 'zrepl Documentation',
'Christian Schwarz', 'manual'),
]
# -- Options for manual page output ---------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
(master_doc, 'zrepl', 'zrepl Documentation',
[author], 1)
]
# -- Options for Texinfo output -------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(master_doc, 'zrepl', 'zrepl Documentation',
author, 'zrepl', 'One line description of project.',
'Miscellaneous'),
]
# -- Options for the extlinks extension -----------------------------------
# http://www.sphinx-doc.org/en/stable/ext/extlinks.html
extlinks = {
'issue':('https://github.com/zrepl/zrepl/issues/%s', 'issue #%s'),
'repomasterlink':('https://github.com/zrepl/zrepl/blob/master/%s', '%s'),
'sampleconf':('https://github.com/zrepl/zrepl/blob/master/config/samples%s', 'config/samples%s'),
'commit':('https://github.com/zrepl/zrepl/commit/%s', 'commit %s'),
}
+1
View File
@@ -0,0 +1 @@
sphinxconf/conf.py
+1 -2
View File
@@ -1,6 +1,5 @@
.. include:: ../global.rst.inc .. include:: ../global.rst.inc
.. _conflict_resolution-options:
Conflict Resolution Options Conflict Resolution Options
=========================== ===========================
@@ -16,7 +15,7 @@ Conflict Resolution Options
... ...
.. _conflict_resolution-initial_replication: .. _conflict_resolution-initial_replication-option-send_all_snapshots:
``initial_replication`` option ``initial_replication`` option
-8
View File
@@ -30,10 +30,6 @@ Job Type ``push``
- |snapshotting-spec| - |snapshotting-spec|
* - ``pruning`` * - ``pruning``
- |pruning-spec| - |pruning-spec|
* - ``replication``
- |replication-options|
* - ``conflict_resolution``
- |conflict-resolution-options|
Example config: :sampleconf:`/push.yml` Example config: :sampleconf:`/push.yml`
@@ -85,10 +81,6 @@ Job Type ``pull``
| ``manual`` disables periodic pulling, replication then only happens on :ref:`wakeup <cli-signal-wakeup>`. | ``manual`` disables periodic pulling, replication then only happens on :ref:`wakeup <cli-signal-wakeup>`.
* - ``pruning`` * - ``pruning``
- |pruning-spec| - |pruning-spec|
* - ``replication``
- |replication-options|
* - ``conflict_resolution``
- |conflict-resolution-options|
Example config: :sampleconf:`/pull.yml` Example config: :sampleconf:`/pull.yml`
+24 -2
View File
@@ -1,5 +1,3 @@
.. _miscellaneous:
Miscellaneous Miscellaneous
============= =============
@@ -44,3 +42,27 @@ Interval & duration fields in job definitions, pruning configurations, etc. must
var durationStringRegex *regexp.Regexp = regexp.MustCompile(`^\s*(\d+)\s*(s|m|h|d|w)\s*$`) var durationStringRegex *regexp.Regexp = regexp.MustCompile(`^\s*(\d+)\s*(s|m|h|d|w)\s*$`)
// s = second, m = minute, h = hour, d = day, w = week (7 days) // s = second, m = minute, h = hour, d = day, w = week (7 days)
Super-Verbose Job Debugging
---------------------------
You have probably landed here because you opened an issue on GitHub and some developer told you to do this...
So just read the annotated comments ;)
::
job:
- name: ...
...
# JOB DEBUGGING OPTIONS
# should be equal for all job types, but each job implements the debugging itself
debug:
conn: # debug the io.ReadWriteCloser connection
read_dump: /tmp/connlog_read # dump results of Read() invocations to this file
write_dump: /tmp/connlog_write # dump results of Write() invocations to this file
rpc: # debug the RPC protocol implementation
log: true # log output from rpc layer to the job log
.. ATTENTION::
Connection dumps will almost certainly contain your or other's private data. Do not share it in a bug report.
+26 -27
View File
@@ -3,17 +3,16 @@ Overview & Terminology
====================== ======================
All work zrepl does is performed by the zrepl daemon which is configured in a single YAML configuration file loaded on startup. All work zrepl does is performed by the zrepl daemon which is configured in a single YAML configuration file loaded on startup.
The following paths are searched, in this order: The following paths are considered:
1. The path specified via the global ``--config`` flag * If set, the location specified via the global ``--config`` flag
2. ``/etc/zrepl/zrepl.yml`` * ``/etc/zrepl/zrepl.yml``
3. ``/usr/local/etc/zrepl/zrepl.yml`` * ``/usr/local/etc/zrepl/zrepl.yml``
``zrepl configcheck`` can be used to validate the configuration. The ``zrepl configcheck`` subcommand can be used to validate the configuration.
If the configuration is valid, it will output nothing and exit with code ``0``. The command will output nothing and exit with zero status code if the configuration is valid.
The error messages vary in quality and usefulness: please report confusing config errors to the tracking :issue:`155`. The error messages vary in quality and usefulness: please report confusing config errors to the tracking :issue:`155`.
Full example configs such as in the :ref:`quick-start guides <quickstart-toc>` or the :sampleconf:`/` directory might also be helpful.
Full example configs are available at :ref:`quick-start guides <quickstart-toc>` and :sampleconf:`/`.
However, copy-pasting examples is no substitute for reading documentation! However, copy-pasting examples is no substitute for reading documentation!
Config File Structure Config File Structure
@@ -27,8 +26,9 @@ Config File Structure
type: push type: push
- ... - ...
A zrepl configuration file is divided in to two main sections: ``global`` and ``jobs``. zrepl is configured using a single YAML configuration file with two main sections: ``global`` and ``jobs``.
``global`` has sensible defaults. It is covered in :ref:`logging <logging>`, :ref:`monitoring <monitoring>` \& :ref:`miscellaneous <miscellaneous>`. The ``global`` section is filled with sensible defaults and is covered later in this chapter.
The ``jobs`` section is a list of jobs which we are going to explain now.
.. _job-overview: .. _job-overview:
@@ -42,7 +42,8 @@ Jobs are identified by their ``name``, both in log files and the ``zrepl status`
.. NOTE:: .. NOTE::
The job name is persisted in several places on disk and thus :issue:`cannot be changed easily<327>`. The job name is persisted in several places on disk and thus :issue:`cannot be changed easily<327>`.
Replication always happens between a pair of jobs: one **active side** and one **passive side**.
Replication always happens between a pair of jobs: one is the **active side**, and one the **passive side**.
The active side connects to the passive side using a :ref:`transport <transport>` and starts executing the replication logic. The active side connects to the passive side using a :ref:`transport <transport>` and starts executing the replication logic.
The passive side responds to requests from the active side after checking its permissions. The passive side responds to requests from the active side after checking its permissions.
@@ -71,29 +72,30 @@ How the Active Side Works
The active side (:ref:`push <job-push>` and :ref:`pull <job-pull>` job) executes the replication and pruning logic: The active side (:ref:`push <job-push>` and :ref:`pull <job-pull>` job) executes the replication and pruning logic:
1. Wakeup after snapshotting (``push`` job) or pull interval ticker (``pull`` job). * Wakeup because of finished snapshotting (``push`` job) or pull interval ticker (``pull`` job).
2. Connect to the passive side and instantiate an RPC client. * Connect to the corresponding passive side using a :ref:`transport <transport>` and instantiate an RPC client.
3. Replicate data from the sender to the receiver. * Replicate data from the sending to the receiving side (see below).
4. Prune on sender & receiver. * Prune on sender & receiver.
.. TIP:: .. TIP::
The progress of the active side can be watched live using ``zrepl status``. The progress of the active side can be watched live using the ``zrepl status`` subcommand.
.. _overview-passive-side--client-identity: .. _overview-passive-side--client-identity:
How the Passive Side Works How the Passive Side Works
-------------------------- --------------------------
The passive side (:ref:`sink <job-sink>` and :ref:`source <job-source>`) waits for connections from the active side, The passive side (:ref:`sink <job-sink>` and :ref:`source <job-source>`) waits for connections from the corresponding active side,
on the :ref:`transport <transport>` specified with ``serve`` in the job configuration. using the transport listener type specified in the ``serve`` field of the job configuration.
The respective transport then perfoms authentication & authorization, resulting in a stable *client identity*. When a client connects, the transport listener performS listener-specific access control (cert validation, IP ACLs, etc)
The passive side job uses this *client identity* as follows: and determines the *client identity*.
The passive side job then uses this client identity as follows:
* In ``sink`` jobs, to map requests from different *client identities* to their respective sub-filesystem tree ``root_fs/${client_identity}``. * The ``sink`` job maps requests from different client identities to their respective sub-filesystem tree ``root_fs/${client_identity}``.
* *In the future, ``source`` might embed the client identity in :ref:`zrepl's ZFS abstraction names <zrepl-zfs-abstractions>`, to support multi-host replication.* * The ``source`` might, in the future, embed the client identity in :ref:`zrepl's ZFS abstraction names <zrepl-zfs-abstractions>` in order to support multi-host replication.
.. TIP:: .. TIP::
The use of the client identity in the ``sink`` job implies that it must be usable as a ZFS ZFS filesystem name component. The implementation of the ``sink`` job requires that the connecting client identities be a valid ZFS filesystem name components.
.. _overview-how-replication-works: .. _overview-how-replication-works:
@@ -104,7 +106,7 @@ One of the major design goals of the replication module is to avoid any duplicat
As such, the code works on abstract senders and receiver **endpoints**, where typically one will be implemented by a local program object and the other is an RPC client instance. As such, the code works on abstract senders and receiver **endpoints**, where typically one will be implemented by a local program object and the other is an RPC client instance.
Regardless of push- or pull-style setup, the logic executes on the active side, i.e. in the ``push`` or ``pull`` job. Regardless of push- or pull-style setup, the logic executes on the active side, i.e. in the ``push`` or ``pull`` job.
The following high-level steps take place during replication and can be monitored using ``zrepl status``: The following high-level steps take place during replication and can be monitored using the ``zrepl status`` subcommand:
* Plan the replication: * Plan the replication:
@@ -258,9 +260,6 @@ On your setup, ensure that
* all ``filesystems`` filter specifications are disjoint * all ``filesystems`` filter specifications are disjoint
* no ``root_fs`` is a prefix or equal to another ``root_fs`` * no ``root_fs`` is a prefix or equal to another ``root_fs``
* For ``sink`` jobs, consider all possible ``root_fs/${client_identity}``.
* no ``filesystems`` filter matches any ``root_fs`` * no ``filesystems`` filter matches any ``root_fs``
**Exceptions to the rule**: **Exceptions to the rule**:
-1
View File
@@ -1,6 +1,5 @@
.. include:: ../global.rst.inc .. include:: ../global.rst.inc
.. _replication-options:
Replication Options Replication Options
=================== ===================
+6 -3
View File
@@ -197,8 +197,11 @@ Mount behaviour
* ``canmount`` * ``canmount``
* ``overlay`` * ``overlay``
Note: Before `OpenZFS 2.0.5 <https://github.com/openzfs/zfs/issues/11416>`_, inheriting or overriding the ``mountpoint`` property on ZVOLs fails in ``zfs recv``. Note: inheriting or overriding the ``mountpoint`` property on ZVOLs fails in ``zfs recv``.
If you are on such an older version, consider creating separate zrepl jobs for your ZVOL and filesystem datasets. This is an `issue in OpenZFS <https://github.com/openzfs/zfs/issues/11416>`_ .
As a workaround, consider creating separate zrepl jobs for your ZVOL and filesystem datasets.
Please comment at zrepl :issue:`430` if you encounter this issue and/or would like zrepl to automatically work around it.
Systemd Systemd
------- -------
@@ -224,7 +227,7 @@ and property replication is enabled, the receiver must :ref:`inherit the followi
.. _job-recv-options--placeholder: .. _job-recv-options--placeholder:
Placeholders Placeholders
~~~~~~~~~~~~ ------------
:: ::
+33 -118
View File
@@ -5,138 +5,53 @@
Taking Snaphots Taking Snaphots
=============== ===============
You can configure zrepl to take snapshots of the filesystems in the ``filesystems`` field specified in ``push``, ``source`` and ``snap`` jobs. The ``push``, ``source`` and ``snap`` jobs can automatically take periodic snapshots of the filesystems matched by the ``filesystems`` filter field.
The snapshot names are composed of a user-defined prefix followed by a UTC date formatted like ``20060102_150405_000``.
We use UTC because it will avoid name conflicts when switching time zones or between summer and winter time.
The following snapshotting types are supported: When a job is started, the snapshotter attempts to get the snapshotting rhythms of the matched ``filesystems`` in sync because snapshotting all filesystems at the same time results in a more consistent backup.
To find that sync point, the most recent snapshot, made by the snapshotter, in any of the matched ``filesystems`` is used.
.. list-table::
:widths: 20 70
:header-rows: 1
* - ``snapshotting.type``
- Comment
* - ``periodic``
- Ensure that snapshots are taken at a particular interval.
* - ``cron``
- Use cron spec to take snapshots at particular points in time.
* - ``manual``
- zrepl does not take any snapshots by itself.
The ``periodic`` and ``cron`` snapshotting types share some common options and behavior:
* **Naming:** The snapshot names are composed of a user-defined ``prefix`` followed by a UTC date formatted like ``20060102_150405_000``.
We use UTC because it will avoid name conflicts when switching time zones or between summer and winter time.
* **Hooks:** You can configure hooks to run before or after zrepl takes the snapshots. See :ref:`below <job-snapshotting-hooks>` for details.
* **Push replication:** After creating all snapshots, the snapshotter will wake up the replication part of the job, if it's a ``push`` job.
Note that snapshotting is decoupled from replication, i.e., if it is down or takes too long, snapshots will still be taken.
Note further that other jobs are not woken up by snapshotting.
.. NOTE::
There is **no concept of ownership** of the snapshots that are created by ``periodic`` or ``cron``.
Thus, there is no distinction between zrepl-created snapshots and user-created snapshots during replication or pruning.
In particular, pruning will take all snapshots into consideration by default.
To constrain pruning to just zrepl-created snapshots:
1. Assign a unique `prefix` to the snapshotter and
2. Use the ``regex`` functionality of the various pruning ``keep`` rules to just consider snapshots with that prefix.
There is currently no way to constrain replication to just zrepl-created snapshots.
Follow and comment at :issue:`403` if you need this functionality.
.. NOTE::
The ``zrepl signal wakeup JOB`` subcommand does not trigger snapshotting.
``periodic`` Snapshotting
-------------------------
::
jobs:
- ...
filesystems: { ... }
snapshotting:
type: periodic
prefix: zrepl_
interval: 10m
# Timestamp format that is used as snapshot suffix.
# Can be any of "dense" (default), "human", "iso-8601", "unix-seconds" or a custom Go time format (see https://go.dev/src/time/format.go)
timestamp_format: dense
hooks: ...
pruning: ...
The ``periodic`` snapshotter ensures that snapshots are taken in the specified ``interval``.
If you use zrepl for backup, this translates into your recovery point objective (RPO).
To meet your RPO, you still need to monitor that replication, which happens asynchronously to snapshotting, actually works.
It is desirable to get all ``filesystems`` snapshotted simultaneously because it results in a more consistent backup.
To accomplish this while still maintaining the ``interval``, the ``periodic`` snapshotter attempts to get the snapshotting rhythms in sync.
To find that sync point, the most recent snapshot, created by the snapshotter, in any of the matched ``filesystems`` is used.
A filesystem that does not have snapshots by the snapshotter has lower priority than filesystem that do, and thus might not be snapshotted (and replicated) until it is snapshotted at the next sync point. A filesystem that does not have snapshots by the snapshotter has lower priority than filesystem that do, and thus might not be snapshotted (and replicated) until it is snapshotted at the next sync point.
The snapshotter uses the ``prefix`` to identify which snapshots it created.
.. _job-snapshotting--cron: For ``push`` jobs, replication is automatically triggered after all filesystems have been snapshotted.
``cron`` Snapshotting Note that the ``zrepl signal wakeup JOB`` subcommand does not trigger snapshotting.
---------------------
::
jobs:
- type: snap
filesystems: { ... }
snapshotting:
type: cron
prefix: zrepl_
# (second, optional) minute hour day-of-month month day-of-week
# This example takes snapshots daily at 3:00.
cron: "0 3 * * *"
# Timestamp format that is used as snapshot suffix.
# Can be any of "dense" (default), "human", "iso-8601", "unix-seconds" or a custom Go time format (see https://go.dev/src/time/format.go)
timestamp_format: dense
pruning: ...
In ``cron`` mode, the snapshotter takes snaphots at fixed points in time.
See https://en.wikipedia.org/wiki/Cron for details on the syntax.
zrepl uses the ``the github.com/robfig/cron/v3`` Go package for parsing.
An optional field for "seconds" is supported to take snapshots at sub-minute frequencies.
.. _job-snapshotting-timestamp_format:
Timestamp Format
~~~~~~~~~~~~~~~~
The ``cron`` and ``periodic`` snapshotter support configuring a custom timestamp format that is used as suffix for the snapshot name.
It can be used by setting ``timestamp_format`` to any of the following values:
* ``dense`` (default) looks like ``20060102_150405_000``
* ``human`` looks like ``2006-01-02_15:04:05``
* ``iso-8601`` looks like ``2006-01-02T15:04:05.000Z``
* ``unix-seconds`` looks like ``1136214245``
* Any custom Go time format accepted by `time.Time#Format <https://go.dev/src/time/format.go>`_.
``manual`` Snapshotting
-----------------------
:: ::
jobs: jobs:
- type: push - type: push
filesystems: {
"<": true,
"tmp": false
}
snapshotting:
type: periodic
prefix: zrepl_
interval: 10m
hooks: ...
...
There is also a ``manual`` snapshotting type, which covers the following use cases:
* Existing infrastructure for automatic snapshots: you only want to use this zrepl job for replication.
* Handling snapshotting through a separate ``snap`` job.
Note that you will have to trigger replication manually using the ``zrepl signal wakeup JOB`` subcommand in that case.
::
jobs:
- type: push
filesystems: {
"<": true,
"tmp": false
}
snapshotting: snapshotting:
type: manual type: manual
... ...
In ``manual`` mode, zrepl does not take snapshots by itself.
Manual snapshotting is most useful if you have existing infrastructure for snapshot management.
Or, if you want to decouple snapshot management from replication using a zrepl ``snap`` job.
See :ref:`this quickstart guide <quickstart-backup-to-external-disk>` for an example.
To trigger replication after taking snapshots, use the ``zrepl signal wakeup JOB`` command.
.. _job-snapshotting-hooks: .. _job-snapshotting-hooks:
Pre- and Post-Snapshot Hooks Pre- and Post-Snapshot Hooks
@@ -1,15 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from pathlib import Path
import subprocess import subprocess
import re import re
import argparse
import distutils
argparser = argparse.ArgumentParser()
argparser.add_argument("docsroot")
argparser.add_argument("outdir")
args = argparser.parse_args()
output = subprocess.run(["git", "tag", "-l"], capture_output=True, check=True, text=True) 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+))?$") tagRE = re.compile(r"^v(?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(-rc(?P<rc>\d+))?$")
@@ -57,16 +49,26 @@ for (mm, l) in by_major_minor.items():
latest_by_major_minor.append(l[-1]) latest_by_major_minor.append(l[-1])
latest_by_major_minor.sort(key=lambda tag: (tag.major, tag.minor)) latest_by_major_minor.sort(key=lambda tag: (tag.major, tag.minor))
cmdline = [ # print(by_major_minor)
"sphinx-multiversion", # print(latest_by_major_minor)
"-D", "smv_tag_whitelist=^({})$".format("|".join([re.escape(tag.orig) for tag in latest_by_major_minor])),
"-D", "smv_branch_whitelist=^(master|stable)$", cmdline = []
"-D", "smv_remote_whitelist=^.*$",
"-D", "smv_latest_version=stable", for latest_patch in latest_by_major_minor:
"-D", r"smv_released_pattern=^refs/(tags|heads|remotes/[^/]+)/(?!master).*$", # treat everything except master as released, that way, the banner message makes sense cmdline.append("--whitelist-tags")
# "--dump-metadata", # for debugging cmdline.append(f"^{re.escape(latest_patch.orig)}$")
args.docsroot,
args.outdir, # we want flexibility to update docs for the latest stable release
] # => we have a branch for that, called `stable` which we move manually
print(cmdline) # TODO: in the future, have f"stable-{latest_by_major_minor[-1]}"
subprocess.run(cmdline, check=True) default_version = "stable"
cmdline.extend(["--whitelist-branches", default_version])
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
View File
@@ -26,8 +26,6 @@
.. |connect-transport| replace:: :ref:`connect specification<transport>` .. |connect-transport| replace:: :ref:`connect specification<transport>`
.. |send-options| replace:: :ref:`send options<job-send-options>`, e.g. for encrypted sends .. |send-options| replace:: :ref:`send options<job-send-options>`, e.g. for encrypted sends
.. |recv-options| replace:: :ref:`recv options<job-recv-options>` .. |recv-options| replace:: :ref:`recv options<job-recv-options>`
.. |replication-options| replace:: :ref:`replication options<replication-options>`
.. |conflict-resolution-options| replace:: :ref:`conflict resolution options<conflict_resolution-options>`
.. |snapshotting-spec| replace:: :ref:`snapshotting specification <job-snapshotting-spec>` .. |snapshotting-spec| replace:: :ref:`snapshotting specification <job-snapshotting-spec>`
.. |pruning-spec| replace:: :ref:`pruning specification <prune>` .. |pruning-spec| replace:: :ref:`pruning specification <prune>`
.. |filter-spec| replace:: :ref:`filter specification<pattern-filter>` .. |filter-spec| replace:: :ref:`filter specification<pattern-filter>`
-2
View File
@@ -15,5 +15,3 @@ Talks & Presentations
`Event <https://wiki.freebsd.org/DevSummit/201709>`__ `Event <https://wiki.freebsd.org/DevSummit/201709>`__
) )
* Note: The remarks on ``keep_bookmarks`` are irrelevant as of zrepl 0.1 which introduced the zrepl-managed replication cursor bookmark.
Read the `Overview <overview-how-replication-works>`_ section to learn more.
+15 -17
View File
@@ -3,8 +3,7 @@ set -euo pipefail
NON_INTERACTIVE=false NON_INTERACTIVE=false
DO_CLONE=false DO_CLONE=false
PUSH=false while getopts "ca" arg; do
while getopts "caP" arg; do
case "$arg" in case "$arg" in
"a") "a")
NON_INTERACTIVE=true NON_INTERACTIVE=true
@@ -12,9 +11,6 @@ while getopts "caP" arg; do
"c") "c")
DO_CLONE=true DO_CLONE=true
;; ;;
"P")
PUSH=true
;;
*) *)
echo "invalid option '-$arg'" echo "invalid option '-$arg'"
exit 1 exit 1
@@ -30,8 +26,8 @@ checkout_repo_msg() {
echo "clone ${GHPAGESREPO} to ${PUBLICDIR}:" echo "clone ${GHPAGESREPO} to ${PUBLICDIR}:"
} }
if ! type sphinx-multiversion >/dev/null; then if ! type sphinx-versioning >/dev/null; then
echo "install sphinx-multiversion and come back" echo "install sphinx-versioning and come back"
exit 1 exit 1
fi fi
@@ -77,15 +73,22 @@ popd
echo "building site" echo "building site"
python3 run-sphinx-multiversion.py . ./public_git flags="$(python3 gen-sphinx-versioning-flags.py)"
set -e
sphinx-versioning build \
$flags \
docs ./public_git \
-- -c sphinxconf # older conf.py throw errors because they used
# version = subprocess.show_output(["git", "describe"])
# which fails when building with sphinxcontrib-versioning
set +e
CURRENT_COMMIT=$(git rev-parse HEAD) CURRENT_COMMIT=$(git rev-parse HEAD)
git status --porcelain git status --porcelain
if [[ "$(git status --porcelain)" != "" ]]; then if [[ "$(git status --porcelain)" != "" ]]; then
CURRENT_COMMIT="${CURRENT_COMMIT}(dirty)" CURRENT_COMMIT="${CURRENT_COMMIT}(dirty)"
fi fi
COMMIT_MSG="render from publish.sh - $(date -u) - ${CURRENT_COMMIT}" COMMIT_MSG="sphinx-versioning render from publish.sh - $(date -u) - ${CURRENT_COMMIT}"
pushd "$PUBLICDIR" pushd "$PUBLICDIR"
@@ -97,11 +100,6 @@ if [ "$(git status --porcelain)" != "" ]; then
else else
echo "nothing to commit" echo "nothing to commit"
fi fi
echo "pushing to GitHub pages repo"
if $PUSH; then git push origin master
echo "pushing to GitHub pages repo"
git push origin master
else
echo "not pushing to GitHub pages repo, set -P flag to push"
fi
@@ -33,17 +33,4 @@ You will likely want to customize some aspects mentioned in the top comment in t
.. literalinclude:: ../../config/samples/quickstart_backup_to_external_disk.yml .. literalinclude:: ../../config/samples/quickstart_backup_to_external_disk.yml
Offline Backups with two (or more) External Disks
-------------------------------------------------
It can be desirable to have multiple disk-based backups of the same machine.
To accomplish this,
* create one zpool per external HDD, each with a unique name, and
* define a pair of ``push`` and ``sink`` job **for each** of these zpools, each with a unique ``name``, ``listener_name``, and ``root_fs``.
The unique names ensure that the jobs don't step on each others' toes when managing :ref:`zrepl's ZFS abstractions <zrepl-zfs-abstractions>` .
:ref:`Click here <quickstart-apply-config>` to go back to the quickstart guide. :ref:`Click here <quickstart-apply-config>` to go back to the quickstart guide.
+29 -23
View File
@@ -1,24 +1,30 @@
alabaster==0.7.13 alabaster==0.7.12
Babel==2.12.1 attrs==19.1.0
certifi==2023.7.22 Babel==2.7.0
charset-normalizer==3.2.0 certifi==2019.6.16
docutils==0.18.1 chardet==3.0.4
idna==3.4 Click==7.0
imagesize==1.4.1 colorclass==2.2.0
Jinja2==3.1.2 docutils==0.15.2
MarkupSafe==2.1.3 idna==2.8
packaging==23.1 imagesize==1.1.0
Pygments==2.16.1 Jinja2==2.10.1
requests==2.31.0 MarkupSafe==1.1.1
snowballstemmer==2.2.0 packaging==19.1
Sphinx==7.2.5 Pygments==2.4.2
sphinx-multiversion @ git+https://github.com/zrepl/sphinx-multiversion/@52c915d7ad898d9641ec48c8bbccb7d4f079db93 pyparsing==2.4.2
sphinx-rtd-theme==1.3.0 pytz==2019.2
sphinxcontrib-applehelp==1.0.7 requests==2.22.0
sphinxcontrib-devhelp==1.0.5 six==1.12.0
sphinxcontrib-htmlhelp==2.0.4 snowballstemmer==1.9.1
sphinxcontrib-jquery==4.1 Sphinx==1.8.5
sphinx-rtd-theme==0.4.3
sphinxcontrib-applehelp==1.0.1
sphinxcontrib-devhelp==1.0.1
sphinxcontrib-htmlhelp==1.0.2
sphinxcontrib-jsmath==1.0.1 sphinxcontrib-jsmath==1.0.1
sphinxcontrib-qthelp==1.0.6 sphinxcontrib-qthelp==1.0.2
sphinxcontrib-serializinghtml==1.1.9 sphinxcontrib-serializinghtml==1.1.3
urllib3==2.0.4 git+https://github.com/rwblair/sphinxcontrib-versioning.git@7e3885a389a809e17ea55261316b7b0e98dbf98f#egg=sphinxcontrib-versioning
sphinxcontrib-websupport==1.1.2
urllib3==1.25.3
+182
View File
@@ -0,0 +1,182 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# zrepl documentation build configuration file, created by
# sphinx-quickstart on Wed Nov 8 22:28:10 2017.
#
# This file is execfile()d with the current directory set to its
# containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
# import os
# import sys
# sys.path.insert(0, os.path.abspath('.'))
# -- General configuration ------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#
# needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = ['sphinx.ext.todo',
'sphinx.ext.githubpages',
'sphinx.ext.extlinks']
suppress_warnings = ['image.nonlocal_uri']
# Add any paths that contain templates here, relative to this directory.
templates_path = ['../_templates']
# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
#
# source_suffix = ['.rst', '.md']
source_suffix = '.rst'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = 'zrepl'
copyright = '2017-2019, Christian Schwarz'
author = 'Christian Schwarz'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
#version = set by sphinxcontrib-versioning
# The full version, including alpha/beta/rc tags.
#release = version
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = None
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This patterns also effect to html_static_path and html_extra_path
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = True
# -- Options for HTML output ----------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = 'sphinx_rtd_theme'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#
# html_theme_options = {}
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['../_static']
html_logo = '../_static/zrepl.svg'
html_context = {
# https://github.com/rtfd/sphinx_rtd_theme/issues/205
# Add 'Edit on Github' link instead of 'View page source'
"display_github": True,
"github_user": "zrepl",
"github_repo": "zrepl",
"github_version": "master",
"conf_py_path": "/docs/",
"source_suffix": source_suffix,
}
# -- Options for HTMLHelp output ------------------------------------------
# Output file base name for HTML help builder.
htmlhelp_basename = 'zrepldoc'
# -- Options for LaTeX output ---------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#
# 'preamble': '',
# Latex figure (float) alignment
#
# 'figure_align': 'htbp',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
(master_doc, 'zrepl.tex', 'zrepl Documentation',
'Christian Schwarz', 'manual'),
]
# -- Options for manual page output ---------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
(master_doc, 'zrepl', 'zrepl Documentation',
[author], 1)
]
# -- Options for Texinfo output -------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(master_doc, 'zrepl', 'zrepl Documentation',
author, 'zrepl', 'One line description of project.',
'Miscellaneous'),
]
# -- Options for the extlinks extension -----------------------------------
# http://www.sphinx-doc.org/en/stable/ext/extlinks.html
extlinks = {
'issue':('https://github.com/zrepl/zrepl/issues/%s', 'issue #'),
'repomasterlink':('https://github.com/zrepl/zrepl/blob/master/%s', ''),
'sampleconf':('https://github.com/zrepl/zrepl/blob/master/config/samples%s', 'config/samples'),
'commit':('https://github.com/zrepl/zrepl/commit/%s', 'commit '),
}
-1
View File
@@ -32,7 +32,6 @@ We would like to thank the following people and organizations for supporting zre
<div class="fa fa-code" style="width: 1em;"></div> <div class="fa fa-code" style="width: 1em;"></div>
* |supporter-std| `Max Christian Pohle <https://coderonline.de>`_
* |supporter-gold| Prominic.NET, Inc. * |supporter-gold| Prominic.NET, Inc.
* |supporter-std| Torsten Blum * |supporter-std| Torsten Blum
* |supporter-gold| Cyberiada GmbH * |supporter-gold| Cyberiada GmbH
+51 -56
View File
@@ -7,7 +7,6 @@ import (
"fmt" "fmt"
"io" "io"
"path" "path"
"strings"
"github.com/kr/pretty" "github.com/kr/pretty"
"github.com/pkg/errors" "github.com/pkg/errors"
@@ -106,10 +105,15 @@ func (s *Sender) ListFilesystems(ctx context.Context, r *pdu.ListFilesystemReq)
} }
rfss := make([]*pdu.Filesystem, len(fss)) rfss := make([]*pdu.Filesystem, len(fss))
for i := range fss { for i := range fss {
encEnabled, err := zfs.ZFSGetEncryptionEnabled(ctx, fss[i].ToString())
if err != nil {
return nil, errors.Wrap(err, "cannot get filesystem encryption status")
}
rfss[i] = &pdu.Filesystem{ rfss[i] = &pdu.Filesystem{
Path: fss[i].ToString(), Path: fss[i].ToString(),
// ResumeToken does not make sense from Sender // ResumeToken does not make sense from Sender
IsPlaceholder: false, // sender FSs are never placeholders IsPlaceholder: false, // sender FSs are never placeholders
IsEncrypted: encEnabled,
} }
} }
res := &pdu.ListFilesystemRes{Filesystems: rfss} res := &pdu.ListFilesystemRes{Filesystems: rfss}
@@ -161,6 +165,23 @@ func (s *Sender) sendMakeArgs(ctx context.Context, r *pdu.SendReq) (sendArgs zfs
if err != nil { if err != nil {
return sendArgs, err return sendArgs, err
} }
switch r.Encrypted {
case pdu.Tri_DontCare:
// use s.encrypt setting
// ok, fallthrough outer
case pdu.Tri_False:
if s.config.Encrypt.B {
return sendArgs, errors.New("only encrypted sends allowed (send -w + encryption!= off), but unencrypted send requested")
}
// fallthrough outer
case pdu.Tri_True:
if !s.config.Encrypt.B {
return sendArgs, errors.New("only unencrypted sends allowed, but encrypted send requested")
}
// fallthrough outer
default:
return sendArgs, fmt.Errorf("unknown pdu.Tri variant %q", r.Encrypted)
}
sendArgsUnvalidated := zfs.ZFSSendArgsUnvalidated{ sendArgsUnvalidated := zfs.ZFSSendArgsUnvalidated{
FS: r.Filesystem, FS: r.Filesystem,
@@ -234,21 +255,6 @@ func (s *Sender) Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, io.Rea
// //
// Note further that a resuming send, due to the idempotent nature of func CreateReplicationCursor and HoldStep, // Note further that a resuming send, due to the idempotent nature of func CreateReplicationCursor and HoldStep,
// will never lose its step holds because we just (idempotently re-)created them above, before attempting the cleanup. // will never lose its step holds because we just (idempotently re-)created them above, before attempting the cleanup.
destroyTypes := AbstractionTypeSet{
AbstractionStepHold: true,
AbstractionTentativeReplicationCursorBookmark: true,
}
// The replication planner can also pick an endpoint zfs abstraction as FromVersion.
// Keep it, so that the replication will succeed.
//
// NB: there is no abstraction for snapshots, so, we only need to check bookmarks.
if sendArgs.FromVersion != nil && sendArgs.FromVersion.IsBookmark() {
dp, err := zfs.NewDatasetPath(sendArgs.FS)
if err != nil {
panic(err) // sendArgs is validated, this shouldn't happen
}
liveAbs = append(liveAbs, destroyTypes.ExtractBookmark(dp, sendArgs.FromVersion))
}
func() { func() {
ctx, endSpan := trace.WithSpan(ctx, "cleanup-stale-abstractions") ctx, endSpan := trace.WithSpan(ctx, "cleanup-stale-abstractions")
defer endSpan() defer endSpan()
@@ -261,45 +267,35 @@ func (s *Sender) Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, io.Rea
return keep return keep
} }
check := func(obsoleteAbs []Abstraction) { check := func(obsoleteAbs []Abstraction) {
// Ensure that we don't delete `From` or `To`. // last line of defense: check that we don't destroy the incremental `from` and `to`
// Regardless of whether they are in AbstractionTypeSet or not. // if we did that, we might be about to blow away the last common filesystem version between sender and receiver
// And produce a nice error message in case we do, to aid debugging the resulting panic. mustLiveVersions := []zfs.FilesystemVersion{sendArgs.ToVersion}
//
// This is especially important for `From`. We could break incremental replication
// if we deleted the last common filesystem version between sender and receiver.
type Problem struct {
sendArgsWhat string
fullpath string
obsoleteAbs Abstraction
}
problems := make([]Problem, 0)
checkFullpaths := make(map[string]string, 2)
checkFullpaths["ToVersion"] = sendArgs.ToVersion.FullPath(sendArgs.FS)
if sendArgs.FromVersion != nil { if sendArgs.FromVersion != nil {
checkFullpaths["FromVersion"] = sendArgs.FromVersion.FullPath(sendArgs.FS) mustLiveVersions = append(mustLiveVersions, *sendArgs.FromVersion)
} }
for _, a := range obsoleteAbs { for _, staleVersion := range obsoleteAbs {
for what, fullpath := range checkFullpaths { for _, mustLiveVersion := range mustLiveVersions {
if a.GetFullPath() == fullpath && a.GetType().IsSnapshotOrBookmark() { isSendArg := zfs.FilesystemVersionEqualIdentity(mustLiveVersion, staleVersion.GetFilesystemVersion())
problems = append(problems, Problem{ stepHoldBasedGuaranteeStrategy := false
sendArgsWhat: what, k := replicationGuaranteeStrategy.Kind()
fullpath: fullpath, switch k {
obsoleteAbs: a, 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) {
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)))
} }
} }
} }
if len(problems) == 0 {
return
} }
var msg strings.Builder destroyTypes := AbstractionTypeSet{
fmt.Fprintf(&msg, "cleaning up send stale would destroy send args:\n") AbstractionStepHold: true,
fmt.Fprintf(&msg, " SendArgs: %s\n", pretty.Sprint(sendArgs)) AbstractionTentativeReplicationCursorBookmark: true,
for _, check := range problems {
fmt.Fprintf(&msg, "would delete %s %s because it was deemed an obsolete abstraction: %s\n",
check.sendArgsWhat, check.fullpath, check.obsoleteAbs)
}
panic(msg.String())
} }
abstractionsCacheSingleton.TryBatchDestroy(ctx, s.jobId, sendArgs.FS, destroyTypes, keep, check) abstractionsCacheSingleton.TryBatchDestroy(ctx, s.jobId, sendArgs.FS, destroyTypes, keep, check)
}() }()
@@ -454,7 +450,6 @@ func (p *Sender) Receive(ctx context.Context, r *pdu.ReceiveReq, _ io.ReadCloser
type FSFilter interface { // FIXME unused type FSFilter interface { // FIXME unused
Filter(path *zfs.DatasetPath) (pass bool, err error) Filter(path *zfs.DatasetPath) (pass bool, err error)
UserSpecifiedDatasets() zfs.UserSpecifiedDatasetsSet
} }
// FIXME: can we get away without error types here? // FIXME: can we get away without error types here?
@@ -614,12 +609,6 @@ func (f subroot) Filter(p *zfs.DatasetPath) (pass bool, err error) {
return p.HasPrefix(f.localRoot) && !p.Equal(f.localRoot), nil return p.HasPrefix(f.localRoot) && !p.Equal(f.localRoot), nil
} }
func (f subroot) UserSpecifiedDatasets() zfs.UserSpecifiedDatasetsSet {
return zfs.UserSpecifiedDatasetsSet{
f.localRoot.ToString(): true,
}
}
func (f subroot) MapToLocal(fs string) (*zfs.DatasetPath, error) { func (f subroot) MapToLocal(fs string) (*zfs.DatasetPath, error) {
p, err := zfs.NewDatasetPath(fs) p, err := zfs.NewDatasetPath(fs)
if err != nil { if err != nil {
@@ -669,6 +658,11 @@ func (s *Receiver) ListFilesystems(ctx context.Context, req *pdu.ListFilesystemR
l.WithError(err).Error("cannot get receive resume token") l.WithError(err).Error("cannot get receive resume token")
return nil, err return nil, err
} }
encEnabled, err := zfs.ZFSGetEncryptionEnabled(ctx, a.ToString())
if err != nil {
l.WithError(err).Error("cannot get encryption enabled status")
return nil, err
}
l.WithField("receive_resume_token", token).Debug("receive resume token") l.WithField("receive_resume_token", token).Debug("receive resume token")
a.TrimPrefix(root) a.TrimPrefix(root)
@@ -677,6 +671,7 @@ func (s *Receiver) ListFilesystems(ctx context.Context, req *pdu.ListFilesystemR
Path: a.ToString(), Path: a.ToString(),
IsPlaceholder: ph.IsPlaceholder, IsPlaceholder: ph.IsPlaceholder,
ResumeToken: token, ResumeToken: token,
IsEncrypted: encEnabled,
} }
fss = append(fss, fs) fss = append(fss, fs)
} }
-6
View File
@@ -89,8 +89,6 @@ func ReplicationGuaranteeFromKind(k ReplicationGuaranteeKind) ReplicationGuarant
type ReplicationGuaranteeNone struct{} type ReplicationGuaranteeNone struct{}
func (g ReplicationGuaranteeNone) String() string { return "none" }
func (g ReplicationGuaranteeNone) Kind() ReplicationGuaranteeKind { func (g ReplicationGuaranteeNone) Kind() ReplicationGuaranteeKind {
return ReplicationGuaranteeKindNone return ReplicationGuaranteeKindNone
} }
@@ -109,8 +107,6 @@ func (g ReplicationGuaranteeNone) SenderPostRecvConfirmed(ctx context.Context, j
type ReplicationGuaranteeIncremental struct{} type ReplicationGuaranteeIncremental struct{}
func (g ReplicationGuaranteeIncremental) String() string { return "incremental" }
func (g ReplicationGuaranteeIncremental) Kind() ReplicationGuaranteeKind { func (g ReplicationGuaranteeIncremental) Kind() ReplicationGuaranteeKind {
return ReplicationGuaranteeKindIncremental return ReplicationGuaranteeKindIncremental
} }
@@ -148,8 +144,6 @@ func (g ReplicationGuaranteeIncremental) SenderPostRecvConfirmed(ctx context.Con
type ReplicationGuaranteeResumability struct{} type ReplicationGuaranteeResumability struct{}
func (g ReplicationGuaranteeResumability) String() string { return "resumability" }
func (g ReplicationGuaranteeResumability) Kind() ReplicationGuaranteeKind { func (g ReplicationGuaranteeResumability) Kind() ReplicationGuaranteeKind {
return ReplicationGuaranteeKindResumability return ReplicationGuaranteeKindResumability
} }
+9 -64
View File
@@ -31,7 +31,7 @@ const (
AbstractionReplicationCursorBookmarkV2 AbstractionType = "replication-cursor-bookmark-v2" AbstractionReplicationCursorBookmarkV2 AbstractionType = "replication-cursor-bookmark-v2"
) )
var AbstractionTypesAll = AbstractionTypeSet{ var AbstractionTypesAll = map[AbstractionType]bool{
AbstractionStepHold: true, AbstractionStepHold: true,
AbstractionLastReceivedHold: true, AbstractionLastReceivedHold: true,
AbstractionTentativeReplicationCursorBookmark: true, AbstractionTentativeReplicationCursorBookmark: true,
@@ -168,7 +168,7 @@ func (s AbstractionTypeSet) String() string {
for i := range s { for i := range s {
sts = append(sts, string(i)) sts = append(sts, string(i))
} }
sort.Strings(sts) sts = sort.StringSlice(sts)
return strings.Join(sts, ",") return strings.Join(sts, ",")
} }
@@ -181,38 +181,6 @@ func (s AbstractionTypeSet) Validate() error {
return nil return nil
} }
// Use the `BookmarkExtractor()` method of each abstraction type in this set
// to try extract an abstraction from the given FilesystemVersion.
//
// Abstraction types in this set that don't have a bookmark extractor are skipped.
//
// Panics if more than one abstraction type matches.
func (s AbstractionTypeSet) ExtractBookmark(dp *zfs.DatasetPath, v *zfs.FilesystemVersion) Abstraction {
matched := make(AbstractionTypeSet, 1)
var matchedAbs Abstraction
for absType := range s {
extractor := absType.BookmarkExtractor()
if extractor == nil {
continue
}
abstraction := extractor(dp, *v)
if abstraction != nil {
matched[absType] = true
matchedAbs = abstraction
}
}
if len(matched) == 0 {
return nil
}
if len(matched) == 1 {
if matchedAbs == nil {
panic("loop above should always set matchedAbs if there is a match")
}
return matchedAbs
}
panic(fmt.Sprintf("abstraction types extractors should not overlap: %s", matched))
}
type BookmarkExtractor func(fs *zfs.DatasetPath, v zfs.FilesystemVersion) Abstraction type BookmarkExtractor func(fs *zfs.DatasetPath, v zfs.FilesystemVersion) Abstraction
// returns nil if the abstraction type is not bookmark-based // returns nil if the abstraction type is not bookmark-based
@@ -270,23 +238,6 @@ func (t AbstractionType) BookmarkNamer() func(fs string, guid uint64, jobId JobI
} }
} }
func (t AbstractionType) IsSnapshotOrBookmark() bool {
switch t {
case AbstractionTentativeReplicationCursorBookmark:
return true
case AbstractionReplicationCursorBookmarkV1:
return true
case AbstractionReplicationCursorBookmarkV2:
return true
case AbstractionStepHold:
return false
case AbstractionLastReceivedHold:
return false
default:
panic(fmt.Sprintf("unimpl: %q", t))
}
}
type ListZFSHoldsAndBookmarksQuery struct { type ListZFSHoldsAndBookmarksQuery struct {
FS ListZFSHoldsAndBookmarksQueryFilesystemFilter FS ListZFSHoldsAndBookmarksQueryFilesystemFilter
// What abstraction types should match (any contained in the set) // What abstraction types should match (any contained in the set)
@@ -565,12 +516,10 @@ func (e ListAbstractionsErrors) Error() string {
} }
func ListAbstractions(ctx context.Context, query ListZFSHoldsAndBookmarksQuery) (out []Abstraction, outErrs []ListAbstractionsError, err error) { func ListAbstractions(ctx context.Context, query ListZFSHoldsAndBookmarksQuery) (out []Abstraction, outErrs []ListAbstractionsError, err error) {
outChan, outErrsChan, drainDone, err := ListAbstractionsStreamed(ctx, query) outChan, outErrsChan, err := ListAbstractionsStreamed(ctx, query)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
defer drainDone()
var wg sync.WaitGroup var wg sync.WaitGroup
wg.Add(1) wg.Add(1)
go func() { go func() {
@@ -591,20 +540,19 @@ func ListAbstractions(ctx context.Context, query ListZFSHoldsAndBookmarksQuery)
} }
// if err != nil, the returned channels are both nil // if err != nil, the returned channels are both nil
// if err == nil, both channels must be fully drained by the caller to avoid leaking goroutines. // if err == nil, both channels must be fully drained by the caller to avoid leaking goroutines
// After draining is done, the caller must call the returned drainDone func. func ListAbstractionsStreamed(ctx context.Context, query ListZFSHoldsAndBookmarksQuery) (<-chan Abstraction, <-chan ListAbstractionsError, error) {
func ListAbstractionsStreamed(ctx context.Context, query ListZFSHoldsAndBookmarksQuery) (_ <-chan Abstraction, _ <-chan ListAbstractionsError, drainDone func(), _ error) {
// impl note: structure the query processing in such a way that // impl note: structure the query processing in such a way that
// a minimum amount of zfs shell-outs needs to be done // a minimum amount of zfs shell-outs needs to be done
if err := query.Validate(); err != nil { if err := query.Validate(); err != nil {
return nil, nil, nil, errors.Wrap(err, "validate query") return nil, nil, errors.Wrap(err, "validate query")
} }
fss, err := query.FS.Filesystems(ctx) fss, err := query.FS.Filesystems(ctx)
if err != nil { if err != nil {
return nil, nil, nil, errors.Wrap(err, "list filesystems") return nil, nil, errors.Wrap(err, "list filesystems")
} }
outErrs := make(chan ListAbstractionsError) outErrs := make(chan ListAbstractionsError)
@@ -626,6 +574,7 @@ func ListAbstractionsStreamed(ctx context.Context, query ListZFSHoldsAndBookmark
sem := semaphore.New(int64(query.Concurrency)) sem := semaphore.New(int64(query.Concurrency))
ctx, endTask := trace.WithTask(ctx, "list-abstractions-streamed-producer") ctx, endTask := trace.WithTask(ctx, "list-abstractions-streamed-producer")
go func() { go func() {
defer endTask()
defer close(out) defer close(out)
defer close(outErrs) defer close(outErrs)
@@ -647,11 +596,7 @@ func ListAbstractionsStreamed(ctx context.Context, query ListZFSHoldsAndBookmark
} }
}() }()
drainDone = func() { return out, outErrs, nil
endTask()
}
return out, outErrs, drainDone, nil
} }
func listAbstractionsImplFS(ctx context.Context, fs string, query *ListZFSHoldsAndBookmarksQuery, emitCandidate putListAbstraction, errCb putListAbstractionErr) { func listAbstractionsImplFS(ctx context.Context, fs string, query *ListZFSHoldsAndBookmarksQuery, emitCandidate putListAbstraction, errCb putListAbstractionErr) {
@@ -23,7 +23,7 @@ func replicationCursorBookmarkNameImpl(fs string, guid uint64, jobid string) (st
var ErrV1ReplicationCursor = fmt.Errorf("bookmark name is a v1-replication cursor") var ErrV1ReplicationCursor = fmt.Errorf("bookmark name is a v1-replication cursor")
// err != nil always means that the bookmark is not a valid replication bookmark //err != nil always means that the bookmark is not a valid replication bookmark
// //
// Returns ErrV1ReplicationCursor as error if the bookmark is a v1 replication cursor // Returns ErrV1ReplicationCursor as error if the bookmark is a v1 replication cursor
func ParseReplicationCursorBookmarkName(fullname string) (uint64, JobID, error) { func ParseReplicationCursorBookmarkName(fullname string) (uint64, JobID, error) {
-1
View File
@@ -29,7 +29,6 @@ require (
github.com/problame/go-netssh v0.0.0-20200601114649-26439f9f0dc5 github.com/problame/go-netssh v0.0.0-20200601114649-26439f9f0dc5
github.com/prometheus/client_golang v1.2.1 github.com/prometheus/client_golang v1.2.1
github.com/prometheus/common v0.7.0 github.com/prometheus/common v0.7.0
github.com/robfig/cron/v3 v3.0.1
github.com/sergi/go-diff v1.0.1-0.20180205163309-da645544ed44 // indirect; go1.12 thinks it needs this github.com/sergi/go-diff v1.0.1-0.20180205163309-da645544ed44 // indirect; go1.12 thinks it needs this
github.com/spf13/cobra v0.0.2 github.com/spf13/cobra v0.0.2
github.com/spf13/pflag v1.0.5 github.com/spf13/pflag v1.0.5
-2
View File
@@ -161,8 +161,6 @@ github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDa
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/sergi/go-diff v1.0.1-0.20180205163309-da645544ed44 h1:tB9NOR21++IjLyVx3/PCPhWMwqGNCMQEH96A6dMZ/gc= github.com/sergi/go-diff v1.0.1-0.20180205163309-da645544ed44 h1:tB9NOR21++IjLyVx3/PCPhWMwqGNCMQEH96A6dMZ/gc=
github.com/sergi/go-diff v1.0.1-0.20180205163309-da645544ed44/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sergi/go-diff v1.0.1-0.20180205163309-da645544ed44/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
+3 -1
View File
@@ -150,7 +150,9 @@ func NewOutlets() *Outlets {
func (os *Outlets) DeepCopy() (copy *Outlets) { func (os *Outlets) DeepCopy() (copy *Outlets) {
copy = NewOutlets() copy = NewOutlets()
for level := range os.outs { for level := range os.outs {
copy.outs[level] = append(copy.outs[level], os.outs[level]...) for i := range os.outs[level] {
copy.outs[level] = append(copy.outs[level], os.outs[level][i])
}
} }
return copy return copy
} }
+1 -2
View File
@@ -7,8 +7,7 @@ RUN apt-get update && apt-get install -y \
dh-exec \ dh-exec \
binutils-aarch64-linux-gnu \ binutils-aarch64-linux-gnu \
binutils-arm-linux-gnueabihf \ binutils-arm-linux-gnueabihf \
binutils-i686-linux-gnu \ binutils-i686-linux-gnu
binutils-x86-64-linux-gnu
RUN mkdir -p /build/src && chmod -R 0777 /build RUN mkdir -p /build/src && chmod -R 0777 /build
+1 -1
View File
@@ -16,7 +16,7 @@ zrepl is a one-stop, integrated solution for ZFS replication.}
%define __strip /usr/bin/true %define __strip /usr/bin/true
Name: zrepl Name: zrepl
Release: SUBSTITUTED_BY_MAKEFILE Release: 1
Summary: One-stop, integrated solution for ZFS replication Summary: One-stop, integrated solution for ZFS replication
License: MIT License: MIT
URL: https://zrepl.github.io/ URL: https://zrepl.github.io/
+11 -56
View File
@@ -1,7 +1,6 @@
package main package main
import ( import (
"container/list"
"context" "context"
"flag" "flag"
"fmt" "fmt"
@@ -54,40 +53,6 @@ type HarnessArgs struct {
Run string Run string
} }
type invocation struct {
runFunc tests.Case
idstring string
result *testCaseResult
children map[string]*invocation
}
func newInvocation(runFunc tests.Case, id string) *invocation {
return &invocation{
runFunc: runFunc,
idstring: id,
children: make(map[string]*invocation),
}
}
func (i *invocation) String() string {
idsuffix := ""
if i.idstring != "" {
idsuffix = fmt.Sprintf(": %s", i.idstring)
}
return fmt.Sprintf("%s%s", i.runFunc.String(), idsuffix)
}
func (i *invocation) RegisterChild(c *invocation) error {
if c.idstring == "" {
return fmt.Errorf("child must have id string")
}
if oc := i.children[c.idstring]; oc != nil {
return fmt.Errorf("idstring %q is already taken by %s", c.idstring, oc)
}
i.children[c.idstring] = c
return nil
}
func HarnessRun(args HarnessArgs) error { func HarnessRun(args HarnessArgs) error {
runRE := regexp.MustCompile(args.Run) runRE := regexp.MustCompile(args.Run)
@@ -114,19 +79,21 @@ func HarnessRun(args HarnessArgs) error {
ctx = logging.WithLoggers(ctx, logging.SubsystemLoggersWithUniversalLogger(logger)) ctx = logging.WithLoggers(ctx, logging.SubsystemLoggersWithUniversalLogger(logger))
ex := platformtest.NewEx(logger) ex := platformtest.NewEx(logger)
testQueue := list.New() type invocation struct {
runFunc tests.Case
result *testCaseResult
}
invocations := make([]*invocation, 0, len(tests.Cases))
for _, c := range tests.Cases { for _, c := range tests.Cases {
if runRE.MatchString(c.String()) { if runRE.MatchString(c.String()) {
testQueue.PushBack(newInvocation(c, "")) invocations = append(invocations, &invocation{runFunc: c})
} }
} }
completedTests := list.New() for _, inv := range invocations {
for testQueue.Len() > 0 { bold.Printf("BEGIN TEST CASE %s\n", inv.runFunc.String())
inv := testQueue.Remove(testQueue.Front()).(*invocation)
bold.Printf("BEGIN TEST CASE %s\n", inv)
pool, err := platformtest.CreateOrReplaceZpool(ctx, ex, args.CreateArgs) pool, err := platformtest.CreateOrReplaceZpool(ctx, ex, args.CreateArgs)
if err != nil { if err != nil {
@@ -136,15 +103,6 @@ func HarnessRun(args HarnessArgs) error {
ctx := &platformtest.Context{ ctx := &platformtest.Context{
Context: ctx, Context: ctx,
RootDataset: filepath.Join(pool.Name(), "rootds"), RootDataset: filepath.Join(pool.Name(), "rootds"),
QueueSubtest: func(id string, stf func(*platformtest.Context)) {
stinv := newInvocation(stf, id)
err := inv.RegisterChild(stinv)
if err != nil {
panic(err)
}
bold.Printf(" QUEUING SUBTEST %q\n", id)
testQueue.PushFront(stinv)
},
} }
res := runTestCase(ctx, ex, inv.runFunc) res := runTestCase(ctx, ex, inv.runFunc)
@@ -162,8 +120,6 @@ func HarnessRun(args HarnessArgs) error {
panic(fmt.Sprintf("error destroying test pool: %s", err)) panic(fmt.Sprintf("error destroying test pool: %s", err))
} }
completedTests.PushBack(inv)
if res.failed { if res.failed {
boldRed.Printf("TEST FAILED\n") boldRed.Printf("TEST FAILED\n")
} else if res.skipped { } else if res.skipped {
@@ -180,8 +136,7 @@ func HarnessRun(args HarnessArgs) error {
var summary struct { var summary struct {
succ, fail, skip []*invocation succ, fail, skip []*invocation
} }
for completedTests.Len() > 0 { for _, inv := range invocations {
inv := completedTests.Remove(completedTests.Front()).(*invocation)
var bucket *[]*invocation var bucket *[]*invocation
if inv.result.failed { if inv.result.failed {
bucket = &summary.fail bucket = &summary.fail
@@ -202,7 +157,7 @@ func HarnessRun(args HarnessArgs) error {
} }
fmt.Printf("\n") fmt.Printf("\n")
for _, inv := range bucket { for _, inv := range bucket {
fmt.Printf(" %s\n", inv) fmt.Printf(" %s\n", inv.runFunc.String())
} }
} }
printBucket("PASSING TESTS", boldGreen, summary.succ) printBucket("PASSING TESTS", boldGreen, summary.succ)
+1 -16
View File
@@ -4,21 +4,6 @@ export ZREPL_MOCK_ZFS_COMMAND_LOG="$1"
shift shift
export ZREPL_MOCK_ZFS_PATH="$1" export ZREPL_MOCK_ZFS_PATH="$1"
shift shift
dirname="$(dirname "${BASH_SOURCE[0]}")" export PATH="$(dirname "${BASH_SOURCE[0]}" )":"$PATH"
# If we invoke this script from the top zrepl source tree, like so:
# ./platformtest/logmockzfs/logzfsenv ...
# then dirname is relative, i.e., ./platformtest/logmockzfs.
# If we put that relative dir in PATH, then Go >= 1.19 will refuse to
# exec the `./platformtest/logmockzfs/zfs` wrapper script if it finds
# it via PATH lookup. For Example:
# cmd := exec.Command("zfs")
# err := cmd.Run()
# will fail with an error that errors.Is(err, exec.ErrDot), message:
# cannot run executable found relative to current directory
# The solution is to use an abspath.
# Learn more at https://pkg.go.dev/os/exec#hdr-Executables_in_the_current_directory
# and https://go-review.googlesource.com/c/go/+/381374
absdirname="$(readlink -e "$dirname")"
export PATH="$absdirname":"$PATH"
args=("$@") args=("$@")
exec "${args[@]}" exec "${args[@]}"
-7
View File
@@ -11,13 +11,6 @@ import (
type Context struct { type Context struct {
context.Context context.Context
RootDataset string RootDataset string
// Use this callback from a top-level test case to queue the
// execution of sub-tests after this test case is complete.
//
// Note that the testing harness executes the subtest
// _after_ the current top-level test. Hence, the subtest
// cannot use any ZFS state of the top-level test.
QueueSubtest func(id string, stf func(*Context))
} }
var FailNowSentinel = fmt.Errorf("platformtest: FailNow called on context") var FailNowSentinel = fmt.Errorf("platformtest: FailNow called on context")
-2
View File
@@ -20,7 +20,6 @@ var Cases = []Case{BatchDestroy,
ReplicationIncrementalCleansUpStaleAbstractionsWithCacheOnSecondReplication, ReplicationIncrementalCleansUpStaleAbstractionsWithCacheOnSecondReplication,
ReplicationIncrementalCleansUpStaleAbstractionsWithoutCacheOnSecondReplication, ReplicationIncrementalCleansUpStaleAbstractionsWithoutCacheOnSecondReplication,
ReplicationIncrementalDestroysStepHoldsIffIncrementalStepHoldsAreDisabledButStepHoldsExist, ReplicationIncrementalDestroysStepHoldsIffIncrementalStepHoldsAreDisabledButStepHoldsExist,
ReplicationIncrementalHandlesFromVersionEqTentativeCursorCorrectly,
ReplicationIncrementalIsPossibleIfCommonSnapshotIsDestroyed, ReplicationIncrementalIsPossibleIfCommonSnapshotIsDestroyed,
ReplicationInitialAll, ReplicationInitialAll,
ReplicationInitialFail, ReplicationInitialFail,
@@ -37,7 +36,6 @@ var Cases = []Case{BatchDestroy,
ReplicationStepCompletedLostBehavior__GuaranteeResumability, ReplicationStepCompletedLostBehavior__GuaranteeResumability,
ResumableRecvAndTokenHandling, ResumableRecvAndTokenHandling,
ResumeTokenParsing, ResumeTokenParsing,
SendArgsValidationEE_EncryptionAndRaw,
SendArgsValidationEncryptedSendOfUnencryptedDatasetForbidden__EncryptionSupported_false, SendArgsValidationEncryptedSendOfUnencryptedDatasetForbidden__EncryptionSupported_false,
SendArgsValidationEncryptedSendOfUnencryptedDatasetForbidden__EncryptionSupported_true, SendArgsValidationEncryptedSendOfUnencryptedDatasetForbidden__EncryptionSupported_true,
SendArgsValidationResumeTokenDifferentFilesystemForbidden, SendArgsValidationResumeTokenDifferentFilesystemForbidden,
+5 -99
View File
@@ -45,7 +45,6 @@ type replicationInvocation struct {
senderConfigHook func(*endpoint.SenderConfig) senderConfigHook func(*endpoint.SenderConfig)
receiverConfigHook func(*endpoint.ReceiverConfig) receiverConfigHook func(*endpoint.ReceiverConfig)
plannerPolicyHook func(*logic.PlannerPolicy) plannerPolicyHook func(*logic.PlannerPolicy)
skipSendArgsValidation bool
} }
func (i replicationInvocation) Do(ctx *platformtest.Context) *report.Report { func (i replicationInvocation) Do(ctx *platformtest.Context) *report.Report {
@@ -93,6 +92,7 @@ func (i replicationInvocation) Do(ctx *platformtest.Context) *report.Report {
sender := i.interceptSender(endpoint.NewSender(senderConfig)) sender := i.interceptSender(endpoint.NewSender(senderConfig))
receiver := i.interceptReceiver(endpoint.NewReceiver(receiverConfig)) receiver := i.interceptReceiver(endpoint.NewReceiver(receiverConfig))
plannerPolicy := logic.PlannerPolicy{ plannerPolicy := logic.PlannerPolicy{
EncryptedSend: logic.TriFromBool(false),
ReplicationConfig: &pdu.ReplicationConfig{ ReplicationConfig: &pdu.ReplicationConfig{
Protection: i.guarantee, Protection: i.guarantee,
}, },
@@ -105,13 +105,8 @@ func (i replicationInvocation) Do(ctx *platformtest.Context) *report.Report {
i.plannerPolicyHook(&plannerPolicy) i.plannerPolicyHook(&plannerPolicy)
} }
var doCtx context.Context = ctx
if i.skipSendArgsValidation {
doCtx = zfs.ZFSSendArgsSkipValidation(ctx)
}
report, wait := replication.Do( report, wait := replication.Do(
doCtx, ctx,
driver.Config{ driver.Config{
MaxAttempts: 1, MaxAttempts: 1,
StepQueueConcurrency: 1, StepQueueConcurrency: 1,
@@ -248,10 +243,7 @@ func implReplicationIncrementalCleansUpStaleAbstractions(ctx *platformtest.Conte
require.NoError(ctx, err) require.NoError(ctx, err)
snap2Hold, err := endpoint.HoldStep(ctx, sfs, snap2, jobId) // no shadow snap2Hold, err := endpoint.HoldStep(ctx, sfs, snap2, jobId) // no shadow
require.NoError(ctx, err) require.NoError(ctx, err)
// create artificial tentative cursor return []endpoint.Abstraction{snap2Cursor, snap1Hold, snap2Hold}
snap3TentativeCursor, err := endpoint.CreateTentativeReplicationCursor(ctx, sfs, snap3, jobId)
require.NoError(ctx, err)
return []endpoint.Abstraction{snap2Cursor, snap1Hold, snap2Hold, snap3TentativeCursor}
} }
createArtificalStaleAbstractions(sjid) createArtificalStaleAbstractions(sjid)
ojidSendAbstractions := createArtificalStaleAbstractions(ojid) ojidSendAbstractions := createArtificalStaleAbstractions(ojid)
@@ -336,29 +328,21 @@ func implReplicationIncrementalCleansUpStaleAbstractions(ctx *platformtest.Conte
require.NoError(ctx, err) require.NoError(ctx, err)
snap2OjidCursorName, err := endpoint.ReplicationCursorBookmarkName(sfs, snap2.Guid, ojid) snap2OjidCursorName, err := endpoint.ReplicationCursorBookmarkName(sfs, snap2.Guid, ojid)
require.NoError(ctx, err) require.NoError(ctx, err)
snap3SjidTentativeCursorName, err := endpoint.TentativeReplicationCursorBookmarkName(sfs, snap3.Guid, sjid)
require.NoError(ctx, err)
snap3OjidTentativeCursorName, err := endpoint.TentativeReplicationCursorBookmarkName(sfs, snap3.Guid, ojid)
require.NoError(ctx, err)
var bmNames []string var bmNames []string
for _, bm := range sBms { for _, bm := range sBms {
bmNames = append(bmNames, bm.Name) bmNames = append(bmNames, bm.Name)
} }
if invalidateCacheBeforeSecondReplication { if invalidateCacheBeforeSecondReplication {
require.Len(ctx, sBms, 4) require.Len(ctx, sBms, 3)
require.Contains(ctx, bmNames, snap5SjidCursorName) require.Contains(ctx, bmNames, snap5SjidCursorName)
require.Contains(ctx, bmNames, snap2OjidCursorName) require.Contains(ctx, bmNames, snap2OjidCursorName)
require.Contains(ctx, bmNames, snap3OjidTentativeCursorName)
require.Contains(ctx, bmNames, "2") require.Contains(ctx, bmNames, "2")
} else { } else {
require.Len(ctx, sBms, 6) require.Len(ctx, sBms, 4)
ctx.Logf("%s", pretty.Sprint(sBms))
require.Contains(ctx, bmNames, snap5SjidCursorName) require.Contains(ctx, bmNames, snap5SjidCursorName)
require.Contains(ctx, bmNames, snap2SjidCursorName) require.Contains(ctx, bmNames, snap2SjidCursorName)
require.Contains(ctx, bmNames, snap2OjidCursorName) require.Contains(ctx, bmNames, snap2OjidCursorName)
require.Contains(ctx, bmNames, snap3SjidTentativeCursorName)
require.Contains(ctx, bmNames, snap3OjidTentativeCursorName)
require.Contains(ctx, bmNames, "2") require.Contains(ctx, bmNames, "2")
} }
} }
@@ -381,84 +365,6 @@ func implReplicationIncrementalCleansUpStaleAbstractions(ctx *platformtest.Conte
} }
func ReplicationIncrementalHandlesFromVersionEqTentativeCursorCorrectly(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"
rep := replicationInvocation{
sjid: sjid,
rjid: rjid,
sfs: sfs,
rfsRoot: rfsRoot,
// It doesn't really matter what guarantee we use here, as the second replication will configure another.
// But, in the real world, the only way for a stale tentative cursor to appear is if the guarantee is set to
// incremental replication and we crash before converting the tentative cursor into a regular cursor.
guarantee: pdu.ReplicationConfigProtectionWithKind(pdu.ReplicationGuaranteeKind_GuaranteeIncrementalReplication),
}
// Do initial replication to set up the test.
rep1 := rep.Do(ctx)
ctx.Logf("\n%s", pretty.Sprint(rep1))
sfsDs := mustDatasetPath(sfs)
snap1_sender := mustGetFilesystemVersion(ctx, sfs+"@1")
snap1_replicationCursor_name, err := endpoint.ReplicationCursorBookmarkName(sfs, snap1_sender.Guid, sjid)
require.NoError(ctx, err)
snap1_replicationCursor := mustGetFilesystemVersion(ctx, sfs+"#"+snap1_replicationCursor_name)
// The second replication will be done with a guarantee kind that doesn't create tentative cursors by itself.
// So, it would generally be right to clean up any tentative cursors on sfs since they're stale abstractions.
// However, if the cursor is used as the `from` version in any send step, we must not destroy it, as that
// would break incremental replication.
// NB: we only need to test the first step as all subsequent steps will be snapshot->snapshot.
rep.guarantee = pdu.ReplicationConfigProtectionWithKind(pdu.ReplicationGuaranteeKind_GuaranteeNothing)
// create the artificial cursor
snap1_tentativeCursor, err := endpoint.CreateTentativeReplicationCursor(ctx, sfs, snap1_sender, sjid)
require.NoError(ctx, err)
endpoint.AbstractionsCacheInvalidate(sfs)
// remove other bookmarks of snap1, and snap1 itself, to force the replication planner to use the tentative cursor
err = zfs.ZFSDestroyFilesystemVersion(ctx, sfsDs, &snap1_sender)
require.NoError(ctx, err)
err = zfs.ZFSDestroyFilesystemVersion(ctx, sfsDs, &snap1_replicationCursor)
require.NoError(ctx, err)
versions, err := zfs.ZFSListFilesystemVersions(ctx, sfsDs, zfs.ListFilesystemVersionsOptions{})
require.NoError(ctx, err)
require.Len(ctx, versions, 1)
require.Equal(ctx, versions[0].Guid, snap1_tentativeCursor.GetFilesystemVersion().Guid)
// create another snapshot so that replication does one incremental step `tentative_cursor` -> `@2`
mustSnapshot(ctx, sfs+"@2")
mustGetFilesystemVersion(ctx, sfs+"@2")
// do the replication
rep2 := rep.Do(ctx)
ctx.Logf("\n%s", pretty.Sprint(rep2))
// Ensure that the tentative cursor was used.
require.Len(ctx, rep2.Attempts, 1)
require.Equal(ctx, rep2.Attempts[0].State, report.AttemptDone)
require.Len(ctx, rep2.Attempts[0].Filesystems, 1)
require.Nil(ctx, rep2.Attempts[0].Filesystems[0].Error())
require.Len(ctx, rep2.Attempts[0].Filesystems[0].Steps, 1)
require.EqualValues(ctx, rep2.Attempts[0].Filesystems[0].CurrentStep, 1)
require.Len(ctx, rep2.Attempts[0].Filesystems[0].Steps, 1)
require.Equal(ctx, rep2.Attempts[0].Filesystems[0].Steps[0].Info.From, snap1_tentativeCursor.GetFilesystemVersion().RelName())
// Ensure that the tentative cursor was destroyed as part of SendPost.
_, err = zfs.ZFSGetFilesystemVersion(ctx, snap1_replicationCursor.FullPath(sfs))
_, ok := err.(*zfs.DatasetDoesNotExist)
require.True(ctx, ok)
}
type PartialSender struct { type PartialSender struct {
*endpoint.Sender *endpoint.Sender
failAfterByteCount int64 failAfterByteCount int64
-228
View File
@@ -2,16 +2,10 @@ package tests
import ( import (
"fmt" "fmt"
"path"
"github.com/kr/pretty"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/zrepl/zrepl/endpoint"
"github.com/zrepl/zrepl/platformtest" "github.com/zrepl/zrepl/platformtest"
"github.com/zrepl/zrepl/replication/logic"
"github.com/zrepl/zrepl/replication/logic/pdu"
"github.com/zrepl/zrepl/replication/report"
"github.com/zrepl/zrepl/util/nodefault" "github.com/zrepl/zrepl/util/nodefault"
"github.com/zrepl/zrepl/zfs" "github.com/zrepl/zrepl/zfs"
) )
@@ -214,225 +208,3 @@ func SendArgsValidationResumeTokenDifferentFilesystemForbidden(ctx *platformtest
require.True(ctx, ok) require.True(ctx, ok)
require.Equal(ctx, mismatchError.What, zfs.ZFSSendArgsResumeTokenMismatchFilesystem) require.Equal(ctx, mismatchError.What, zfs.ZFSSendArgsResumeTokenMismatchFilesystem)
} }
type sendArgsValidationEndToEndTestOutcome string
const (
ValidationAccepts sendArgsValidationEndToEndTestOutcome = "accept"
ValidationRejects sendArgsValidationEndToEndTestOutcome = "rejects"
)
type sendArgsValidationEndToEndTest struct {
encryptedSenderFilesystem bool
senderConfigHook func(config *endpoint.SenderConfig)
expectedOutcome sendArgsValidationEndToEndTestOutcome
outcomeRejectsInspectError func(require.TestingT, *report.FilesystemReport, bool)
inspectReceiverFSAfterSuccessfulCycle func(rfs string)
}
func implSendArgsValidationEndToEndTest(ctx *platformtest.Context, setup sendArgsValidationEndToEndTest) {
senderEncrypted := ""
if setup.encryptedSenderFilesystem {
senderEncrypted = "encrypted"
}
platformtest.Run(ctx, platformtest.PanicErr, ctx.RootDataset, fmt.Sprintf(`
CREATEROOT
+ "sender" %s
+ "receiver"
R zfs create -p "${ROOTDS}/receiver/${ROOTDS}"
`, senderEncrypted))
sjid := endpoint.MustMakeJobID("sender-job")
rjid := endpoint.MustMakeJobID("receiver-job")
sfs := ctx.RootDataset + "/sender"
rfsRoot := ctx.RootDataset + "/receiver"
sfsmp, err := zfs.ZFSGetMountpoint(ctx, sfs)
require.NoError(ctx, err)
require.True(ctx, sfsmp.Mounted)
// Two cycles. one initial replication, one incremental replication.
// Within each cycle: interrupt replication at least once.
// This exercises both the no-resume-token-present and the resume-token-present validation code paths.
initial_then_incremental:
for i := 0; i < 2; i++ {
writeDummyData(path.Join(sfsmp.Mountpoint, "dummy.data"), 2*(1<<20))
mustSnapshot(ctx, fmt.Sprintf("%s@%d", sfs, i))
rep := replicationInvocation{
sjid: sjid,
rjid: rjid,
sfs: sfs,
rfsRoot: rfsRoot,
senderConfigHook: setup.senderConfigHook,
interceptSender: func(e *endpoint.Sender) logic.Sender {
return &PartialSender{Sender: e, failAfterByteCount: 1 << 20}
},
guarantee: pdu.ReplicationConfigProtectionWithKind(pdu.ReplicationGuaranteeKind_GuaranteeResumability),
skipSendArgsValidation: false,
}
rfs := rep.ReceiveSideFilesystem()
// PartialSender interrupts after 1MiB, and we wrote 2 MiB of data
// => Give it 3 attempts to replicate. after that, we should have a stable outcome
var lastReport *report.Report
lastResumeToken := ""
interrupt_current_step:
for j := 0; j < 3; j++ {
lastReport = rep.Do(ctx)
ctx.Logf("\nreport=%s", pretty.Sprint(lastReport))
require.Len(ctx, lastReport.Attempts, 1)
require.Len(ctx, lastReport.Attempts[0].Filesystems, 1)
lastReportFS := lastReport.Attempts[0].Filesystems[0]
var rfsExists bool
rfsResumeToken, err := zfs.ZFSGetReceiveResumeTokenOrEmptyStringIfNotSupported(ctx, mustDatasetPath(rfs))
if err != nil {
_, ok := err.(*zfs.DatasetDoesNotExist) // no shadow
require.True(ctx, ok, "no other errors expected")
rfsExists = false
rfsResumeToken = ""
} else {
rfsExists = true
}
if setup.expectedOutcome == ValidationRejects {
// When expecting rejection, it should manifest immediately, before sending anything.
// This is tested in the j=0 iteration (for both initial and incremental repl (i=0, i=1)).
// But we also want to assert correct behavior in case zrepl observes resume tokens.
// Specifically, cases where the send parameters encoded in the token conflict with the
// configured encryption policy.
// Hence, for scenarios that are expected to reject, after we validated that they reject
// for the non-resuming case (j==0), fabricate a resuming scenario by temporarily disabling
// send args validation. After fabricating the scenario, proceed into j==1 to exercise
// the resume token validation.
if j == 0 {
if i == 0 {
require.False(ctx, rfsExists, "the sender should not have sent anything")
} else {
// we fabricate a scenario where rfsExists below, hence can't assert non-existence anymore
}
ctx.Logf("skipping send args validation to test resuming case")
rep.skipSendArgsValidation = true
setupResumeReport := rep.Do(ctx)
ctx.Logf("setupResumeReport=%s", pretty.Sprint(setupResumeReport))
rep.skipSendArgsValidation = false
rt, err := zfs.ZFSGetReceiveResumeTokenOrEmptyStringIfNotSupported(ctx, mustDatasetPath(rfs))
require.NoError(ctx, err)
require.NotEmpty(ctx, rt, "we disabled send args validation, so the .Do above should have resulted in a resume token on rfs")
lastResumeToken = rt
continue interrupt_current_step // next iteration will test resume case with send args validation enabled
} else { // j > 0
require.Equal(ctx, lastResumeToken, rfsResumeToken, "we expect policy to refuse replication, no progress must happen")
_, err := zfs.ZFSGetFilesystemVersion(ctx, fmt.Sprintf("%s@%d", rfs, i))
_, ok := err.(*zfs.DatasetDoesNotExist)
require.True(ctx, ok, "another check that no progress is happening")
}
setup.outcomeRejectsInspectError(ctx, lastReportFS, j == 0)
// XXX: check rejection cases for incremental replication as well
break initial_then_incremental
} else {
require.Equal(ctx, ValidationAccepts, setup.expectedOutcome)
_, err := zfs.ZFSGetFilesystemVersion(ctx, fmt.Sprintf("%s@%d", rfs, i))
_, notExist := err.(*zfs.DatasetDoesNotExist)
if notExist {
require.NotEmpty(ctx, rfsResumeToken)
continue interrupt_current_step // next iteration will resume
} else {
require.NoError(ctx, err)
// version exists
// make sure all the filesystem versions we created so far were replicated by the replication loop
for j := 0; j <= i; j++ {
_ = fsversion(ctx, rfs, fmt.Sprintf("@%d", j))
}
setup.inspectReceiverFSAfterSuccessfulCycle(rfs)
continue initial_then_incremental
}
}
}
}
}
func SendArgsValidationEE_EncryptionAndRaw(ctx *platformtest.Context) {
type TC struct {
// create sender filesystem with encryption enabled yes/no
SFSEnc bool
SndEnc bool // send flag
SndRaw bool // send flag
RFSEnc bool
Outcome sendArgsValidationEndToEndTestOutcome
RejectErrorNoResume string
RejectErrorResume string
}
tcs := []TC{
// Sender FS is unencrypted
{SFSEnc: false, SndEnc: false, SndRaw: false, RFSEnc: false, Outcome: ValidationAccepts},
{SFSEnc: false, SndEnc: false, SndRaw: true, RFSEnc: false, Outcome: ValidationAccepts}, // allow unencrypted raw sends (#503)
{SFSEnc: false, SndEnc: true, SndRaw: false, RFSEnc: false, Outcome: ValidationRejects,
RejectErrorNoResume: `encrypted send mandated by policy, but filesystem .* is not encrypted`,
RejectErrorResume: `encrypted send mandated by policy, but filesystem .* is not encrypted`,
},
{SFSEnc: false, SndEnc: true, SndRaw: true, RFSEnc: false, Outcome: ValidationRejects,
RejectErrorNoResume: `encrypted send mandated by policy, but filesystem .* is not encrypted`,
RejectErrorResume: `encrypted send mandated by policy, but filesystem .* is not encrypted`,
},
// Sender FS is encrypted
{SFSEnc: true, SndEnc: false, SndRaw: false, RFSEnc: false, Outcome: ValidationAccepts}, // passes because keys are loaded, thus can send plain.
{SFSEnc: true, SndEnc: false, SndRaw: true, RFSEnc: false, Outcome: ValidationRejects,
RejectErrorNoResume: `policy mandates raw\+unencrypted sends, but filesystem .* is encrypted`,
RejectErrorResume: `resume token has rawok=true which would result in encrypted send, but policy mandates unencrypted sends only`,
},
{SFSEnc: true, SndEnc: true, SndRaw: false, RFSEnc: true, Outcome: ValidationAccepts},
{SFSEnc: true, SndEnc: true, SndRaw: true, RFSEnc: true, Outcome: ValidationAccepts},
}
for _, tc := range tcs {
tc := tc // closure would copy by ref otherwise
ctx.QueueSubtest(fmt.Sprintf("%#v", tc), func(ctx *platformtest.Context) {
implSendArgsValidationEndToEndTest(ctx, sendArgsValidationEndToEndTest{
encryptedSenderFilesystem: tc.SFSEnc,
senderConfigHook: func(c *endpoint.SenderConfig) {
c.Encrypt = &nodefault.Bool{B: tc.SndEnc}
c.SendRaw = tc.SndRaw
},
expectedOutcome: tc.Outcome,
outcomeRejectsInspectError: func(ctx require.TestingT, fr *report.FilesystemReport, isResume bool) {
// this callback is only called for ValidationRejects
// validation should be failing during dry send => planning stage
// XXX mock out ZFS to ensure we never call a zfs send that would send data
// if we're expecting validation to fail
require.Equal(ctx, report.FilesystemPlanningErrored, fr.State)
if isResume {
require.NotEmpty(ctx, tc.RejectErrorResume)
require.Regexp(ctx, tc.RejectErrorResume, fr.PlanError)
} else {
require.NotEmpty(ctx, tc.RejectErrorNoResume)
require.Regexp(ctx, tc.RejectErrorNoResume, fr.PlanError)
}
},
inspectReceiverFSAfterSuccessfulCycle: func(rfs string) {
enabled, err := zfs.ZFSGetEncryptionEnabled(ctx, rfs)
require.NoError(ctx, err)
require.Equal(ctx, tc.RFSEnc, enabled, "receiver filesystem encryption settings unexpected")
},
})
})
}
}
+5
View File
@@ -33,6 +33,11 @@ func NewKeepLastN(n int, regex string) (*KeepLastN, error) {
} }
func (k KeepLastN) KeepRule(snaps []Snapshot) (destroyList []Snapshot) { func (k KeepLastN) KeepRule(snaps []Snapshot) (destroyList []Snapshot) {
if k.n > len(snaps) {
return []Snapshot{}
}
matching, notMatching := partitionSnapList(snaps, func(snapshot Snapshot) bool { matching, notMatching := partitionSnapList(snaps, func(snapshot Snapshot) bool {
return k.re.MatchString(snapshot.Name()) return k.re.MatchString(snapshot.Name())
}) })
+1 -1
View File
@@ -90,7 +90,7 @@ func TestKeepLastN(t *testing.T) {
stubSnap{"a2", false, o(12)}, stubSnap{"a2", false, o(12)},
}, },
rules: []KeepRule{ rules: []KeepRule{
MustKeepLastN(4, "a"), MustKeepLastN(3, "a"),
}, },
expDestroy: map[string]bool{ expDestroy: map[string]bool{
"b1": true, "b1": true,
+1 -1
View File
@@ -797,7 +797,7 @@ func (a *attempt) errorReport() *errorReport {
r.byClass[class] = errs r.byClass[class] = errs
} }
for _, err := range r.flattened { for _, err := range r.flattened {
if neterr, ok := err.Err.(net.Error); ok && neterr.Timeout() { if neterr, ok := err.Err.(net.Error); ok && neterr.Temporary() {
putClass(err, errorClassTemporaryConnectivityRelated) putClass(err, errorClassTemporaryConnectivityRelated)
continue continue
} }
@@ -13,7 +13,7 @@ func init() {
} }
} }
//nolint:deadcode,unused //nolint[:deadcode,unused]
func debug(format string, args ...interface{}) { func debug(format string, args ...interface{}) {
if debugEnabled { if debugEnabled {
fmt.Fprintf(os.Stderr, "repl: driver: %s\n", fmt.Sprintf(format, args...)) fmt.Fprintf(os.Stderr, "repl: driver: %s\n", fmt.Sprintf(format, args...))
@@ -22,7 +22,7 @@ func debug(format string, args ...interface{}) {
type debugFunc func(format string, args ...interface{}) type debugFunc func(format string, args ...interface{})
//nolint:deadcode,unused //nolint[:deadcode,unused]
func debugPrefix(prefixFormat string, prefixFormatArgs ...interface{}) debugFunc { func debugPrefix(prefixFormat string, prefixFormatArgs ...interface{}) debugFunc {
prefix := fmt.Sprintf(prefixFormat, prefixFormatArgs...) prefix := fmt.Sprintf(prefixFormat, prefixFormatArgs...)
return func(format string, args ...interface{}) { return func(format string, args ...interface{}) {
-19
View File
@@ -88,27 +88,8 @@ func SortVersionListByCreateTXGThenBookmarkLTSnapshot(fsvslice []*FilesystemVers
return sorted return sorted
} }
func StripBookmarksFromVersionList(fsvslice []*FilesystemVersion) []*FilesystemVersion {
fslice := make([]*FilesystemVersion, 0, len(fsvslice))
for _, fv := range fsvslice {
if fv.Type != FilesystemVersion_Bookmark {
fslice = append(fslice, fv)
}
}
return fslice
}
func IncrementalPath(receiver, sender []*FilesystemVersion) (incPath []*FilesystemVersion, conflict error) { func IncrementalPath(receiver, sender []*FilesystemVersion) (incPath []*FilesystemVersion, conflict error) {
// Receive-side bookmarks can't be used as incremental-from,
// and don't cause recv to fail if there is a newer bookmark than incremetal-form on the receiver.
// So, simply mask them out.
// This will also hide them in the report, but it keeps the code in this function simple,
// and a user who complains about them missing in a conflict message will likely require
// more education about bookmarks than a slightly more accurate error message. They'll get
// that when they open an issue.
receiver = StripBookmarksFromVersionList(receiver)
receiver = SortVersionListByCreateTXGThenBookmarkLTSnapshot(receiver) receiver = SortVersionListByCreateTXGThenBookmarkLTSnapshot(receiver)
sender = SortVersionListByCreateTXGThenBookmarkLTSnapshot(sender) sender = SortVersionListByCreateTXGThenBookmarkLTSnapshot(sender)
-14
View File
@@ -152,18 +152,4 @@ func TestIncrementalPath_BookmarkSupport(t *testing.T) {
assert.Equal(t, l("@a,1", "@b,2"), path) assert.Equal(t, l("@a,1", "@b,2"), path)
}) })
// test that receive-side bookmarks younger than the most recent common ancestor do not cause a conflict
doTest(l("@a,1", "#b,2"), l("@a,1", "@c,3"), func(path []*FilesystemVersion, conflict error) {
assert.NoError(t, conflict)
require.Len(t, path, 2)
assert.Equal(t, l("@a,1")[0], path[0])
assert.Equal(t, l("@c,3")[0], path[1])
})
doTest(l("#a,1"), l("@a,1", "@b,2"), func(path []*FilesystemVersion, conflict error) {
assert.Nil(t, path)
ca, ok := conflict.(*ConflictNoCommonAncestor)
require.True(t, ok)
assert.Equal(t, l(), ca.SortedReceiverVersions, "See comment in IncrementalPath() on why we don't include the boomkmark here")
})
} }
+283 -209
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.25.0 // protoc-gen-go v1.25.0
// protoc v3.14.0 // protoc v3.12.4
// source: pdu.proto // source: pdu.proto
package pdu package pdu
@@ -25,6 +25,55 @@ const (
// of the legacy proto package is being used. // of the legacy proto package is being used.
const _ = proto.ProtoPackageIsVersion4 const _ = proto.ProtoPackageIsVersion4
type Tri int32
const (
Tri_DontCare Tri = 0
Tri_False Tri = 1
Tri_True Tri = 2
)
// Enum value maps for Tri.
var (
Tri_name = map[int32]string{
0: "DontCare",
1: "False",
2: "True",
}
Tri_value = map[string]int32{
"DontCare": 0,
"False": 1,
"True": 2,
}
)
func (x Tri) Enum() *Tri {
p := new(Tri)
*p = x
return p
}
func (x Tri) String() string {
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
}
func (Tri) Descriptor() protoreflect.EnumDescriptor {
return file_pdu_proto_enumTypes[0].Descriptor()
}
func (Tri) Type() protoreflect.EnumType {
return &file_pdu_proto_enumTypes[0]
}
func (x Tri) Number() protoreflect.EnumNumber {
return protoreflect.EnumNumber(x)
}
// Deprecated: Use Tri.Descriptor instead.
func (Tri) EnumDescriptor() ([]byte, []int) {
return file_pdu_proto_rawDescGZIP(), []int{0}
}
type ReplicationGuaranteeKind int32 type ReplicationGuaranteeKind int32
const ( const (
@@ -61,11 +110,11 @@ func (x ReplicationGuaranteeKind) String() string {
} }
func (ReplicationGuaranteeKind) Descriptor() protoreflect.EnumDescriptor { func (ReplicationGuaranteeKind) Descriptor() protoreflect.EnumDescriptor {
return file_pdu_proto_enumTypes[0].Descriptor() return file_pdu_proto_enumTypes[1].Descriptor()
} }
func (ReplicationGuaranteeKind) Type() protoreflect.EnumType { func (ReplicationGuaranteeKind) Type() protoreflect.EnumType {
return &file_pdu_proto_enumTypes[0] return &file_pdu_proto_enumTypes[1]
} }
func (x ReplicationGuaranteeKind) Number() protoreflect.EnumNumber { func (x ReplicationGuaranteeKind) Number() protoreflect.EnumNumber {
@@ -74,7 +123,7 @@ func (x ReplicationGuaranteeKind) Number() protoreflect.EnumNumber {
// Deprecated: Use ReplicationGuaranteeKind.Descriptor instead. // Deprecated: Use ReplicationGuaranteeKind.Descriptor instead.
func (ReplicationGuaranteeKind) EnumDescriptor() ([]byte, []int) { func (ReplicationGuaranteeKind) EnumDescriptor() ([]byte, []int) {
return file_pdu_proto_rawDescGZIP(), []int{0} return file_pdu_proto_rawDescGZIP(), []int{1}
} }
type FilesystemVersion_VersionType int32 type FilesystemVersion_VersionType int32
@@ -107,11 +156,11 @@ func (x FilesystemVersion_VersionType) String() string {
} }
func (FilesystemVersion_VersionType) Descriptor() protoreflect.EnumDescriptor { func (FilesystemVersion_VersionType) Descriptor() protoreflect.EnumDescriptor {
return file_pdu_proto_enumTypes[1].Descriptor() return file_pdu_proto_enumTypes[2].Descriptor()
} }
func (FilesystemVersion_VersionType) Type() protoreflect.EnumType { func (FilesystemVersion_VersionType) Type() protoreflect.EnumType {
return &file_pdu_proto_enumTypes[1] return &file_pdu_proto_enumTypes[2]
} }
func (x FilesystemVersion_VersionType) Number() protoreflect.EnumNumber { func (x FilesystemVersion_VersionType) Number() protoreflect.EnumNumber {
@@ -216,6 +265,7 @@ type Filesystem struct {
Path string `protobuf:"bytes,1,opt,name=Path,proto3" json:"Path,omitempty"` Path string `protobuf:"bytes,1,opt,name=Path,proto3" json:"Path,omitempty"`
ResumeToken string `protobuf:"bytes,2,opt,name=ResumeToken,proto3" json:"ResumeToken,omitempty"` ResumeToken string `protobuf:"bytes,2,opt,name=ResumeToken,proto3" json:"ResumeToken,omitempty"`
IsPlaceholder bool `protobuf:"varint,3,opt,name=IsPlaceholder,proto3" json:"IsPlaceholder,omitempty"` IsPlaceholder bool `protobuf:"varint,3,opt,name=IsPlaceholder,proto3" json:"IsPlaceholder,omitempty"`
IsEncrypted bool `protobuf:"varint,4,opt,name=IsEncrypted,proto3" json:"IsEncrypted,omitempty"`
} }
func (x *Filesystem) Reset() { func (x *Filesystem) Reset() {
@@ -271,6 +321,13 @@ func (x *Filesystem) GetIsPlaceholder() bool {
return false return false
} }
func (x *Filesystem) GetIsEncrypted() bool {
if x != nil {
return x.IsEncrypted
}
return false
}
type ListFilesystemVersionsReq struct { type ListFilesystemVersionsReq struct {
state protoimpl.MessageState state protoimpl.MessageState
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
@@ -460,6 +517,7 @@ type SendReq struct {
// ResumeToken is not empty, the GUIDs of From and To MUST correspond to those // ResumeToken is not empty, the GUIDs of From and To MUST correspond to those
// encoded in the ResumeToken. Otherwise, the Sender MUST return an error. // encoded in the ResumeToken. Otherwise, the Sender MUST return an error.
ResumeToken string `protobuf:"bytes,4,opt,name=ResumeToken,proto3" json:"ResumeToken,omitempty"` ResumeToken string `protobuf:"bytes,4,opt,name=ResumeToken,proto3" json:"ResumeToken,omitempty"`
Encrypted Tri `protobuf:"varint,5,opt,name=Encrypted,proto3,enum=Tri" json:"Encrypted,omitempty"`
ReplicationConfig *ReplicationConfig `protobuf:"bytes,6,opt,name=ReplicationConfig,proto3" json:"ReplicationConfig,omitempty"` ReplicationConfig *ReplicationConfig `protobuf:"bytes,6,opt,name=ReplicationConfig,proto3" json:"ReplicationConfig,omitempty"`
} }
@@ -523,6 +581,13 @@ func (x *SendReq) GetResumeToken() string {
return "" return ""
} }
func (x *SendReq) GetEncrypted() Tri {
if x != nil {
return x.Encrypted
}
return Tri_DontCare
}
func (x *SendReq) GetReplicationConfig() *ReplicationConfig { func (x *SendReq) GetReplicationConfig() *ReplicationConfig {
if x != nil { if x != nil {
return x.ReplicationConfig return x.ReplicationConfig
@@ -1331,155 +1396,162 @@ var file_pdu_proto_rawDesc = []byte{
0x65, 0x6d, 0x52, 0x65, 0x73, 0x12, 0x2d, 0x0a, 0x0b, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x65, 0x6d, 0x52, 0x65, 0x73, 0x12, 0x2d, 0x0a, 0x0b, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73,
0x74, 0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x46, 0x69, 0x6c,
0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x52, 0x0b, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x52, 0x0b, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73,
0x74, 0x65, 0x6d, 0x73, 0x22, 0x68, 0x0a, 0x0a, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x74, 0x65, 0x6d, 0x73, 0x22, 0x8a, 0x01, 0x0a, 0x0a, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73,
0x65, 0x6d, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x74, 0x65, 0x6d, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28,
0x52, 0x04, 0x50, 0x61, 0x74, 0x68, 0x12, 0x20, 0x0a, 0x0b, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x65, 0x09, 0x52, 0x04, 0x50, 0x61, 0x74, 0x68, 0x12, 0x20, 0x0a, 0x0b, 0x52, 0x65, 0x73, 0x75, 0x6d,
0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x52, 0x65, 0x73, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x52, 0x65,
0x75, 0x6d, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x24, 0x0a, 0x0d, 0x49, 0x73, 0x50, 0x6c, 0x73, 0x75, 0x6d, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x24, 0x0a, 0x0d, 0x49, 0x73, 0x50,
0x61, 0x63, 0x65, 0x68, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x6c, 0x61, 0x63, 0x65, 0x68, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08,
0x0d, 0x49, 0x73, 0x50, 0x6c, 0x61, 0x63, 0x65, 0x68, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x22, 0x3b, 0x52, 0x0d, 0x49, 0x73, 0x50, 0x6c, 0x61, 0x63, 0x65, 0x68, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x12,
0x20, 0x0a, 0x0b, 0x49, 0x73, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x18, 0x04,
0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x49, 0x73, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65,
0x64, 0x22, 0x3b, 0x0a, 0x19, 0x4c, 0x69, 0x73, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73,
0x74, 0x65, 0x6d, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x12, 0x1e,
0x0a, 0x0a, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01,
0x28, 0x09, 0x52, 0x0a, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x22, 0x4b,
0x0a, 0x19, 0x4c, 0x69, 0x73, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x0a, 0x19, 0x4c, 0x69, 0x73, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d,
0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x12, 0x1e, 0x0a, 0x0a, 0x46, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x12, 0x2e, 0x0a, 0x08, 0x56,
0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e,
0x0a, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x22, 0x4b, 0x0a, 0x19, 0x4c, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f,
0x69, 0x73, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x56, 0x65, 0x72, 0x6e, 0x52, 0x08, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0xd4, 0x01, 0x0a, 0x11,
0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x12, 0x2e, 0x0a, 0x08, 0x56, 0x65, 0x72, 0x73, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f,
0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x46, 0x69, 0x6c, 0x6e, 0x12, 0x32, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32,
0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x08, 0x1e, 0x2e, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x56, 0x65, 0x72, 0x73,
0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0xd4, 0x01, 0x0a, 0x11, 0x46, 0x69, 0x6c, 0x69, 0x6f, 0x6e, 0x2e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x52,
0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x32, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20,
0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1e, 0x2e, 0x46, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x47, 0x75, 0x69,
0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x04, 0x47, 0x75, 0x69, 0x64, 0x12, 0x1c, 0x0a,
0x09, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x58, 0x47, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04,
0x52, 0x09, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x58, 0x47, 0x12, 0x1a, 0x0a, 0x08, 0x43,
0x72, 0x65, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x43,
0x72, 0x65, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x29, 0x0a, 0x0b, 0x56, 0x65, 0x72, 0x73, 0x69,
0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0c, 0x0a, 0x08, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68,
0x6f, 0x74, 0x10, 0x00, 0x12, 0x0c, 0x0a, 0x08, 0x42, 0x6f, 0x6f, 0x6b, 0x6d, 0x61, 0x72, 0x6b,
0x10, 0x01, 0x22, 0xfd, 0x01, 0x0a, 0x07, 0x53, 0x65, 0x6e, 0x64, 0x52, 0x65, 0x71, 0x12, 0x1e,
0x0a, 0x0a, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01,
0x28, 0x09, 0x52, 0x0a, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x12, 0x26,
0x0a, 0x04, 0x46, 0x72, 0x6f, 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x46,
0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e,
0x2e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x54, 0x79, 0x52, 0x04, 0x46, 0x72, 0x6f, 0x6d, 0x12, 0x22, 0x0a, 0x02, 0x54, 0x6f, 0x18, 0x03, 0x20, 0x01,
0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x56,
0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x47, 0x75, 0x69, 0x64, 0x18, 0x03, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x02, 0x54, 0x6f, 0x12, 0x20, 0x0a, 0x0b, 0x52, 0x65,
0x20, 0x01, 0x28, 0x04, 0x52, 0x04, 0x47, 0x75, 0x69, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x43, 0x72, 0x73, 0x75, 0x6d, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52,
0x65, 0x61, 0x74, 0x65, 0x54, 0x58, 0x47, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x43, 0x0b, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x22, 0x0a, 0x09,
0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x58, 0x47, 0x12, 0x1a, 0x0a, 0x08, 0x43, 0x72, 0x65, 0x61, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0e, 0x32,
0x74, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x43, 0x72, 0x65, 0x61, 0x04, 0x2e, 0x54, 0x72, 0x69, 0x52, 0x09, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64,
0x74, 0x69, 0x6f, 0x6e, 0x22, 0x29, 0x0a, 0x0b, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x54, 0x12, 0x40, 0x0a, 0x11, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43,
0x79, 0x70, 0x65, 0x12, 0x0c, 0x0a, 0x08, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x10, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x52, 0x65,
0x00, 0x12, 0x0c, 0x0a, 0x08, 0x42, 0x6f, 0x6f, 0x6b, 0x6d, 0x61, 0x72, 0x6b, 0x10, 0x01, 0x22, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52,
0xd9, 0x01, 0x0a, 0x07, 0x53, 0x65, 0x6e, 0x64, 0x52, 0x65, 0x71, 0x12, 0x1e, 0x0a, 0x0a, 0x46, 0x11, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66,
0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x69, 0x67, 0x22, 0x51, 0x0a, 0x11, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f,
0x0a, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x12, 0x26, 0x0a, 0x04, 0x46, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x3c, 0x0a, 0x0a, 0x70, 0x72, 0x6f, 0x74, 0x65,
0x72, 0x6f, 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x46, 0x69, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x52, 0x65,
0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x04, 0x46, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x50,
0x72, 0x6f, 0x6d, 0x12, 0x22, 0x0a, 0x02, 0x54, 0x6f, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x72, 0x6f, 0x74, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0a, 0x70, 0x72, 0x6f, 0x74, 0x65,
0x12, 0x2e, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x56, 0x65, 0x72, 0x73, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x8f, 0x01, 0x0a, 0x1b, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63,
0x69, 0x6f, 0x6e, 0x52, 0x02, 0x54, 0x6f, 0x12, 0x20, 0x0a, 0x0b, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x50, 0x72, 0x6f, 0x74, 0x65,
0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x52, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x33, 0x0a, 0x07, 0x49, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x6c,
0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x19, 0x2e, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61,
0x74, 0x69, 0x6f, 0x6e, 0x47, 0x75, 0x61, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x65, 0x4b, 0x69, 0x6e,
0x64, 0x52, 0x07, 0x49, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x6c, 0x12, 0x3b, 0x0a, 0x0b, 0x49, 0x6e,
0x63, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32,
0x19, 0x2e, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x47, 0x75, 0x61,
0x72, 0x61, 0x6e, 0x74, 0x65, 0x65, 0x4b, 0x69, 0x6e, 0x64, 0x52, 0x0b, 0x49, 0x6e, 0x63, 0x72,
0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x6c, 0x22, 0x34, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x70, 0x65,
0x72, 0x74, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28,
0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x56, 0x61, 0x6c, 0x75, 0x65,
0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x57, 0x0a,
0x07, 0x53, 0x65, 0x6e, 0x64, 0x52, 0x65, 0x73, 0x12, 0x28, 0x0a, 0x0f, 0x55, 0x73, 0x65, 0x64,
0x52, 0x65, 0x73, 0x75, 0x6d, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28,
0x08, 0x52, 0x0f, 0x55, 0x73, 0x65, 0x64, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x65, 0x54, 0x6f, 0x6b,
0x65, 0x6e, 0x12, 0x22, 0x0a, 0x0c, 0x45, 0x78, 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, 0x53, 0x69,
0x7a, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0c, 0x45, 0x78, 0x70, 0x65, 0x63, 0x74,
0x65, 0x64, 0x53, 0x69, 0x7a, 0x65, 0x22, 0x3e, 0x0a, 0x10, 0x53, 0x65, 0x6e, 0x64, 0x43, 0x6f,
0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x52, 0x65, 0x71, 0x12, 0x2a, 0x0a, 0x0b, 0x4f, 0x72,
0x69, 0x67, 0x69, 0x6e, 0x61, 0x6c, 0x52, 0x65, 0x71, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32,
0x08, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x52, 0x65, 0x71, 0x52, 0x0b, 0x4f, 0x72, 0x69, 0x67, 0x69,
0x6e, 0x61, 0x6c, 0x52, 0x65, 0x71, 0x22, 0x12, 0x0a, 0x10, 0x53, 0x65, 0x6e, 0x64, 0x43, 0x6f,
0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x52, 0x65, 0x73, 0x22, 0xbe, 0x01, 0x0a, 0x0a, 0x52,
0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x52, 0x65, 0x71, 0x12, 0x1e, 0x0a, 0x0a, 0x46, 0x69, 0x6c,
0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x46,
0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x12, 0x22, 0x0a, 0x02, 0x54, 0x6f, 0x18,
0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74,
0x65, 0x6d, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x02, 0x54, 0x6f, 0x12, 0x2a, 0x0a,
0x10, 0x43, 0x6c, 0x65, 0x61, 0x72, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x65, 0x54, 0x6f, 0x6b, 0x65,
0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x43, 0x6c, 0x65, 0x61, 0x72, 0x52, 0x65,
0x73, 0x75, 0x6d, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x40, 0x0a, 0x11, 0x52, 0x65, 0x70, 0x73, 0x75, 0x6d, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x40, 0x0a, 0x11, 0x52, 0x65, 0x70,
0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x06, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x04,
0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69,
0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x11, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x11, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63,
0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x51, 0x0a, 0x11, 0x52, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x0c, 0x0a, 0x0a, 0x52,
0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x52, 0x65, 0x73, 0x22, 0x67, 0x0a, 0x13, 0x44, 0x65, 0x73,
0x12, 0x3c, 0x0a, 0x0a, 0x70, 0x72, 0x6f, 0x74, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x74, 0x72, 0x6f, 0x79, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x52, 0x65, 0x71,
0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x12, 0x1e, 0x0a, 0x0a, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x18, 0x01,
0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x50, 0x72, 0x6f, 0x74, 0x65, 0x63, 0x74, 0x69, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d,
0x6f, 0x6e, 0x52, 0x0a, 0x70, 0x72, 0x6f, 0x74, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x8f, 0x12, 0x30, 0x0a, 0x09, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x18, 0x02, 0x20,
0x01, 0x0a, 0x1b, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d,
0x6e, 0x66, 0x69, 0x67, 0x50, 0x72, 0x6f, 0x74, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x33, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f,
0x0a, 0x07, 0x49, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x74, 0x73, 0x22, 0x5a, 0x0a, 0x12, 0x44, 0x65, 0x73, 0x74, 0x72, 0x6f, 0x79, 0x53, 0x6e, 0x61,
0x19, 0x2e, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x47, 0x75, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x52, 0x65, 0x73, 0x12, 0x2e, 0x0a, 0x08, 0x53, 0x6e, 0x61, 0x70,
0x72, 0x61, 0x6e, 0x74, 0x65, 0x65, 0x4b, 0x69, 0x6e, 0x64, 0x52, 0x07, 0x49, 0x6e, 0x69, 0x74, 0x73, 0x68, 0x6f, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x46, 0x69, 0x6c,
0x69, 0x61, 0x6c, 0x12, 0x3b, 0x0a, 0x0b, 0x49, 0x6e, 0x63, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x08,
0x61, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x19, 0x2e, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x45, 0x72, 0x72, 0x6f,
0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x47, 0x75, 0x61, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x65, 0x4b, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x44,
0x69, 0x6e, 0x64, 0x52, 0x0b, 0x49, 0x6e, 0x63, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x6c, 0x0a, 0x13, 0x44, 0x65, 0x73, 0x74, 0x72, 0x6f, 0x79, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f,
0x22, 0x34, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x73, 0x52, 0x65, 0x73, 0x12, 0x2d, 0x0a, 0x07, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73,
0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x44, 0x65, 0x73, 0x74, 0x72, 0x6f, 0x79,
0x12, 0x14, 0x0a, 0x05, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x52, 0x65, 0x73, 0x52, 0x07, 0x52, 0x65, 0x73,
0x05, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x57, 0x0a, 0x07, 0x53, 0x65, 0x6e, 0x64, 0x52, 0x65, 0x75, 0x6c, 0x74, 0x73, 0x22, 0x36, 0x0a, 0x14, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74,
0x73, 0x12, 0x28, 0x0a, 0x0f, 0x55, 0x73, 0x65, 0x64, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x65, 0x54, 0x69, 0x6f, 0x6e, 0x43, 0x75, 0x72, 0x73, 0x6f, 0x72, 0x52, 0x65, 0x71, 0x12, 0x1e, 0x0a, 0x0a,
0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x55, 0x73, 0x65, 0x64, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
0x52, 0x65, 0x73, 0x75, 0x6d, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x22, 0x0a, 0x0c, 0x45, 0x52, 0x0a, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x22, 0x54, 0x0a, 0x14,
0x78, 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, 0x53, 0x69, 0x7a, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28,
0x04, 0x52, 0x0c, 0x45, 0x78, 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, 0x53, 0x69, 0x7a, 0x65, 0x22,
0x3e, 0x0a, 0x10, 0x53, 0x65, 0x6e, 0x64, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64,
0x52, 0x65, 0x71, 0x12, 0x2a, 0x0a, 0x0b, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x61, 0x6c, 0x52,
0x65, 0x71, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x08, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x52,
0x65, 0x71, 0x52, 0x0b, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x61, 0x6c, 0x52, 0x65, 0x71, 0x22,
0x12, 0x0a, 0x10, 0x53, 0x65, 0x6e, 0x64, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64,
0x52, 0x65, 0x73, 0x22, 0xbe, 0x01, 0x0a, 0x0a, 0x52, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x52,
0x65, 0x71, 0x12, 0x1e, 0x0a, 0x0a, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d,
0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74,
0x65, 0x6d, 0x12, 0x22, 0x0a, 0x02, 0x54, 0x6f, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12,
0x2e, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x56, 0x65, 0x72, 0x73, 0x69,
0x6f, 0x6e, 0x52, 0x02, 0x54, 0x6f, 0x12, 0x2a, 0x0a, 0x10, 0x43, 0x6c, 0x65, 0x61, 0x72, 0x52,
0x65, 0x73, 0x75, 0x6d, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08,
0x52, 0x10, 0x43, 0x6c, 0x65, 0x61, 0x72, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x65, 0x54, 0x6f, 0x6b,
0x65, 0x6e, 0x12, 0x40, 0x0a, 0x11, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f,
0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e,
0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69,
0x67, 0x52, 0x11, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f,
0x6e, 0x66, 0x69, 0x67, 0x22, 0x0c, 0x0a, 0x0a, 0x52, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x52,
0x65, 0x73, 0x22, 0x67, 0x0a, 0x13, 0x44, 0x65, 0x73, 0x74, 0x72, 0x6f, 0x79, 0x53, 0x6e, 0x61,
0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x52, 0x65, 0x71, 0x12, 0x1e, 0x0a, 0x0a, 0x46, 0x69, 0x6c,
0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x46,
0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x12, 0x30, 0x0a, 0x09, 0x53, 0x6e, 0x61,
0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x46,
0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e,
0x52, 0x09, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x22, 0x5a, 0x0a, 0x12, 0x44,
0x65, 0x73, 0x74, 0x72, 0x6f, 0x79, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x52, 0x65,
0x73, 0x12, 0x2e, 0x0a, 0x08, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x18, 0x01, 0x20,
0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d,
0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x08, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f,
0x74, 0x12, 0x14, 0x0a, 0x05, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09,
0x52, 0x05, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x44, 0x0a, 0x13, 0x44, 0x65, 0x73, 0x74, 0x72,
0x6f, 0x79, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x52, 0x65, 0x73, 0x12, 0x2d,
0x0a, 0x07, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32,
0x13, 0x2e, 0x44, 0x65, 0x73, 0x74, 0x72, 0x6f, 0x79, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f,
0x74, 0x52, 0x65, 0x73, 0x52, 0x07, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x22, 0x36, 0x0a,
0x14, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x75, 0x72, 0x73,
0x6f, 0x72, 0x52, 0x65, 0x71, 0x12, 0x1e, 0x0a, 0x0a, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73,
0x74, 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x46, 0x69, 0x6c, 0x65, 0x73,
0x79, 0x73, 0x74, 0x65, 0x6d, 0x22, 0x54, 0x0a, 0x14, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61,
0x74, 0x69, 0x6f, 0x6e, 0x43, 0x75, 0x72, 0x73, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x12, 0x14, 0x0a,
0x04, 0x47, 0x75, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x48, 0x00, 0x52, 0x04, 0x47,
0x75, 0x69, 0x64, 0x12, 0x1c, 0x0a, 0x08, 0x4e, 0x6f, 0x74, 0x65, 0x78, 0x69, 0x73, 0x74, 0x18,
0x02, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x08, 0x4e, 0x6f, 0x74, 0x65, 0x78, 0x69, 0x73,
0x74, 0x42, 0x08, 0x0a, 0x06, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x22, 0x23, 0x0a, 0x07, 0x50,
0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x12, 0x18, 0x0a, 0x07, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67,
0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65,
0x22, 0x1d, 0x0a, 0x07, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x45,
0x63, 0x68, 0x6f, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x45, 0x63, 0x68, 0x6f, 0x2a,
0x86, 0x01, 0x0a, 0x18, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x47,
0x75, 0x61, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x65, 0x4b, 0x69, 0x6e, 0x64, 0x12, 0x14, 0x0a, 0x10,
0x47, 0x75, 0x61, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x65, 0x49, 0x6e, 0x76, 0x61, 0x6c, 0x69, 0x64,
0x10, 0x00, 0x12, 0x19, 0x0a, 0x15, 0x47, 0x75, 0x61, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x65, 0x52,
0x65, 0x73, 0x75, 0x6d, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x10, 0x01, 0x12, 0x23, 0x0a,
0x1f, 0x47, 0x75, 0x61, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x65, 0x49, 0x6e, 0x63, 0x72, 0x65, 0x6d,
0x65, 0x6e, 0x74, 0x61, 0x6c, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e,
0x10, 0x02, 0x12, 0x14, 0x0a, 0x10, 0x47, 0x75, 0x61, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x65, 0x4e,
0x6f, 0x74, 0x68, 0x69, 0x6e, 0x67, 0x10, 0x03, 0x32, 0x8f, 0x03, 0x0a, 0x0b, 0x52, 0x65, 0x70,
0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1a, 0x0a, 0x04, 0x50, 0x69, 0x6e, 0x67,
0x12, 0x08, 0x2e, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x1a, 0x08, 0x2e, 0x50, 0x69, 0x6e,
0x67, 0x52, 0x65, 0x73, 0x12, 0x39, 0x0a, 0x0f, 0x4c, 0x69, 0x73, 0x74, 0x46, 0x69, 0x6c, 0x65,
0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x73, 0x12, 0x12, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x46, 0x69,
0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x52, 0x65, 0x71, 0x1a, 0x12, 0x2e, 0x4c, 0x69,
0x73, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x52, 0x65, 0x73, 0x12,
0x50, 0x0a, 0x16, 0x4c, 0x69, 0x73, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65,
0x6d, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x1a, 0x2e, 0x4c, 0x69, 0x73, 0x74,
0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f,
0x6e, 0x73, 0x52, 0x65, 0x71, 0x1a, 0x1a, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x46, 0x69, 0x6c, 0x65,
0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65,
0x73, 0x12, 0x3e, 0x0a, 0x10, 0x44, 0x65, 0x73, 0x74, 0x72, 0x6f, 0x79, 0x53, 0x6e, 0x61, 0x70,
0x73, 0x68, 0x6f, 0x74, 0x73, 0x12, 0x14, 0x2e, 0x44, 0x65, 0x73, 0x74, 0x72, 0x6f, 0x79, 0x53,
0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x52, 0x65, 0x71, 0x1a, 0x14, 0x2e, 0x44, 0x65,
0x73, 0x74, 0x72, 0x6f, 0x79, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x52, 0x65,
0x73, 0x12, 0x41, 0x0a, 0x11, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e,
0x43, 0x75, 0x72, 0x73, 0x6f, 0x72, 0x12, 0x15, 0x2e, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61,
0x74, 0x69, 0x6f, 0x6e, 0x43, 0x75, 0x72, 0x73, 0x6f, 0x72, 0x52, 0x65, 0x71, 0x1a, 0x15, 0x2e,
0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x75, 0x72, 0x73, 0x6f, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x75, 0x72, 0x73, 0x6f,
0x72, 0x52, 0x65, 0x73, 0x12, 0x1d, 0x0a, 0x07, 0x53, 0x65, 0x6e, 0x64, 0x44, 0x72, 0x79, 0x12, 0x72, 0x52, 0x65, 0x73, 0x12, 0x14, 0x0a, 0x04, 0x47, 0x75, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01,
0x08, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x52, 0x65, 0x71, 0x1a, 0x08, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x28, 0x04, 0x48, 0x00, 0x52, 0x04, 0x47, 0x75, 0x69, 0x64, 0x12, 0x1c, 0x0a, 0x08, 0x4e, 0x6f,
0x52, 0x65, 0x73, 0x12, 0x35, 0x0a, 0x0d, 0x53, 0x65, 0x6e, 0x64, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x74, 0x65, 0x78, 0x69, 0x73, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x08,
0x65, 0x74, 0x65, 0x64, 0x12, 0x11, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x4e, 0x6f, 0x74, 0x65, 0x78, 0x69, 0x73, 0x74, 0x42, 0x08, 0x0a, 0x06, 0x52, 0x65, 0x73, 0x75,
0x65, 0x74, 0x65, 0x64, 0x52, 0x65, 0x71, 0x1a, 0x11, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x43, 0x6f, 0x6c, 0x74, 0x22, 0x23, 0x0a, 0x07, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x12, 0x18, 0x0a,
0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x52, 0x65, 0x73, 0x42, 0x07, 0x5a, 0x05, 0x2e, 0x3b, 0x07, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07,
0x70, 0x64, 0x75, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x1d, 0x0a, 0x07, 0x50, 0x69, 0x6e, 0x67, 0x52,
0x65, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x45, 0x63, 0x68, 0x6f, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
0x52, 0x04, 0x45, 0x63, 0x68, 0x6f, 0x2a, 0x28, 0x0a, 0x03, 0x54, 0x72, 0x69, 0x12, 0x0c, 0x0a,
0x08, 0x44, 0x6f, 0x6e, 0x74, 0x43, 0x61, 0x72, 0x65, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x46,
0x61, 0x6c, 0x73, 0x65, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x54, 0x72, 0x75, 0x65, 0x10, 0x02,
0x2a, 0x86, 0x01, 0x0a, 0x18, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e,
0x47, 0x75, 0x61, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x65, 0x4b, 0x69, 0x6e, 0x64, 0x12, 0x14, 0x0a,
0x10, 0x47, 0x75, 0x61, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x65, 0x49, 0x6e, 0x76, 0x61, 0x6c, 0x69,
0x64, 0x10, 0x00, 0x12, 0x19, 0x0a, 0x15, 0x47, 0x75, 0x61, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x65,
0x52, 0x65, 0x73, 0x75, 0x6d, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x10, 0x01, 0x12, 0x23,
0x0a, 0x1f, 0x47, 0x75, 0x61, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x65, 0x49, 0x6e, 0x63, 0x72, 0x65,
0x6d, 0x65, 0x6e, 0x74, 0x61, 0x6c, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f,
0x6e, 0x10, 0x02, 0x12, 0x14, 0x0a, 0x10, 0x47, 0x75, 0x61, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x65,
0x4e, 0x6f, 0x74, 0x68, 0x69, 0x6e, 0x67, 0x10, 0x03, 0x32, 0x8f, 0x03, 0x0a, 0x0b, 0x52, 0x65,
0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1a, 0x0a, 0x04, 0x50, 0x69, 0x6e,
0x67, 0x12, 0x08, 0x2e, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x1a, 0x08, 0x2e, 0x50, 0x69,
0x6e, 0x67, 0x52, 0x65, 0x73, 0x12, 0x39, 0x0a, 0x0f, 0x4c, 0x69, 0x73, 0x74, 0x46, 0x69, 0x6c,
0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x73, 0x12, 0x12, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x46,
0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x52, 0x65, 0x71, 0x1a, 0x12, 0x2e, 0x4c,
0x69, 0x73, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x52, 0x65, 0x73,
0x12, 0x50, 0x0a, 0x16, 0x4c, 0x69, 0x73, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74,
0x65, 0x6d, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x1a, 0x2e, 0x4c, 0x69, 0x73,
0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x56, 0x65, 0x72, 0x73, 0x69,
0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x1a, 0x1a, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x46, 0x69, 0x6c,
0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52,
0x65, 0x73, 0x12, 0x3e, 0x0a, 0x10, 0x44, 0x65, 0x73, 0x74, 0x72, 0x6f, 0x79, 0x53, 0x6e, 0x61,
0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x12, 0x14, 0x2e, 0x44, 0x65, 0x73, 0x74, 0x72, 0x6f, 0x79,
0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x52, 0x65, 0x71, 0x1a, 0x14, 0x2e, 0x44,
0x65, 0x73, 0x74, 0x72, 0x6f, 0x79, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x52,
0x65, 0x73, 0x12, 0x41, 0x0a, 0x11, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f,
0x6e, 0x43, 0x75, 0x72, 0x73, 0x6f, 0x72, 0x12, 0x15, 0x2e, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63,
0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x75, 0x72, 0x73, 0x6f, 0x72, 0x52, 0x65, 0x71, 0x1a, 0x15,
0x2e, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x75, 0x72, 0x73,
0x6f, 0x72, 0x52, 0x65, 0x73, 0x12, 0x1d, 0x0a, 0x07, 0x53, 0x65, 0x6e, 0x64, 0x44, 0x72, 0x79,
0x12, 0x08, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x52, 0x65, 0x71, 0x1a, 0x08, 0x2e, 0x53, 0x65, 0x6e,
0x64, 0x52, 0x65, 0x73, 0x12, 0x35, 0x0a, 0x0d, 0x53, 0x65, 0x6e, 0x64, 0x43, 0x6f, 0x6d, 0x70,
0x6c, 0x65, 0x74, 0x65, 0x64, 0x12, 0x11, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x43, 0x6f, 0x6d, 0x70,
0x6c, 0x65, 0x74, 0x65, 0x64, 0x52, 0x65, 0x71, 0x1a, 0x11, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x43,
0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x52, 0x65, 0x73, 0x42, 0x07, 0x5a, 0x05, 0x2e,
0x3b, 0x70, 0x64, 0x75, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
} }
var ( var (
@@ -1494,69 +1566,71 @@ func file_pdu_proto_rawDescGZIP() []byte {
return file_pdu_proto_rawDescData return file_pdu_proto_rawDescData
} }
var file_pdu_proto_enumTypes = make([]protoimpl.EnumInfo, 2) var file_pdu_proto_enumTypes = make([]protoimpl.EnumInfo, 3)
var file_pdu_proto_msgTypes = make([]protoimpl.MessageInfo, 22) var file_pdu_proto_msgTypes = make([]protoimpl.MessageInfo, 22)
var file_pdu_proto_goTypes = []interface{}{ var file_pdu_proto_goTypes = []interface{}{
(ReplicationGuaranteeKind)(0), // 0: ReplicationGuaranteeKind (Tri)(0), // 0: Tri
(FilesystemVersion_VersionType)(0), // 1: FilesystemVersion.VersionType (ReplicationGuaranteeKind)(0), // 1: ReplicationGuaranteeKind
(*ListFilesystemReq)(nil), // 2: ListFilesystemReq (FilesystemVersion_VersionType)(0), // 2: FilesystemVersion.VersionType
(*ListFilesystemRes)(nil), // 3: ListFilesystemRes (*ListFilesystemReq)(nil), // 3: ListFilesystemReq
(*Filesystem)(nil), // 4: Filesystem (*ListFilesystemRes)(nil), // 4: ListFilesystemRes
(*ListFilesystemVersionsReq)(nil), // 5: ListFilesystemVersionsReq (*Filesystem)(nil), // 5: Filesystem
(*ListFilesystemVersionsRes)(nil), // 6: ListFilesystemVersionsRes (*ListFilesystemVersionsReq)(nil), // 6: ListFilesystemVersionsReq
(*FilesystemVersion)(nil), // 7: FilesystemVersion (*ListFilesystemVersionsRes)(nil), // 7: ListFilesystemVersionsRes
(*SendReq)(nil), // 8: SendReq (*FilesystemVersion)(nil), // 8: FilesystemVersion
(*ReplicationConfig)(nil), // 9: ReplicationConfig (*SendReq)(nil), // 9: SendReq
(*ReplicationConfigProtection)(nil), // 10: ReplicationConfigProtection (*ReplicationConfig)(nil), // 10: ReplicationConfig
(*Property)(nil), // 11: Property (*ReplicationConfigProtection)(nil), // 11: ReplicationConfigProtection
(*SendRes)(nil), // 12: SendRes (*Property)(nil), // 12: Property
(*SendCompletedReq)(nil), // 13: SendCompletedReq (*SendRes)(nil), // 13: SendRes
(*SendCompletedRes)(nil), // 14: SendCompletedRes (*SendCompletedReq)(nil), // 14: SendCompletedReq
(*ReceiveReq)(nil), // 15: ReceiveReq (*SendCompletedRes)(nil), // 15: SendCompletedRes
(*ReceiveRes)(nil), // 16: ReceiveRes (*ReceiveReq)(nil), // 16: ReceiveReq
(*DestroySnapshotsReq)(nil), // 17: DestroySnapshotsReq (*ReceiveRes)(nil), // 17: ReceiveRes
(*DestroySnapshotRes)(nil), // 18: DestroySnapshotRes (*DestroySnapshotsReq)(nil), // 18: DestroySnapshotsReq
(*DestroySnapshotsRes)(nil), // 19: DestroySnapshotsRes (*DestroySnapshotRes)(nil), // 19: DestroySnapshotRes
(*ReplicationCursorReq)(nil), // 20: ReplicationCursorReq (*DestroySnapshotsRes)(nil), // 20: DestroySnapshotsRes
(*ReplicationCursorRes)(nil), // 21: ReplicationCursorRes (*ReplicationCursorReq)(nil), // 21: ReplicationCursorReq
(*PingReq)(nil), // 22: PingReq (*ReplicationCursorRes)(nil), // 22: ReplicationCursorRes
(*PingRes)(nil), // 23: PingRes (*PingReq)(nil), // 23: PingReq
(*PingRes)(nil), // 24: PingRes
} }
var file_pdu_proto_depIdxs = []int32{ var file_pdu_proto_depIdxs = []int32{
4, // 0: ListFilesystemRes.Filesystems:type_name -> Filesystem 5, // 0: ListFilesystemRes.Filesystems:type_name -> Filesystem
7, // 1: ListFilesystemVersionsRes.Versions:type_name -> FilesystemVersion 8, // 1: ListFilesystemVersionsRes.Versions:type_name -> FilesystemVersion
1, // 2: FilesystemVersion.Type:type_name -> FilesystemVersion.VersionType 2, // 2: FilesystemVersion.Type:type_name -> FilesystemVersion.VersionType
7, // 3: SendReq.From:type_name -> FilesystemVersion 8, // 3: SendReq.From:type_name -> FilesystemVersion
7, // 4: SendReq.To:type_name -> FilesystemVersion 8, // 4: SendReq.To:type_name -> FilesystemVersion
9, // 5: SendReq.ReplicationConfig:type_name -> ReplicationConfig 0, // 5: SendReq.Encrypted:type_name -> Tri
10, // 6: ReplicationConfig.protection:type_name -> ReplicationConfigProtection 10, // 6: SendReq.ReplicationConfig:type_name -> ReplicationConfig
0, // 7: ReplicationConfigProtection.Initial:type_name -> ReplicationGuaranteeKind 11, // 7: ReplicationConfig.protection:type_name -> ReplicationConfigProtection
0, // 8: ReplicationConfigProtection.Incremental:type_name -> ReplicationGuaranteeKind 1, // 8: ReplicationConfigProtection.Initial:type_name -> ReplicationGuaranteeKind
8, // 9: SendCompletedReq.OriginalReq:type_name -> SendReq 1, // 9: ReplicationConfigProtection.Incremental:type_name -> ReplicationGuaranteeKind
7, // 10: ReceiveReq.To:type_name -> FilesystemVersion 9, // 10: SendCompletedReq.OriginalReq:type_name -> SendReq
9, // 11: ReceiveReq.ReplicationConfig:type_name -> ReplicationConfig 8, // 11: ReceiveReq.To:type_name -> FilesystemVersion
7, // 12: DestroySnapshotsReq.Snapshots:type_name -> FilesystemVersion 10, // 12: ReceiveReq.ReplicationConfig:type_name -> ReplicationConfig
7, // 13: DestroySnapshotRes.Snapshot:type_name -> FilesystemVersion 8, // 13: DestroySnapshotsReq.Snapshots:type_name -> FilesystemVersion
18, // 14: DestroySnapshotsRes.Results:type_name -> DestroySnapshotRes 8, // 14: DestroySnapshotRes.Snapshot:type_name -> FilesystemVersion
22, // 15: Replication.Ping:input_type -> PingReq 19, // 15: DestroySnapshotsRes.Results:type_name -> DestroySnapshotRes
2, // 16: Replication.ListFilesystems:input_type -> ListFilesystemReq 23, // 16: Replication.Ping:input_type -> PingReq
5, // 17: Replication.ListFilesystemVersions:input_type -> ListFilesystemVersionsReq 3, // 17: Replication.ListFilesystems:input_type -> ListFilesystemReq
17, // 18: Replication.DestroySnapshots:input_type -> DestroySnapshotsReq 6, // 18: Replication.ListFilesystemVersions:input_type -> ListFilesystemVersionsReq
20, // 19: Replication.ReplicationCursor:input_type -> ReplicationCursorReq 18, // 19: Replication.DestroySnapshots:input_type -> DestroySnapshotsReq
8, // 20: Replication.SendDry:input_type -> SendReq 21, // 20: Replication.ReplicationCursor:input_type -> ReplicationCursorReq
13, // 21: Replication.SendCompleted:input_type -> SendCompletedReq 9, // 21: Replication.SendDry:input_type -> SendReq
23, // 22: Replication.Ping:output_type -> PingRes 14, // 22: Replication.SendCompleted:input_type -> SendCompletedReq
3, // 23: Replication.ListFilesystems:output_type -> ListFilesystemRes 24, // 23: Replication.Ping:output_type -> PingRes
6, // 24: Replication.ListFilesystemVersions:output_type -> ListFilesystemVersionsRes 4, // 24: Replication.ListFilesystems:output_type -> ListFilesystemRes
19, // 25: Replication.DestroySnapshots:output_type -> DestroySnapshotsRes 7, // 25: Replication.ListFilesystemVersions:output_type -> ListFilesystemVersionsRes
21, // 26: Replication.ReplicationCursor:output_type -> ReplicationCursorRes 20, // 26: Replication.DestroySnapshots:output_type -> DestroySnapshotsRes
12, // 27: Replication.SendDry:output_type -> SendRes 22, // 27: Replication.ReplicationCursor:output_type -> ReplicationCursorRes
14, // 28: Replication.SendCompleted:output_type -> SendCompletedRes 13, // 28: Replication.SendDry:output_type -> SendRes
22, // [22:29] is the sub-list for method output_type 15, // 29: Replication.SendCompleted:output_type -> SendCompletedRes
15, // [15:22] is the sub-list for method input_type 23, // [23:30] is the sub-list for method output_type
15, // [15:15] is the sub-list for extension type_name 16, // [16:23] is the sub-list for method input_type
15, // [15:15] is the sub-list for extension extendee 16, // [16:16] is the sub-list for extension type_name
0, // [0:15] is the sub-list for field type_name 16, // [16:16] is the sub-list for extension extendee
0, // [0:16] is the sub-list for field type_name
} }
func init() { file_pdu_proto_init() } func init() { file_pdu_proto_init() }
@@ -1839,7 +1913,7 @@ func file_pdu_proto_init() {
File: protoimpl.DescBuilder{ File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(), GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_pdu_proto_rawDesc, RawDescriptor: file_pdu_proto_rawDesc,
NumEnums: 2, NumEnums: 3,
NumMessages: 22, NumMessages: 22,
NumExtensions: 0, NumExtensions: 0,
NumServices: 1, NumServices: 1,
+8
View File
@@ -21,6 +21,7 @@ message Filesystem {
string Path = 1; string Path = 1;
string ResumeToken = 2; string ResumeToken = 2;
bool IsPlaceholder = 3; bool IsPlaceholder = 3;
bool IsEncrypted = 4;
} }
message ListFilesystemVersionsReq { string Filesystem = 1; } message ListFilesystemVersionsReq { string Filesystem = 1; }
@@ -39,6 +40,12 @@ message FilesystemVersion {
string Creation = 5; // RFC 3339 string Creation = 5; // RFC 3339
} }
enum Tri {
DontCare = 0;
False = 1;
True = 2;
}
message SendReq { message SendReq {
string Filesystem = 1; string Filesystem = 1;
// May be empty / null to request a full transfer of To // May be empty / null to request a full transfer of To
@@ -52,6 +59,7 @@ message SendReq {
// ResumeToken is not empty, the GUIDs of From and To MUST correspond to those // ResumeToken is not empty, the GUIDs of From and To MUST correspond to those
// encoded in the ResumeToken. Otherwise, the Sender MUST return an error. // encoded in the ResumeToken. Otherwise, the Sender MUST return an error.
string ResumeToken = 4; string ResumeToken = 4;
Tri Encrypted = 5;
ReplicationConfig ReplicationConfig = 6; ReplicationConfig ReplicationConfig = 6;
} }
+39 -1
View File
@@ -156,6 +156,7 @@ type Step struct {
parent *Filesystem parent *Filesystem
from, to *pdu.FilesystemVersion // from may be nil, indicating full send from, to *pdu.FilesystemVersion // from may be nil, indicating full send
encrypt tri
resumeToken string // empty means no resume token shall be used resumeToken string // empty means no resume token shall be used
expectedSize uint64 // 0 means no size estimate present / possible expectedSize uint64 // 0 means no size estimate present / possible
@@ -200,10 +201,22 @@ func (s *Step) ReportInfo() *report.StepInfo {
if s.from != nil { if s.from != nil {
from = s.from.RelName() from = s.from.RelName()
} }
var encrypted report.EncryptedEnum
switch s.encrypt {
case DontCare:
encrypted = report.EncryptedSenderDependent
case True:
encrypted = report.EncryptedTrue
case False:
encrypted = report.EncryptedFalse
default:
panic(fmt.Sprintf("unknown variant %s", s.encrypt))
}
return &report.StepInfo{ return &report.StepInfo{
From: from, From: from,
To: s.to.RelName(), To: s.to.RelName(),
Resumed: s.resumeToken != "", Resumed: s.resumeToken != "",
Encrypted: encrypted,
BytesExpected: s.expectedSize, BytesExpected: s.expectedSize,
BytesReplicated: byteCounter, BytesReplicated: byteCounter,
} }
@@ -333,6 +346,10 @@ func (fs *Filesystem) doPlanning(ctx context.Context) ([]*Step, error) {
log(ctx).Debug("assessing filesystem") log(ctx).Debug("assessing filesystem")
if fs.policy.EncryptedSend == True && !fs.senderFS.GetIsEncrypted() {
return nil, fmt.Errorf("sender filesystem is not encrypted but policy mandates encrypted send")
}
sfsvsres, err := fs.sender.ListFilesystemVersions(ctx, &pdu.ListFilesystemVersionsReq{Filesystem: fs.Path}) sfsvsres, err := fs.sender.ListFilesystemVersions(ctx, &pdu.ListFilesystemVersionsReq{Filesystem: fs.Path})
if err != nil { if err != nil {
log(ctx).WithError(err).Error("cannot get remote filesystem versions") log(ctx).WithError(err).Error("cannot get remote filesystem versions")
@@ -406,7 +423,24 @@ func (fs *Filesystem) doPlanning(ctx context.Context) ([]*Step, error) {
} }
} }
if toVersion == nil { encryptionMatches := false
switch fs.policy.EncryptedSend {
case True:
encryptionMatches = resumeToken.RawOK && resumeToken.CompressOK
case False:
encryptionMatches = !resumeToken.RawOK && !resumeToken.CompressOK
case DontCare:
encryptionMatches = true
}
log(ctx).WithField("fromVersion", fromVersion).
WithField("toVersion", toVersion).
WithField("encryptionMatches", encryptionMatches).
Debug("result of resume-token-matching to sender's versions")
if !encryptionMatches {
return nil, fmt.Errorf("resume token `rawok`=%v and `compressok`=%v are incompatible with encryption policy=%v", resumeToken.RawOK, resumeToken.CompressOK, fs.policy.EncryptedSend)
} else if toVersion == nil {
return nil, fmt.Errorf("resume token `toguid` = %v not found on sender (`toname` = %q)", resumeToken.ToGUID, resumeToken.ToName) return nil, fmt.Errorf("resume token `toguid` = %v not found on sender (`toname` = %q)", resumeToken.ToGUID, resumeToken.ToName)
} else if fromVersion == toVersion { } else if fromVersion == toVersion {
return nil, fmt.Errorf("resume token `fromguid` and `toguid` match same version on sener") return nil, fmt.Errorf("resume token `fromguid` and `toguid` match same version on sener")
@@ -420,6 +454,7 @@ func (fs *Filesystem) doPlanning(ctx context.Context) ([]*Step, error) {
from: fromVersion, from: fromVersion,
to: toVersion, to: toVersion,
encrypt: fs.policy.EncryptedSend,
resumeToken: resumeTokenRaw, resumeToken: resumeTokenRaw,
} }
@@ -445,6 +480,7 @@ func (fs *Filesystem) doPlanning(ctx context.Context) ([]*Step, error) {
receiver: fs.receiver, receiver: fs.receiver,
from: remainingSFSVs[i], from: remainingSFSVs[i],
to: remainingSFSVs[i+1], to: remainingSFSVs[i+1],
encrypt: fs.policy.EncryptedSend,
}) })
} }
} else { // resumeToken == nil } else { // resumeToken == nil
@@ -476,6 +512,7 @@ func (fs *Filesystem) doPlanning(ctx context.Context) ([]*Step, error) {
from: path[i], // nil in case of initial repl from: path[i], // nil in case of initial repl
to: path[i+1], to: path[i+1],
encrypt: fs.policy.EncryptedSend,
}) })
} }
} }
@@ -555,6 +592,7 @@ func (s *Step) buildSendRequest() (sr *pdu.SendReq) {
Filesystem: fs, Filesystem: fs,
From: s.from, // may be nil From: s.from, // may be nil
To: s.to, To: s.to,
Encrypted: s.encrypt.ToPDU(),
ResumeToken: s.resumeToken, ResumeToken: s.resumeToken,
ReplicationConfig: s.parent.policy.ReplicationConfig, ReplicationConfig: s.parent.policy.ReplicationConfig,
} }
@@ -63,6 +63,7 @@ func ConflictResolutionFromConfig(in *config.ConflictResolution) (*ConflictResol
} }
type PlannerPolicy struct { type PlannerPolicy struct {
EncryptedSend tri // all sends must be encrypted (send -w, and encryption!=off)
ConflictResolution *ConflictResolution `validate:"ne=nil"` ConflictResolution *ConflictResolution `validate:"ne=nil"`
ReplicationConfig *pdu.ReplicationConfig `validate:"ne=nil"` ReplicationConfig *pdu.ReplicationConfig `validate:"ne=nil"`
SizeEstimationConcurrency int `validate:"gte=1"` SizeEstimationConcurrency int `validate:"gte=1"`
@@ -0,0 +1,46 @@
package logic
import (
"fmt"
"github.com/zrepl/zrepl/replication/logic/pdu"
)
type tri int
const (
DontCare = 0x0
False = 0x1
True = 0x2
)
func (t tri) String() string {
switch t {
case DontCare:
return "dontcare"
case False:
return "false"
case True:
return "true"
}
panic(fmt.Sprintf("unknown variant %v", int(t)))
}
func (t tri) ToPDU() pdu.Tri {
switch t {
case DontCare:
return pdu.Tri_DontCare
case False:
return pdu.Tri_False
case True:
return pdu.Tri_True
}
panic(fmt.Sprintf("unknown variant %v", int(t)))
}
func TriFromBool(b bool) tri {
if b {
return True
}
return False
}
+9 -11
View File
@@ -97,9 +97,18 @@ type StepReport struct {
Info *StepInfo Info *StepInfo
} }
type EncryptedEnum string
const (
EncryptedTrue EncryptedEnum = "yes"
EncryptedFalse EncryptedEnum = "no"
EncryptedSenderDependent EncryptedEnum = "sender-dependent"
)
type StepInfo struct { type StepInfo struct {
From, To string From, To string
Resumed bool Resumed bool
Encrypted EncryptedEnum
BytesExpected uint64 BytesExpected uint64
BytesReplicated uint64 BytesReplicated uint64
} }
@@ -192,14 +201,3 @@ func (r *Report) GetFailedFilesystemsCountInLatestAttempt() int {
return 0 return 0
} }
} }
// Returns true in case the AttemptState is a terminal
// state(AttemptPlanningError, AttemptFanOutError, AttemptDone)
func (a AttemptState) IsTerminal() bool {
switch a {
case AttemptPlanningError, AttemptFanOutError, AttemptDone:
return true
default:
return false
}
}
+1 -1
View File
@@ -14,7 +14,7 @@ func init() {
} }
} }
//nolint:deadcode,unused //nolint[:deadcode,unused]
func debug(format string, args ...interface{}) { func debug(format string, args ...interface{}) {
if debugEnabled { if debugEnabled {
fmt.Fprintf(os.Stderr, "rpc/dataconn: %s\n", fmt.Sprintf(format, args...)) fmt.Fprintf(os.Stderr, "rpc/dataconn: %s\n", fmt.Sprintf(format, args...))
@@ -24,9 +24,6 @@ func (e HeartbeatTimeout) Error() string {
return "heartbeat timeout" return "heartbeat timeout"
} }
// This function is deprecated in net.Error and since this
// function is not involved in .Accept() code path, nothing
// really needs this method to be here.
func (e HeartbeatTimeout) Temporary() bool { return true } func (e HeartbeatTimeout) Temporary() bool { return true }
func (e HeartbeatTimeout) Timeout() bool { return true } func (e HeartbeatTimeout) Timeout() bool { return true }
@@ -13,7 +13,7 @@ func init() {
} }
} }
//nolint:deadcode,unused //nolint[:deadcode,unused]
func debug(format string, args ...interface{}) { func debug(format string, args ...interface{}) {
if debugEnabled { if debugEnabled {
fmt.Fprintf(os.Stderr, "rpc/dataconn/heartbeatconn: %s\n", fmt.Sprintf(format, args...)) fmt.Fprintf(os.Stderr, "rpc/dataconn/heartbeatconn: %s\n", fmt.Sprintf(format, args...))

Some files were not shown because too many files have changed in this diff Show More