Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5976264bce | |||
| f28676b8d7 | |||
| 3e6cae1c8f | |||
| a4a2cb2833 | |||
| 97a14dba90 | |||
| 40be626b3a | |||
| 68b895d0bc |
+185
-103
@@ -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,41 +50,94 @@ commands:
|
|||||||
- invoke-lazy-sh:
|
- invoke-lazy-sh:
|
||||||
subcommand: docdep
|
subcommand: docdep
|
||||||
|
|
||||||
docs-publish-sh:
|
download-and-install-minio-client:
|
||||||
parameters:
|
|
||||||
push:
|
|
||||||
type: boolean
|
|
||||||
steps:
|
steps:
|
||||||
- checkout
|
- setup-home-local-bin
|
||||||
|
- restore_cache:
|
||||||
|
key: minio-client-v2
|
||||||
- run:
|
- run:
|
||||||
|
shell: /bin/bash -eo pipefail
|
||||||
command: |
|
command: |
|
||||||
git config --global user.email "zreplbot@cschwarz.com"
|
if which mc; then exit 0; fi
|
||||||
git config --global user.name "zrepl-github-io-ci"
|
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"
|
||||||
|
|
||||||
# https://circleci.com/docs/2.0/add-ssh-key/#adding-multiple-keys-with-blank-hostnames
|
upload-minio:
|
||||||
- run: ssh-add -D
|
parameters:
|
||||||
# the default circleci ssh config only additional ssh keys for Host !github.com
|
src:
|
||||||
|
type: string
|
||||||
|
dst:
|
||||||
|
type: string
|
||||||
|
steps:
|
||||||
- run:
|
- run:
|
||||||
|
shell: /bin/bash -eo pipefail
|
||||||
|
when: always
|
||||||
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
|
mc config host add --api s3v4 zrepl-minio https://minio.cschwarz.com ${MINIO_ACCESS_KEY} ${MINIO_SECRET_KEY}
|
||||||
- when:
|
|
||||||
condition: << parameters.push >>
|
# keep in sync with set-github-minio-status
|
||||||
steps:
|
jobprefix=zrepl-ci-artifacts/${CIRCLE_SHA1}-pipeline-<<pipeline.number>>/${CIRCLE_JOB}
|
||||||
- run: bash -x docs/publish.sh -c -a -P
|
|
||||||
- when:
|
# Upload artifacts
|
||||||
condition:
|
mkdir -p ./artifacts
|
||||||
not: << parameters.push >>
|
mc cp -r <<parameters.src>> "zrepl-minio/$jobprefix/<<parameters.dst>>"
|
||||||
steps:
|
|
||||||
- run: bash -x docs/publish.sh -c -a
|
set-github-minio-status:
|
||||||
|
parameters:
|
||||||
|
context:
|
||||||
|
type: string
|
||||||
|
description:
|
||||||
|
type: string
|
||||||
|
minio-dst:
|
||||||
|
type: string
|
||||||
|
steps:
|
||||||
|
- run:
|
||||||
|
shell: /bin/bash -eo pipefail
|
||||||
|
command: |
|
||||||
|
if [ -n "$CIRCLE_PR_NUMBER" ]; then # CIRCLE_PR_NUMBER is guaranteed to be only present in forked PRs (external)
|
||||||
|
echo "Forked PR detected. Sry, can't trust you with credentials to external artifact store, use CircleCI's instead."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
set -u # from now on
|
||||||
|
|
||||||
|
# keep in sync with with upload-minio command
|
||||||
|
jobprefix=zrepl-ci-artifacts/${CIRCLE_SHA1}-pipeline-<<pipeline.number>>/${CIRCLE_JOB}
|
||||||
|
# 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:
|
||||||
|
- run: |
|
||||||
|
curl -X POST https://circleci.com/api/v2/project/github/zrepl/zrepl/pipeline \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-H 'Accept: application/json' \
|
||||||
|
-H "Circle-Token: $ZREPL_BOT_CIRCLE_TOKEN" \
|
||||||
|
--data '<<parameters.body_no_shell_subst>>'
|
||||||
|
|
||||||
parameters:
|
parameters:
|
||||||
do_ci:
|
do_ci:
|
||||||
@@ -94,7 +150,7 @@ parameters:
|
|||||||
|
|
||||||
release_docker_baseimage_tag:
|
release_docker_baseimage_tag:
|
||||||
type: string
|
type: string
|
||||||
default: "1.21"
|
default: "1.16"
|
||||||
|
|
||||||
workflows:
|
workflows:
|
||||||
version: 2
|
version: 2
|
||||||
@@ -104,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.16
|
||||||
goversion: &latest-go-release "1.21"
|
goversion: &latest-go-release "1.16"
|
||||||
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.16 #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:
|
||||||
@@ -124,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 >>
|
||||||
@@ -149,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:
|
||||||
@@ -160,15 +225,22 @@ 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:
|
- store_artifacts:
|
||||||
push: false
|
path: artifacts
|
||||||
|
- download-and-install-minio-client
|
||||||
|
- 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:
|
||||||
@@ -179,7 +251,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>>
|
||||||
@@ -187,66 +259,44 @@ 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: true
|
||||||
image: ubuntu-2004:202201-02
|
|
||||||
steps:
|
steps:
|
||||||
- checkout
|
- checkout
|
||||||
- run: make release-docker RELEASE_DOCKER_BASEIMAGE_TAG=<<pipeline.parameters.release_docker_baseimage_tag>>
|
- run: make release-docker RELEASE_DOCKER_BASEIMAGE_TAG=<<pipeline.parameters.release_docker_baseimage_tag>>
|
||||||
@@ -254,8 +304,7 @@ jobs:
|
|||||||
root: .
|
root: .
|
||||||
paths: [.]
|
paths: [.]
|
||||||
release-deb:
|
release-deb:
|
||||||
machine:
|
machine: true
|
||||||
image: ubuntu-2004:202201-02
|
|
||||||
steps:
|
steps:
|
||||||
- attach_workspace:
|
- attach_workspace:
|
||||||
at: .
|
at: .
|
||||||
@@ -266,8 +315,7 @@ jobs:
|
|||||||
- "artifacts/*.deb"
|
- "artifacts/*.deb"
|
||||||
|
|
||||||
release-rpm:
|
release-rpm:
|
||||||
machine:
|
machine: true
|
||||||
image: ubuntu-2004:202201-02
|
|
||||||
steps:
|
steps:
|
||||||
- attach_workspace:
|
- attach_workspace:
|
||||||
at: .
|
at: .
|
||||||
@@ -285,12 +333,46 @@ jobs:
|
|||||||
at: .
|
at: .
|
||||||
- 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 "me@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
|
||||||
|
|||||||
@@ -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:"
|
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ GO_BUILDFLAGS := $(GO_MOD_READONLY) $(GO_EXTRA_BUILDFLAGS)
|
|||||||
GO_BUILD := $(GO_ENV_VARS) $(GO) build $(GO_BUILDFLAGS) -ldflags $(GO_LDFLAGS)
|
GO_BUILD := $(GO_ENV_VARS) $(GO) build $(GO_BUILDFLAGS) -ldflags $(GO_LDFLAGS)
|
||||||
GOLANGCI_LINT := golangci-lint
|
GOLANGCI_LINT := golangci-lint
|
||||||
GOCOVMERGE := gocovmerge
|
GOCOVMERGE := gocovmerge
|
||||||
RELEASE_DOCKER_BASEIMAGE_TAG ?= 1.21
|
RELEASE_DOCKER_BASEIMAGE_TAG ?= 1.16
|
||||||
RELEASE_DOCKER_BASEIMAGE ?= golang:$(RELEASE_DOCKER_BASEIMAGE_TAG)
|
RELEASE_DOCKER_BASEIMAGE ?= golang:$(RELEASE_DOCKER_BASEIMAGE_TAG)
|
||||||
|
|
||||||
ifneq ($(GOARM),)
|
ifneq ($(GOARM),)
|
||||||
@@ -55,7 +55,7 @@ release: clean
|
|||||||
$(MAKE) wrapup-and-checksum
|
$(MAKE) wrapup-and-checksum
|
||||||
$(MAKE) check-git-clean
|
$(MAKE) check-git-clean
|
||||||
ifeq (SIGN, 1)
|
ifeq (SIGN, 1)
|
||||||
$(MAKE) sign
|
$(make) sign
|
||||||
endif
|
endif
|
||||||
@echo "ZREPL RELEASE ARTIFACTS AVAILABLE IN artifacts/release"
|
@echo "ZREPL RELEASE ARTIFACTS AVAILABLE IN artifacts/release"
|
||||||
|
|
||||||
@@ -134,13 +134,7 @@ endif
|
|||||||
|
|
||||||
deb-docker:
|
deb-docker:
|
||||||
docker build -t zrepl_debian_pkg --pull -f packaging/deb/Dockerfile .
|
docker build -t zrepl_debian_pkg --pull -f packaging/deb/Dockerfile .
|
||||||
# Use a small open file limit to make fakeroot work. If we don't
|
|
||||||
# specify it, docker daemon will use its file limit. I don't know
|
|
||||||
# what changed (Docker, its systemd service, its Go version). But I
|
|
||||||
# observed fakeroot iterating close(i) up to i > 1000000, which costs
|
|
||||||
# a good amount of CPU time and makes the build slow.
|
|
||||||
docker run --rm -i -v $(CURDIR):/build/src -u $$(id -u):$$(id -g) \
|
docker run --rm -i -v $(CURDIR):/build/src -u $$(id -u):$$(id -g) \
|
||||||
--ulimit nofile=1024:1024 \
|
|
||||||
zrepl_debian_pkg \
|
zrepl_debian_pkg \
|
||||||
make deb GOOS=$(GOOS) GOARCH=$(GOARCH) GOARM=$(GOARM)
|
make deb GOOS=$(GOOS) GOARCH=$(GOARCH) GOARM=$(GOARM)
|
||||||
|
|
||||||
@@ -179,10 +173,6 @@ check-git-clean:
|
|||||||
fi; \
|
fi; \
|
||||||
fi;
|
fi;
|
||||||
|
|
||||||
tag-release:
|
|
||||||
test -n "$(ZREPL_TAG_VERSION)" || exit 1
|
|
||||||
git tag -u E27CA5FC -m "$(ZREPL_TAG_VERSION)" "$(ZREPL_TAG_VERSION)"
|
|
||||||
|
|
||||||
sign:
|
sign:
|
||||||
gpg -u "89BC 5D89 C845 568B F578 B306 CDBD 8EC8 E27C A5FC" \
|
gpg -u "89BC 5D89 C845 568B F578 B306 CDBD 8EC8 E27C A5FC" \
|
||||||
--armor \
|
--armor \
|
||||||
@@ -199,8 +189,6 @@ GO_SUPPORTS_ILLUMOS := $(shell $(GO) version | gawk -F '.' '/^go version /{split
|
|||||||
bins-all:
|
bins-all:
|
||||||
$(MAKE) $(BINS_ALL_TARGETS) GOOS=freebsd GOARCH=amd64
|
$(MAKE) $(BINS_ALL_TARGETS) GOOS=freebsd GOARCH=amd64
|
||||||
$(MAKE) $(BINS_ALL_TARGETS) GOOS=freebsd GOARCH=386
|
$(MAKE) $(BINS_ALL_TARGETS) GOOS=freebsd GOARCH=386
|
||||||
$(MAKE) $(BINS_ALL_TARGETS) GOOS=freebsd GOARCH=arm GOARM=7
|
|
||||||
$(MAKE) $(BINS_ALL_TARGETS) GOOS=freebsd GOARCH=arm64
|
|
||||||
$(MAKE) $(BINS_ALL_TARGETS) GOOS=linux GOARCH=amd64
|
$(MAKE) $(BINS_ALL_TARGETS) GOOS=linux GOARCH=amd64
|
||||||
$(MAKE) $(BINS_ALL_TARGETS) GOOS=linux GOARCH=arm64
|
$(MAKE) $(BINS_ALL_TARGETS) GOOS=linux GOARCH=arm64
|
||||||
$(MAKE) $(BINS_ALL_TARGETS) GOOS=linux GOARCH=arm GOARM=7
|
$(MAKE) $(BINS_ALL_TARGETS) GOOS=linux GOARCH=arm GOARM=7
|
||||||
@@ -339,12 +327,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
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
[](https://github.com/zrepl/zrepl/blob/master/LICENSE)
|
[](https://github.com/zrepl/zrepl/blob/master/LICENSE)
|
||||||
[](https://golang.org/)
|
[](https://golang.org/)
|
||||||
[](https://zrepl.github.io)
|
[](https://zrepl.github.io)
|
||||||
[](https://patreon.com/zrepl)
|
[](https://patreon.com/zrepl)
|
||||||
[](https://github.com/sponsors/problame)
|
[](https://github.com/sponsors/problame)
|
||||||
[](https://liberapay.com/zrepl/donate)
|
[](https://liberapay.com/zrepl/donate)
|
||||||
[](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=R5QSXJVYHGX96)
|
[](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=R5QSXJVYHGX96)
|
||||||
[](https://twitter.com/intent/tweet?text=Wow:&url=https%3A%2F%2Fgithub.com%2Fzrepl%2Fzrepl)
|
[](https://twitter.com/intent/tweet?text=Wow:&url=https%3A%2F%2Fgithub.com%2Fzrepl%2Fzrepl)
|
||||||
[](https://matrix.to/#/#zrepl:matrix.org)
|
|
||||||
|
|
||||||
# zrepl
|
# zrepl
|
||||||
zrepl is a one-stop ZFS backup & replication solution.
|
zrepl is a one-stop ZFS backup & replication solution.
|
||||||
@@ -96,7 +95,7 @@ Downstream packagers can read the changelog to determine whether they want to pu
|
|||||||
|
|
||||||
### Additional Notes to Distro Package Maintainers
|
### Additional Notes to Distro Package Maintainers
|
||||||
|
|
||||||
* Run the platform tests (Docs -> Usage -> Platform Tests) **on a test system** to validate that zrepl's abstractions on top of ZFS work with the system ZFS.
|
* Use `sudo make test-platform-bin && sudo make test-platform` **on a test system** to validate that zrepl's abstractions on top of ZFS work with the system ZFS.
|
||||||
* Ship a default config that adheres to your distro's `hier` and logging system.
|
* Ship a default config that adheres to your distro's `hier` and logging system.
|
||||||
* Ship a service manager file and _please_ try to upstream it to this repository.
|
* Ship a service manager file and _please_ try to upstream it to this repository.
|
||||||
* `dist/systemd` contains a Systemd unit template.
|
* `dist/systemd` contains a Systemd unit template.
|
||||||
|
|||||||
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,3 @@
|
|||||||
//go:build tools
|
|
||||||
// +build tools
|
// +build tools
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|||||||
+3
-13
@@ -19,9 +19,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var configcheckArgs struct {
|
var configcheckArgs struct {
|
||||||
format string
|
format string
|
||||||
what string
|
what string
|
||||||
skipCertCheck bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var ConfigcheckCmd = &cli.Subcommand{
|
var ConfigcheckCmd = &cli.Subcommand{
|
||||||
@@ -30,7 +29,6 @@ var ConfigcheckCmd = &cli.Subcommand{
|
|||||||
SetupFlags: func(f *pflag.FlagSet) {
|
SetupFlags: func(f *pflag.FlagSet) {
|
||||||
f.StringVar(&configcheckArgs.format, "format", "", "dump parsed config object [pretty|yaml|json]")
|
f.StringVar(&configcheckArgs.format, "format", "", "dump parsed config object [pretty|yaml|json]")
|
||||||
f.StringVar(&configcheckArgs.what, "what", "all", "what to print [all|config|jobs|logging]")
|
f.StringVar(&configcheckArgs.what, "what", "all", "what to print [all|config|jobs|logging]")
|
||||||
f.BoolVar(&configcheckArgs.skipCertCheck, "skip-cert-check", false, "skip checking cert files")
|
|
||||||
},
|
},
|
||||||
Run: func(ctx context.Context, subcommand *cli.Subcommand, args []string) error {
|
Run: func(ctx context.Context, subcommand *cli.Subcommand, args []string) error {
|
||||||
formatMap := map[string]func(interface{}){
|
formatMap := map[string]func(interface{}){
|
||||||
@@ -58,16 +56,8 @@ var ConfigcheckCmd = &cli.Subcommand{
|
|||||||
}
|
}
|
||||||
|
|
||||||
var hadErr bool
|
var hadErr bool
|
||||||
|
|
||||||
parseFlags := config.ParseFlagsNone
|
|
||||||
|
|
||||||
if configcheckArgs.skipCertCheck {
|
|
||||||
parseFlags |= config.ParseFlagsNoCertCheck
|
|
||||||
}
|
|
||||||
|
|
||||||
// further: try to build jobs
|
// further: try to build jobs
|
||||||
confJobs, err := job.JobsFromConfig(subcommand.Config(), parseFlags)
|
confJobs, err := job.JobsFromConfig(subcommand.Config())
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err := errors.Wrap(err, "cannot build jobs from config")
|
err := errors.Wrap(err, "cannot build jobs from config")
|
||||||
if configcheckArgs.what == "jobs" {
|
if configcheckArgs.what == "jobs" {
|
||||||
|
|||||||
+1
-1
@@ -129,7 +129,7 @@ func doMigrateReplicationCursor(ctx context.Context, sc *cli.Subcommand, args []
|
|||||||
}
|
}
|
||||||
|
|
||||||
cfg := sc.Config()
|
cfg := sc.Config()
|
||||||
jobs, err := job.JobsFromConfig(cfg, config.ParseFlagsNone)
|
jobs, err := job.JobsFromConfig(cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("cannot parse config:\n%s\n\n", err)
|
fmt.Printf("cannot parse config:\n%s\n\n", err)
|
||||||
fmt.Printf("NOTE: this migration was released together with a change in job name requirements.\n")
|
fmt.Printf("NOTE: this migration was released together with a change in job name requirements.\n")
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/kr/pretty"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/spf13/pflag"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/cli"
|
||||||
|
"github.com/zrepl/zrepl/config"
|
||||||
|
"github.com/zrepl/zrepl/daemon"
|
||||||
|
"github.com/zrepl/zrepl/daemon/job"
|
||||||
|
)
|
||||||
|
|
||||||
|
var resetCmdArgs struct {
|
||||||
|
verbose bool
|
||||||
|
interval time.Duration
|
||||||
|
token string
|
||||||
|
}
|
||||||
|
|
||||||
|
var ResetCmd = &cli.Subcommand{
|
||||||
|
Use: "reset [-t TOKEN | JOB INVOCATION [replication|snapshotting|prune_sender|prune_receiver]]",
|
||||||
|
Short: "",
|
||||||
|
Run: func(ctx context.Context, subcommand *cli.Subcommand, args []string) error {
|
||||||
|
return runResetCmd(subcommand.Config(), args)
|
||||||
|
},
|
||||||
|
SetupFlags: func(f *pflag.FlagSet) {
|
||||||
|
f.BoolVarP(&resetCmdArgs.verbose, "verbose", "v", false, "verbose output")
|
||||||
|
f.DurationVarP(&resetCmdArgs.interval, "poll-interval", "i", 100*time.Millisecond, "poll interval")
|
||||||
|
f.StringVarP(&resetCmdArgs.token, "token", "t", "", "token produced by 'signal' subcommand")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func runResetCmd(config *config.Config, args []string) error {
|
||||||
|
|
||||||
|
httpc, err := controlHttpClient(config.Global.Control.SockPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var req daemon.ControlJobEndpointResetActiveRequest
|
||||||
|
if resetCmdArgs.token != "" {
|
||||||
|
var token TriggerToken
|
||||||
|
err := token.Decode(resetCmdArgs.token)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "cannot decode token")
|
||||||
|
}
|
||||||
|
req = token.ToReset()
|
||||||
|
} else {
|
||||||
|
jobName := args[0]
|
||||||
|
|
||||||
|
invocationId, err := strconv.ParseUint(args[1], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "parse invocation id")
|
||||||
|
}
|
||||||
|
|
||||||
|
// what := args[2]
|
||||||
|
|
||||||
|
// updated by subsequent requests
|
||||||
|
req = daemon.ControlJobEndpointResetActiveRequest{
|
||||||
|
Job: jobName,
|
||||||
|
ActiveSideResetRequest: job.ActiveSideResetRequest{
|
||||||
|
InvocationId: invocationId,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var res job.ActiveSideResetResponse
|
||||||
|
if resetCmdArgs.verbose {
|
||||||
|
pretty.Println("making request", req)
|
||||||
|
}
|
||||||
|
err = jsonRequestResponse(httpc, daemon.ControlJobEndpointResetActive,
|
||||||
|
req,
|
||||||
|
&res,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if resetCmdArgs.verbose {
|
||||||
|
pretty.Println("got response", res)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/cli"
|
|
||||||
"github.com/zrepl/zrepl/config"
|
|
||||||
"github.com/zrepl/zrepl/daemon"
|
|
||||||
)
|
|
||||||
|
|
||||||
var SignalCmd = &cli.Subcommand{
|
|
||||||
Use: "signal [wakeup|reset] JOB",
|
|
||||||
Short: "wake up a job from wait state or abort its current invocation",
|
|
||||||
Run: func(ctx context.Context, subcommand *cli.Subcommand, args []string) error {
|
|
||||||
return runSignalCmd(subcommand.Config(), args)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func runSignalCmd(config *config.Config, args []string) error {
|
|
||||||
if len(args) != 2 {
|
|
||||||
return errors.Errorf("Expected 2 arguments: [wakeup|reset] JOB")
|
|
||||||
}
|
|
||||||
|
|
||||||
httpc, err := controlHttpClient(config.Global.Control.SockPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = jsonRequestResponse(httpc, daemon.ControlJobEndpointSignal,
|
|
||||||
struct {
|
|
||||||
Name string
|
|
||||||
Op string
|
|
||||||
}{
|
|
||||||
Name: args[1],
|
|
||||||
Op: args[0],
|
|
||||||
},
|
|
||||||
struct{}{},
|
|
||||||
)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
@@ -11,14 +11,15 @@ import (
|
|||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/daemon"
|
"github.com/zrepl/zrepl/daemon"
|
||||||
|
"github.com/zrepl/zrepl/daemon/job"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Client struct {
|
type Client struct {
|
||||||
h *http.Client
|
h http.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(network, addr string) (*Client, error) {
|
func New(network, addr string) (*Client, error) {
|
||||||
httpc, err := makeControlHttpClient(func(_ context.Context) (net.Conn, error) { return net.Dial(network, addr) })
|
httpc, err := controlHttpClient(func(_ context.Context) (net.Conn, error) { return net.Dial(network, addr) })
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -42,29 +43,35 @@ func (c *Client) StatusRaw() ([]byte, error) {
|
|||||||
return r, nil
|
return r, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) signal(job, sig string) error {
|
func (c *Client) signal(jobName, sig string) error {
|
||||||
return jsonRequestResponse(c.h, daemon.ControlJobEndpointSignal,
|
return jsonRequestResponse(c.h, daemon.ControlJobEndpointTriggerActive,
|
||||||
struct {
|
struct {
|
||||||
Name string
|
Job string
|
||||||
Op string
|
job.ActiveSideTriggerRequest
|
||||||
}{
|
}{
|
||||||
Name: job,
|
Job: jobName,
|
||||||
Op: sig,
|
ActiveSideTriggerRequest: job.ActiveSideTriggerRequest{
|
||||||
|
What: sig,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
struct{}{},
|
struct{}{},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) SignalWakeup(job string) error {
|
func (c *Client) SignalReplication(job string) error {
|
||||||
return c.signal(job, "wakeup")
|
return c.signal(job, "replication")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) SignalSnapshot(job string) error {
|
||||||
|
return c.signal(job, "snapshot")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) SignalReset(job string) error {
|
func (c *Client) SignalReset(job string) error {
|
||||||
return c.signal(job, "reset")
|
return c.signal(job, "reset")
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeControlHttpClient(dialfunc func(context.Context) (net.Conn, error)) (client *http.Client, err error) {
|
func controlHttpClient(dialfunc func(context.Context) (net.Conn, error)) (client http.Client, err error) {
|
||||||
return &http.Client{
|
return http.Client{
|
||||||
Transport: &http.Transport{
|
Transport: &http.Transport{
|
||||||
DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
|
DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
|
||||||
return dialfunc(ctx)
|
return dialfunc(ctx)
|
||||||
@@ -73,24 +80,14 @@ func makeControlHttpClient(dialfunc func(context.Context) (net.Conn, error)) (cl
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func jsonRequestResponse(c *http.Client, endpoint string, req interface{}, res interface{}) error {
|
func jsonRequestResponse(c http.Client, endpoint string, req interface{}, res interface{}) error {
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
encodeErr := json.NewEncoder(&buf).Encode(req)
|
encodeErr := json.NewEncoder(&buf).Encode(req)
|
||||||
if encodeErr != nil {
|
if encodeErr != nil {
|
||||||
return encodeErr
|
return encodeErr
|
||||||
}
|
}
|
||||||
|
|
||||||
hreq, err := http.NewRequest("POST", "http://unix"+endpoint, &buf)
|
resp, err := c.Post("http://unix"+endpoint, "application/json", &buf)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
hreq.Header.Set("Content-Type", "application/json")
|
|
||||||
// Prevent EOF errors when client request frequency and server keepalive close are at the same time.
|
|
||||||
// Found this by watching http.Server.ConnState changes, then found
|
|
||||||
// https://stackoverflow.com/questions/17714494/golang-http-request-results-in-eof-errors-when-making-multiple-requests-successi
|
|
||||||
// Note: The issue seems even more prounounced with local TCP sockets than unix domain sockets. So, I used that for debugging.
|
|
||||||
hreq.Close = true
|
|
||||||
resp, err := c.Do(hreq)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ import (
|
|||||||
type Client interface {
|
type Client interface {
|
||||||
Status() (daemon.Status, error)
|
Status() (daemon.Status, error)
|
||||||
StatusRaw() ([]byte, error)
|
StatusRaw() ([]byte, error)
|
||||||
SignalWakeup(job string) error
|
SignalReplication(job string) error
|
||||||
|
SignalSnapshot(job string) error
|
||||||
SignalReset(job string) error
|
SignalReset(job string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,16 +71,12 @@ func runStatusV2Command(ctx context.Context, config *config.Config, args []strin
|
|||||||
|
|
||||||
mode := statusv2Flags.Mode.Value().(statusv2Mode)
|
mode := statusv2Flags.Mode.Value().(statusv2Mode)
|
||||||
|
|
||||||
if !isatty.IsTerminal(os.Stdout.Fd()) && mode != StatusV2ModeDump && mode != StatusV2ModeRaw {
|
if !isatty.IsTerminal(os.Stdout.Fd()) && mode != StatusV2ModeDump {
|
||||||
dumpmode, err := statusv2Flags.Mode.InputForChoice(StatusV2ModeDump)
|
usemode, err := statusv2Flags.Mode.InputForChoice(StatusV2ModeDump)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
rawmode, err := statusv2Flags.Mode.InputForChoice(StatusV2ModeRaw)
|
return errors.Errorf("error: stdout is not a tty, please use --mode %s", usemode)
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return errors.Errorf("error: stdout is not a tty, please use --mode %s or --mode %s", dumpmode, rawmode)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
switch mode {
|
switch mode {
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -281,8 +281,8 @@ func interactive(c Client, flag statusFlags) error {
|
|||||||
if !ok {
|
if !ok {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
signals := []string{"wakeup", "reset"}
|
signals := []string{"replication", "snapshot", "reset"}
|
||||||
clientFuncs := []func(job string) error{c.SignalWakeup, c.SignalReset}
|
clientFuncs := []func(job string) error{c.SignalReplication, c.SignalSnapshot, c.SignalReset}
|
||||||
sigMod := tview.NewModal()
|
sigMod := tview.NewModal()
|
||||||
sigMod.SetBackgroundColor(tcell.ColorDefault)
|
sigMod.SetBackgroundColor(tcell.ColorDefault)
|
||||||
sigMod.SetBorder(true)
|
sigMod.SetBorder(true)
|
||||||
|
|||||||
@@ -2,22 +2,14 @@ package viewmodel
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func ByteCountBinaryUint(b uint64) string {
|
|
||||||
if b > math.MaxInt64 {
|
|
||||||
panic(b)
|
|
||||||
}
|
|
||||||
return ByteCountBinary(int64(b))
|
|
||||||
}
|
|
||||||
|
|
||||||
func ByteCountBinary(b int64) string {
|
func ByteCountBinary(b int64) string {
|
||||||
const unit = 1024
|
const unit = 1024
|
||||||
if b < unit {
|
if b < unit {
|
||||||
return fmt.Sprintf("%d B", b)
|
return fmt.Sprintf("%d B", b)
|
||||||
}
|
}
|
||||||
div, exp := unit, 0
|
div, exp := int64(unit), 0
|
||||||
for n := b / unit; n >= unit; n /= unit {
|
for n := b / unit; n >= unit; n /= unit {
|
||||||
div *= unit
|
div *= unit
|
||||||
exp++
|
exp++
|
||||||
|
|||||||
@@ -4,16 +4,17 @@ import "time"
|
|||||||
|
|
||||||
type byteProgressMeasurement struct {
|
type byteProgressMeasurement struct {
|
||||||
time time.Time
|
time time.Time
|
||||||
val uint64
|
val int64
|
||||||
}
|
}
|
||||||
|
|
||||||
type bytesProgressHistory struct {
|
type bytesProgressHistory struct {
|
||||||
last *byteProgressMeasurement // pointer as poor man's optional
|
last *byteProgressMeasurement // pointer as poor man's optional
|
||||||
changeCount int
|
changeCount int
|
||||||
lastChange time.Time
|
lastChange time.Time
|
||||||
|
bpsAvg float64
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *bytesProgressHistory) Update(currentVal uint64) (bytesPerSecondAvg int64, changeCount int) {
|
func (p *bytesProgressHistory) Update(currentVal int64) (bytesPerSecondAvg int64, changeCount int) {
|
||||||
|
|
||||||
if p.last == nil {
|
if p.last == nil {
|
||||||
p.last = &byteProgressMeasurement{
|
p.last = &byteProgressMeasurement{
|
||||||
@@ -33,17 +34,15 @@ func (p *bytesProgressHistory) Update(currentVal uint64) (bytesPerSecondAvg int6
|
|||||||
return 0, 0
|
return 0, 0
|
||||||
}
|
}
|
||||||
|
|
||||||
var deltaV int64
|
deltaV := currentVal - p.last.val
|
||||||
if currentVal >= p.last.val {
|
|
||||||
deltaV = int64(currentVal - p.last.val)
|
|
||||||
} else {
|
|
||||||
deltaV = -int64(p.last.val - currentVal)
|
|
||||||
}
|
|
||||||
deltaT := time.Since(p.last.time)
|
deltaT := time.Since(p.last.time)
|
||||||
rate := float64(deltaV) / deltaT.Seconds()
|
rate := float64(deltaV) / deltaT.Seconds()
|
||||||
|
|
||||||
|
factor := 0.3
|
||||||
|
p.bpsAvg = (1-factor)*p.bpsAvg + factor*rate
|
||||||
|
|
||||||
p.last.time = time.Now()
|
p.last.time = time.Now()
|
||||||
p.last.val = currentVal
|
p.last.val = currentVal
|
||||||
|
|
||||||
return int64(rate), p.changeCount
|
return int64(p.bpsAvg), p.changeCount
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -216,7 +216,7 @@ func drawJob(t *stringbuilder.B, name string, v *job.Status, history *bytesProgr
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func printFilesystemStatus(t *stringbuilder.B, rep *report.FilesystemReport, maxFS int) {
|
func printFilesystemStatus(t *stringbuilder.B, rep *report.FilesystemReport, active bool, maxFS int) {
|
||||||
|
|
||||||
expected, replicated, containsInvalidSizeEstimates := rep.BytesSum()
|
expected, replicated, containsInvalidSizeEstimates := rep.BytesSum()
|
||||||
sizeEstimationImpreciseNotice := ""
|
sizeEstimationImpreciseNotice := ""
|
||||||
@@ -227,28 +227,15 @@ func printFilesystemStatus(t *stringbuilder.B, rep *report.FilesystemReport, max
|
|||||||
sizeEstimationImpreciseNotice = " (step lacks size estimation)"
|
sizeEstimationImpreciseNotice = " (step lacks size estimation)"
|
||||||
}
|
}
|
||||||
|
|
||||||
userVisisbleCurrentStep, userVisibleTotalSteps := rep.CurrentStep, len(rep.Steps)
|
|
||||||
// `.CurrentStep` is == len(rep.Steps) if all steps are done.
|
|
||||||
// Until then, it's an index into .Steps that starts at 0.
|
|
||||||
// For the user, we want it to start at 1.
|
|
||||||
if rep.CurrentStep >= len(rep.Steps) {
|
|
||||||
// rep.CurrentStep is what we want to show.
|
|
||||||
// We check for >= and not == for robustness.
|
|
||||||
} else {
|
|
||||||
// We're not done yet, so, make step count start at 1
|
|
||||||
// (The `.State` is included in the output, indicating we're not done yet)
|
|
||||||
userVisisbleCurrentStep = rep.CurrentStep + 1
|
|
||||||
}
|
|
||||||
status := fmt.Sprintf("%s (step %d/%d, %s/%s)%s",
|
status := fmt.Sprintf("%s (step %d/%d, %s/%s)%s",
|
||||||
strings.ToUpper(string(rep.State)),
|
strings.ToUpper(string(rep.State)),
|
||||||
userVisisbleCurrentStep, userVisibleTotalSteps,
|
rep.CurrentStep, len(rep.Steps),
|
||||||
ByteCountBinaryUint(replicated), ByteCountBinaryUint(expected),
|
ByteCountBinary(replicated), ByteCountBinary(expected),
|
||||||
sizeEstimationImpreciseNotice,
|
sizeEstimationImpreciseNotice,
|
||||||
)
|
)
|
||||||
|
|
||||||
activeIndicator := " "
|
activeIndicator := " "
|
||||||
if rep.BlockedOn == report.FsBlockedOnNothing &&
|
if active {
|
||||||
(rep.State == report.FilesystemPlanning || rep.State == report.FilesystemStepping) {
|
|
||||||
activeIndicator = "*"
|
activeIndicator = "*"
|
||||||
}
|
}
|
||||||
t.AddIndent(1)
|
t.AddIndent(1)
|
||||||
@@ -273,9 +260,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
|
||||||
}
|
}
|
||||||
@@ -369,20 +356,10 @@ func renderReplicationReport(t *stringbuilder.B, rep *report.Report, history *by
|
|||||||
// Progress: [---------------]
|
// Progress: [---------------]
|
||||||
expected, replicated, containsInvalidSizeEstimates := latest.BytesSum()
|
expected, replicated, containsInvalidSizeEstimates := latest.BytesSum()
|
||||||
rate, changeCount := history.Update(replicated)
|
rate, changeCount := history.Update(replicated)
|
||||||
eta := time.Duration(0)
|
t.Write("Progress: ")
|
||||||
if rate > 0 {
|
t.DrawBar(50, replicated, expected, changeCount)
|
||||||
eta = time.Duration((float64(expected)-float64(replicated))/float64(rate)) * time.Second
|
t.Write(fmt.Sprintf(" %s / %s @ %s/s", ByteCountBinary(replicated), ByteCountBinary(expected), ByteCountBinary(rate)))
|
||||||
}
|
t.Newline()
|
||||||
|
|
||||||
if !latest.State.IsTerminal() {
|
|
||||||
t.Write("Progress: ")
|
|
||||||
t.DrawBar(50, replicated, expected, changeCount)
|
|
||||||
t.Write(fmt.Sprintf(" %s / %s @ %s/s", ByteCountBinaryUint(replicated), ByteCountBinaryUint(expected), ByteCountBinary(rate)))
|
|
||||||
if eta != 0 {
|
|
||||||
t.Write(fmt.Sprintf(" (%s remaining)", humanizeDuration(eta)))
|
|
||||||
}
|
|
||||||
t.Newline()
|
|
||||||
}
|
|
||||||
if containsInvalidSizeEstimates {
|
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()
|
||||||
@@ -400,36 +377,12 @@ func renderReplicationReport(t *stringbuilder.B, rep *report.Report, history *by
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, fs := range latest.Filesystems {
|
for _, fs := range latest.Filesystems {
|
||||||
printFilesystemStatus(t, fs, maxFSLen)
|
printFilesystemStatus(t, fs, false, maxFSLen) // FIXME bring 'active' flag back
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func humanizeDuration(duration time.Duration) string {
|
|
||||||
days := int64(duration.Hours() / 24)
|
|
||||||
hours := int64(math.Mod(duration.Hours(), 24))
|
|
||||||
minutes := int64(math.Mod(duration.Minutes(), 60))
|
|
||||||
seconds := int64(math.Mod(duration.Seconds(), 60))
|
|
||||||
|
|
||||||
var parts []string
|
|
||||||
|
|
||||||
force := false
|
|
||||||
chunks := []int64{days, hours, minutes, seconds}
|
|
||||||
for i, chunk := range chunks {
|
|
||||||
if force || chunk > 0 {
|
|
||||||
padding := 0
|
|
||||||
if force {
|
|
||||||
padding = 2
|
|
||||||
}
|
|
||||||
parts = append(parts, fmt.Sprintf("%*d%c", padding, chunk, "dhms"[i]))
|
|
||||||
force = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return strings.Join(parts, " ")
|
|
||||||
}
|
|
||||||
|
|
||||||
func renderPrunerReport(t *stringbuilder.B, r *pruner.Report, fsfilter FilterFunc) {
|
func renderPrunerReport(t *stringbuilder.B, r *pruner.Report, fsfilter FilterFunc) {
|
||||||
if r == nil {
|
if r == nil {
|
||||||
t.Printf("...\n")
|
t.Printf("...\n")
|
||||||
@@ -495,17 +448,15 @@ 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("[")
|
t.Write(stringbuilder.Times("=", progress))
|
||||||
t.Write(stringbuilder.Times("=", progress))
|
t.Write(">")
|
||||||
t.Write(">")
|
t.Write(stringbuilder.Times("-", 80-progress))
|
||||||
t.Write(stringbuilder.Times("-", 80-progress))
|
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 +502,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 +516,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 +530,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 +574,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()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,11 +99,11 @@ func RightPad(str string, length int, pad string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// changeCount = 0 indicates stall / no progress
|
// changeCount = 0 indicates stall / no progress
|
||||||
func (w *B) DrawBar(length int, bytes, totalBytes uint64, changeCount int) {
|
func (w *B) DrawBar(length int, bytes, totalBytes int64, changeCount int) {
|
||||||
const arrowPositions = `>\|/`
|
const arrowPositions = `>\|/`
|
||||||
var completedLength int
|
var completedLength int
|
||||||
if totalBytes > 0 {
|
if totalBytes > 0 {
|
||||||
completedLength = int(uint64(length) * bytes / totalBytes)
|
completedLength = int(int64(length) * bytes / totalBytes)
|
||||||
if completedLength > length {
|
if completedLength > length {
|
||||||
completedLength = length
|
completedLength = length
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-9
@@ -139,11 +139,9 @@ var testPlaceholder = &cli.Subcommand{
|
|||||||
func runTestPlaceholder(ctx context.Context, subcommand *cli.Subcommand, args []string) error {
|
func runTestPlaceholder(ctx context.Context, subcommand *cli.Subcommand, args []string) error {
|
||||||
|
|
||||||
var checkDPs []*zfs.DatasetPath
|
var checkDPs []*zfs.DatasetPath
|
||||||
var datasetWasExplicitArgument bool
|
|
||||||
|
|
||||||
// all actions first
|
// all actions first
|
||||||
if testPlaceholderArgs.all {
|
if testPlaceholderArgs.all {
|
||||||
datasetWasExplicitArgument = false
|
|
||||||
out, err := zfs.ZFSList(ctx, []string{"name"})
|
out, err := zfs.ZFSList(ctx, []string{"name"})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "could not list ZFS filesystems")
|
return errors.Wrap(err, "could not list ZFS filesystems")
|
||||||
@@ -156,7 +154,6 @@ func runTestPlaceholder(ctx context.Context, subcommand *cli.Subcommand, args []
|
|||||||
checkDPs = append(checkDPs, dp)
|
checkDPs = append(checkDPs, dp)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
datasetWasExplicitArgument = true
|
|
||||||
dp, err := zfs.NewDatasetPath(testPlaceholderArgs.ds)
|
dp, err := zfs.NewDatasetPath(testPlaceholderArgs.ds)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -174,12 +171,7 @@ func runTestPlaceholder(ctx context.Context, subcommand *cli.Subcommand, args []
|
|||||||
return errors.Wrap(err, "cannot get placeholder state")
|
return errors.Wrap(err, "cannot get placeholder state")
|
||||||
}
|
}
|
||||||
if !ph.FSExists {
|
if !ph.FSExists {
|
||||||
if datasetWasExplicitArgument {
|
panic("placeholder state inconsistent: filesystem " + ph.FS + " must exist in this context")
|
||||||
return errors.Errorf("filesystem %q does not exist", ph.FS)
|
|
||||||
} else {
|
|
||||||
// got deleted between ZFSList and ZFSGetFilesystemPlaceholderState
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
is := "yes"
|
is := "yes"
|
||||||
if !ph.IsPlaceholder {
|
if !ph.IsPlaceholder {
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/cli"
|
||||||
|
"github.com/zrepl/zrepl/config"
|
||||||
|
"github.com/zrepl/zrepl/daemon"
|
||||||
|
"github.com/zrepl/zrepl/daemon/job"
|
||||||
|
)
|
||||||
|
|
||||||
|
var TriggerCmd = &cli.Subcommand{
|
||||||
|
Use: "trigger JOB [replication|snapshot]",
|
||||||
|
Short: "",
|
||||||
|
Run: func(ctx context.Context, subcommand *cli.Subcommand, args []string) error {
|
||||||
|
return runTriggerCmd(subcommand.Config(), args)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
type TriggerToken struct {
|
||||||
|
// TODO version, daemon invocation id, etc.
|
||||||
|
Job string
|
||||||
|
InvocationId uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t TriggerToken) Encode() string {
|
||||||
|
j, err := json.Marshal(t)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return string(j)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TriggerToken) Decode(s string) error {
|
||||||
|
return json.Unmarshal([]byte(s), t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t TriggerToken) ToReset() daemon.ControlJobEndpointResetActiveRequest {
|
||||||
|
return daemon.ControlJobEndpointResetActiveRequest{
|
||||||
|
Job: t.Job,
|
||||||
|
ActiveSideResetRequest: job.ActiveSideResetRequest{
|
||||||
|
InvocationId: t.InvocationId,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t TriggerToken) ToWait() daemon.ControlJobEndpointWaitActiveRequest {
|
||||||
|
return daemon.ControlJobEndpointWaitActiveRequest{
|
||||||
|
Job: t.Job,
|
||||||
|
ActiveSidePollRequest: job.ActiveSidePollRequest{
|
||||||
|
InvocationId: t.InvocationId,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runTriggerCmd(config *config.Config, args []string) error {
|
||||||
|
if len(args) != 2 {
|
||||||
|
return errors.Errorf("Expected 2 arguments: [replication|reset|snapshot] JOB")
|
||||||
|
}
|
||||||
|
|
||||||
|
httpc, err := controlHttpClient(config.Global.Control.SockPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
jobName := args[0]
|
||||||
|
what := args[1]
|
||||||
|
|
||||||
|
var res job.ActiveSideSignalResponse
|
||||||
|
err = jsonRequestResponse(httpc, daemon.ControlJobEndpointTriggerActive,
|
||||||
|
struct {
|
||||||
|
Job string
|
||||||
|
job.ActiveSideTriggerRequest
|
||||||
|
}{
|
||||||
|
Job: jobName,
|
||||||
|
ActiveSideTriggerRequest: job.ActiveSideTriggerRequest{
|
||||||
|
What: what,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&res,
|
||||||
|
)
|
||||||
|
|
||||||
|
token := TriggerToken{
|
||||||
|
Job: jobName,
|
||||||
|
InvocationId: res.InvocationId,
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(token.Encode())
|
||||||
|
return err
|
||||||
|
}
|
||||||
+110
@@ -0,0 +1,110 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/kr/pretty"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/spf13/pflag"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/cli"
|
||||||
|
"github.com/zrepl/zrepl/config"
|
||||||
|
"github.com/zrepl/zrepl/daemon"
|
||||||
|
"github.com/zrepl/zrepl/daemon/job"
|
||||||
|
)
|
||||||
|
|
||||||
|
var waitCmdArgs struct {
|
||||||
|
verbose bool
|
||||||
|
interval time.Duration
|
||||||
|
token string
|
||||||
|
}
|
||||||
|
|
||||||
|
var WaitCmd = &cli.Subcommand{
|
||||||
|
Use: "wait [-t TOKEN | JOB INVOCATION [replication|snapshotting|prune_sender|prune_receiver]]",
|
||||||
|
Short: "",
|
||||||
|
Run: func(ctx context.Context, subcommand *cli.Subcommand, args []string) error {
|
||||||
|
return runWaitCmd(subcommand.Config(), args)
|
||||||
|
},
|
||||||
|
SetupFlags: func(f *pflag.FlagSet) {
|
||||||
|
f.BoolVarP(&waitCmdArgs.verbose, "verbose", "v", false, "verbose output")
|
||||||
|
f.DurationVarP(&waitCmdArgs.interval, "poll-interval", "i", 100*time.Millisecond, "poll interval")
|
||||||
|
f.StringVarP(&waitCmdArgs.token, "token", "t", "", "token produced by 'signal' subcommand")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func runWaitCmd(config *config.Config, args []string) error {
|
||||||
|
|
||||||
|
httpc, err := controlHttpClient(config.Global.Control.SockPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var req daemon.ControlJobEndpointWaitActiveRequest
|
||||||
|
if waitCmdArgs.token != "" {
|
||||||
|
var token TriggerToken
|
||||||
|
err := token.Decode(resetCmdArgs.token)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "cannot decode token")
|
||||||
|
}
|
||||||
|
req = token.ToWait()
|
||||||
|
} else {
|
||||||
|
|
||||||
|
jobName := args[0]
|
||||||
|
|
||||||
|
invocationId, err := strconv.ParseUint(args[1], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "parse invocation id")
|
||||||
|
}
|
||||||
|
|
||||||
|
// updated by subsequent requests
|
||||||
|
req = daemon.ControlJobEndpointWaitActiveRequest{
|
||||||
|
Job: jobName,
|
||||||
|
ActiveSidePollRequest: job.ActiveSidePollRequest{
|
||||||
|
InvocationId: invocationId,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
doneErr := fmt.Errorf("done")
|
||||||
|
|
||||||
|
pollOnce := func() error {
|
||||||
|
var res job.ActiveSidePollResponse
|
||||||
|
if waitCmdArgs.verbose {
|
||||||
|
pretty.Println("making poll request", req)
|
||||||
|
}
|
||||||
|
err = jsonRequestResponse(httpc, daemon.ControlJobEndpointPollActive,
|
||||||
|
req,
|
||||||
|
&res,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if waitCmdArgs.verbose {
|
||||||
|
pretty.Println("got poll response", res)
|
||||||
|
}
|
||||||
|
|
||||||
|
if res.Done {
|
||||||
|
return doneErr
|
||||||
|
}
|
||||||
|
|
||||||
|
req.InvocationId = res.InvocationId
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
t := time.NewTicker(waitCmdArgs.interval)
|
||||||
|
for range t.C {
|
||||||
|
err := pollOnce()
|
||||||
|
if err == doneErr {
|
||||||
|
return nil
|
||||||
|
} else if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
@@ -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))
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
+72
-80
@@ -6,23 +6,16 @@ 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"
|
|
||||||
zfsprop "github.com/zrepl/zrepl/zfs/property"
|
zfsprop "github.com/zrepl/zrepl/zfs/property"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ParseFlags uint
|
|
||||||
|
|
||||||
const (
|
|
||||||
ParseFlagsNone ParseFlags = 0
|
|
||||||
ParseFlagsNoCertCheck ParseFlags = 1 << iota
|
|
||||||
)
|
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Jobs []JobEnum `yaml:"jobs"`
|
Jobs []JobEnum `yaml:"jobs"`
|
||||||
Global *Global `yaml:"global,optional,fromdefaults"`
|
Global *Global `yaml:"global,optional,fromdefaults"`
|
||||||
@@ -61,28 +54,26 @@ func (j JobEnum) Name() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ActiveJob struct {
|
type ActiveJob struct {
|
||||||
Type string `yaml:"type"`
|
Type string `yaml:"type"`
|
||||||
Name string `yaml:"name"`
|
Name string `yaml:"name"`
|
||||||
Connect ConnectEnum `yaml:"connect"`
|
Connect ConnectEnum `yaml:"connect"`
|
||||||
Pruning PruningSenderReceiver `yaml:"pruning"`
|
Pruning PruningSenderReceiver `yaml:"pruning"`
|
||||||
Replication *Replication `yaml:"replication,optional,fromdefaults"`
|
Debug JobDebugSettings `yaml:"debug,optional"`
|
||||||
ConflictResolution *ConflictResolution `yaml:"conflict_resolution,optional,fromdefaults"`
|
Replication *Replication `yaml:"replication,optional,fromdefaults"`
|
||||||
}
|
|
||||||
|
|
||||||
type ConflictResolution struct {
|
|
||||||
InitialReplication string `yaml:"initial_replication,optional,default=most_recent"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type PassiveJob struct {
|
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"`
|
||||||
}
|
}
|
||||||
@@ -94,10 +85,8 @@ type SendOptions struct {
|
|||||||
BackupProperties bool `yaml:"backup_properties,optional,default=false"`
|
BackupProperties bool `yaml:"backup_properties,optional,default=false"`
|
||||||
LargeBlocks bool `yaml:"large_blocks,optional,default=false"`
|
LargeBlocks bool `yaml:"large_blocks,optional,default=false"`
|
||||||
Compressed bool `yaml:"compressed,optional,default=false"`
|
Compressed bool `yaml:"compressed,optional,default=false"`
|
||||||
EmbeddedData bool `yaml:"embedded_data,optional,default=false"`
|
EmbeddedData bool `yaml:"embbeded_data,optional,default=false"`
|
||||||
Saved bool `yaml:"saved,optional,default=false"`
|
Saved bool `yaml:"saved,optional,default=false"`
|
||||||
|
|
||||||
BandwidthLimit *BandwidthLimit `yaml:"bandwidth_limit,optional,fromdefaults"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type RecvOptions struct {
|
type RecvOptions struct {
|
||||||
@@ -107,17 +96,6 @@ type RecvOptions struct {
|
|||||||
// Reencrypt bool `yaml:"reencrypt"`
|
// Reencrypt bool `yaml:"reencrypt"`
|
||||||
|
|
||||||
Properties *PropertyRecvOptions `yaml:"properties,fromdefaults"`
|
Properties *PropertyRecvOptions `yaml:"properties,fromdefaults"`
|
||||||
|
|
||||||
BandwidthLimit *BandwidthLimit `yaml:"bandwidth_limit,optional,fromdefaults"`
|
|
||||||
|
|
||||||
Placeholder *PlaceholderRecvOptions `yaml:"placeholder,fromdefaults"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ yaml.Unmarshaler = &datasizeunit.Bits{}
|
|
||||||
|
|
||||||
type BandwidthLimit struct {
|
|
||||||
Max datasizeunit.Bits `yaml:"max,default=-1 B"`
|
|
||||||
BucketCapacity datasizeunit.Bits `yaml:"bucket_capacity,default=128 KiB"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Replication struct {
|
type Replication struct {
|
||||||
@@ -135,15 +113,15 @@ type ReplicationOptionsConcurrency struct {
|
|||||||
SizeEstimates int `yaml:"size_estimates,optional,default=4"`
|
SizeEstimates int `yaml:"size_estimates,optional,default=4"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (l *RecvOptions) SetDefault() {
|
||||||
|
*l = RecvOptions{Properties: &PropertyRecvOptions{}}
|
||||||
|
}
|
||||||
|
|
||||||
type PropertyRecvOptions struct {
|
type PropertyRecvOptions struct {
|
||||||
Inherit []zfsprop.Property `yaml:"inherit,optional"`
|
Inherit []zfsprop.Property `yaml:"inherit,optional"`
|
||||||
Override map[zfsprop.Property]string `yaml:"override,optional"`
|
Override map[zfsprop.Property]string `yaml:"override,optional"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PlaceholderRecvOptions struct {
|
|
||||||
Encryption string `yaml:"encryption,default=unspecified"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PushJob struct {
|
type PushJob struct {
|
||||||
ActiveJob `yaml:",inline"`
|
ActiveJob `yaml:",inline"`
|
||||||
Snapshotting SnapshottingEnum `yaml:"snapshotting"`
|
Snapshotting SnapshottingEnum `yaml:"snapshotting"`
|
||||||
@@ -185,10 +163,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
|
||||||
}
|
}
|
||||||
@@ -220,44 +201,10 @@ type SnapshottingEnum struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type SnapshottingPeriodic struct {
|
type SnapshottingPeriodic struct {
|
||||||
Type string `yaml:"type"`
|
Type string `yaml:"type"`
|
||||||
Prefix string `yaml:"prefix"`
|
Prefix string `yaml:"prefix"`
|
||||||
Interval *PositiveDuration `yaml:"interval"`
|
Interval time.Duration `yaml:"interval,positive"`
|
||||||
Hooks HookList `yaml:"hooks,optional"`
|
Hooks HookList `yaml:"hooks,optional"`
|
||||||
TimestampFormat string `yaml:"timestamp_format,optional,default=dense"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type CronSpec struct {
|
|
||||||
Schedule cron.Schedule
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ yaml.Unmarshaler = &CronSpec{}
|
|
||||||
|
|
||||||
func (s *CronSpec) UnmarshalYAML(unmarshal func(v interface{}, not_strict bool) error) error {
|
|
||||||
var specString string
|
|
||||||
if err := unmarshal(&specString, false); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use standard cron format.
|
|
||||||
// Disable the various "descriptors" (@daily, etc)
|
|
||||||
// They are just aliases to "top of hour", "midnight", etc.
|
|
||||||
parser := cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.SecondOptional)
|
|
||||||
|
|
||||||
sched, err := parser.Parse(specString)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "cron syntax invalid")
|
|
||||||
}
|
|
||||||
s.Schedule = sched
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type SnapshottingCron struct {
|
|
||||||
Type string `yaml:"type"`
|
|
||||||
Prefix string `yaml:"prefix"`
|
|
||||||
Cron CronSpec `yaml:"cron"`
|
|
||||||
Hooks HookList `yaml:"hooks,optional"`
|
|
||||||
TimestampFormat string `yaml:"timestamp_format,optional,default=dense"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type SnapshottingManual struct {
|
type SnapshottingManual struct {
|
||||||
@@ -477,6 +424,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 {
|
||||||
@@ -575,7 +530,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
|
||||||
}
|
}
|
||||||
@@ -701,3 +655,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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -70,9 +70,9 @@ func TestPrometheusMonitoring(t *testing.T) {
|
|||||||
global:
|
global:
|
||||||
monitoring:
|
monitoring:
|
||||||
- type: prometheus
|
- type: prometheus
|
||||||
listen: ':9811'
|
listen: ':9091'
|
||||||
`)
|
`)
|
||||||
assert.Equal(t, ":9811", conf.Global.Monitoring[0].Ret.(*PrometheusMonitoring).Listen)
|
assert.Equal(t, ":9091", conf.Global.Monitoring[0].Ret.(*PrometheusMonitoring).Listen)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSyslogLoggingOutletFacility(t *testing.T) {
|
func TestSyslogLoggingOutletFacility(t *testing.T) {
|
||||||
|
|||||||
@@ -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
@@ -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)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
|
|
||||||
jobs:
|
|
||||||
- type: sink
|
|
||||||
name: "limited_sink"
|
|
||||||
root_fs: "fs0"
|
|
||||||
recv:
|
|
||||||
bandwidth_limit:
|
|
||||||
max: 12345 B
|
|
||||||
serve:
|
|
||||||
type: local
|
|
||||||
listener_name: localsink
|
|
||||||
|
|
||||||
- type: push
|
|
||||||
name: "limited_push"
|
|
||||||
connect:
|
|
||||||
type: local
|
|
||||||
listener_name: localsink
|
|
||||||
client_identity: local_backup
|
|
||||||
filesystems: {
|
|
||||||
"root<": true,
|
|
||||||
}
|
|
||||||
send:
|
|
||||||
bandwidth_limit:
|
|
||||||
max: 54321 B
|
|
||||||
bucket_capacity: 1024 B
|
|
||||||
snapshotting:
|
|
||||||
type: manual
|
|
||||||
pruning:
|
|
||||||
keep_sender:
|
|
||||||
- type: last_n
|
|
||||||
count: 1
|
|
||||||
keep_receiver:
|
|
||||||
- type: last_n
|
|
||||||
count: 1
|
|
||||||
|
|
||||||
- type: sink
|
|
||||||
name: "nolimit_sink"
|
|
||||||
root_fs: "fs1"
|
|
||||||
serve:
|
|
||||||
type: local
|
|
||||||
listener_name: localsink
|
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
# quick start section which inlines this example.
|
# quick start section which inlines this example.
|
||||||
#
|
#
|
||||||
# CUSTOMIZATIONS YOU WILL LIKELY WANT TO APPLY:
|
# CUSTOMIZATIONS YOU WILL LIKELY WANT TO APPLY:
|
||||||
# - adjust the name of the production pool `system` in the `filesystems` filter of jobs `snapjob` and `push_to_drive`
|
# - adjust the name of the production pool `system` in the `filesystems` filter of jobs `snapjob` and `push_to_derive`
|
||||||
# - adjust the name of the backup pool `backuppool` in the `backuppool_sink` job
|
# - adjust the name of the backup pool `backuppool` in the `backuppool_sink` job
|
||||||
# - adjust the occurences of `myhostname` to the name of the system you are backing up (cannot be easily changed once you start replicating)
|
# - adjust the occurences of `myhostname` to the name of the system you are backing up (cannot be easily changed once you start replicating)
|
||||||
# - make sure the `zrepl_` prefix is not being used by any other zfs tools you might have installed (it likely isn't)
|
# - make sure the `zrepl_` prefix is not being used by any other zfs tools you might have installed (it likely isn't)
|
||||||
@@ -40,7 +40,7 @@ jobs:
|
|||||||
|
|
||||||
# This job pushes to the local sink defined in job `backuppool_sink`.
|
# This job pushes to the local sink defined in job `backuppool_sink`.
|
||||||
# We trigger replication manually from the command line / udev rules using
|
# We trigger replication manually from the command line / udev rules using
|
||||||
# `zrepl signal wakeup push_to_drive`
|
# `zrepl signal replication push_to_drive`
|
||||||
- type: push
|
- type: push
|
||||||
name: push_to_drive
|
name: push_to_drive
|
||||||
connect:
|
connect:
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ jobs:
|
|||||||
key: /etc/zrepl/prod.key
|
key: /etc/zrepl/prod.key
|
||||||
server_cn: "backups"
|
server_cn: "backups"
|
||||||
filesystems: {
|
filesystems: {
|
||||||
"zroot<": true,
|
"zroot/var/db": true,
|
||||||
"zroot/var/tmp<": false,
|
"zroot/usr/home<": true,
|
||||||
"zroot/usr/home/paranoid": false
|
"zroot/usr/home/paranoid": false
|
||||||
}
|
}
|
||||||
snapshotting:
|
snapshotting:
|
||||||
|
|||||||
@@ -1,59 +0,0 @@
|
|||||||
jobs:
|
|
||||||
# Separate job for snapshots and pruning
|
|
||||||
- name: snapshots
|
|
||||||
type: snap
|
|
||||||
filesystems:
|
|
||||||
'tank<': true # all filesystems
|
|
||||||
snapshotting:
|
|
||||||
type: periodic
|
|
||||||
prefix: zrepl_
|
|
||||||
interval: 10m
|
|
||||||
pruning:
|
|
||||||
keep:
|
|
||||||
# Keep non-zrepl snapshots
|
|
||||||
- type: regex
|
|
||||||
negate: true
|
|
||||||
regex: '^zrepl_'
|
|
||||||
# Time-based snapshot retention
|
|
||||||
- type: grid
|
|
||||||
grid: 1x1h(keep=all) | 24x1h | 30x1d | 12x30d
|
|
||||||
regex: '^zrepl_'
|
|
||||||
|
|
||||||
# Source job for target B
|
|
||||||
- name: target_b
|
|
||||||
type: source
|
|
||||||
serve:
|
|
||||||
type: tls
|
|
||||||
listen: :8888
|
|
||||||
ca: /etc/zrepl/b.example.com.crt
|
|
||||||
cert: /etc/zrepl/a.example.com.crt
|
|
||||||
key: /etc/zrepl/a.example.com.key
|
|
||||||
client_cns:
|
|
||||||
- b.example.com
|
|
||||||
filesystems:
|
|
||||||
'tank<': true # all filesystems
|
|
||||||
# Snapshots are handled by the separate snap job
|
|
||||||
snapshotting:
|
|
||||||
type: manual
|
|
||||||
|
|
||||||
# Source job for target C
|
|
||||||
- name: target_c
|
|
||||||
type: source
|
|
||||||
serve:
|
|
||||||
type: tls
|
|
||||||
listen: :8889
|
|
||||||
ca: /etc/zrepl/c.example.com.crt
|
|
||||||
cert: /etc/zrepl/a.example.com.crt
|
|
||||||
key: /etc/zrepl/a.example.com.key
|
|
||||||
client_cns:
|
|
||||||
- c.example.com
|
|
||||||
filesystems:
|
|
||||||
'tank<': true # all filesystems
|
|
||||||
# Snapshots are handled by the separate snap job
|
|
||||||
snapshotting:
|
|
||||||
type: manual
|
|
||||||
|
|
||||||
# Source jobs for remaining targets. Each one should listen on a different port
|
|
||||||
# and reference the correct certificate and client CN.
|
|
||||||
# - name: target_c
|
|
||||||
# ...
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
jobs:
|
|
||||||
# Pull from source server A
|
|
||||||
- name: source_a
|
|
||||||
type: pull
|
|
||||||
connect:
|
|
||||||
type: tls
|
|
||||||
# Use the correct port for this specific client (eg. B is 8888, C is 8889, etc.)
|
|
||||||
address: a.example.com:8888
|
|
||||||
ca: /etc/zrepl/a.example.com.crt
|
|
||||||
# Use the correct key pair for this specific client
|
|
||||||
cert: /etc/zrepl/b.example.com.crt
|
|
||||||
key: /etc/zrepl/b.example.com.key
|
|
||||||
server_cn: a.example.com
|
|
||||||
root_fs: pool0/backup
|
|
||||||
interval: 10m
|
|
||||||
pruning:
|
|
||||||
keep_sender:
|
|
||||||
# Source does the pruning in its snap job
|
|
||||||
- type: regex
|
|
||||||
regex: '.*'
|
|
||||||
# Receiver-side pruning can be configured as desired on each target server
|
|
||||||
keep_receiver:
|
|
||||||
# Keep non-zrepl snapshots
|
|
||||||
- type: regex
|
|
||||||
negate: true
|
|
||||||
regex: '^zrepl_'
|
|
||||||
# Time-based snapshot retention
|
|
||||||
- type: grid
|
|
||||||
grid: 1x1h(keep=all) | 24x1h | 30x1d | 12x30d
|
|
||||||
regex: '^zrepl_'
|
|
||||||
@@ -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
|
|
||||||
+115
-23
@@ -8,7 +8,6 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
@@ -74,12 +73,29 @@ func (j *controlJob) RegisterMetrics(registerer prometheus.Registerer) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ControlJobEndpointPProf string = "/debug/pprof"
|
ControlJobEndpointPProf string = "/debug/pprof"
|
||||||
ControlJobEndpointVersion string = "/version"
|
ControlJobEndpointVersion string = "/version"
|
||||||
ControlJobEndpointStatus string = "/status"
|
ControlJobEndpointStatus string = "/status"
|
||||||
ControlJobEndpointSignal string = "/signal"
|
ControlJobEndpointTriggerActive string = "/signal/active"
|
||||||
|
ControlJobEndpointPollActive string = "/poll/active"
|
||||||
|
ControlJobEndpointResetActive string = "/reset/active"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type ControlJobEndpointTriggerActiveRequest struct {
|
||||||
|
Job string
|
||||||
|
job.ActiveSideTriggerRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
type ControlJobEndpointResetActiveRequest struct {
|
||||||
|
Job string
|
||||||
|
job.ActiveSideResetRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
type ControlJobEndpointWaitActiveRequest struct {
|
||||||
|
Job string
|
||||||
|
job.ActiveSidePollRequest
|
||||||
|
}
|
||||||
|
|
||||||
func (j *controlJob) Run(ctx context.Context) {
|
func (j *controlJob) Run(ctx context.Context) {
|
||||||
|
|
||||||
log := job.GetLogger(ctx)
|
log := job.GetLogger(ctx)
|
||||||
@@ -125,41 +141,117 @@ func (j *controlJob) Run(ctx context.Context) {
|
|||||||
s := Status{
|
s := Status{
|
||||||
Jobs: jobs,
|
Jobs: jobs,
|
||||||
Global: GlobalStatus{
|
Global: GlobalStatus{
|
||||||
ZFSCmds: globalZFS,
|
ZFSCmds: globalZFS,
|
||||||
Envconst: envconstReport,
|
Envconst: envconstReport,
|
||||||
OsEnviron: os.Environ(),
|
|
||||||
}}
|
}}
|
||||||
return s, nil
|
return s, nil
|
||||||
}})
|
}})
|
||||||
|
|
||||||
mux.Handle(ControlJobEndpointSignal,
|
mux.Handle(ControlJobEndpointPollActive, requestLogger{log: log, handler: jsonRequestResponder{log, func(decoder jsonDecoder) (v interface{}, err error) {
|
||||||
requestLogger{log: log, handler: jsonRequestResponder{log, func(decoder jsonDecoder) (interface{}, error) {
|
var req ControlJobEndpointWaitActiveRequest
|
||||||
|
if decoder(&req) != nil {
|
||||||
|
return nil, errors.Errorf("decode failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
j.jobs.m.RLock()
|
||||||
|
|
||||||
|
jo, ok := j.jobs.jobs[req.Job]
|
||||||
|
if !ok {
|
||||||
|
j.jobs.m.RUnlock()
|
||||||
|
return struct{}{}, fmt.Errorf("unknown job name %q", req.Job)
|
||||||
|
}
|
||||||
|
|
||||||
|
ajo, ok := jo.(*job.ActiveSide)
|
||||||
|
if !ok {
|
||||||
|
v, err = struct{}{}, fmt.Errorf("job %q is not an active side (it's a %T)", jo.Name(), jo)
|
||||||
|
j.jobs.m.RUnlock()
|
||||||
|
return v, err
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := ajo.Poll(req.ActiveSidePollRequest)
|
||||||
|
|
||||||
|
j.jobs.m.RUnlock()
|
||||||
|
|
||||||
|
return res, err
|
||||||
|
}}})
|
||||||
|
|
||||||
|
mux.Handle(ControlJobEndpointTriggerActive,
|
||||||
|
requestLogger{log: log, handler: jsonRequestResponder{log, func(decoder jsonDecoder) (v interface{}, err error) {
|
||||||
type reqT struct {
|
type reqT struct {
|
||||||
Name string
|
Job string
|
||||||
Op string
|
job.ActiveSideTriggerRequest
|
||||||
}
|
}
|
||||||
var req reqT
|
var req reqT
|
||||||
if decoder(&req) != nil {
|
if decoder(&req) != nil {
|
||||||
return nil, errors.Errorf("decode failed")
|
return nil, errors.Errorf("decode failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
// FIXME dedup the following code with ControlJobEndpointPollActive
|
||||||
switch req.Op {
|
|
||||||
case "wakeup":
|
j.jobs.m.RLock()
|
||||||
err = j.jobs.wakeup(req.Name)
|
|
||||||
case "reset":
|
jo, ok := j.jobs.jobs[req.Job]
|
||||||
err = j.jobs.reset(req.Name)
|
if !ok {
|
||||||
default:
|
j.jobs.m.RUnlock()
|
||||||
err = fmt.Errorf("operation %q is invalid", req.Op)
|
return struct{}{}, fmt.Errorf("unknown job name %q", req.Job)
|
||||||
}
|
}
|
||||||
|
|
||||||
return struct{}{}, err
|
ajo, ok := jo.(*job.ActiveSide)
|
||||||
|
if !ok {
|
||||||
|
v, err = struct{}{}, fmt.Errorf("job %q is not an active side (it's a %T)", jo.Name(), jo)
|
||||||
|
j.jobs.m.RUnlock()
|
||||||
|
return v, err
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := ajo.Trigger(req.ActiveSideTriggerRequest)
|
||||||
|
|
||||||
|
j.jobs.m.RUnlock()
|
||||||
|
|
||||||
|
return res, err
|
||||||
|
|
||||||
}}})
|
}}})
|
||||||
|
|
||||||
|
mux.Handle(ControlJobEndpointResetActive,
|
||||||
|
requestLogger{log: log, handler: jsonRequestResponder{log, func(decoder jsonDecoder) (v interface{}, err error) {
|
||||||
|
type reqT struct {
|
||||||
|
Job string
|
||||||
|
job.ActiveSideResetRequest
|
||||||
|
}
|
||||||
|
var req reqT
|
||||||
|
if decoder(&req) != nil {
|
||||||
|
return nil, errors.Errorf("decode failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME dedup the following code with ControlJobEndpointPollActive
|
||||||
|
|
||||||
|
j.jobs.m.RLock()
|
||||||
|
|
||||||
|
jo, ok := j.jobs.jobs[req.Job]
|
||||||
|
if !ok {
|
||||||
|
j.jobs.m.RUnlock()
|
||||||
|
return struct{}{}, fmt.Errorf("unknown job name %q", req.Job)
|
||||||
|
}
|
||||||
|
|
||||||
|
ajo, ok := jo.(*job.ActiveSide)
|
||||||
|
if !ok {
|
||||||
|
v, err = struct{}{}, fmt.Errorf("job %q is not an active side (it's a %T)", jo.Name(), jo)
|
||||||
|
j.jobs.m.RUnlock()
|
||||||
|
return v, err
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := ajo.Reset(req.ActiveSideResetRequest)
|
||||||
|
|
||||||
|
j.jobs.m.RUnlock()
|
||||||
|
|
||||||
|
return res, err
|
||||||
|
|
||||||
|
}}})
|
||||||
|
|
||||||
server := http.Server{
|
server := http.Server{
|
||||||
Handler: mux,
|
Handler: mux,
|
||||||
// control socket is local, 1s timeout should be more than sufficient, even on a loaded system
|
// control socket is local, 1s timeout should be more than sufficient, even on a loaded system
|
||||||
WriteTimeout: envconst.Duration("ZREPL_DAEMON_CONTROL_SERVER_WRITE_TIMEOUT", 1*time.Second),
|
WriteTimeout: 1 * time.Second,
|
||||||
ReadTimeout: envconst.Duration("ZREPL_DAEMON_CONTROL_SERVER_READ_TIMEOUT", 1*time.Second),
|
ReadTimeout: 1 * time.Second,
|
||||||
}
|
}
|
||||||
|
|
||||||
outer:
|
outer:
|
||||||
|
|||||||
+6
-39
@@ -20,8 +20,6 @@ import (
|
|||||||
|
|
||||||
"github.com/zrepl/zrepl/config"
|
"github.com/zrepl/zrepl/config"
|
||||||
"github.com/zrepl/zrepl/daemon/job"
|
"github.com/zrepl/zrepl/daemon/job"
|
||||||
"github.com/zrepl/zrepl/daemon/job/reset"
|
|
||||||
"github.com/zrepl/zrepl/daemon/job/wakeup"
|
|
||||||
"github.com/zrepl/zrepl/daemon/logging"
|
"github.com/zrepl/zrepl/daemon/logging"
|
||||||
"github.com/zrepl/zrepl/logger"
|
"github.com/zrepl/zrepl/logger"
|
||||||
"github.com/zrepl/zrepl/version"
|
"github.com/zrepl/zrepl/version"
|
||||||
@@ -51,7 +49,7 @@ func Run(ctx context.Context, conf *config.Config) error {
|
|||||||
}
|
}
|
||||||
outlets.Add(newPrometheusLogOutlet(), logger.Debug)
|
outlets.Add(newPrometheusLogOutlet(), logger.Debug)
|
||||||
|
|
||||||
confJobs, err := job.JobsFromConfig(conf, config.ParseFlagsNone)
|
confJobs, err := job.JobsFromConfig(conf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "cannot build jobs from config")
|
return errors.Wrap(err, "cannot build jobs from config")
|
||||||
}
|
}
|
||||||
@@ -131,17 +129,13 @@ type jobs struct {
|
|||||||
wg sync.WaitGroup
|
wg sync.WaitGroup
|
||||||
|
|
||||||
// m protects all fields below it
|
// m protects all fields below it
|
||||||
m sync.RWMutex
|
m sync.RWMutex
|
||||||
wakeups map[string]wakeup.Func // by Job.Name
|
jobs map[string]job.Job
|
||||||
resets map[string]reset.Func // by Job.Name
|
|
||||||
jobs map[string]job.Job
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func newJobs() *jobs {
|
func newJobs() *jobs {
|
||||||
return &jobs{
|
return &jobs{
|
||||||
wakeups: make(map[string]wakeup.Func),
|
jobs: make(map[string]job.Job),
|
||||||
resets: make(map[string]reset.Func),
|
|
||||||
jobs: make(map[string]job.Job),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,9 +154,8 @@ type Status struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type GlobalStatus struct {
|
type GlobalStatus struct {
|
||||||
ZFSCmds *zfscmd.Report
|
ZFSCmds *zfscmd.Report
|
||||||
Envconst *envconst.Report
|
Envconst *envconst.Report
|
||||||
OsEnviron []string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *jobs) status() map[string]*job.Status {
|
func (s *jobs) status() map[string]*job.Status {
|
||||||
@@ -191,28 +184,6 @@ func (s *jobs) status() map[string]*job.Status {
|
|||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *jobs) wakeup(job string) error {
|
|
||||||
s.m.RLock()
|
|
||||||
defer s.m.RUnlock()
|
|
||||||
|
|
||||||
wu, ok := s.wakeups[job]
|
|
||||||
if !ok {
|
|
||||||
return errors.Errorf("Job %s does not exist", job)
|
|
||||||
}
|
|
||||||
return wu()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *jobs) reset(job string) error {
|
|
||||||
s.m.RLock()
|
|
||||||
defer s.m.RUnlock()
|
|
||||||
|
|
||||||
wu, ok := s.resets[job]
|
|
||||||
if !ok {
|
|
||||||
return errors.Errorf("Job %s does not exist", job)
|
|
||||||
}
|
|
||||||
return wu()
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
jobNamePrometheus = "_prometheus"
|
jobNamePrometheus = "_prometheus"
|
||||||
jobNameControl = "_control"
|
jobNameControl = "_control"
|
||||||
@@ -243,10 +214,6 @@ func (s *jobs) start(ctx context.Context, j job.Job, internal bool) {
|
|||||||
|
|
||||||
s.jobs[jobName] = j
|
s.jobs[jobName] = j
|
||||||
ctx = zfscmd.WithJobID(ctx, j.Name())
|
ctx = zfscmd.WithJobID(ctx, j.Name())
|
||||||
ctx, wakeup := wakeup.Context(ctx)
|
|
||||||
ctx, resetFunc := reset.Context(ctx)
|
|
||||||
s.wakeups[jobName] = wakeup
|
|
||||||
s.resets[jobName] = resetFunc
|
|
||||||
|
|
||||||
s.wg.Add(1)
|
s.wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,22 +17,19 @@ import (
|
|||||||
"github.com/zrepl/zrepl/zfs"
|
"github.com/zrepl/zrepl/zfs"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Hook to implement the following recommmendation from MySQL docs
|
|
||||||
// https://dev.mysql.com/doc/mysql-backup-excerpt/5.7/en/backup-methods.html
|
// https://dev.mysql.com/doc/mysql-backup-excerpt/5.7/en/backup-methods.html
|
||||||
//
|
//
|
||||||
// Making Backups Using a File System Snapshot:
|
// Making Backups Using a File System Snapshot:
|
||||||
//
|
//
|
||||||
// If you are using a Veritas file system, you can make a backup like this:
|
// If you are using a Veritas file system, you can make a backup like this:
|
||||||
//
|
//
|
||||||
// From a client program, execute FLUSH TABLES WITH READ LOCK.
|
// From a client program, execute FLUSH TABLES WITH READ LOCK.
|
||||||
// From another shell, execute mount vxfs snapshot.
|
// From another shell, execute mount vxfs snapshot.
|
||||||
// From the first client, execute UNLOCK TABLES.
|
// From the first client, execute UNLOCK TABLES.
|
||||||
// Copy files from the snapshot.
|
// Copy files from the snapshot.
|
||||||
// Unmount the snapshot.
|
// Unmount the snapshot.
|
||||||
//
|
//
|
||||||
// Similar snapshot capabilities may be available in other file systems, such as LVM or ZFS.
|
// Similar snapshot capabilities may be available in other file systems, such as LVM or ZFS.
|
||||||
//
|
|
||||||
|
|
||||||
type MySQLLockTables struct {
|
type MySQLLockTables struct {
|
||||||
errIsFatal bool
|
errIsFatal bool
|
||||||
connector sqldriver.Connector
|
connector sqldriver.Connector
|
||||||
|
|||||||
@@ -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{
|
||||||
|
|||||||
+208
-75
@@ -8,14 +8,11 @@ import (
|
|||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
"github.com/prometheus/common/log"
|
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/daemon/logging/trace"
|
"github.com/zrepl/zrepl/daemon/logging/trace"
|
||||||
"github.com/zrepl/zrepl/util/envconst"
|
"github.com/zrepl/zrepl/util/envconst"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/config"
|
"github.com/zrepl/zrepl/config"
|
||||||
"github.com/zrepl/zrepl/daemon/job/reset"
|
|
||||||
"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"
|
||||||
"github.com/zrepl/zrepl/endpoint"
|
"github.com/zrepl/zrepl/endpoint"
|
||||||
@@ -42,10 +39,13 @@ type ActiveSide struct {
|
|||||||
promPruneSecs *prometheus.HistogramVec // labels: prune_side
|
promPruneSecs *prometheus.HistogramVec // labels: prune_side
|
||||||
promBytesReplicated *prometheus.CounterVec // labels: filesystem
|
promBytesReplicated *prometheus.CounterVec // labels: filesystem
|
||||||
promReplicationErrors prometheus.Gauge
|
promReplicationErrors prometheus.Gauge
|
||||||
promLastSuccessful prometheus.Gauge
|
|
||||||
|
|
||||||
tasksMtx sync.Mutex
|
tasksMtx sync.Mutex
|
||||||
tasks activeSideTasks
|
tasks activeSideTasks
|
||||||
|
nextInvocationId uint64
|
||||||
|
activeInvocationId uint64 // 0 <=> inactive
|
||||||
|
trigger chan struct{}
|
||||||
|
reset chan uint64
|
||||||
}
|
}
|
||||||
|
|
||||||
//go:generate enumer -type=ActiveSideState
|
//go:generate enumer -type=ActiveSideState
|
||||||
@@ -58,18 +58,17 @@ const (
|
|||||||
ActiveSideDone // also errors
|
ActiveSideDone // also errors
|
||||||
)
|
)
|
||||||
|
|
||||||
type activeSideTasks struct {
|
type activeSideReplicationAndTriggerRemotePruneSequence struct {
|
||||||
state ActiveSideState
|
state ActiveSideState
|
||||||
|
|
||||||
// valid for state ActiveSideReplicating, ActiveSidePruneSender, ActiveSidePruneReceiver, ActiveSideDone
|
// valid for state ActiveSideReplicating, ActiveSidePruneSender, ActiveSidePruneReceiver, ActiveSideDone
|
||||||
replicationReport driver.ReportFunc
|
replicationReport driver.ReportFunc
|
||||||
replicationCancel context.CancelFunc
|
replicationCancel context.CancelFunc
|
||||||
|
replicationDone *report.Report
|
||||||
|
|
||||||
// valid for state ActiveSidePruneSender, ActiveSidePruneReceiver, ActiveSideDone
|
// valid for state ActiveSidePruneSender, ActiveSidePruneReceiver, ActiveSideDone
|
||||||
prunerSender, prunerReceiver *pruner.Pruner
|
pruneRemote *pruner.Pruner
|
||||||
|
pruneRemoteCancel context.CancelFunc
|
||||||
// valid for state ActiveSidePruneReceiver, ActiveSideDone
|
|
||||||
prunerSenderCancel, prunerReceiverCancel context.CancelFunc
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *ActiveSide) updateTasks(u func(*activeSideTasks)) activeSideTasks {
|
func (a *ActiveSide) updateTasks(u func(*activeSideTasks)) activeSideTasks {
|
||||||
@@ -90,7 +89,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, wakeUpCommon chan<- struct{})
|
RunPeriodic(ctx context.Context, wakePeriodic <-chan struct{}, replicationCommon chan<- struct{})
|
||||||
SnapperReport() *snapper.Report
|
SnapperReport() *snapper.Report
|
||||||
ResetConnectBackoff()
|
ResetConnectBackoff()
|
||||||
}
|
}
|
||||||
@@ -101,7 +100,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) {
|
||||||
@@ -132,13 +131,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, wakeUpCommon chan<- struct{}) {
|
func (m *modePush) RunPeriodic(ctx context.Context, wakePeriodic <-chan struct{}, replicationCommon chan<- struct{}) {
|
||||||
m.snapper.Run(ctx, wakeUpCommon)
|
m.snapper.Run(ctx, replicationCommon)
|
||||||
}
|
}
|
||||||
|
|
||||||
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() {
|
||||||
@@ -163,13 +161,8 @@ func modePushFromConfig(g *config.Global, in *config.PushJob, jobID endpoint.Job
|
|||||||
return nil, errors.Wrap(err, "field `replication`")
|
return nil, errors.Wrap(err, "field `replication`")
|
||||||
}
|
}
|
||||||
|
|
||||||
conflictResolution, err := logic.ConflictResolutionFromConfig(in.ConflictResolution)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "field `conflict_resolution`")
|
|
||||||
}
|
|
||||||
|
|
||||||
m.plannerPolicy = &logic.PlannerPolicy{
|
m.plannerPolicy = &logic.PlannerPolicy{
|
||||||
ConflictResolution: conflictResolution,
|
EncryptedSend: logic.TriFromBool(in.Send.Encrypted),
|
||||||
ReplicationConfig: replicationConfig,
|
ReplicationConfig: replicationConfig,
|
||||||
SizeEstimationConcurrency: in.Replication.Concurrency.SizeEstimates,
|
SizeEstimationConcurrency: in.Replication.Concurrency.SizeEstimates,
|
||||||
}
|
}
|
||||||
@@ -221,10 +214,10 @@ 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, wakeUpCommon chan<- struct{}) {
|
func (m *modePull) RunPeriodic(ctx context.Context, wakePeriodic <-chan struct{}, replicationCommon 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 wakeup replications" is printed in common ActiveSide.do
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
t := time.NewTicker(m.interval.Interval)
|
t := time.NewTicker(m.interval.Interval)
|
||||||
@@ -233,12 +226,12 @@ func (m *modePull) RunPeriodic(ctx context.Context, wakeUpCommon chan<- struct{}
|
|||||||
select {
|
select {
|
||||||
case <-t.C:
|
case <-t.C:
|
||||||
select {
|
select {
|
||||||
case wakeUpCommon <- struct{}{}:
|
case replicationCommon <- struct{}{}:
|
||||||
default:
|
default:
|
||||||
GetLogger(ctx).
|
GetLogger(ctx).
|
||||||
WithField("pull_interval", m.interval).
|
WithField("pull_interval", m.interval).
|
||||||
Warn("pull job took longer than pull interval")
|
Warn("pull job took longer than pull interval")
|
||||||
wakeUpCommon <- struct{}{} // block anyways, to queue up the wakeup
|
replicationCommon <- struct{}{} // block anyways, to queue up the wakeup replication
|
||||||
}
|
}
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return
|
return
|
||||||
@@ -267,13 +260,8 @@ func modePullFromConfig(g *config.Global, in *config.PullJob, jobID endpoint.Job
|
|||||||
return nil, errors.Wrap(err, "field `replication`")
|
return nil, errors.Wrap(err, "field `replication`")
|
||||||
}
|
}
|
||||||
|
|
||||||
conflictResolution, err := logic.ConflictResolutionFromConfig(in.ConflictResolution)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "field `conflict_resolution`")
|
|
||||||
}
|
|
||||||
|
|
||||||
m.plannerPolicy = &logic.PlannerPolicy{
|
m.plannerPolicy = &logic.PlannerPolicy{
|
||||||
ConflictResolution: conflictResolution,
|
EncryptedSend: logic.DontCare,
|
||||||
ReplicationConfig: replicationConfig,
|
ReplicationConfig: replicationConfig,
|
||||||
SizeEstimationConcurrency: in.Replication.Concurrency.SizeEstimates,
|
SizeEstimationConcurrency: in.Replication.Concurrency.SizeEstimates,
|
||||||
}
|
}
|
||||||
@@ -299,7 +287,7 @@ func replicationDriverConfigFromConfig(in *config.Replication) (c driver.Config,
|
|||||||
return c, err
|
return c, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func activeSide(g *config.Global, in *config.ActiveJob, configJob interface{}, parseFlags config.ParseFlags) (j *ActiveSide, err error) {
|
func activeSide(g *config.Global, in *config.ActiveJob, configJob interface{}) (j *ActiveSide, err error) {
|
||||||
|
|
||||||
j = &ActiveSide{}
|
j = &ActiveSide{}
|
||||||
j.name, err = endpoint.MakeJobID(in.Name)
|
j.name, err = endpoint.MakeJobID(in.Name)
|
||||||
@@ -333,6 +321,7 @@ func activeSide(g *config.Global, in *config.ActiveJob, configJob interface{}, p
|
|||||||
Help: "number of bytes replicated from sender to receiver per filesystem",
|
Help: "number of bytes replicated from sender to receiver per filesystem",
|
||||||
ConstLabels: prometheus.Labels{"zrepl_job": j.name.String()},
|
ConstLabels: prometheus.Labels{"zrepl_job": j.name.String()},
|
||||||
}, []string{"filesystem"})
|
}, []string{"filesystem"})
|
||||||
|
|
||||||
j.promReplicationErrors = prometheus.NewGauge(prometheus.GaugeOpts{
|
j.promReplicationErrors = prometheus.NewGauge(prometheus.GaugeOpts{
|
||||||
Namespace: "zrepl",
|
Namespace: "zrepl",
|
||||||
Subsystem: "replication",
|
Subsystem: "replication",
|
||||||
@@ -340,15 +329,8 @@ func activeSide(g *config.Global, in *config.ActiveJob, configJob interface{}, p
|
|||||||
Help: "number of filesystems that failed replication in the latest replication attempt, or -1 if the job failed before enumerating the filesystems",
|
Help: "number of filesystems that failed replication in the latest replication attempt, or -1 if the job failed before enumerating the filesystems",
|
||||||
ConstLabels: prometheus.Labels{"zrepl_job": j.name.String()},
|
ConstLabels: prometheus.Labels{"zrepl_job": j.name.String()},
|
||||||
})
|
})
|
||||||
j.promLastSuccessful = prometheus.NewGauge(prometheus.GaugeOpts{
|
|
||||||
Namespace: "zrepl",
|
|
||||||
Subsystem: "replication",
|
|
||||||
Name: "last_successful",
|
|
||||||
Help: "timestamp of last successful replication",
|
|
||||||
ConstLabels: prometheus.Labels{"zrepl_job": j.name.String()},
|
|
||||||
})
|
|
||||||
|
|
||||||
j.connecter, err = fromconfig.ConnecterFromConfig(g, in.Connect, parseFlags)
|
j.connecter, err = fromconfig.ConnecterFromConfig(g, in.Connect)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "cannot build client")
|
return nil, errors.Wrap(err, "cannot build client")
|
||||||
}
|
}
|
||||||
@@ -378,7 +360,6 @@ func (j *ActiveSide) RegisterMetrics(registerer prometheus.Registerer) {
|
|||||||
registerer.MustRegister(j.promPruneSecs)
|
registerer.MustRegister(j.promPruneSecs)
|
||||||
registerer.MustRegister(j.promBytesReplicated)
|
registerer.MustRegister(j.promBytesReplicated)
|
||||||
registerer.MustRegister(j.promReplicationErrors)
|
registerer.MustRegister(j.promReplicationErrors)
|
||||||
registerer.MustRegister(j.promLastSuccessful)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *ActiveSide) Name() string { return j.name.String() }
|
func (j *ActiveSide) Name() string { return j.name.String() }
|
||||||
@@ -444,50 +425,205 @@ func (j *ActiveSide) Run(ctx context.Context) {
|
|||||||
|
|
||||||
defer log.Info("job exiting")
|
defer log.Info("job exiting")
|
||||||
|
|
||||||
|
type Activity interface {
|
||||||
|
Trigger() (interface{}, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
var periodicActivity Activity
|
||||||
|
var replicationActivity Activity
|
||||||
|
|
||||||
periodicDone := make(chan struct{})
|
periodicDone := make(chan struct{})
|
||||||
ctx, cancel := context.WithCancel(ctx)
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
periodicCtx, endTask := trace.WithTask(ctx, "periodic")
|
periodicCtx, endTask := trace.WithTask(ctx, "periodic")
|
||||||
defer endTask()
|
defer endTask()
|
||||||
go j.mode.RunPeriodic(periodicCtx, periodicDone)
|
|
||||||
|
|
||||||
invocationCount := 0
|
wakePeriodic := make(chan struct{})
|
||||||
outer:
|
go j.mode.RunPeriodic(periodicCtx, wakePeriodic, periodicDone)
|
||||||
|
|
||||||
|
j.trigger = make(chan struct{})
|
||||||
|
j.reset = make(chan uint64)
|
||||||
|
j.nextInvocationId = 1
|
||||||
|
|
||||||
|
type WaitTriggerResult interface {
|
||||||
|
|
||||||
|
}
|
||||||
|
var t interface{
|
||||||
|
WaitForTrigger(context.Context) (context.Context, <-chan WaitTriggerResult)
|
||||||
|
}
|
||||||
|
|
||||||
for {
|
for {
|
||||||
log.Info("wait for wakeups")
|
log.Info("wait for triggers")
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
log.WithError(ctx.Err()).Info("context")
|
|
||||||
break outer
|
|
||||||
|
|
||||||
case <-wakeup.Wait(ctx):
|
// j.tasksMtx.Lock()
|
||||||
j.mode.ResetConnectBackoff()
|
// j.activeInvocationId = j.nextInvocationId
|
||||||
case <-periodicDone:
|
// j.nextInvocationId++
|
||||||
|
// thisInvocation := j.activeInvocationId // stack-local, for use in reset-handler goroutine below
|
||||||
|
// j.tasksMtx.Unlock()
|
||||||
|
|
||||||
|
// // setup the goroutine that waits for task resets
|
||||||
|
// // Task resets are converted into cancellations of the invocation context.
|
||||||
|
|
||||||
|
invocationCtx, cancelInvocation := context.WithCancel(invocationCtx)
|
||||||
|
// waitForResetCtx, stopWaitForReset := context.WithCancel(ctx)
|
||||||
|
// var wg sync.WaitGroup
|
||||||
|
// wg.Add(1)
|
||||||
|
// go func() {
|
||||||
|
// defer wg.Done()
|
||||||
|
// select {
|
||||||
|
// case <-waitForResetCtx.Done():
|
||||||
|
// return
|
||||||
|
// case reqResetInvocation := <-j.reset:
|
||||||
|
// l := log.WithField("requested_invocation_id", reqResetInvocation).
|
||||||
|
// WithField("this_invocation_id", thisInvocation)
|
||||||
|
// if reqResetInvocation == thisInvocation {
|
||||||
|
// l.Info("reset received, cancelling current invocation")
|
||||||
|
// cancelInvocation()
|
||||||
|
// } else {
|
||||||
|
// l.Debug("received reset for invocation id that is not us, discarding request")
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }()
|
||||||
|
|
||||||
|
// j.tasksMtx.Lock()
|
||||||
|
// j.activeInvocationId = 0
|
||||||
|
// j.tasksMtx.Unlock()
|
||||||
|
|
||||||
|
invocationCtx, err := t.WaitForTrigger(ctx)
|
||||||
|
if err != nil {
|
||||||
|
log.WithError(ctx.Err()).Info("error waiting for trigger")
|
||||||
|
break
|
||||||
}
|
}
|
||||||
invocationCount++
|
|
||||||
invocationCtx, endSpan := trace.WithSpan(ctx, fmt.Sprintf("invocation-%d", invocationCount))
|
j.mode.ResetConnectBackoff()
|
||||||
|
|
||||||
|
// setup the invocation context
|
||||||
|
invocationCtx, endSpan := trace.WithSpan(invocationCtx, fmt.Sprintf("invocation-%d", j.nextInvocationId))
|
||||||
|
|
||||||
|
|
||||||
j.do(invocationCtx)
|
j.do(invocationCtx)
|
||||||
|
stopWaitForReset()
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
endSpan()
|
endSpan()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ActiveSidePollRequest struct {
|
||||||
|
InvocationId uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActiveSidePollResponse struct {
|
||||||
|
Done bool
|
||||||
|
InvocationId uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *ActiveSide) Poll(req ActiveSidePollRequest) (*ActiveSidePollResponse, error) {
|
||||||
|
j.tasksMtx.Lock()
|
||||||
|
defer j.tasksMtx.Unlock()
|
||||||
|
|
||||||
|
waitForId := req.InvocationId
|
||||||
|
if req.InvocationId == 0 {
|
||||||
|
// handle the case where the client doesn't know what the current invocation id is
|
||||||
|
if j.activeInvocationId != 0 {
|
||||||
|
waitForId = j.activeInvocationId
|
||||||
|
} else {
|
||||||
|
waitForId = j.nextInvocationId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var done bool
|
||||||
|
if j.activeInvocationId == 0 {
|
||||||
|
done = waitForId < j.nextInvocationId
|
||||||
|
} else {
|
||||||
|
done = waitForId < j.activeInvocationId
|
||||||
|
}
|
||||||
|
res := &ActiveSidePollResponse{Done: done, InvocationId: waitForId}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActiveSideResetRequest struct {
|
||||||
|
InvocationId uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActiveSideResetResponse struct {
|
||||||
|
InvocationId uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *ActiveSide) Reset(req ActiveSideResetRequest) (*ActiveSideResetResponse, error) {
|
||||||
|
j.tasksMtx.Lock()
|
||||||
|
defer j.tasksMtx.Unlock()
|
||||||
|
|
||||||
|
resetId := req.InvocationId
|
||||||
|
if req.InvocationId == 0 {
|
||||||
|
// handle the case where the client doesn't know what the current invocation id is
|
||||||
|
resetId = j.activeInvocationId
|
||||||
|
}
|
||||||
|
|
||||||
|
if resetId == 0 {
|
||||||
|
return nil, fmt.Errorf("no active invocation")
|
||||||
|
}
|
||||||
|
|
||||||
|
if resetId != j.activeInvocationId {
|
||||||
|
return nil, fmt.Errorf("active invocation (%d) is not the invocation requested for reset (%d); (active invocation '0' indicates no active invocation)", j.activeInvocationId, resetId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// non-blocking send (.Run() must not hold mutex while waiting for resets)
|
||||||
|
select {
|
||||||
|
case j.reset <- resetId:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ActiveSideResetResponse{InvocationId: resetId}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActiveSideTriggerRequest struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActiveSideSignalResponse struct {
|
||||||
|
InvocationId uint64
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *ActiveSide) Trigger(req ActiveSideTriggerRequest) (*ActiveSideSignalResponse, error) {
|
||||||
|
// switch req.What {
|
||||||
|
// case "replication":
|
||||||
|
// invocationId, err = j.jobs.doreplication(req.Name)
|
||||||
|
// case "reset":
|
||||||
|
// err = j.jobs.reset(req.Name)
|
||||||
|
// case "snapshot":
|
||||||
|
// err = j.jobs.dosnapshot(req.Name)
|
||||||
|
// default:
|
||||||
|
// err = fmt.Errorf("operation %q is invalid", req.Op)
|
||||||
|
// }
|
||||||
|
|
||||||
|
j.tasksMtx.Lock()
|
||||||
|
var invocationId uint64
|
||||||
|
if j.activeInvocationId != 0 {
|
||||||
|
invocationId = j.activeInvocationId
|
||||||
|
} else {
|
||||||
|
invocationId = j.nextInvocationId
|
||||||
|
}
|
||||||
|
// non-blocking send (.Run() must not hold mutex while waiting for signals)
|
||||||
|
select {
|
||||||
|
case j.trigger <- struct{}{}:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
j.tasksMtx.Unlock()
|
||||||
|
return &ActiveSideSignalResponse{InvocationId: invocationId}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type ReplicationPlusRemotePruneSequence struct {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
func (j *ActiveSide) do(ctx context.Context) {
|
func (j *ActiveSide) do(ctx context.Context) {
|
||||||
|
|
||||||
j.mode.ConnectEndpoints(ctx, j.connecter)
|
j.mode.ConnectEndpoints(ctx, j.connecter)
|
||||||
defer j.mode.DisconnectEndpoints()
|
defer j.mode.DisconnectEndpoints()
|
||||||
|
|
||||||
// allow cancellation of an invocation (this function)
|
|
||||||
ctx, cancelThisRun := context.WithCancel(ctx)
|
|
||||||
defer cancelThisRun()
|
|
||||||
go func() {
|
|
||||||
select {
|
|
||||||
case <-reset.Wait(ctx):
|
|
||||||
log.Info("reset received, cancelling current invocation")
|
|
||||||
cancelThisRun()
|
|
||||||
case <-ctx.Done():
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
sender, receiver := j.mode.SenderReceiver()
|
sender, receiver := j.mode.SenderReceiver()
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -511,14 +647,11 @@ func (j *ActiveSide) do(ctx context.Context) {
|
|||||||
GetLogger(ctx).Info("start replication")
|
GetLogger(ctx).Info("start replication")
|
||||||
repWait(true) // wait blocking
|
repWait(true) // wait blocking
|
||||||
repCancel() // always cancel to free up context resources
|
repCancel() // always cancel to free up context resources
|
||||||
|
|
||||||
replicationReport := j.tasks.replicationReport()
|
replicationReport := j.tasks.replicationReport()
|
||||||
var numErrors = replicationReport.GetFailedFilesystemsCountInLatestAttempt()
|
j.promReplicationErrors.Set(float64(replicationReport.GetFailedFilesystemsCountInLatestAttempt()))
|
||||||
j.promReplicationErrors.Set(float64(numErrors))
|
j.updateTasks(func(tasks *activeSideTasks) {
|
||||||
if numErrors == 0 {
|
tasks.replicationDone = replicationReport
|
||||||
j.promLastSuccessful.SetToCurrentTime()
|
})
|
||||||
}
|
|
||||||
|
|
||||||
endSpan()
|
endSpan()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,13 +8,12 @@ import (
|
|||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/config"
|
"github.com/zrepl/zrepl/config"
|
||||||
"github.com/zrepl/zrepl/util/bandwidthlimit"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func JobsFromConfig(c *config.Config, parseFlags config.ParseFlags) ([]Job, error) {
|
func JobsFromConfig(c *config.Config) ([]Job, error) {
|
||||||
js := make([]Job, len(c.Jobs))
|
js := make([]Job, len(c.Jobs))
|
||||||
for i := range c.Jobs {
|
for i := range c.Jobs {
|
||||||
j, err := buildJob(c.Global, c.Jobs[i], parseFlags)
|
j, err := buildJob(c.Global, c.Jobs[i])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -42,19 +41,19 @@ func JobsFromConfig(c *config.Config, parseFlags config.ParseFlags) ([]Job, erro
|
|||||||
return js, nil
|
return js, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildJob(c *config.Global, in config.JobEnum, parseFlags config.ParseFlags) (j Job, err error) {
|
func buildJob(c *config.Global, in config.JobEnum) (j Job, err error) {
|
||||||
cannotBuildJob := func(e error, name string) (Job, error) {
|
cannotBuildJob := func(e error, name string) (Job, error) {
|
||||||
return nil, errors.Wrapf(e, "cannot build job %q", name)
|
return nil, errors.Wrapf(e, "cannot build job %q", name)
|
||||||
}
|
}
|
||||||
// FIXME prettify this
|
// FIXME prettify this
|
||||||
switch v := in.Ret.(type) {
|
switch v := in.Ret.(type) {
|
||||||
case *config.SinkJob:
|
case *config.SinkJob:
|
||||||
j, err = passiveSideFromConfig(c, &v.PassiveJob, v, parseFlags)
|
j, err = passiveSideFromConfig(c, &v.PassiveJob, v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return cannotBuildJob(err, v.Name)
|
return cannotBuildJob(err, v.Name)
|
||||||
}
|
}
|
||||||
case *config.SourceJob:
|
case *config.SourceJob:
|
||||||
j, err = passiveSideFromConfig(c, &v.PassiveJob, v, parseFlags)
|
j, err = passiveSideFromConfig(c, &v.PassiveJob, v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return cannotBuildJob(err, v.Name)
|
return cannotBuildJob(err, v.Name)
|
||||||
}
|
}
|
||||||
@@ -64,12 +63,12 @@ func buildJob(c *config.Global, in config.JobEnum, parseFlags config.ParseFlags)
|
|||||||
return cannotBuildJob(err, v.Name)
|
return cannotBuildJob(err, v.Name)
|
||||||
}
|
}
|
||||||
case *config.PushJob:
|
case *config.PushJob:
|
||||||
j, err = activeSide(c, &v.ActiveJob, v, parseFlags)
|
j, err = activeSide(c, &v.ActiveJob, v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return cannotBuildJob(err, v.Name)
|
return cannotBuildJob(err, v.Name)
|
||||||
}
|
}
|
||||||
case *config.PullJob:
|
case *config.PullJob:
|
||||||
j, err = activeSide(c, &v.ActiveJob, v, parseFlags)
|
j, err = activeSide(c, &v.ActiveJob, v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return cannotBuildJob(err, v.Name)
|
return cannotBuildJob(err, v.Name)
|
||||||
}
|
}
|
||||||
@@ -108,13 +107,3 @@ func validateReceivingSidesDoNotOverlap(receivingRootFSs []string) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildBandwidthLimitConfig(in *config.BandwidthLimit) (c bandwidthlimit.Config, _ error) {
|
|
||||||
if in.Max.ToBytes() > 0 && int64(in.Max.ToBytes()) == 0 {
|
|
||||||
return c, fmt.Errorf("bandwidth limit `max` is too small, must at least specify one byte")
|
|
||||||
}
|
|
||||||
return bandwidthlimit.Config{
|
|
||||||
Max: int64(in.Max.ToBytes()),
|
|
||||||
BucketCapacity: int64(in.BucketCapacity.ToBytes()),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -22,12 +22,7 @@ func buildSenderConfig(in SendingJobConfig, jobID endpoint.JobID) (*endpoint.Sen
|
|||||||
return nil, errors.Wrap(err, "cannot build filesystem filter")
|
return nil, errors.Wrap(err, "cannot build filesystem filter")
|
||||||
}
|
}
|
||||||
sendOpts := in.GetSendOptions()
|
sendOpts := in.GetSendOptions()
|
||||||
bwlim, err := buildBandwidthLimitConfig(sendOpts.BandwidthLimit)
|
return &endpoint.SenderConfig{
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "cannot build bandwith limit config")
|
|
||||||
}
|
|
||||||
|
|
||||||
sc := &endpoint.SenderConfig{
|
|
||||||
FSF: fsf,
|
FSF: fsf,
|
||||||
JobID: jobID,
|
JobID: jobID,
|
||||||
|
|
||||||
@@ -39,15 +34,7 @@ func buildSenderConfig(in SendingJobConfig, jobID endpoint.JobID) (*endpoint.Sen
|
|||||||
SendCompressed: sendOpts.Compressed,
|
SendCompressed: sendOpts.Compressed,
|
||||||
SendEmbeddedData: sendOpts.EmbeddedData,
|
SendEmbeddedData: sendOpts.EmbeddedData,
|
||||||
SendSaved: sendOpts.Saved,
|
SendSaved: sendOpts.Saved,
|
||||||
|
}, nil
|
||||||
BandwidthLimit: bwlim,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := sc.Validate(); err != nil {
|
|
||||||
return nil, errors.Wrap(err, "cannot build sender config")
|
|
||||||
}
|
|
||||||
|
|
||||||
return sc, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ReceivingJobConfig interface {
|
type ReceivingJobConfig interface {
|
||||||
@@ -66,22 +53,6 @@ func buildReceiverConfig(in ReceivingJobConfig, jobID endpoint.JobID) (rc endpoi
|
|||||||
}
|
}
|
||||||
|
|
||||||
recvOpts := in.GetRecvOptions()
|
recvOpts := in.GetRecvOptions()
|
||||||
|
|
||||||
bwlim, err := buildBandwidthLimitConfig(recvOpts.BandwidthLimit)
|
|
||||||
if err != nil {
|
|
||||||
return rc, errors.Wrap(err, "cannot build bandwith limit config")
|
|
||||||
}
|
|
||||||
|
|
||||||
placeholderEncryption, err := endpoint.PlaceholderCreationEncryptionPropertyString(recvOpts.Placeholder.Encryption)
|
|
||||||
if err != nil {
|
|
||||||
options := []string{}
|
|
||||||
for _, v := range endpoint.PlaceholderCreationEncryptionPropertyValues() {
|
|
||||||
options = append(options, endpoint.PlaceholderCreationEncryptionProperty(v).String())
|
|
||||||
}
|
|
||||||
return rc, errors.Errorf("placeholder encryption value %q is invalid, must be one of %s",
|
|
||||||
recvOpts.Placeholder.Encryption, options)
|
|
||||||
}
|
|
||||||
|
|
||||||
rc = endpoint.ReceiverConfig{
|
rc = endpoint.ReceiverConfig{
|
||||||
JobID: jobID,
|
JobID: jobID,
|
||||||
RootWithoutClientComponent: rootFs,
|
RootWithoutClientComponent: rootFs,
|
||||||
@@ -89,10 +60,6 @@ func buildReceiverConfig(in ReceivingJobConfig, jobID endpoint.JobID) (rc endpoi
|
|||||||
|
|
||||||
InheritProperties: recvOpts.Properties.Inherit,
|
InheritProperties: recvOpts.Properties.Inherit,
|
||||||
OverrideProperties: recvOpts.Properties.Override,
|
OverrideProperties: recvOpts.Properties.Override,
|
||||||
|
|
||||||
BandwidthLimit: bwlim,
|
|
||||||
|
|
||||||
PlaceholderEncryption: placeholderEncryption,
|
|
||||||
}
|
}
|
||||||
if err := rc.Validate(); err != nil {
|
if err := rc.Validate(); err != nil {
|
||||||
return rc, errors.Wrap(err, "cannot build receiver config")
|
return rc, errors.Wrap(err, "cannot build receiver config")
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/config"
|
"github.com/zrepl/zrepl/config"
|
||||||
|
"github.com/zrepl/zrepl/transport/tls"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestValidateReceivingSidesDoNotOverlap(t *testing.T) {
|
func TestValidateReceivingSidesDoNotOverlap(t *testing.T) {
|
||||||
@@ -95,7 +96,7 @@ jobs:
|
|||||||
conf, err := config.ParseConfigBytes([]byte(fill(c.jobName)))
|
conf, err := config.ParseConfigBytes([]byte(fill(c.jobName)))
|
||||||
require.NoError(t, err, "not expecting yaml-config to know about job ids")
|
require.NoError(t, err, "not expecting yaml-config to know about job ids")
|
||||||
require.NotNil(t, conf)
|
require.NotNil(t, conf)
|
||||||
jobs, err := JobsFromConfig(conf, config.ParseFlagsNone)
|
jobs, err := JobsFromConfig(conf)
|
||||||
|
|
||||||
if c.valid {
|
if c.valid {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
@@ -118,14 +119,6 @@ func TestSampleConfigsAreBuiltWithoutErrors(t *testing.T) {
|
|||||||
t.Errorf("glob failed: %+v", err)
|
t.Errorf("glob failed: %+v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
type additionalCheck struct {
|
|
||||||
state int
|
|
||||||
test func(t *testing.T, jobs []Job)
|
|
||||||
}
|
|
||||||
additionalChecks := map[string]*additionalCheck{
|
|
||||||
"bandwidth_limit.yml": {test: testSampleConfig_BandwidthLimit},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, p := range paths {
|
for _, p := range paths {
|
||||||
|
|
||||||
if path.Ext(p) != ".yml" {
|
if path.Ext(p) != ".yml" {
|
||||||
@@ -133,78 +126,23 @@ func TestSampleConfigsAreBuiltWithoutErrors(t *testing.T) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
filename := path.Base(p)
|
|
||||||
t.Logf("checking for presence additonal checks for file %q", filename)
|
|
||||||
additionalCheck := additionalChecks[filename]
|
|
||||||
if additionalCheck == nil {
|
|
||||||
t.Logf("no additional checks")
|
|
||||||
} else {
|
|
||||||
t.Logf("additional check present")
|
|
||||||
additionalCheck.state = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run(p, func(t *testing.T) {
|
t.Run(p, func(t *testing.T) {
|
||||||
c, err := config.ParseConfig(p)
|
c, err := config.ParseConfig(p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("error parsing %s:\n%+v", p, err)
|
t.Errorf("error parsing %s:\n%+v", p, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Logf("file: %s", p)
|
t.Logf("file: %s", p)
|
||||||
t.Log(pretty.Sprint(c))
|
t.Log(pretty.Sprint(c))
|
||||||
|
|
||||||
jobs, err := JobsFromConfig(c, config.ParseFlagsNoCertCheck)
|
tls.FakeCertificateLoading(t)
|
||||||
|
jobs, err := JobsFromConfig(c)
|
||||||
t.Logf("jobs: %#v", jobs)
|
t.Logf("jobs: %#v", jobs)
|
||||||
require.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
if additionalCheck != nil {
|
|
||||||
additionalCheck.test(t, jobs)
|
|
||||||
additionalCheck.state = 2
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for basename, c := range additionalChecks {
|
|
||||||
if c.state == 0 {
|
|
||||||
panic("univisited additional check " + basename)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func testSampleConfig_BandwidthLimit(t *testing.T, jobs []Job) {
|
|
||||||
require.Len(t, jobs, 3)
|
|
||||||
|
|
||||||
{
|
|
||||||
limitedSink, ok := jobs[0].(*PassiveSide)
|
|
||||||
require.True(t, ok, "%T", jobs[0])
|
|
||||||
limitedSinkMode, ok := limitedSink.mode.(*modeSink)
|
|
||||||
require.True(t, ok, "%T", limitedSink)
|
|
||||||
|
|
||||||
assert.Equal(t, int64(12345), limitedSinkMode.receiverConfig.BandwidthLimit.Max)
|
|
||||||
assert.Equal(t, int64(1<<17), limitedSinkMode.receiverConfig.BandwidthLimit.BucketCapacity)
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
limitedPush, ok := jobs[1].(*ActiveSide)
|
|
||||||
require.True(t, ok, "%T", jobs[1])
|
|
||||||
limitedPushMode, ok := limitedPush.mode.(*modePush)
|
|
||||||
require.True(t, ok, "%T", limitedPush)
|
|
||||||
|
|
||||||
assert.Equal(t, int64(54321), limitedPushMode.senderConfig.BandwidthLimit.Max)
|
|
||||||
assert.Equal(t, int64(1024), limitedPushMode.senderConfig.BandwidthLimit.BucketCapacity)
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
unlimitedSink, ok := jobs[2].(*PassiveSide)
|
|
||||||
require.True(t, ok, "%T", jobs[2])
|
|
||||||
unlimitedSinkMode, ok := unlimitedSink.mode.(*modeSink)
|
|
||||||
require.True(t, ok, "%T", unlimitedSink)
|
|
||||||
|
|
||||||
max := unlimitedSinkMode.receiverConfig.BandwidthLimit.Max
|
|
||||||
assert.Less(t, max, int64(0), max, "unlimited mode <=> negative value for .Max, see bandwidthlimit.Config")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestReplicationOptions(t *testing.T) {
|
func TestReplicationOptions(t *testing.T) {
|
||||||
@@ -297,7 +235,7 @@ jobs:
|
|||||||
t.Logf("testing config:\n%s", cstr)
|
t.Logf("testing config:\n%s", cstr)
|
||||||
c, err := config.ParseConfigBytes([]byte(cstr))
|
c, err := config.ParseConfigBytes([]byte(cstr))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
jobs, err := JobsFromConfig(c, config.ParseFlagsNone)
|
jobs, err := JobsFromConfig(c)
|
||||||
if ts.expectOk != nil {
|
if ts.expectOk != nil {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotNil(t, c)
|
require.NotNil(t, c)
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ func GetLogger(ctx context.Context) Logger {
|
|||||||
return logging.GetLogger(ctx, logging.SubsysJob)
|
return logging.GetLogger(ctx, logging.SubsysJob)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
type Job interface {
|
type Job interface {
|
||||||
Name() string
|
Name() string
|
||||||
Run(ctx context.Context)
|
Run(ctx context.Context)
|
||||||
|
|||||||
@@ -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,11 +88,10 @@ 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{}) (s *PassiveSide, err error) {
|
||||||
|
|
||||||
s = &PassiveSide{}
|
s = &PassiveSide{}
|
||||||
|
|
||||||
@@ -111,7 +110,7 @@ func passiveSideFromConfig(g *config.Global, in *config.PassiveJob, configJob in
|
|||||||
return nil, err // no wrapping necessary
|
return nil, err // no wrapping necessary
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.listen, err = fromconfig.ListenerFactoryFromConfig(g, in.Serve, parseFlags); err != nil {
|
if s.listen, err = fromconfig.ListenerFactoryFromConfig(g, in.Serve); err != nil {
|
||||||
return nil, errors.Wrap(err, "cannot build listener factory")
|
return nil, errors.Wrap(err, "cannot build listener factory")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
package reset
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
type contextKey int
|
|
||||||
|
|
||||||
const contextKeyReset contextKey = iota
|
|
||||||
|
|
||||||
func Wait(ctx context.Context) <-chan struct{} {
|
|
||||||
wc, ok := ctx.Value(contextKeyReset).(chan struct{})
|
|
||||||
if !ok {
|
|
||||||
wc = make(chan struct{})
|
|
||||||
}
|
|
||||||
return wc
|
|
||||||
}
|
|
||||||
|
|
||||||
type Func func() error
|
|
||||||
|
|
||||||
var AlreadyReset = errors.New("already reset")
|
|
||||||
|
|
||||||
func Context(ctx context.Context) (context.Context, Func) {
|
|
||||||
wc := make(chan struct{})
|
|
||||||
wuf := func() error {
|
|
||||||
select {
|
|
||||||
case wc <- struct{}{}:
|
|
||||||
return nil
|
|
||||||
default:
|
|
||||||
return AlreadyReset
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return context.WithValue(ctx, contextKeyReset, wc), wuf
|
|
||||||
}
|
|
||||||
+8
-14
@@ -10,12 +10,10 @@ import (
|
|||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/daemon/logging/trace"
|
"github.com/zrepl/zrepl/daemon/logging/trace"
|
||||||
"github.com/zrepl/zrepl/util/bandwidthlimit"
|
|
||||||
"github.com/zrepl/zrepl/util/nodefault"
|
"github.com/zrepl/zrepl/util/nodefault"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/config"
|
"github.com/zrepl/zrepl/config"
|
||||||
"github.com/zrepl/zrepl/daemon/filters"
|
"github.com/zrepl/zrepl/daemon/filters"
|
||||||
"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"
|
||||||
"github.com/zrepl/zrepl/endpoint"
|
"github.com/zrepl/zrepl/endpoint"
|
||||||
@@ -26,7 +24,7 @@ import (
|
|||||||
type SnapJob struct {
|
type SnapJob struct {
|
||||||
name endpoint.JobID
|
name endpoint.JobID
|
||||||
fsfilter zfs.DatasetFilter
|
fsfilter zfs.DatasetFilter
|
||||||
snapper snapper.Snapper
|
snapper *snapper.PeriodicOrManual
|
||||||
|
|
||||||
prunerFactory *pruner.LocalPrunerFactory
|
prunerFactory *pruner.LocalPrunerFactory
|
||||||
|
|
||||||
@@ -86,8 +84,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}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,13 +111,13 @@ func (j *SnapJob) Run(ctx context.Context) {
|
|||||||
invocationCount := 0
|
invocationCount := 0
|
||||||
outer:
|
outer:
|
||||||
for {
|
for {
|
||||||
log.Info("wait for wakeups")
|
log.Info("wait for replications")
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
log.WithError(ctx.Err()).Info("context")
|
log.WithError(ctx.Err()).Info("context")
|
||||||
break outer
|
break outer
|
||||||
|
|
||||||
case <-wakeup.Wait(ctx):
|
// case <-doreplication.Wait(ctx):
|
||||||
case <-periodicDone:
|
case <-periodicDone:
|
||||||
}
|
}
|
||||||
invocationCount++
|
invocationCount++
|
||||||
@@ -138,7 +135,7 @@ outer:
|
|||||||
// TODO:
|
// TODO:
|
||||||
// This is a work-around for the current package daemon/pruner
|
// This is a work-around for the current package daemon/pruner
|
||||||
// and package pruning.Snapshot limitation: they require the
|
// and package pruning.Snapshot limitation: they require the
|
||||||
// `Replicated` getter method be present, but obviously,
|
// `Replicated` getter method be present, but obviously,
|
||||||
// a local job like SnapJob can't deliver on that.
|
// a local job like SnapJob can't deliver on that.
|
||||||
// But the pruner.Pruner gives up on an FS if no replication
|
// But the pruner.Pruner gives up on an FS if no replication
|
||||||
// cursor is present, which is why this pruner returns the
|
// cursor is present, which is why this pruner returns the
|
||||||
@@ -148,7 +145,7 @@ type alwaysUpToDateReplicationCursorHistory struct {
|
|||||||
target pruner.Target
|
target pruner.Target
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ pruner.Sender = (*alwaysUpToDateReplicationCursorHistory)(nil)
|
var _ pruner.History = (*alwaysUpToDateReplicationCursorHistory)(nil)
|
||||||
|
|
||||||
func (h alwaysUpToDateReplicationCursorHistory) ReplicationCursor(ctx context.Context, req *pdu.ReplicationCursorReq) (*pdu.ReplicationCursorRes, error) {
|
func (h alwaysUpToDateReplicationCursorHistory) ReplicationCursor(ctx context.Context, req *pdu.ReplicationCursorReq) (*pdu.ReplicationCursorRes, error) {
|
||||||
fsvReq := &pdu.ListFilesystemVersionsReq{
|
fsvReq := &pdu.ListFilesystemVersionsReq{
|
||||||
@@ -181,11 +178,8 @@ func (j *SnapJob) doPrune(ctx context.Context) {
|
|||||||
sender := endpoint.NewSender(endpoint.SenderConfig{
|
sender := endpoint.NewSender(endpoint.SenderConfig{
|
||||||
JobID: j.name,
|
JobID: j.name,
|
||||||
FSF: j.fsfilter,
|
FSF: j.fsfilter,
|
||||||
// FIXME the following config fields are irrelevant for SnapJob
|
// FIXME encryption setting is irrelevant for SnapJob because the endpoint is only used as pruner.Target
|
||||||
// because the endpoint is only used as pruner.Target.
|
Encrypt: &nodefault.Bool{B: true},
|
||||||
// However, the implementation requires them to be set.
|
|
||||||
Encrypt: &nodefault.Bool{B: true},
|
|
||||||
BandwidthLimit: bandwidthlimit.NoLimitConfig(),
|
|
||||||
})
|
})
|
||||||
j.prunerMtx.Lock()
|
j.prunerMtx.Lock()
|
||||||
j.pruner = j.prunerFactory.BuildLocalPruner(ctx, sender, alwaysUpToDateReplicationCursorHistory{sender})
|
j.pruner = j.prunerFactory.BuildLocalPruner(ctx, sender, alwaysUpToDateReplicationCursorHistory{sender})
|
||||||
|
|||||||
@@ -0,0 +1,184 @@
|
|||||||
|
//
|
||||||
|
//
|
||||||
|
// Alternative Design (in "RustGo")
|
||||||
|
//
|
||||||
|
// enum InternalMsg {
|
||||||
|
// Trigger((), chan (TriggerResponse, error)),
|
||||||
|
// Poll(PollRequest, chan PollResponse),
|
||||||
|
// Reset(ResetRequest, chan (ResetResponse, error)),
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// enum State {
|
||||||
|
// Running{
|
||||||
|
// invocationId: u32,
|
||||||
|
// cancelCurrentInvocation: context.CancelFunc
|
||||||
|
// }
|
||||||
|
// Waiting{
|
||||||
|
// nextInvocationId: u32,
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// for msg := <- t.internalMsgs {
|
||||||
|
// match (msg, state) {
|
||||||
|
// ...
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
package trigger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type T struct {
|
||||||
|
mtx sync.Mutex
|
||||||
|
cv sync.Cond
|
||||||
|
|
||||||
|
nextInvocationId uint64
|
||||||
|
activeInvocationId uint64 // 0 <=> inactive
|
||||||
|
triggerPending bool
|
||||||
|
contextDone bool
|
||||||
|
reset chan uint64
|
||||||
|
stopWaitForReset chan struct{}
|
||||||
|
cancelCurrentInvocation context.CancelFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
func New() *T {
|
||||||
|
t := &T{
|
||||||
|
activeInvocationId: math.MaxUint64,
|
||||||
|
nextInvocationId: 1,
|
||||||
|
}
|
||||||
|
t.cv.L = &t.mtx
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *T) WaitForTrigger(ctx context.Context) (rctx context.Context, err error) {
|
||||||
|
t.mtx.Lock()
|
||||||
|
defer t.mtx.Unlock()
|
||||||
|
|
||||||
|
if t.activeInvocationId == 0 {
|
||||||
|
return nil, fmt.Errorf("must be running when calling this function")
|
||||||
|
}
|
||||||
|
t.activeInvocationId = 0
|
||||||
|
t.cancelCurrentInvocation = nil
|
||||||
|
|
||||||
|
if t.contextDone == true {
|
||||||
|
panic("implementation error: this variable is only true while in WaitForTrigger, and that's a mutually exclusive function")
|
||||||
|
}
|
||||||
|
stopWaitingForDone := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
select {
|
||||||
|
case <-stopWaitingForDone:
|
||||||
|
case <-ctx.Done():
|
||||||
|
t.mtx.Lock()
|
||||||
|
t.contextDone = true
|
||||||
|
t.cv.Broadcast()
|
||||||
|
t.mtx.Unlock()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
t.triggerPending = false
|
||||||
|
t.contextDone = false
|
||||||
|
}()
|
||||||
|
for !t.triggerPending && !t.contextDone {
|
||||||
|
t.cv.Wait()
|
||||||
|
}
|
||||||
|
close(stopWaitingForDone)
|
||||||
|
if t.contextDone {
|
||||||
|
if ctx.Err() == nil {
|
||||||
|
panic("implementation error: contextDone <=> ctx.Err() != nil")
|
||||||
|
}
|
||||||
|
return nil, ctx.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
t.activeInvocationId = t.nextInvocationId
|
||||||
|
t.nextInvocationId++
|
||||||
|
rctx, t.cancelCurrentInvocation = context.WithCancel(ctx)
|
||||||
|
|
||||||
|
return rctx, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type TriggerResponse struct {
|
||||||
|
InvocationId uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *T) Trigger() (TriggerResponse, error) {
|
||||||
|
t.mtx.Lock()
|
||||||
|
defer t.mtx.Unlock()
|
||||||
|
var invocationId uint64
|
||||||
|
if t.activeInvocationId != 0 {
|
||||||
|
invocationId = t.activeInvocationId
|
||||||
|
} else {
|
||||||
|
invocationId = t.nextInvocationId
|
||||||
|
}
|
||||||
|
// non-blocking send (.Run() must not hold mutex while waiting for signals)
|
||||||
|
t.triggerPending = true
|
||||||
|
t.cv.Broadcast()
|
||||||
|
return TriggerResponse{InvocationId: invocationId}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type PollRequest struct {
|
||||||
|
InvocationId uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
type PollResponse struct {
|
||||||
|
Done bool
|
||||||
|
InvocationId uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *T) Poll(req PollRequest) (res PollResponse) {
|
||||||
|
t.mtx.Lock()
|
||||||
|
defer t.mtx.Unlock()
|
||||||
|
|
||||||
|
waitForId := req.InvocationId
|
||||||
|
if req.InvocationId == 0 {
|
||||||
|
// handle the case where the client doesn't know what the current invocation id is
|
||||||
|
if t.activeInvocationId != 0 {
|
||||||
|
waitForId = t.activeInvocationId
|
||||||
|
} else {
|
||||||
|
waitForId = t.nextInvocationId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var done bool
|
||||||
|
if t.activeInvocationId == 0 {
|
||||||
|
done = waitForId < t.nextInvocationId
|
||||||
|
} else {
|
||||||
|
done = waitForId < t.activeInvocationId
|
||||||
|
}
|
||||||
|
return PollResponse{Done: done, InvocationId: waitForId}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ResetRequest struct {
|
||||||
|
InvocationId uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
type ResetResponse struct {
|
||||||
|
InvocationId uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *T) Reset(req ResetRequest) (*ResetResponse, error) {
|
||||||
|
t.mtx.Lock()
|
||||||
|
defer t.mtx.Unlock()
|
||||||
|
|
||||||
|
resetId := req.InvocationId
|
||||||
|
if req.InvocationId == 0 {
|
||||||
|
// handle the case where the client doesn't know what the current invocation id is
|
||||||
|
resetId = t.activeInvocationId
|
||||||
|
}
|
||||||
|
|
||||||
|
if resetId == 0 {
|
||||||
|
return nil, fmt.Errorf("no active invocation")
|
||||||
|
}
|
||||||
|
|
||||||
|
if resetId != t.activeInvocationId {
|
||||||
|
return nil, fmt.Errorf("active invocation (%d) is not the invocation requested for reset (%d); (active invocation '0' indicates no active invocation)", t.activeInvocationId, resetId)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.cancelCurrentInvocation()
|
||||||
|
|
||||||
|
return &ResetResponse{InvocationId: resetId}, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,205 @@
|
|||||||
|
package trigger_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/zrepl/zrepl/daemon/job/trigger"
|
||||||
|
"github.com/zrepl/zrepl/daemon/logging/trace"
|
||||||
|
"github.com/zrepl/zrepl/replication"
|
||||||
|
"github.com/zrepl/zrepl/replication/driver"
|
||||||
|
"github.com/zrepl/zrepl/replication/logic"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBasics(t *testing.T) {
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
defer wg.Wait()
|
||||||
|
|
||||||
|
tr := trigger.New()
|
||||||
|
|
||||||
|
triggered := make(chan int)
|
||||||
|
waitForTriggerError := make(chan error)
|
||||||
|
waitForResetCallToBeMadeByMainGoroutine := make(chan struct{})
|
||||||
|
postResetAssertionsDone := make(chan struct{})
|
||||||
|
|
||||||
|
taskCtx := context.Background()
|
||||||
|
taskCtx, cancelTaskCtx := context.WithCancel(taskCtx)
|
||||||
|
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
taskCtx := context.WithValue(taskCtx, "mykey", "myvalue")
|
||||||
|
|
||||||
|
triggers := 0
|
||||||
|
|
||||||
|
outer:
|
||||||
|
for {
|
||||||
|
invocationCtx, err := tr.WaitForTrigger(taskCtx)
|
||||||
|
if err != nil {
|
||||||
|
waitForTriggerError <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
require.Equal(t, invocationCtx.Value("mykey"), "myvalue")
|
||||||
|
|
||||||
|
triggers++
|
||||||
|
triggered <- triggers
|
||||||
|
|
||||||
|
switch triggers {
|
||||||
|
case 1:
|
||||||
|
continue outer
|
||||||
|
case 2:
|
||||||
|
<-waitForResetCallToBeMadeByMainGoroutine
|
||||||
|
require.Equal(t, context.Canceled, invocationCtx.Err(), "Reset() cancels invocation context")
|
||||||
|
require.Nil(t, taskCtx.Err(), "Reset() does not cancel task context")
|
||||||
|
close(postResetAssertionsDone)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}()
|
||||||
|
|
||||||
|
t.Logf("trigger 1")
|
||||||
|
_, err := tr.Trigger()
|
||||||
|
require.NoError(t, err)
|
||||||
|
v := <-triggered
|
||||||
|
require.Equal(t, 1, v)
|
||||||
|
|
||||||
|
t.Logf("trigger 2")
|
||||||
|
triggerResponse, err := tr.Trigger()
|
||||||
|
require.NoError(t, err)
|
||||||
|
v = <-triggered
|
||||||
|
require.Equal(t, 2, v)
|
||||||
|
|
||||||
|
t.Logf("reset")
|
||||||
|
resetResponse, err := tr.Reset(trigger.ResetRequest{InvocationId: triggerResponse.InvocationId})
|
||||||
|
require.NoError(t, err)
|
||||||
|
t.Logf("reset response: %#v", resetResponse)
|
||||||
|
close(waitForResetCallToBeMadeByMainGoroutine)
|
||||||
|
<-postResetAssertionsDone
|
||||||
|
|
||||||
|
t.Logf("cancel the context")
|
||||||
|
cancelTaskCtx()
|
||||||
|
wfte := <-waitForTriggerError
|
||||||
|
require.Equal(t, taskCtx.Err(), wfte)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
type PushJob struct {
|
||||||
|
snap *Snapshotter
|
||||||
|
repl *ReplicationAndTriggerRemotePruningSequence
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *PushJob) Handle(w http.ResponseWriter, r *http.Request) {
|
||||||
|
panic("unimplemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *PushJob) HandleTrigger(w http.ResponseWriter, r *http.Request) {
|
||||||
|
panic("unimplemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *PushJob) Run(ctx context.Context) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
type SnapshotSequence struct {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
type ReplicationAndTriggerRemotePruningSequence struct {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s ReplicationAndTriggerRemotePruningSequence) Run(ctx context.Context) {
|
||||||
|
|
||||||
|
j.mode.ConnectEndpoints(ctx, j.connecter)
|
||||||
|
defer j.mode.DisconnectEndpoints()
|
||||||
|
|
||||||
|
sender, receiver := j.mode.SenderReceiver()
|
||||||
|
|
||||||
|
{
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
ctx, endSpan := trace.WithSpan(ctx, "replication")
|
||||||
|
ctx, repCancel := context.WithCancel(ctx)
|
||||||
|
var repWait driver.WaitFunc
|
||||||
|
j.updateTasks(func(tasks *activeSideTasks) {
|
||||||
|
// reset it
|
||||||
|
*tasks = activeSideTasks{}
|
||||||
|
tasks.replicationCancel = func() { repCancel(); endSpan() }
|
||||||
|
tasks.replicationReport, repWait = replication.Do(
|
||||||
|
ctx, j.replicationDriverConfig, logic.NewPlanner(j.promRepStateSecs, j.promBytesReplicated, sender, receiver, j.mode.PlannerPolicy()),
|
||||||
|
)
|
||||||
|
tasks.state = ActiveSideReplicating
|
||||||
|
})
|
||||||
|
GetLogger(ctx).Info("start replication")
|
||||||
|
repWait(true) // wait blocking
|
||||||
|
repCancel() // always cancel to free up context resources
|
||||||
|
replicationReport := j.tasks.replicationReport()
|
||||||
|
j.promReplicationErrors.Set(float64(replicationReport.GetFailedFilesystemsCountInLatestAttempt()))
|
||||||
|
j.updateTasks(func(tasks *activeSideTasks) {
|
||||||
|
tasks.replicationDone = replicationReport
|
||||||
|
})
|
||||||
|
endSpan()
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
ctx, endSpan := trace.WithSpan(ctx, "prune_sender")
|
||||||
|
ctx, senderCancel := context.WithCancel(ctx)
|
||||||
|
tasks := j.updateTasks(func(tasks *activeSideTasks) {
|
||||||
|
tasks.prunerSender = j.prunerFactory.BuildSenderPruner(ctx, sender, sender)
|
||||||
|
tasks.prunerSenderCancel = func() { senderCancel(); endSpan() }
|
||||||
|
tasks.state = ActiveSidePruneSender
|
||||||
|
})
|
||||||
|
GetLogger(ctx).Info("start pruning sender")
|
||||||
|
tasks.prunerSender.Prune()
|
||||||
|
GetLogger(ctx).Info("finished pruning sender")
|
||||||
|
senderCancel()
|
||||||
|
endSpan()
|
||||||
|
}
|
||||||
|
{
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
ctx, endSpan := trace.WithSpan(ctx, "prune_recever")
|
||||||
|
ctx, receiverCancel := context.WithCancel(ctx)
|
||||||
|
tasks := j.updateTasks(func(tasks *activeSideTasks) {
|
||||||
|
tasks.prunerReceiver = j.prunerFactory.BuildReceiverPruner(ctx, receiver, sender)
|
||||||
|
tasks.prunerReceiverCancel = func() { receiverCancel(); endSpan() }
|
||||||
|
tasks.state = ActiveSidePruneReceiver
|
||||||
|
})
|
||||||
|
GetLogger(ctx).Info("start pruning receiver")
|
||||||
|
tasks.prunerReceiver.Prune()
|
||||||
|
GetLogger(ctx).Info("finished pruning receiver")
|
||||||
|
receiverCancel()
|
||||||
|
endSpan()
|
||||||
|
}
|
||||||
|
|
||||||
|
j.updateTasks(func(tasks *activeSideTasks) {
|
||||||
|
tasks.state = ActiveSideDone
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func TestUseCase(t *testing.T) {
|
||||||
|
|
||||||
|
var as ActiveSide
|
||||||
|
|
||||||
|
as.
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
package wakeup
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
type contextKey int
|
|
||||||
|
|
||||||
const contextKeyWakeup contextKey = iota
|
|
||||||
|
|
||||||
func Wait(ctx context.Context) <-chan struct{} {
|
|
||||||
wc, ok := ctx.Value(contextKeyWakeup).(chan struct{})
|
|
||||||
if !ok {
|
|
||||||
wc = make(chan struct{})
|
|
||||||
}
|
|
||||||
return wc
|
|
||||||
}
|
|
||||||
|
|
||||||
type Func func() error
|
|
||||||
|
|
||||||
var AlreadyWokenUp = errors.New("already woken up")
|
|
||||||
|
|
||||||
func Context(ctx context.Context) (context.Context, Func) {
|
|
||||||
wc := make(chan struct{})
|
|
||||||
wuf := func() error {
|
|
||||||
select {
|
|
||||||
case wc <- struct{}{}:
|
|
||||||
return nil
|
|
||||||
default:
|
|
||||||
return AlreadyWokenUp
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return context.WithValue(ctx, contextKeyWakeup, wc), wuf
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
// package trace provides activity tracing via ctx through Tasks and Spans
|
// package trace provides activity tracing via ctx through Tasks and Spans
|
||||||
//
|
//
|
||||||
// # Basic Concepts
|
// Basic Concepts
|
||||||
//
|
//
|
||||||
// Tracing can be used to identify where a piece of code spends its time.
|
// Tracing can be used to identify where a piece of code spends its time.
|
||||||
//
|
//
|
||||||
@@ -10,50 +10,51 @@
|
|||||||
// to tech-savvy users (albeit not developers).
|
// to tech-savvy users (albeit not developers).
|
||||||
//
|
//
|
||||||
// This package provides the concept of Tasks and Spans to express what activity is happening within an application:
|
// This package provides the concept of Tasks and Spans to express what activity is happening within an application:
|
||||||
// - Neither task nor span is really tangible but instead contained within the context.Context tree
|
//
|
||||||
// - Tasks represent concurrent activity (i.e. goroutines).
|
// - Neither task nor span is really tangible but instead contained within the context.Context tree
|
||||||
// - Spans represent a semantic stack trace within a task.
|
// - Tasks represent concurrent activity (i.e. goroutines).
|
||||||
|
// - Spans represent a semantic stack trace within a task.
|
||||||
|
//
|
||||||
// As a consequence, whenever a context is propagated across goroutine boundary, you need to create a child task:
|
// As a consequence, whenever a context is propagated across goroutine boundary, you need to create a child task:
|
||||||
//
|
//
|
||||||
// go func(ctx context.Context) {
|
// go func(ctx context.Context) {
|
||||||
// ctx, endTask = WithTask(ctx, "what-happens-inside-the-child-task")
|
// ctx, endTask = WithTask(ctx, "what-happens-inside-the-child-task")
|
||||||
// defer endTask()
|
// defer endTask()
|
||||||
// // ...
|
// // ...
|
||||||
// }(ctx)
|
// }(ctx)
|
||||||
//
|
//
|
||||||
// Within the task, you can open up a hierarchy of spans.
|
// Within the task, you can open up a hierarchy of spans.
|
||||||
// In contrast to tasks, which have can multiple concurrently running child tasks,
|
// In contrast to tasks, which have can multiple concurrently running child tasks,
|
||||||
// spans must nest and not cross the goroutine boundary.
|
// spans must nest and not cross the goroutine boundary.
|
||||||
//
|
//
|
||||||
// ctx, endSpan = WithSpan(ctx, "copy-dir")
|
// ctx, endSpan = WithSpan(ctx, "copy-dir")
|
||||||
// defer endSpan()
|
// defer endSpan()
|
||||||
// for _, f := range dir.Files() {
|
// for _, f := range dir.Files() {
|
||||||
// func() {
|
// func() {
|
||||||
// ctx, endSpan := WithSpan(ctx, fmt.Sprintf("copy-file %q", f))
|
// ctx, endSpan := WithSpan(ctx, fmt.Sprintf("copy-file %q", f))
|
||||||
// defer endspan()
|
// defer endspan()
|
||||||
// b, _ := ioutil.ReadFile(f)
|
// b, _ := ioutil.ReadFile(f)
|
||||||
// _ = ioutil.WriteFile(f + ".copy", b, 0600)
|
// _ = ioutil.WriteFile(f + ".copy", b, 0600)
|
||||||
// }()
|
// }()
|
||||||
// }
|
// }
|
||||||
//
|
//
|
||||||
// In combination:
|
// In combination:
|
||||||
//
|
// ctx, endTask = WithTask(ctx, "copy-dirs")
|
||||||
// ctx, endTask = WithTask(ctx, "copy-dirs")
|
// defer endTask()
|
||||||
// defer endTask()
|
// for i := range dirs {
|
||||||
// for i := range dirs {
|
// go func(dir string) {
|
||||||
// go func(dir string) {
|
// ctx, endTask := WithTask(ctx, "copy-dir")
|
||||||
// ctx, endTask := WithTask(ctx, "copy-dir")
|
// defer endTask()
|
||||||
// defer endTask()
|
// for _, f := range filesIn(dir) {
|
||||||
// for _, f := range filesIn(dir) {
|
// func() {
|
||||||
// func() {
|
// ctx, endSpan := WithSpan(ctx, fmt.Sprintf("copy-file %q", f))
|
||||||
// ctx, endSpan := WithSpan(ctx, fmt.Sprintf("copy-file %q", f))
|
// defer endspan()
|
||||||
// defer endspan()
|
// b, _ := ioutil.ReadFile(f)
|
||||||
// b, _ := ioutil.ReadFile(f)
|
// _ = ioutil.WriteFile(f + ".copy", b, 0600)
|
||||||
// _ = ioutil.WriteFile(f + ".copy", b, 0600)
|
// }()
|
||||||
// }()
|
// }
|
||||||
// }
|
// }()
|
||||||
// }()
|
// }
|
||||||
// }
|
|
||||||
//
|
//
|
||||||
// Note that a span ends at the time you call endSpan - not before and not after that.
|
// Note that a span ends at the time you call endSpan - not before and not after that.
|
||||||
// If you violate the stack-like nesting of spans by forgetting an endSpan() invocation,
|
// If you violate the stack-like nesting of spans by forgetting an endSpan() invocation,
|
||||||
@@ -64,7 +65,8 @@
|
|||||||
//
|
//
|
||||||
// Recovering from endSpan() or endTask() panics will corrupt the trace stack and lead to corrupt tracefile output.
|
// Recovering from endSpan() or endTask() panics will corrupt the trace stack and lead to corrupt tracefile output.
|
||||||
//
|
//
|
||||||
// # Best Practices For Naming Tasks And Spans
|
//
|
||||||
|
// Best Practices For Naming Tasks And Spans
|
||||||
//
|
//
|
||||||
// Tasks should always have string constants as names, and must not contain the `#` character. WHy?
|
// Tasks should always have string constants as names, and must not contain the `#` character. WHy?
|
||||||
// First, the visualization by chrome://tracing draws a horizontal bar for each task in the trace.
|
// First, the visualization by chrome://tracing draws a horizontal bar for each task in the trace.
|
||||||
@@ -72,7 +74,8 @@
|
|||||||
// Note that the `#NUM` suffix will be reused if a task has ended, in order to avoid an
|
// Note that the `#NUM` suffix will be reused if a task has ended, in order to avoid an
|
||||||
// infinite number of horizontal bars in the visualization.
|
// infinite number of horizontal bars in the visualization.
|
||||||
//
|
//
|
||||||
// # Chrome-compatible Tracefile Support
|
//
|
||||||
|
// Chrome-compatible Tracefile Support
|
||||||
//
|
//
|
||||||
// The activity trace generated by usage of WithTask and WithSpan can be rendered to a JSON output file
|
// The activity trace generated by usage of WithTask and WithSpan can be rendered to a JSON output file
|
||||||
// that can be loaded into chrome://tracing .
|
// that can be loaded into chrome://tracing .
|
||||||
@@ -243,7 +246,7 @@ func WithTask(ctx context.Context, taskName string) (context.Context, DoneFunc)
|
|||||||
// the debugString can be quite long and panic won't print it completely
|
// the debugString can be quite long and panic won't print it completely
|
||||||
fmt.Fprintf(os.Stderr, "going to panic due to activeChildTasks:\n%s\n", this.debugString())
|
fmt.Fprintf(os.Stderr, "going to panic due to activeChildTasks:\n%s\n", this.debugString())
|
||||||
}
|
}
|
||||||
panic(errors.WithMessagef(ErrTaskStillHasActiveChildTasks, "end task: %v active child tasks (run daemon with env var %s=1 for more details)\n", this.activeChildTasks, debugEnabledEnvVar))
|
panic(errors.WithMessagef(ErrTaskStillHasActiveChildTasks, "end task: %v active child tasks\n", this.activeChildTasks))
|
||||||
}
|
}
|
||||||
|
|
||||||
// support idempotent task ends
|
// support idempotent task ends
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -7,9 +7,7 @@ import (
|
|||||||
"github.com/zrepl/zrepl/util/envconst"
|
"github.com/zrepl/zrepl/util/envconst"
|
||||||
)
|
)
|
||||||
|
|
||||||
const debugEnabledEnvVar = "ZREPL_TRACE_DEBUG_ENABLED"
|
var debugEnabled = envconst.Bool("ZREPL_TRACE_DEBUG_ENABLED", false)
|
||||||
|
|
||||||
var debugEnabled = envconst.Bool(debugEnabledEnvVar, false)
|
|
||||||
|
|
||||||
func debug(format string, args ...interface{}) {
|
func debug(format string, args ...interface{}) {
|
||||||
if !debugEnabled {
|
if !debugEnabled {
|
||||||
|
|||||||
+12
-30
@@ -19,20 +19,12 @@ import (
|
|||||||
"github.com/zrepl/zrepl/util/envconst"
|
"github.com/zrepl/zrepl/util/envconst"
|
||||||
)
|
)
|
||||||
|
|
||||||
// The sender in the replication setup.
|
|
||||||
// The pruner uses the Sender to determine which of the Target's filesystems need to be pruned.
|
|
||||||
// Also, it asks the Sender about the replication cursor of each filesystem
|
|
||||||
// to enable the 'not_replicated' pruning rule.
|
|
||||||
//
|
|
||||||
// Try to keep it compatible with github.com/zrepl/zrepl/endpoint.Endpoint
|
// Try to keep it compatible with github.com/zrepl/zrepl/endpoint.Endpoint
|
||||||
type Sender interface {
|
type History interface {
|
||||||
ReplicationCursor(ctx context.Context, req *pdu.ReplicationCursorReq) (*pdu.ReplicationCursorRes, error)
|
ReplicationCursor(ctx context.Context, req *pdu.ReplicationCursorReq) (*pdu.ReplicationCursorRes, error)
|
||||||
ListFilesystems(ctx context.Context, req *pdu.ListFilesystemReq) (*pdu.ListFilesystemRes, error)
|
ListFilesystems(ctx context.Context, req *pdu.ListFilesystemReq) (*pdu.ListFilesystemRes, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// The pruning target, i.e., on which snapshots are destroyed.
|
|
||||||
// This can be a replication sender or receiver.
|
|
||||||
//
|
|
||||||
// Try to keep it compatible with github.com/zrepl/zrepl/endpoint.Endpoint
|
// Try to keep it compatible with github.com/zrepl/zrepl/endpoint.Endpoint
|
||||||
type Target interface {
|
type Target interface {
|
||||||
ListFilesystems(ctx context.Context, req *pdu.ListFilesystemReq) (*pdu.ListFilesystemRes, error)
|
ListFilesystems(ctx context.Context, req *pdu.ListFilesystemReq) (*pdu.ListFilesystemRes, error)
|
||||||
@@ -56,7 +48,7 @@ func GetLogger(ctx context.Context) Logger {
|
|||||||
type args struct {
|
type args struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
target Target
|
target Target
|
||||||
sender Sender
|
receiver History
|
||||||
rules []pruning.KeepRule
|
rules []pruning.KeepRule
|
||||||
retryWait time.Duration
|
retryWait time.Duration
|
||||||
considerSnapAtCursorReplicated bool
|
considerSnapAtCursorReplicated bool
|
||||||
@@ -140,12 +132,12 @@ func NewPrunerFactory(in config.PruningSenderReceiver, promPruneSecs *prometheus
|
|||||||
return f, nil
|
return f, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *PrunerFactory) BuildSenderPruner(ctx context.Context, target Target, sender Sender) *Pruner {
|
func (f *PrunerFactory) BuildSenderPruner(ctx context.Context, target Target, receiver History) *Pruner {
|
||||||
p := &Pruner{
|
p := &Pruner{
|
||||||
args: args{
|
args: args{
|
||||||
context.WithValue(ctx, contextKeyPruneSide, "sender"),
|
context.WithValue(ctx, contextKeyPruneSide, "sender"),
|
||||||
target,
|
target,
|
||||||
sender,
|
receiver,
|
||||||
f.senderRules,
|
f.senderRules,
|
||||||
f.retryWait,
|
f.retryWait,
|
||||||
f.considerSnapAtCursorReplicated,
|
f.considerSnapAtCursorReplicated,
|
||||||
@@ -156,12 +148,12 @@ func (f *PrunerFactory) BuildSenderPruner(ctx context.Context, target Target, se
|
|||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *PrunerFactory) BuildReceiverPruner(ctx context.Context, target Target, sender Sender) *Pruner {
|
func (f *PrunerFactory) BuildReceiverPruner(ctx context.Context, target Target, receiver History) *Pruner {
|
||||||
p := &Pruner{
|
p := &Pruner{
|
||||||
args: args{
|
args: args{
|
||||||
context.WithValue(ctx, contextKeyPruneSide, "receiver"),
|
context.WithValue(ctx, contextKeyPruneSide, "receiver"),
|
||||||
target,
|
target,
|
||||||
sender,
|
receiver,
|
||||||
f.receiverRules,
|
f.receiverRules,
|
||||||
f.retryWait,
|
f.retryWait,
|
||||||
false, // senseless here anyways
|
false, // senseless here anyways
|
||||||
@@ -172,12 +164,12 @@ func (f *PrunerFactory) BuildReceiverPruner(ctx context.Context, target Target,
|
|||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *LocalPrunerFactory) BuildLocalPruner(ctx context.Context, target Target, history Sender) *Pruner {
|
func (f *LocalPrunerFactory) BuildLocalPruner(ctx context.Context, target Target, receiver History) *Pruner {
|
||||||
p := &Pruner{
|
p := &Pruner{
|
||||||
args: args{
|
args: args{
|
||||||
context.WithValue(ctx, contextKeyPruneSide, "local"),
|
context.WithValue(ctx, contextKeyPruneSide, "local"),
|
||||||
target,
|
target,
|
||||||
history,
|
receiver,
|
||||||
f.keepRules,
|
f.keepRules,
|
||||||
f.retryWait,
|
f.retryWait,
|
||||||
false, // considerSnapAtCursorReplicated is not relevant for local pruning
|
false, // considerSnapAtCursorReplicated is not relevant for local pruning
|
||||||
@@ -199,16 +191,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() {
|
||||||
@@ -361,9 +343,9 @@ func (s snapshot) Date() time.Time { return s.date }
|
|||||||
|
|
||||||
func doOneAttempt(a *args, u updater) {
|
func doOneAttempt(a *args, u updater) {
|
||||||
|
|
||||||
ctx, target, sender := a.ctx, a.target, a.sender
|
ctx, target, receiver := a.ctx, a.target, a.receiver
|
||||||
|
|
||||||
sfssres, err := sender.ListFilesystems(ctx, &pdu.ListFilesystemReq{})
|
sfssres, err := receiver.ListFilesystems(ctx, &pdu.ListFilesystemReq{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
u(func(p *Pruner) {
|
u(func(p *Pruner) {
|
||||||
p.state = PlanErr
|
p.state = PlanErr
|
||||||
@@ -428,7 +410,7 @@ tfss_loop:
|
|||||||
rcReq := &pdu.ReplicationCursorReq{
|
rcReq := &pdu.ReplicationCursorReq{
|
||||||
Filesystem: tfs.Path,
|
Filesystem: tfs.Path,
|
||||||
}
|
}
|
||||||
rc, err := sender.ReplicationCursor(ctx, rcReq)
|
rc, err := receiver.ReplicationCursor(ctx, rcReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
pfsPlanErrAndLog(err, "cannot get replication cursor bookmark")
|
pfsPlanErrAndLog(err, "cannot get replication cursor bookmark")
|
||||||
continue tfss_loop
|
continue tfss_loop
|
||||||
@@ -474,7 +456,7 @@ tfss_loop:
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
if preCursor {
|
if preCursor {
|
||||||
pfsPlanErrAndLog(fmt.Errorf("prune target has no snapshot that corresponds to sender replication cursor bookmark"), "")
|
pfsPlanErrAndLog(fmt.Errorf("replication cursor not found in prune target filesystem versions"), "")
|
||||||
continue tfss_loop
|
continue tfss_loop
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,152 +0,0 @@
|
|||||||
package snapper
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/config"
|
|
||||||
"github.com/zrepl/zrepl/daemon/hooks"
|
|
||||||
"github.com/zrepl/zrepl/util/suspendresumesafetimer"
|
|
||||||
"github.com/zrepl/zrepl/zfs"
|
|
||||||
)
|
|
||||||
|
|
||||||
func cronFromConfig(fsf zfs.DatasetFilter, in config.SnapshottingCron) (*Cron, error) {
|
|
||||||
|
|
||||||
hooksList, err := hooks.ListFromConfig(&in.Hooks)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "hook config error")
|
|
||||||
}
|
|
||||||
planArgs := planArgs{
|
|
||||||
prefix: in.Prefix,
|
|
||||||
timestampFormat: in.TimestampFormat,
|
|
||||||
hooks: hooksList,
|
|
||||||
}
|
|
||||||
return &Cron{config: in, fsf: fsf, planArgs: planArgs}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type Cron struct {
|
|
||||||
config config.SnapshottingCron
|
|
||||||
fsf zfs.DatasetFilter
|
|
||||||
planArgs planArgs
|
|
||||||
|
|
||||||
mtx sync.RWMutex
|
|
||||||
|
|
||||||
running bool
|
|
||||||
wakeupTime time.Time // zero value means uninit
|
|
||||||
lastError error
|
|
||||||
lastPlan *plan
|
|
||||||
wakeupWhileRunningCount int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Cron) Run(ctx context.Context, snapshotsTaken chan<- struct{}) {
|
|
||||||
|
|
||||||
for {
|
|
||||||
now := time.Now()
|
|
||||||
s.mtx.Lock()
|
|
||||||
s.wakeupTime = s.config.Cron.Schedule.Next(now)
|
|
||||||
s.mtx.Unlock()
|
|
||||||
|
|
||||||
ctxDone := suspendresumesafetimer.SleepUntil(ctx, s.wakeupTime)
|
|
||||||
if ctxDone != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
getLogger(ctx).Debug("cron timer fired")
|
|
||||||
s.mtx.Lock()
|
|
||||||
if s.running {
|
|
||||||
getLogger(ctx).Warn("snapshotting triggered according to cron rules but previous snapshotting is not done; not taking a snapshot this time")
|
|
||||||
s.wakeupWhileRunningCount++
|
|
||||||
s.mtx.Unlock()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
s.lastError = nil
|
|
||||||
s.lastPlan = nil
|
|
||||||
s.wakeupWhileRunningCount = 0
|
|
||||||
s.running = true
|
|
||||||
s.mtx.Unlock()
|
|
||||||
go func() {
|
|
||||||
err := s.do(ctx)
|
|
||||||
s.mtx.Lock()
|
|
||||||
s.lastError = err
|
|
||||||
s.running = false
|
|
||||||
s.mtx.Unlock()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case snapshotsTaken <- struct{}{}:
|
|
||||||
default:
|
|
||||||
if snapshotsTaken != nil {
|
|
||||||
getLogger(ctx).Warn("callback channel is full, discarding snapshot update event")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Cron) do(ctx context.Context) error {
|
|
||||||
fss, err := zfs.ZFSListMapping(ctx, s.fsf)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "cannot list filesystems")
|
|
||||||
}
|
|
||||||
p := makePlan(s.planArgs, fss)
|
|
||||||
|
|
||||||
s.mtx.Lock()
|
|
||||||
s.lastPlan = p
|
|
||||||
s.lastError = nil
|
|
||||||
s.mtx.Unlock()
|
|
||||||
|
|
||||||
ok := p.execute(ctx, false)
|
|
||||||
if !ok {
|
|
||||||
return errors.New("one or more snapshots could not be created, check logs for details")
|
|
||||||
} else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type CronState string
|
|
||||||
|
|
||||||
const (
|
|
||||||
CronStateRunning CronState = "running"
|
|
||||||
CronStateWaiting CronState = "waiting"
|
|
||||||
)
|
|
||||||
|
|
||||||
type CronReport struct {
|
|
||||||
State CronState
|
|
||||||
WakeupTime time.Time
|
|
||||||
Errors []string
|
|
||||||
Progress []*ReportFilesystem
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Cron) Report() Report {
|
|
||||||
|
|
||||||
s.mtx.Lock()
|
|
||||||
defer s.mtx.Unlock()
|
|
||||||
|
|
||||||
r := CronReport{}
|
|
||||||
|
|
||||||
r.WakeupTime = s.wakeupTime
|
|
||||||
|
|
||||||
if s.running {
|
|
||||||
r.State = CronStateRunning
|
|
||||||
} else {
|
|
||||||
r.State = CronStateWaiting
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.lastError != nil {
|
|
||||||
r.Errors = append(r.Errors, s.lastError.Error())
|
|
||||||
}
|
|
||||||
if s.wakeupWhileRunningCount > 0 {
|
|
||||||
r.Errors = append(r.Errors, fmt.Sprintf("cron frequency is too high; snapshots were not taken %d times", s.wakeupWhileRunningCount))
|
|
||||||
}
|
|
||||||
|
|
||||||
r.Progress = nil
|
|
||||||
if s.lastPlan != nil {
|
|
||||||
r.Progress = s.lastPlan.report()
|
|
||||||
}
|
|
||||||
|
|
||||||
return Report{Type: TypeCron, Cron: &r}
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
package snapper
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
)
|
|
||||||
|
|
||||||
type manual struct{}
|
|
||||||
|
|
||||||
func (s *manual) Run(ctx context.Context, wakeUpCommon chan<- struct{}) {
|
|
||||||
// nothing to do
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *manual) Report() Report {
|
|
||||||
return Report{Type: TypeManual, Manual: &struct{}{}}
|
|
||||||
}
|
|
||||||
@@ -1,394 +0,0 @@
|
|||||||
package snapper
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"sort"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/daemon/logging/trace"
|
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/config"
|
|
||||||
"github.com/zrepl/zrepl/daemon/hooks"
|
|
||||||
"github.com/zrepl/zrepl/daemon/logging"
|
|
||||||
"github.com/zrepl/zrepl/util/envconst"
|
|
||||||
"github.com/zrepl/zrepl/util/suspendresumesafetimer"
|
|
||||||
"github.com/zrepl/zrepl/zfs"
|
|
||||||
)
|
|
||||||
|
|
||||||
func periodicFromConfig(g *config.Global, fsf zfs.DatasetFilter, in *config.SnapshottingPeriodic) (*Periodic, error) {
|
|
||||||
if in.Prefix == "" {
|
|
||||||
return nil, errors.New("prefix must not be empty")
|
|
||||||
}
|
|
||||||
if in.Interval.Duration() <= 0 {
|
|
||||||
return nil, errors.New("interval must be positive")
|
|
||||||
}
|
|
||||||
|
|
||||||
hookList, err := hooks.ListFromConfig(&in.Hooks)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "hook config error")
|
|
||||||
}
|
|
||||||
|
|
||||||
args := periodicArgs{
|
|
||||||
interval: in.Interval.Duration(),
|
|
||||||
fsf: fsf,
|
|
||||||
planArgs: planArgs{
|
|
||||||
prefix: in.Prefix,
|
|
||||||
timestampFormat: in.TimestampFormat,
|
|
||||||
hooks: hookList,
|
|
||||||
},
|
|
||||||
// ctx and log is set in Run()
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Periodic{state: SyncUp, args: args}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type periodicArgs struct {
|
|
||||||
ctx context.Context
|
|
||||||
interval time.Duration
|
|
||||||
fsf zfs.DatasetFilter
|
|
||||||
planArgs planArgs
|
|
||||||
snapshotsTaken chan<- struct{}
|
|
||||||
dryRun bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type Periodic struct {
|
|
||||||
args periodicArgs
|
|
||||||
|
|
||||||
mtx sync.Mutex
|
|
||||||
state State
|
|
||||||
|
|
||||||
// set in state Plan, used in Waiting
|
|
||||||
lastInvocation time.Time
|
|
||||||
|
|
||||||
// valid for state Snapshotting
|
|
||||||
plan *plan
|
|
||||||
|
|
||||||
// valid for state SyncUp and Waiting
|
|
||||||
sleepUntil time.Time
|
|
||||||
|
|
||||||
// valid for state Err
|
|
||||||
err error
|
|
||||||
}
|
|
||||||
|
|
||||||
//go:generate stringer -type=State
|
|
||||||
type State uint
|
|
||||||
|
|
||||||
const (
|
|
||||||
SyncUp State = 1 << iota
|
|
||||||
SyncUpErrWait
|
|
||||||
Planning
|
|
||||||
Snapshotting
|
|
||||||
Waiting
|
|
||||||
ErrorWait
|
|
||||||
Stopped
|
|
||||||
)
|
|
||||||
|
|
||||||
func (s State) sf() state {
|
|
||||||
m := map[State]state{
|
|
||||||
SyncUp: periodicStateSyncUp,
|
|
||||||
SyncUpErrWait: periodicStateWait,
|
|
||||||
Planning: periodicStatePlan,
|
|
||||||
Snapshotting: periodicStateSnapshot,
|
|
||||||
Waiting: periodicStateWait,
|
|
||||||
ErrorWait: periodicStateWait,
|
|
||||||
Stopped: nil,
|
|
||||||
}
|
|
||||||
return m[s]
|
|
||||||
}
|
|
||||||
|
|
||||||
type updater func(u func(*Periodic)) State
|
|
||||||
type state func(a periodicArgs, u updater) state
|
|
||||||
|
|
||||||
func (s *Periodic) Run(ctx context.Context, snapshotsTaken chan<- struct{}) {
|
|
||||||
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
|
|
||||||
getLogger(ctx).Debug("start")
|
|
||||||
defer getLogger(ctx).Debug("stop")
|
|
||||||
|
|
||||||
s.args.snapshotsTaken = snapshotsTaken
|
|
||||||
s.args.ctx = ctx
|
|
||||||
s.args.dryRun = false // for future expansion
|
|
||||||
|
|
||||||
u := func(u func(*Periodic)) State {
|
|
||||||
s.mtx.Lock()
|
|
||||||
defer s.mtx.Unlock()
|
|
||||||
if u != nil {
|
|
||||||
u(s)
|
|
||||||
}
|
|
||||||
return s.state
|
|
||||||
}
|
|
||||||
|
|
||||||
var st state = periodicStateSyncUp
|
|
||||||
|
|
||||||
for st != nil {
|
|
||||||
pre := u(nil)
|
|
||||||
st = st(s.args, u)
|
|
||||||
post := u(nil)
|
|
||||||
getLogger(ctx).
|
|
||||||
WithField("transition", fmt.Sprintf("%s=>%s", pre, post)).
|
|
||||||
Debug("state transition")
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func onErr(err error, u updater) state {
|
|
||||||
return u(func(s *Periodic) {
|
|
||||||
s.err = err
|
|
||||||
preState := s.state
|
|
||||||
switch s.state {
|
|
||||||
case SyncUp:
|
|
||||||
s.state = SyncUpErrWait
|
|
||||||
case Planning:
|
|
||||||
fallthrough
|
|
||||||
case Snapshotting:
|
|
||||||
s.state = ErrorWait
|
|
||||||
}
|
|
||||||
getLogger(s.args.ctx).WithError(err).WithField("pre_state", preState).WithField("post_state", s.state).Error("snapshotting error")
|
|
||||||
}).sf()
|
|
||||||
}
|
|
||||||
|
|
||||||
func onMainCtxDone(ctx context.Context, u updater) state {
|
|
||||||
return u(func(s *Periodic) {
|
|
||||||
s.err = ctx.Err()
|
|
||||||
s.state = Stopped
|
|
||||||
}).sf()
|
|
||||||
}
|
|
||||||
|
|
||||||
func periodicStateSyncUp(a periodicArgs, u updater) state {
|
|
||||||
u(func(snapper *Periodic) {
|
|
||||||
snapper.lastInvocation = time.Now()
|
|
||||||
})
|
|
||||||
fss, err := listFSes(a.ctx, a.fsf)
|
|
||||||
if err != nil {
|
|
||||||
return onErr(err, u)
|
|
||||||
}
|
|
||||||
syncPoint, err := findSyncPoint(a.ctx, fss, a.planArgs.prefix, a.interval)
|
|
||||||
if err != nil {
|
|
||||||
return onErr(err, u)
|
|
||||||
}
|
|
||||||
u(func(s *Periodic) {
|
|
||||||
s.sleepUntil = syncPoint
|
|
||||||
})
|
|
||||||
ctxDone := suspendresumesafetimer.SleepUntil(a.ctx, syncPoint)
|
|
||||||
if ctxDone != nil {
|
|
||||||
return onMainCtxDone(a.ctx, u)
|
|
||||||
}
|
|
||||||
return u(func(s *Periodic) {
|
|
||||||
s.state = Planning
|
|
||||||
}).sf()
|
|
||||||
}
|
|
||||||
|
|
||||||
func periodicStatePlan(a periodicArgs, u updater) state {
|
|
||||||
u(func(snapper *Periodic) {
|
|
||||||
snapper.lastInvocation = time.Now()
|
|
||||||
})
|
|
||||||
fss, err := listFSes(a.ctx, a.fsf)
|
|
||||||
if err != nil {
|
|
||||||
return onErr(err, u)
|
|
||||||
}
|
|
||||||
p := makePlan(a.planArgs, fss)
|
|
||||||
return u(func(s *Periodic) {
|
|
||||||
s.state = Snapshotting
|
|
||||||
s.plan = p
|
|
||||||
s.err = nil
|
|
||||||
}).sf()
|
|
||||||
}
|
|
||||||
|
|
||||||
func periodicStateSnapshot(a periodicArgs, u updater) state {
|
|
||||||
|
|
||||||
var plan *plan
|
|
||||||
u(func(snapper *Periodic) {
|
|
||||||
plan = snapper.plan
|
|
||||||
})
|
|
||||||
|
|
||||||
ok := plan.execute(a.ctx, false)
|
|
||||||
|
|
||||||
select {
|
|
||||||
case a.snapshotsTaken <- struct{}{}:
|
|
||||||
default:
|
|
||||||
if a.snapshotsTaken != nil {
|
|
||||||
getLogger(a.ctx).Warn("callback channel is full, discarding snapshot update event")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return u(func(snapper *Periodic) {
|
|
||||||
if !ok {
|
|
||||||
snapper.state = ErrorWait
|
|
||||||
snapper.err = errors.New("one or more snapshots could not be created, check logs for details")
|
|
||||||
} else {
|
|
||||||
snapper.state = Waiting
|
|
||||||
snapper.err = nil
|
|
||||||
}
|
|
||||||
}).sf()
|
|
||||||
}
|
|
||||||
|
|
||||||
func periodicStateWait(a periodicArgs, u updater) state {
|
|
||||||
var sleepUntil time.Time
|
|
||||||
u(func(snapper *Periodic) {
|
|
||||||
lastTick := snapper.lastInvocation
|
|
||||||
snapper.sleepUntil = lastTick.Add(a.interval)
|
|
||||||
sleepUntil = snapper.sleepUntil
|
|
||||||
log := getLogger(a.ctx).WithField("sleep_until", sleepUntil).WithField("duration", a.interval)
|
|
||||||
logFunc := log.Debug
|
|
||||||
if snapper.state == ErrorWait || snapper.state == SyncUpErrWait {
|
|
||||||
logFunc = log.Error
|
|
||||||
}
|
|
||||||
logFunc("enter wait-state after error")
|
|
||||||
})
|
|
||||||
|
|
||||||
ctxDone := suspendresumesafetimer.SleepUntil(a.ctx, sleepUntil)
|
|
||||||
if ctxDone != nil {
|
|
||||||
return onMainCtxDone(a.ctx, u)
|
|
||||||
}
|
|
||||||
return u(func(snapper *Periodic) {
|
|
||||||
snapper.state = Planning
|
|
||||||
}).sf()
|
|
||||||
}
|
|
||||||
|
|
||||||
func listFSes(ctx context.Context, mf zfs.DatasetFilter) (fss []*zfs.DatasetPath, err error) {
|
|
||||||
return zfs.ZFSListMapping(ctx, mf)
|
|
||||||
}
|
|
||||||
|
|
||||||
var syncUpWarnNoSnapshotUntilSyncupMinDuration = envconst.Duration("ZREPL_SNAPPER_SYNCUP_WARN_MIN_DURATION", 1*time.Second)
|
|
||||||
|
|
||||||
// see docs/snapshotting.rst
|
|
||||||
func findSyncPoint(ctx context.Context, fss []*zfs.DatasetPath, prefix string, interval time.Duration) (syncPoint time.Time, err error) {
|
|
||||||
|
|
||||||
const (
|
|
||||||
prioHasVersions int = iota
|
|
||||||
prioNoVersions
|
|
||||||
)
|
|
||||||
|
|
||||||
type snapTime struct {
|
|
||||||
ds *zfs.DatasetPath
|
|
||||||
prio int // lower is higher
|
|
||||||
time time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(fss) == 0 {
|
|
||||||
return time.Now(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
snaptimes := make([]snapTime, 0, len(fss))
|
|
||||||
hardErrs := 0
|
|
||||||
|
|
||||||
now := time.Now()
|
|
||||||
|
|
||||||
getLogger(ctx).Debug("examine filesystem state to find sync point")
|
|
||||||
for _, d := range fss {
|
|
||||||
ctx := logging.WithInjectedField(ctx, "fs", d.ToString())
|
|
||||||
syncPoint, err := findSyncPointFSNextOptimalSnapshotTime(ctx, now, interval, prefix, d)
|
|
||||||
if err == findSyncPointFSNoFilesystemVersionsErr {
|
|
||||||
snaptimes = append(snaptimes, snapTime{
|
|
||||||
ds: d,
|
|
||||||
prio: prioNoVersions,
|
|
||||||
time: now,
|
|
||||||
})
|
|
||||||
} else if err != nil {
|
|
||||||
hardErrs++
|
|
||||||
getLogger(ctx).WithError(err).Error("cannot determine optimal sync point for this filesystem")
|
|
||||||
} else {
|
|
||||||
getLogger(ctx).WithField("syncPoint", syncPoint).Debug("found optimal sync point for this filesystem")
|
|
||||||
snaptimes = append(snaptimes, snapTime{
|
|
||||||
ds: d,
|
|
||||||
prio: prioHasVersions,
|
|
||||||
time: syncPoint,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if hardErrs == len(fss) {
|
|
||||||
return time.Time{}, fmt.Errorf("hard errors in determining sync point for every matching filesystem")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(snaptimes) == 0 {
|
|
||||||
panic("implementation error: loop must either inc hardErrs or add result to snaptimes")
|
|
||||||
}
|
|
||||||
|
|
||||||
// sort ascending by (prio,time)
|
|
||||||
// => those filesystems with versions win over those without any
|
|
||||||
sort.Slice(snaptimes, func(i, j int) bool {
|
|
||||||
if snaptimes[i].prio == snaptimes[j].prio {
|
|
||||||
return snaptimes[i].time.Before(snaptimes[j].time)
|
|
||||||
}
|
|
||||||
return snaptimes[i].prio < snaptimes[j].prio
|
|
||||||
})
|
|
||||||
|
|
||||||
winnerSyncPoint := snaptimes[0].time
|
|
||||||
l := getLogger(ctx).WithField("syncPoint", winnerSyncPoint.String())
|
|
||||||
l.Info("determined sync point")
|
|
||||||
if winnerSyncPoint.Sub(now) > syncUpWarnNoSnapshotUntilSyncupMinDuration {
|
|
||||||
for _, st := range snaptimes {
|
|
||||||
if st.prio == prioNoVersions {
|
|
||||||
l.WithField("fs", st.ds.ToString()).Warn("filesystem will not be snapshotted until sync point")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return snaptimes[0].time, nil
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
var findSyncPointFSNoFilesystemVersionsErr = fmt.Errorf("no filesystem versions")
|
|
||||||
|
|
||||||
func findSyncPointFSNextOptimalSnapshotTime(ctx context.Context, now time.Time, interval time.Duration, prefix string, d *zfs.DatasetPath) (time.Time, error) {
|
|
||||||
|
|
||||||
fsvs, err := zfs.ZFSListFilesystemVersions(ctx, d, zfs.ListFilesystemVersionsOptions{
|
|
||||||
Types: zfs.Snapshots,
|
|
||||||
ShortnamePrefix: prefix,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return time.Time{}, errors.Wrap(err, "list filesystem versions")
|
|
||||||
}
|
|
||||||
if len(fsvs) <= 0 {
|
|
||||||
return time.Time{}, findSyncPointFSNoFilesystemVersionsErr
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort versions by creation
|
|
||||||
sort.SliceStable(fsvs, func(i, j int) bool {
|
|
||||||
return fsvs[i].CreateTXG < fsvs[j].CreateTXG
|
|
||||||
})
|
|
||||||
|
|
||||||
latest := fsvs[len(fsvs)-1]
|
|
||||||
getLogger(ctx).WithField("creation", latest.Creation).Debug("found latest snapshot")
|
|
||||||
|
|
||||||
since := now.Sub(latest.Creation)
|
|
||||||
if since < 0 {
|
|
||||||
return time.Time{}, fmt.Errorf("snapshot %q is from the future: creation=%q now=%q", latest.ToAbsPath(d), latest.Creation, now)
|
|
||||||
}
|
|
||||||
|
|
||||||
return latest.Creation.Add(interval), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type PeriodicReport struct {
|
|
||||||
State State
|
|
||||||
// valid in state SyncUp and Waiting
|
|
||||||
SleepUntil time.Time
|
|
||||||
// valid in state Err
|
|
||||||
Error string
|
|
||||||
// valid in state Snapshotting
|
|
||||||
Progress []*ReportFilesystem
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Periodic) Report() Report {
|
|
||||||
s.mtx.Lock()
|
|
||||||
defer s.mtx.Unlock()
|
|
||||||
|
|
||||||
var progress []*ReportFilesystem = nil
|
|
||||||
if s.plan != nil {
|
|
||||||
progress = s.plan.report()
|
|
||||||
}
|
|
||||||
|
|
||||||
r := &PeriodicReport{
|
|
||||||
State: s.state,
|
|
||||||
SleepUntil: s.sleepUntil,
|
|
||||||
Error: errOrEmptyString(s.err),
|
|
||||||
Progress: progress,
|
|
||||||
}
|
|
||||||
|
|
||||||
return Report{Type: TypePeriodic, Periodic: r}
|
|
||||||
}
|
|
||||||
+487
-22
@@ -3,40 +3,505 @@ package snapper
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/daemon/logging/trace"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/config"
|
"github.com/zrepl/zrepl/config"
|
||||||
|
"github.com/zrepl/zrepl/daemon/hooks"
|
||||||
|
"github.com/zrepl/zrepl/daemon/logging"
|
||||||
|
"github.com/zrepl/zrepl/logger"
|
||||||
|
"github.com/zrepl/zrepl/util/envconst"
|
||||||
"github.com/zrepl/zrepl/zfs"
|
"github.com/zrepl/zrepl/zfs"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Type string
|
//go:generate stringer -type=SnapState
|
||||||
|
type SnapState uint
|
||||||
|
|
||||||
const (
|
const (
|
||||||
TypePeriodic Type = "periodic"
|
SnapPending SnapState = 1 << iota
|
||||||
TypeCron Type = "cron"
|
SnapStarted
|
||||||
TypeManual Type = "manual"
|
SnapDone
|
||||||
|
SnapError
|
||||||
)
|
)
|
||||||
|
|
||||||
type Snapper interface {
|
// All fields protected by Snapper.mtx
|
||||||
Run(ctx context.Context, snapshotsTaken chan<- struct{})
|
type snapProgress struct {
|
||||||
Report() Report
|
state SnapState
|
||||||
|
|
||||||
|
// SnapStarted, SnapDone, SnapError
|
||||||
|
name string
|
||||||
|
startAt time.Time
|
||||||
|
hookPlan *hooks.Plan
|
||||||
|
|
||||||
|
// SnapDone
|
||||||
|
doneAt time.Time
|
||||||
|
|
||||||
|
// SnapErr TODO disambiguate state
|
||||||
|
runResults hooks.PlanReport
|
||||||
}
|
}
|
||||||
|
|
||||||
type Report struct {
|
type args struct {
|
||||||
Type Type
|
ctx context.Context
|
||||||
Periodic *PeriodicReport
|
prefix string
|
||||||
Cron *CronReport
|
interval time.Duration
|
||||||
Manual *struct{}
|
fsf zfs.DatasetFilter
|
||||||
|
snapshotsTaken chan<- struct{}
|
||||||
|
hooks *hooks.List
|
||||||
|
dryRun bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func FromConfig(g *config.Global, fsf zfs.DatasetFilter, in config.SnapshottingEnum) (Snapper, error) {
|
type Snapper struct {
|
||||||
switch v := in.Ret.(type) {
|
args args
|
||||||
case *config.SnapshottingPeriodic:
|
|
||||||
return periodicFromConfig(g, fsf, v)
|
mtx sync.Mutex
|
||||||
case *config.SnapshottingCron:
|
state State
|
||||||
return cronFromConfig(fsf, *v)
|
|
||||||
case *config.SnapshottingManual:
|
// set in state Plan, used in Waiting
|
||||||
return &manual{}, nil
|
lastInvocation time.Time
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("unknown snapshotting type %T", v)
|
// 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
|
||||||
|
})
|
||||||
|
t := time.NewTimer(time.Until(syncPoint))
|
||||||
|
defer t.Stop()
|
||||||
|
select {
|
||||||
|
case <-t.C:
|
||||||
|
return u(func(s *Snapper) {
|
||||||
|
s.state = Planning
|
||||||
|
}).sf()
|
||||||
|
// case <-dosnapshot.Wait(a.ctx):
|
||||||
|
// return u(func(s *Snapper) {
|
||||||
|
// s.state = Planning
|
||||||
|
// }).sf()
|
||||||
|
case <-a.ctx.Done():
|
||||||
|
return onMainCtxDone(a.ctx, u)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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:
|
||||||
|
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")
|
||||||
|
})
|
||||||
|
|
||||||
|
t := time.NewTimer(time.Until(sleepUntil))
|
||||||
|
defer t.Stop()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-t.C:
|
||||||
|
return u(func(snapper *Snapper) {
|
||||||
|
snapper.state = Planning
|
||||||
|
}).sf()
|
||||||
|
// case <-dosnapshot.Wait(a.ctx):
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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, replicationCommon chan<- struct{}) {
|
||||||
|
if s.s != nil {
|
||||||
|
s.s.Run(ctx, replicationCommon)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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 ""
|
|
||||||
}
|
|
||||||
+1234
-1506
File diff suppressed because it is too large
Load Diff
Vendored
-23
@@ -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
|
|
||||||
Vendored
+1
-5
@@ -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
@@ -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)
|
||||||
|
|||||||
Vendored
-43
@@ -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;
|
|
||||||
}
|
|
||||||
Vendored
-17
@@ -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 %}%
|
|
||||||
Vendored
-27
@@ -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 %}
|
|
||||||
+19
-114
@@ -16,125 +16,23 @@ 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
|
We use the following annotations for classifying changes:
|
||||||
------------
|
|
||||||
|
|
||||||
The plan for the next release is to revisit how zrepl does snapshot management.
|
* |break_config| Change that breaks the config.
|
||||||
High-level goals:
|
As a package maintainer, make sure to warn your users about config breakage somehow.
|
||||||
|
* |break| Change that breaks interoperability or persistent state representation with previous releases.
|
||||||
- Make it easy to decouple snapshot management (snapshotting, pruning) from replication.
|
As a package maintainer, make sure to warn your users about config breakage somehow.
|
||||||
- Ability to include/exclude snapshots from replication.
|
Note that even updating the package on both sides might not be sufficient, e.g. if persistent state needs to be migrated to a new format.
|
||||||
This is useful for aforementioned decoupling, e.g., separate snapshot prefixes for local & remote replication.
|
* |mig| Migration that must be run by the user.
|
||||||
Also, it makes explicit that by default, zrepl replicates all snapshots, and that
|
* |feature| Change that introduces new functionality.
|
||||||
replication has no concept of "zrepl-created snapshots", which is a common misconception.
|
* |bugfix| Change that fixes a bug, no regressions or incompatibilities expected.
|
||||||
- Use of ``zfs snapshot`` comma syntax or channel programs to take snapshots of multiple
|
* |docs| Change to the documentation.
|
||||||
datasets atomically.
|
* |maint| Maintenance changes.
|
||||||
- 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.
|
|
||||||
But the metric *value* is now the Unix timestamp at the time the daemon was started.
|
|
||||||
The Grafana dashboard in :repomasterlink:`dist/grafana` has been updated.
|
|
||||||
|
|
||||||
* |bugfix| transient zrepl status error: ``Post "http://unix/status": EOF``
|
|
||||||
* |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
|
|
||||||
---
|
|
||||||
|
|
||||||
* |feature| :ref:`Bandwidth limiting <job-send-recv-options--bandwidth-limit>` (Thanks, Prominic.NET, Inc.)
|
|
||||||
* |feature| zrepl status: use a ``*`` to indicate which filesystem is currently replicating
|
|
||||||
* |feature| include daemon environment variables in zrepl status (currently only in ``--raw``)
|
|
||||||
* |bugfix| **fix encrypt-on-receive + placeholders use case** (:issue:`504`)
|
|
||||||
|
|
||||||
* Before this fix, **plain sends** to a receiver with an encrypted ``root_fs`` **could be received unencrypted** if zrepl needed to create placeholders on the receiver.
|
|
||||||
* Existing zrepl users should :ref:`read the docs <job-recv-options--placeholder>` and check ``zfs get -r encryption,zrepl:placeholder PATH_TO_ROOTFS`` on the receiver.
|
|
||||||
* Thanks to `@mologie <https://github.com/mologie>`_ and `@razielgn <https://github.com/razielgn>`_ for reporting and testing!
|
|
||||||
|
|
||||||
* |bugfix| Rename mis-spelled :ref:`send option <job-send-options>` ``embbeded_data`` to ``embedded_data``.
|
|
||||||
* |bugfix| zrepl status: replication step numbers should start at 1
|
|
||||||
* |bugfix| incorrect bandwidth averaging in ``zrepl status``.
|
|
||||||
* |bugfix| FreeBSD with OpenZFS 2.0: zrepl would wait indefinitely for zfs send to exit on timeouts.
|
|
||||||
* |bugfix| fix ``strconv.ParseInt: value out of range`` bug (and use the control RPCs).
|
|
||||||
* |docs| improve description of multiple pruning rules.
|
|
||||||
* |docs| document :ref:`platform tests <usage-platform-tests>`.
|
|
||||||
* |docs| quickstart: make users aware that prune rules apply to all snapshots.
|
|
||||||
* |maint| some platformtests were broken.
|
|
||||||
* |maint| FreeBSD: release armv7 and arm64 binaries.
|
|
||||||
* |maint| apt repo: update instructions due to ``apt-key`` deprecation.
|
|
||||||
|
|
||||||
Note to all users: please read up on the following OpenZFS bugs, as you might be affected:
|
|
||||||
|
|
||||||
* `ZFS send/recv with ashift 9->12 leads to data corruption <https://github.com/openzfs/zfs/issues/12762>`_.
|
|
||||||
* Various bugs with encrypted send/recv (`Leadership meeting notes <https://openzfs.topicbox.com/groups/developer/T24bdaa2886c6cbf5-Mc039a11c3f1507ea0664817b/december-openzfs-leadership-meeting>`_)
|
|
||||||
|
|
||||||
Finally, I'd like to point you to the `GitHub discussion <https://github.com/zrepl/zrepl/discussions/547>`_ about which bugfixes and features should be prioritized in zrepl 0.6 and beyond!
|
|
||||||
|
|
||||||
0.4.0
|
0.4.0
|
||||||
-----
|
-----
|
||||||
|
|
||||||
|
* |break| Change syntax to trigger a job replication, rename ``zrepl signal wakeup JOB`` to ``zrepl signal replication JOB``
|
||||||
* |feature| support setting zfs send / recv flags in the config (send: ``-wLcepbS`` , recv: ``-ox`` ).
|
* |feature| support setting zfs send / recv flags in the config (send: ``-wLcepbS`` , recv: ``-ox`` ).
|
||||||
Config docs :ref:`here <job-send-options>` and :ref:`here <job-recv-options>` .
|
Config docs :ref:`here <job-send-options>` and :ref:`here <job-recv-options>` .
|
||||||
* |feature| parallel replication is now configurable (disabled by default, :ref:`config docs here <replication-option-concurrency>` ).
|
* |feature| parallel replication is now configurable (disabled by default, :ref:`config docs here <replication-option-concurrency>` ).
|
||||||
@@ -156,6 +54,13 @@ The following bugfix in 0.3.1 :issue:`caused problems for some users <400>`:
|
|||||||
|
|
||||||
* |bugfix| pruning: ``grid``: add all snapshots that do not match the regex to the rule's destroy list.
|
* |bugfix| pruning: ``grid``: add all snapshots that do not match the regex to the rule's destroy list.
|
||||||
|
|
||||||
|
.. 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.3.1
|
0.3.1
|
||||||
-----
|
-----
|
||||||
|
|
||||||
|
|||||||
-187
@@ -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'),
|
|
||||||
}
|
|
||||||
|
|
||||||
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
sphinxconf/conf.py
|
||||||
@@ -13,7 +13,6 @@ Configuration
|
|||||||
configuration/filter_syntax
|
configuration/filter_syntax
|
||||||
configuration/sendrecvoptions
|
configuration/sendrecvoptions
|
||||||
configuration/replication
|
configuration/replication
|
||||||
configuration/conflict_resolution
|
|
||||||
configuration/snapshotting
|
configuration/snapshotting
|
||||||
configuration/prune
|
configuration/prune
|
||||||
configuration/logging
|
configuration/logging
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
.. include:: ../global.rst.inc
|
|
||||||
|
|
||||||
.. _conflict_resolution-options:
|
|
||||||
|
|
||||||
Conflict Resolution Options
|
|
||||||
===========================
|
|
||||||
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
- type: push
|
|
||||||
filesystems: ...
|
|
||||||
conflict_resolution:
|
|
||||||
initial_replication: most_recent | all | fail # default: most_recent
|
|
||||||
|
|
||||||
...
|
|
||||||
|
|
||||||
.. _conflict_resolution-initial_replication:
|
|
||||||
|
|
||||||
|
|
||||||
``initial_replication`` option
|
|
||||||
------------------------------
|
|
||||||
|
|
||||||
The ``initial_replication`` option determines how many snapshots zrepl replicates if the filesystem has not been replicated before.
|
|
||||||
If ``most_recent`` (the default), the initial replication will only transfer the most recent snapshot, while ignoring previous snapshots.
|
|
||||||
If all snapshots should be replicated, specify ``all``.
|
|
||||||
Use ``fail`` to make replication of the filesystem fail in case there is no corresponding fileystem on the receiver.
|
|
||||||
|
|
||||||
For example, suppose there are snapshosts ``tank@1``, ``tank@2``, ``tank@3`` on a sender.
|
|
||||||
Then ``most_recent`` will replicate just ``@3``, but ``all`` will replicate ``@1``, ``@2``, and ``@3``.
|
|
||||||
|
|
||||||
If initial replication is interrupted, and there is at least one (maybe partial) snapshot on the receiver, zrepl will always resume in **incremental mode**.
|
|
||||||
And that is regardless of where the initial replication was interrupted.
|
|
||||||
|
|
||||||
For example, if ``initial_replication: all`` and the transfer of ``@1`` is interrupted, zrepl would retry/resume at ``@1``.
|
|
||||||
And even if the user changes the config to ``initial_replication: most_recent`` before resuming, **incremental mode** will still resume at ``@1``.
|
|
||||||
@@ -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`
|
||||||
|
|
||||||
@@ -82,13 +78,9 @@ Job Type ``pull``
|
|||||||
``$root_fs/$source_path``
|
``$root_fs/$source_path``
|
||||||
* - ``interval``
|
* - ``interval``
|
||||||
- | Interval at which to pull from the source job (e.g. ``10m``).
|
- | Interval at which to pull from the source job (e.g. ``10m``).
|
||||||
| ``manual`` disables periodic pulling, replication then only happens on :ref:`wakeup <cli-signal-wakeup>`.
|
| ``manual`` disables periodic pulling, replication then only happens on :ref:`replication <cli-signal-replication>`.
|
||||||
* - ``pruning``
|
* - ``pruning``
|
||||||
- |pruning-spec|
|
- |pruning-spec|
|
||||||
* - ``replication``
|
|
||||||
- |replication-options|
|
|
||||||
* - ``conflict_resolution``
|
|
||||||
- |conflict-resolution-options|
|
|
||||||
|
|
||||||
Example config: :sampleconf:`/pull.yml`
|
Example config: :sampleconf:`/pull.yml`
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ Prometheus & Grafana
|
|||||||
--------------------
|
--------------------
|
||||||
|
|
||||||
zrepl can expose `Prometheus metrics <https://prometheus.io/docs/instrumenting/exposition_formats/>`_ via HTTP.
|
zrepl can expose `Prometheus metrics <https://prometheus.io/docs/instrumenting/exposition_formats/>`_ via HTTP.
|
||||||
The ``listen`` attribute is a `net.Listen <https://golang.org/pkg/net/#Listen>`_ string for tcp, e.g. ``:9811`` or ``127.0.0.1:9811`` (port 9811 was reserved to zrepl `on the official list <https://github.com/prometheus/prometheus/wiki/Default-port-allocations/_compare/43e495dd251ee328ac0d08b58084665b5c0f7a7e...459195059b55b414193ebeb80c5ba463d2606951>`_).
|
The ``listen`` attribute is a `net.Listen <https://golang.org/pkg/net/#Listen>`_ string for tcp, e.g. ``:9091`` or ``127.0.0.1:9091``.
|
||||||
The ``listen_freebind`` attribute is :ref:`explained here <listen-freebind-explanation>`.
|
The ``listen_freebind`` attribute is :ref:`explained here <listen-freebind-explanation>`.
|
||||||
The Prometheus monitoring job appears in the ``zrepl control`` job list and may be specified **at most once**.
|
The Prometheus monitoring job appears in the ``zrepl control`` job list and may be specified **at most once**.
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ The dashboard also contains some advice on which metrics are important to monito
|
|||||||
global:
|
global:
|
||||||
monitoring:
|
monitoring:
|
||||||
- type: prometheus
|
- type: prometheus
|
||||||
listen: ':9811'
|
listen: ':9091'
|
||||||
listen_freebind: true # optional, default false
|
listen_freebind: true # optional, default false
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|
||||||
@@ -231,11 +233,22 @@ The ``zrepl zfs-abstraction list`` command provides a listing of all bookmarks a
|
|||||||
|
|
||||||
More details can be found in the design document :repomasterlink:`replication/design.md`.
|
More details can be found in the design document :repomasterlink:`replication/design.md`.
|
||||||
|
|
||||||
|
Limitations
|
||||||
|
^^^^^^^^^^^
|
||||||
|
|
||||||
Caveats With Complex Setups (More Than 2 Jobs or Machines)
|
.. ATTENTION::
|
||||||
----------------------------------------------------------
|
|
||||||
|
|
||||||
Most users are served well with a single sender and a single receiver job.
|
Currently, zrepl does not replicate filesystem properties.
|
||||||
|
When receiving a filesystem, it is never mounted (`-u` flag) and `mountpoint=none` is set.
|
||||||
|
This is temporary and being worked on :issue:`24`.
|
||||||
|
|
||||||
|
|
||||||
|
.. _jobs-multiple-jobs:
|
||||||
|
|
||||||
|
Multiple Jobs & More than 2 Machines
|
||||||
|
------------------------------------
|
||||||
|
|
||||||
|
The quick-start guides focus on simple setups with a single sender and a single receiver.
|
||||||
This section documents considerations for more complex setups.
|
This section documents considerations for more complex setups.
|
||||||
|
|
||||||
.. ATTENTION::
|
.. ATTENTION::
|
||||||
@@ -249,7 +262,7 @@ This section documents considerations for more complex setups.
|
|||||||
|
|
||||||
If you can't find your desired configuration, have questions or would like to see improvements to multi-job setups, please `open an issue on GitHub <https://github.com/zrepl/zrepl/issues/new>`_.
|
If you can't find your desired configuration, have questions or would like to see improvements to multi-job setups, please `open an issue on GitHub <https://github.com/zrepl/zrepl/issues/new>`_.
|
||||||
|
|
||||||
Multiple Jobs on One Machine
|
Multiple Jobs on one Machine
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
As a general rule, multiple jobs configured on one machine **must operate on disjoint sets of filesystems**.
|
As a general rule, multiple jobs configured on one machine **must operate on disjoint sets of filesystems**.
|
||||||
Otherwise, concurrently running jobs might interfere when operating on the same filesystem.
|
Otherwise, concurrently running jobs might interfere when operating on the same filesystem.
|
||||||
@@ -268,53 +281,18 @@ On your setup, ensure that
|
|||||||
This scenario is detailed in one of the :ref:`quick-start guides <quickstart-backup-to-external-disk>`.
|
This scenario is detailed in one of the :ref:`quick-start guides <quickstart-backup-to-external-disk>`.
|
||||||
|
|
||||||
|
|
||||||
Two Or More Machines
|
More Than 2 Machines
|
||||||
^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
This section might be relevant to users who wish to *fan-in* (N machines replicate to 1) or *fan-out* (replicate 1 machine to N machines).
|
This section might be relevant to users who wish to *fan-in* (N machines replicate to 1) or *fan-out* (replicate 1 machine to N machines).
|
||||||
|
|
||||||
**Working setups**:
|
**Working setups**:
|
||||||
|
|
||||||
* **Fan-in: N servers replicated to one receiver, disjoint dataset trees.**
|
* N ``push`` identities, 1 ``sink`` (as long as the different push jobs have a different :ref:`client identity <overview-passive-side--client-identity>`)
|
||||||
|
|
||||||
* This is the common use case of a centralized backup server.
|
* ``sink`` constrains each client to a disjoint sub-tree of the sink-side dataset hierarchy ``${root_fs}/${client_identity}``.
|
||||||
|
|
||||||
* Implementation:
|
|
||||||
|
|
||||||
* N ``push`` jobs (one per sender server), 1 ``sink`` (as long as the different push jobs have a different :ref:`client identity <overview-passive-side--client-identity>`)
|
|
||||||
* N ``source`` jobs (one per sender server), N ``pull`` on the receiver server (unique names, disjoint ``root_fs``)
|
|
||||||
|
|
||||||
* The ``sink`` job automatically constrains each client to a disjoint sub-tree of the sink-side dataset hierarchy ``${root_fs}/${client_identity}``.
|
|
||||||
Therefore, the different clients cannot interfere.
|
Therefore, the different clients cannot interfere.
|
||||||
|
|
||||||
* The ``pull`` job only pulls from one host, so it's up to the zrepl user to ensure that the different ``pull`` jobs don't interfere.
|
|
||||||
|
|
||||||
.. _fan-out-replication:
|
|
||||||
|
|
||||||
* **Fan-out: 1 server replicated to N receivers**
|
|
||||||
|
|
||||||
* Can be implemented either in a pull or push fashion.
|
|
||||||
|
|
||||||
* **pull setup**: 1 ``pull`` job on each receiver server, each with a corresponding **unique** ``source`` job on the sender server.
|
|
||||||
* **push setup**: 1 ``sink`` job on each receiver server, each with a corresponding **unique** ``push`` job on the sender server.
|
|
||||||
|
|
||||||
* It is critical that we have one sending-side job (``source``, ``push``) per receiver.
|
|
||||||
The reason is that :ref:`zrepl's ZFS abstractions <zrepl-zfs-abstractions>` (``zrepl zfs-abstraction list``) include the name of the ``source``/``push`` job, but not the receive-side job name or client identity (see :issue:`380`).
|
|
||||||
As a counter-example, suppose we used multiple ``pull`` jobs with only one ``source`` job.
|
|
||||||
All ``pull`` jobs would share the same :ref:`replication cursor bookmark <replication-cursor-and-last-received-hold>` and trip over each other, breaking incremental replication guarantees quickly.
|
|
||||||
The anlogous problem exists for 1 ``push`` to N ``sink`` jobs.
|
|
||||||
|
|
||||||
* The ``filesystems`` matched by the sending side jobs (``source``, ``push``) need not necessarily be disjoint.
|
|
||||||
For this to work, we need to avoid interference between snapshotting and pruning of the different sending jobs.
|
|
||||||
The solution is to centralize sender-side snapshot management in a separate ``snap`` job.
|
|
||||||
Snapshotting in the ``source``/``push`` job should then be disabled (``type: manual``).
|
|
||||||
And sender-side pruning (``keep_sender``) needs to be disabled in the active side (``pull`` / ``push``), since that'll be done by the ``snap job``.
|
|
||||||
|
|
||||||
* **Restore limitations**: when restoring from one of the ``pull`` targets (e.g., using ``zfs send -R``), the replication cursor bookmarks don't exist on the restored system.
|
|
||||||
This can break incremental replication to all other receive-sides after restore.
|
|
||||||
|
|
||||||
* See :ref:`the fan-out replication quick-start guide <quickstart-fan-out-replication>` for an example of this setup.
|
|
||||||
|
|
||||||
|
|
||||||
**Setups that do not work**:
|
**Setups that do not work**:
|
||||||
|
|
||||||
|
|||||||
@@ -67,9 +67,8 @@ Policy ``not_replicated``
|
|||||||
...
|
...
|
||||||
|
|
||||||
``not_replicated`` keeps all snapshots that have not been replicated to the receiving side.
|
``not_replicated`` keeps all snapshots that have not been replicated to the receiving side.
|
||||||
It only makes sense to specify this rule for the ``keep_sender``.
|
It only makes sense to specify this rule on a sender (source or push job).
|
||||||
The reason is that, by definition, all snapshots on the receiver have already been replicated to there from the sender.
|
The state required to evaluate this rule is stored in the :ref:`replication cursor bookmark <replication-cursor-and-last-received-hold>` on the sending side.
|
||||||
To determine whether a sender-side snapshot has already been replicated, zrepl uses the :ref:`replication cursor bookmark <replication-cursor-and-last-received-hold>` which corresponds to the most recent successfully replicated snapshot.
|
|
||||||
|
|
||||||
.. _prune-keep-retention-grid:
|
.. _prune-keep-retention-grid:
|
||||||
|
|
||||||
@@ -107,7 +106,7 @@ The following procedure happens during pruning:
|
|||||||
#. All subsequent buckets are placed adjacent to their predecessor bucket.
|
#. All subsequent buckets are placed adjacent to their predecessor bucket.
|
||||||
#. Now each snapshot on the axis either falls into one bucket or it is older than our rightmost bucket.
|
#. Now each snapshot on the axis either falls into one bucket or it is older than our rightmost bucket.
|
||||||
Buckets are left-inclusive and right-exclusive which means that a snapshot on the edge of bucket will always 'fall into the right one'.
|
Buckets are left-inclusive and right-exclusive which means that a snapshot on the edge of bucket will always 'fall into the right one'.
|
||||||
#. Snapshots older than the rightmost bucket are **not kept** by the grid specification.
|
#. Snapshots older than the rightmost bucket **not kept** by this gridspec.
|
||||||
#. For each bucket, we only keep the ``keep`` oldest snapshots.
|
#. For each bucket, we only keep the ``keep`` oldest snapshots.
|
||||||
|
|
||||||
The syntax to describe the bucket list is as follows:
|
The syntax to describe the bucket list is as follows:
|
||||||
@@ -125,51 +124,43 @@ The syntax to describe the bucket list is as follows:
|
|||||||
|
|
||||||
::
|
::
|
||||||
|
|
||||||
Assume the following grid specification:
|
This grid spec produces the following list of adjacent buckets. For the sake of simplicity,
|
||||||
|
we subject all snapshots to the grid pruning policy by settings `regex: .*`.
|
||||||
|
|
||||||
grid: 1x1h(keep=all) | 2x2h | 1x3h
|
`
|
||||||
|
grid: 1x1h(keep=all) | 2x2h | 1x3h
|
||||||
|
regex: .*
|
||||||
|
`
|
||||||
|
|
||||||
This grid specification produces the following constellation of buckets:
|
0h 1h 2h 3h 4h 5h 6h 7h 8h 9h
|
||||||
|
| | | | | | | | | |
|
||||||
0h 1h 2h 3h 4h 5h 6h 7h 8h 9h
|
|-Bucket1-|-----Bucket 2------|------Bucket 3-----|-----------Bucket 4----------|
|
||||||
| | | | | | | | | |
|
| keep=all| keep=1 | keep=1 | keep=1 |
|
||||||
|-Bucket1-|-----Bucket2-------|------Bucket3------|-----------Bucket4-----------|
|
|
||||||
| keep=all| keep=1 | keep=1 | keep=1 |
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Now assume that we have a set of snapshots @a, @b, ..., @D.
|
Let us consider the following set of snapshots @a-zA-C:
|
||||||
Snapshot @a is the most recent snapshot.
|
|
||||||
Snapshot @D is the oldest snapshot, it is almost 9 hours older than snapshot @a.
|
|
||||||
We place the snapshots on the same timeline as the buckets:
|
|
||||||
|
|
||||||
|
|
||||||
0h 1h 2h 3h 4h 5h 6h 7h 8h 9h
|
| a b c d e f g h i j k l m n o p q r s t u v w x y z A B C D |
|
||||||
| | | | | | | | | |
|
|
||||||
|-Bucket1-|-----Bucket2-------|------Bucket3------|-----------Bucket4-----------|
|
|
||||||
| keep=all| keep=1 | keep=1 | keep=1 |
|
|
||||||
| | | | |
|
|
||||||
| a b c | d e f g h i j k l m n o p |q r s t u v w x y z |A B C D
|
|
||||||
|
|
||||||
We obtain the following mapping of snapshots to buckets:
|
The `grid` algorithm maps them to their respective buckets:
|
||||||
|
|
||||||
Bucket1: a,b,c
|
Bucket 1: a, b, c
|
||||||
Bucket2: d,e,f,g,h,i
|
Bucket 2: d,e,f,g,h,i,j
|
||||||
Bucket3: j,k,l,m,n,o,p
|
Bucket 3: k,l,m,n,o,p
|
||||||
Bucket4: q,r,s,t,u,v,w,x,y,z
|
Bucket 4: q,r, q,r,s,t,u,v,w,x,y,z
|
||||||
No bucket: A,B,C,D
|
None: A,B,C,D
|
||||||
|
|
||||||
For each bucket, we now prune snapshots until it only contains `keep` snapshots.
|
It then applies the per-bucket pruning logic described above which resulting in the
|
||||||
Newer snapshots are destroyed first.
|
following list of remaining snapshots.
|
||||||
Snapshots that do not fall into a bucket are always destroyed.
|
|
||||||
|
|
||||||
Result after pruning:
|
| a b c j p z |
|
||||||
|
|
||||||
|
Note that it only makes sense to grow (not shorten) the interval duration for buckets
|
||||||
|
further in the past since each bucket acts like a low-pass filter for incoming snapshots
|
||||||
|
and adding a less-low-pass-filter after a low-pass one has no effect.
|
||||||
|
|
||||||
0h 1h 2h 3h 4h 5h 6h 7h 8h 9h
|
|
||||||
| | | | | | | | | |
|
|
||||||
|-Bucket1-|-----Bucket2-------|------Bucket3------|-----------Bucket4-----------|
|
|
||||||
| | | | |
|
|
||||||
| a b c | i | p | z |
|
|
||||||
|
|
||||||
.. _prune-keep-last-n:
|
.. _prune-keep-last-n:
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
.. include:: ../global.rst.inc
|
.. include:: ../global.rst.inc
|
||||||
|
|
||||||
.. _replication-options:
|
|
||||||
|
|
||||||
Replication Options
|
Replication Options
|
||||||
===================
|
===================
|
||||||
|
|||||||
@@ -36,12 +36,9 @@ See the `upstream man page <https://openzfs.github.io/openzfs-docs/man/8/zfs-sen
|
|||||||
* - ``encrypted``
|
* - ``encrypted``
|
||||||
-
|
-
|
||||||
- Specific to zrepl, :ref:`see below <job-send-options-encrypted>`.
|
- Specific to zrepl, :ref:`see below <job-send-options-encrypted>`.
|
||||||
* - ``bandwidth_limit``
|
|
||||||
-
|
|
||||||
- Specific to zrepl, :ref:`see below <job-send-recv-options--bandwidth-limit>`.
|
|
||||||
* - ``raw``
|
* - ``raw``
|
||||||
- ``-w``
|
- ``-w``
|
||||||
- Use ``encrypted`` to only allow encrypted sends. Mixed sends are not supported.
|
- Use ``encrypted`` to only allow encrypted sends.
|
||||||
* - ``send_properties``
|
* - ``send_properties``
|
||||||
- ``-p``
|
- ``-p``
|
||||||
- **Be careful**, read the :ref:`note on property replication below <job-note-property-replication>`.
|
- **Be careful**, read the :ref:`note on property replication below <job-note-property-replication>`.
|
||||||
@@ -54,7 +51,7 @@ See the `upstream man page <https://openzfs.github.io/openzfs-docs/man/8/zfs-sen
|
|||||||
* - ``compressed``
|
* - ``compressed``
|
||||||
- ``-c``
|
- ``-c``
|
||||||
-
|
-
|
||||||
* - ``embedded_data``
|
* - ``embbeded_data``
|
||||||
- ``-e``
|
- ``-e``
|
||||||
-
|
-
|
||||||
* - ``saved``
|
* - ``saved``
|
||||||
@@ -141,16 +138,8 @@ Recv Options
|
|||||||
override: {
|
override: {
|
||||||
"org.openzfs.systemd:ignore": "on"
|
"org.openzfs.systemd:ignore": "on"
|
||||||
}
|
}
|
||||||
bandwidth_limit: ...
|
|
||||||
placeholder:
|
|
||||||
encryption: unspecified | off | inherit
|
|
||||||
...
|
...
|
||||||
|
|
||||||
Jump to
|
|
||||||
:ref:`properties <job-recv-options--inherit-and-override>` ,
|
|
||||||
:ref:`bandwidth_limit <job-send-recv-options--bandwidth-limit>` , and
|
|
||||||
:ref:`placeholder <job-recv-options--placeholder>`.
|
|
||||||
|
|
||||||
.. _job-recv-options--inherit-and-override:
|
.. _job-recv-options--inherit-and-override:
|
||||||
|
|
||||||
``properties``
|
``properties``
|
||||||
@@ -197,8 +186,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
|
||||||
-------
|
-------
|
||||||
@@ -220,60 +212,3 @@ and property replication is enabled, the receiver must :ref:`inherit the followi
|
|||||||
* ``keylocation``
|
* ``keylocation``
|
||||||
* ``keyformat``
|
* ``keyformat``
|
||||||
* ``encryption``
|
* ``encryption``
|
||||||
|
|
||||||
.. _job-recv-options--placeholder:
|
|
||||||
|
|
||||||
Placeholders
|
|
||||||
~~~~~~~~~~~~
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
placeholder:
|
|
||||||
encryption: unspecified | off | inherit
|
|
||||||
|
|
||||||
During replication, zrepl :ref:`creates placeholder datasets <replication-placeholder-property>` on the receiving side if the sending side's ``filesystems`` filter creates gaps in the dataset hierarchy.
|
|
||||||
This is generally fully transparent to the user.
|
|
||||||
However, with OpenZFS Native Encryption, placeholders require zrepl user attention.
|
|
||||||
Specifically, the problem is that, when zrepl attempts to create the placeholder dataset on the receiver, and that placeholder's parent dataset is encrypted, ZFS wants to inherit encryption to the placeholder.
|
|
||||||
This is relevant to two use cases that zrepl supports:
|
|
||||||
|
|
||||||
1. **encrypted-send-to-untrusted-receiver** In this use case, the sender sends an :ref:`encrypted send stream <job-send-options-encrypted>` and the receiver doesn't have the key loaded.
|
|
||||||
2. **send-plain-encrypt-on-receive** The receive-side ``root_fs`` dataset is encrypted, and the senders are unencrypted.
|
|
||||||
The key of ``root_fs`` is loaded, and the goal is that the plain sends (e.g., from production) are encrypted on-the-fly during receive, with ``root_fs``'s key.
|
|
||||||
|
|
||||||
For **encrypted-send-to-untrusted-receiver**, the placeholder datasets need to be created with ``-o encryption=off``.
|
|
||||||
Without it, creation would fail with an error, indicating that the placeholder's parent dataset's key needs to be loaded.
|
|
||||||
But we don't trust the receiver, so we can't expect that to ever happen.
|
|
||||||
|
|
||||||
However, for **send-plain-encrypt-on-receive**, we cannot set ``-o encryption=off``.
|
|
||||||
The reason is that if we did, any of the (non-placeholder) child datasets below the placeholder would inherit ``encryption=off``, thereby silently breaking our encrypt-on-receive use case.
|
|
||||||
So, to cover this use case, we need to create placeholders without specifying ``-o encryption``.
|
|
||||||
This will make ``zfs create`` inherit the encryption mode from the parent dataset, and thereby transitively from ``root_fs``.
|
|
||||||
|
|
||||||
The zrepl config provides the `recv.placeholder.encryption` knob to control this behavior.
|
|
||||||
In ``undefined`` mode (default), placeholder creation bails out and asks the user to configure a behavior.
|
|
||||||
In ``off`` mode, the placeholder is created with ``encryption=off``, i.e., **encrypted-send-to-untrusted-rceiver** use case.
|
|
||||||
In ``inherit`` mode, the placeholder is created without specifying ``-o encryption`` at all, i.e., the **send-plain-encrypt-on-receive** use case.
|
|
||||||
|
|
||||||
|
|
||||||
Common Options
|
|
||||||
~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
.. _job-send-recv-options--bandwidth-limit:
|
|
||||||
|
|
||||||
Bandwidth Limit (send & recv)
|
|
||||||
-----------------------------
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
bandwidth_limit:
|
|
||||||
max: 23.5 MiB # -1 is the default and disabled rate limiting
|
|
||||||
bucket_capacity: # token bucket capacity in bytes; defaults to 128KiB
|
|
||||||
|
|
||||||
Both ``send`` and ``recv`` can be limited to a maximum bandwidth through ``bandwidth_limit``.
|
|
||||||
For most users, it should be sufficient to just set ``bandwidth_limit.max``.
|
|
||||||
The ``bandwidth_limit.bucket_capacity`` refers to the `token bucket size <https://github.com/juju/ratelimit>`_.
|
|
||||||
|
|
||||||
The bandwidth limit only applies to the payload data, i.e., the ZFS send stream.
|
|
||||||
It does not account for transport protocol overheads.
|
|
||||||
The scope is the job level, i.e., all :ref:`concurrent <replication-option-concurrency>` sends or incoming receives of a job share the bandwidth limit.
|
|
||||||
|
|||||||
@@ -5,138 +5,53 @@
|
|||||||
Taking Snaphots
|
Taking Snaphots
|
||||||
===============
|
===============
|
||||||
|
|
||||||
You can configure zrepl to take snapshots of the filesystems in the ``filesystems`` field specified in ``push``, ``source`` and ``snap`` jobs.
|
The ``push``, ``source`` and ``snap`` jobs can automatically take periodic snapshots of the filesystems matched by the ``filesystems`` filter field.
|
||||||
|
The snapshot names are composed of a user-defined prefix followed by a UTC date formatted like ``20060102_150405_000``.
|
||||||
|
We use UTC because it will avoid name conflicts when switching time zones or between summer and winter time.
|
||||||
|
|
||||||
The following snapshotting types are supported:
|
When a job is started, the snapshotter attempts to get the snapshotting rhythms of the matched ``filesystems`` in sync because snapshotting all filesystems at the same time results in a more consistent backup.
|
||||||
|
To find that sync point, the most recent snapshot, made by the snapshotter, in any of the matched ``filesystems`` is used.
|
||||||
|
|
||||||
.. list-table::
|
|
||||||
:widths: 20 70
|
|
||||||
:header-rows: 1
|
|
||||||
|
|
||||||
* - ``snapshotting.type``
|
|
||||||
- Comment
|
|
||||||
* - ``periodic``
|
|
||||||
- Ensure that snapshots are taken at a particular interval.
|
|
||||||
* - ``cron``
|
|
||||||
- Use cron spec to take snapshots at particular points in time.
|
|
||||||
* - ``manual``
|
|
||||||
- zrepl does not take any snapshots by itself.
|
|
||||||
|
|
||||||
The ``periodic`` and ``cron`` snapshotting types share some common options and behavior:
|
|
||||||
|
|
||||||
* **Naming:** The snapshot names are composed of a user-defined ``prefix`` followed by a UTC date formatted like ``20060102_150405_000``.
|
|
||||||
We use UTC because it will avoid name conflicts when switching time zones or between summer and winter time.
|
|
||||||
* **Hooks:** You can configure hooks to run before or after zrepl takes the snapshots. See :ref:`below <job-snapshotting-hooks>` for details.
|
|
||||||
* **Push replication:** After creating all snapshots, the snapshotter will wake up the replication part of the job, if it's a ``push`` job.
|
|
||||||
Note that snapshotting is decoupled from replication, i.e., if it is down or takes too long, snapshots will still be taken.
|
|
||||||
Note further that other jobs are not woken up by snapshotting.
|
|
||||||
|
|
||||||
.. NOTE::
|
|
||||||
|
|
||||||
There is **no concept of ownership** of the snapshots that are created by ``periodic`` or ``cron``.
|
|
||||||
Thus, there is no distinction between zrepl-created snapshots and user-created snapshots during replication or pruning.
|
|
||||||
|
|
||||||
In particular, pruning will take all snapshots into consideration by default.
|
|
||||||
To constrain pruning to just zrepl-created snapshots:
|
|
||||||
|
|
||||||
1. Assign a unique `prefix` to the snapshotter and
|
|
||||||
2. Use the ``regex`` functionality of the various pruning ``keep`` rules to just consider snapshots with that prefix.
|
|
||||||
|
|
||||||
There is currently no way to constrain replication to just zrepl-created snapshots.
|
|
||||||
Follow and comment at :issue:`403` if you need this functionality.
|
|
||||||
|
|
||||||
.. NOTE::
|
|
||||||
|
|
||||||
The ``zrepl signal wakeup JOB`` subcommand does not trigger snapshotting.
|
|
||||||
|
|
||||||
``periodic`` Snapshotting
|
|
||||||
-------------------------
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
- ...
|
|
||||||
filesystems: { ... }
|
|
||||||
snapshotting:
|
|
||||||
type: periodic
|
|
||||||
prefix: zrepl_
|
|
||||||
interval: 10m
|
|
||||||
# Timestamp format that is used as snapshot suffix.
|
|
||||||
# Can be any of "dense" (default), "human", "iso-8601", "unix-seconds" or a custom Go time format (see https://go.dev/src/time/format.go)
|
|
||||||
timestamp_format: dense
|
|
||||||
hooks: ...
|
|
||||||
pruning: ...
|
|
||||||
|
|
||||||
The ``periodic`` snapshotter ensures that snapshots are taken in the specified ``interval``.
|
|
||||||
If you use zrepl for backup, this translates into your recovery point objective (RPO).
|
|
||||||
To meet your RPO, you still need to monitor that replication, which happens asynchronously to snapshotting, actually works.
|
|
||||||
|
|
||||||
It is desirable to get all ``filesystems`` snapshotted simultaneously because it results in a more consistent backup.
|
|
||||||
To accomplish this while still maintaining the ``interval``, the ``periodic`` snapshotter attempts to get the snapshotting rhythms in sync.
|
|
||||||
To find that sync point, the most recent snapshot, created by the snapshotter, in any of the matched ``filesystems`` is used.
|
|
||||||
A filesystem that does not have snapshots by the snapshotter has lower priority than filesystem that do, and thus might not be snapshotted (and replicated) until it is snapshotted at the next sync point.
|
A filesystem that does not have snapshots by the snapshotter has lower priority than filesystem that do, and thus might not be snapshotted (and replicated) until it is snapshotted at the next sync point.
|
||||||
The snapshotter uses the ``prefix`` to identify which snapshots it created.
|
|
||||||
|
|
||||||
.. _job-snapshotting--cron:
|
For ``push`` jobs, replication is automatically triggered after all filesystems have been snapshotted.
|
||||||
|
|
||||||
|
Note that the ``zrepl signal replication JOB`` subcommand does not trigger snapshotting.
|
||||||
|
|
||||||
``cron`` Snapshotting
|
|
||||||
---------------------
|
|
||||||
|
|
||||||
::
|
::
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
- type: snap
|
- type: push
|
||||||
filesystems: { ... }
|
filesystems: {
|
||||||
snapshotting:
|
"<": true,
|
||||||
type: cron
|
"tmp": false
|
||||||
prefix: zrepl_
|
}
|
||||||
# (second, optional) minute hour day-of-month month day-of-week
|
snapshotting:
|
||||||
# This example takes snapshots daily at 3:00.
|
type: periodic
|
||||||
cron: "0 3 * * *"
|
prefix: zrepl_
|
||||||
# Timestamp format that is used as snapshot suffix.
|
interval: 10m
|
||||||
# Can be any of "dense" (default), "human", "iso-8601", "unix-seconds" or a custom Go time format (see https://go.dev/src/time/format.go)
|
hooks: ...
|
||||||
timestamp_format: dense
|
...
|
||||||
pruning: ...
|
|
||||||
|
|
||||||
In ``cron`` mode, the snapshotter takes snaphots at fixed points in time.
|
There is also a ``manual`` snapshotting type, which covers the following use cases:
|
||||||
See https://en.wikipedia.org/wiki/Cron for details on the syntax.
|
|
||||||
zrepl uses the ``the github.com/robfig/cron/v3`` Go package for parsing.
|
|
||||||
An optional field for "seconds" is supported to take snapshots at sub-minute frequencies.
|
|
||||||
|
|
||||||
.. _job-snapshotting-timestamp_format:
|
* Existing infrastructure for automatic snapshots: you only want to use this zrepl job for replication.
|
||||||
|
* Handling snapshotting through a separate ``snap`` job.
|
||||||
|
|
||||||
Timestamp Format
|
Note that you will have to trigger replication manually using the ``zrepl signal replication JOB`` subcommand in that case.
|
||||||
~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
The ``cron`` and ``periodic`` snapshotter support configuring a custom timestamp format that is used as suffix for the snapshot name.
|
|
||||||
It can be used by setting ``timestamp_format`` to any of the following values:
|
|
||||||
|
|
||||||
* ``dense`` (default) looks like ``20060102_150405_000``
|
|
||||||
* ``human`` looks like ``2006-01-02_15:04:05``
|
|
||||||
* ``iso-8601`` looks like ``2006-01-02T15:04:05.000Z``
|
|
||||||
* ``unix-seconds`` looks like ``1136214245``
|
|
||||||
* Any custom Go time format accepted by `time.Time#Format <https://go.dev/src/time/format.go>`_.
|
|
||||||
|
|
||||||
|
|
||||||
``manual`` Snapshotting
|
|
||||||
-----------------------
|
|
||||||
|
|
||||||
::
|
::
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
- type: push
|
- type: push
|
||||||
|
filesystems: {
|
||||||
|
"<": true,
|
||||||
|
"tmp": false
|
||||||
|
}
|
||||||
snapshotting:
|
snapshotting:
|
||||||
type: manual
|
type: manual
|
||||||
...
|
...
|
||||||
|
|
||||||
In ``manual`` mode, zrepl does not take snapshots by itself.
|
|
||||||
Manual snapshotting is most useful if you have existing infrastructure for snapshot management.
|
|
||||||
Or, if you want to decouple snapshot management from replication using a zrepl ``snap`` job.
|
|
||||||
See :ref:`this quickstart guide <quickstart-backup-to-external-disk>` for an example.
|
|
||||||
|
|
||||||
To trigger replication after taking snapshots, use the ``zrepl signal wakeup JOB`` command.
|
|
||||||
|
|
||||||
.. _job-snapshotting-hooks:
|
.. _job-snapshotting-hooks:
|
||||||
|
|
||||||
Pre- and Post-Snapshot Hooks
|
Pre- and Post-Snapshot Hooks
|
||||||
|
|||||||
@@ -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))
|
||||||
+3
-6
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
.. |GitHub license| image:: https://img.shields.io/github/license/zrepl/zrepl.svg
|
.. |GitHub license| image:: https://img.shields.io/github/license/zrepl/zrepl.svg
|
||||||
:target: https://github.com/zrepl/zrepl/blob/master/LICENSE
|
:target: https://github.com/zrepl/zrepl/blob/master/LICENSE
|
||||||
.. |Language: Go| image:: https://img.shields.io/badge/lang-Go-6ad7e5.svg
|
.. |Language: Go| image:: https://img.shields.io/badge/language-Go-6ad7e5.svg
|
||||||
:target: https://golang.org/
|
:target: https://golang.org/
|
||||||
.. |User Docs| image:: https://img.shields.io/badge/docs-web-blue.svg
|
.. |User Docs| image:: https://img.shields.io/badge/docs-web-blue.svg
|
||||||
:target: https://zrepl.github.io
|
:target: https://zrepl.github.io
|
||||||
@@ -13,21 +13,18 @@
|
|||||||
:target: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=R5QSXJVYHGX96
|
:target: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=R5QSXJVYHGX96
|
||||||
.. |Donate via Liberapay| image:: https://img.shields.io/liberapay/patrons/zrepl.svg?logo=liberapay
|
.. |Donate via Liberapay| image:: https://img.shields.io/liberapay/patrons/zrepl.svg?logo=liberapay
|
||||||
:target: https://liberapay.com/zrepl/donate
|
:target: https://liberapay.com/zrepl/donate
|
||||||
.. |Donate via Patreon| image:: https://img.shields.io/badge/dynamic/json?color=yellow&label=Patreon&query=data.attributes.patron_count&url=https%3A%2F%2Fwww.patreon.com%2Fapi%2Fcampaigns%2F3095079
|
.. |Donate via Patreon| image:: https://img.shields.io/badge/dynamic/json?color=yellow&label=Patreon&query=data.attributes.patron_count&suffix=%20patrons&url=https%3A%2F%2Fwww.patreon.com%2Fapi%2Fcampaigns%2F3095079
|
||||||
:target: https://www.patreon.com/zrepl
|
:target: https://www.patreon.com/zrepl
|
||||||
.. |Donate via GitHub Sponsors| image:: https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&style=flat&color=yellow
|
.. |Donate via GitHub Sponsors| image:: https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&style=flat&color=yellow
|
||||||
:target: https://github.com/sponsors/problame
|
:target: https://github.com/sponsors/problame
|
||||||
.. |Twitter| image:: https://img.shields.io/twitter/url/https/github.com/zrepl/zrepl.svg?style=social
|
.. |Twitter| image:: https://img.shields.io/twitter/url/https/github.com/zrepl/zrepl.svg?style=social
|
||||||
:target: https://twitter.com/intent/tweet?text=Wow:&url=https%3A%2F%2Fgithub.com%2Fzrepl%2Fzrepl
|
:target: https://twitter.com/intent/tweet?text=Wow:&url=https%3A%2F%2Fgithub.com%2Fzrepl%2Fzrepl
|
||||||
.. |Matrix| image:: https://img.shields.io/badge/chat-matrix-blue.svg
|
|
||||||
:target: https://matrix.to/#/#zrepl:matrix.org
|
|
||||||
|
|
||||||
.. |serve-transport| replace:: :ref:`serve specification<transport>`
|
.. |serve-transport| replace:: :ref:`serve specification<transport>`
|
||||||
.. |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>`
|
||||||
|
|||||||
+3
-5
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
.. include:: global.rst.inc
|
.. include:: global.rst.inc
|
||||||
|
|
||||||
|GitHub license| |Language: Go| |Twitter| |Donate via Patreon| |Donate via GitHub Sponsors| |Donate via Liberapay| |Donate via PayPal| |Matrix|
|
|GitHub license| |Language: Go| |Twitter| |Donate via Patreon| |Donate via GitHub Sponsors| |Donate via Liberapay| |Donate via PayPal|
|
||||||
|
|
||||||
|
|
||||||
zrepl - ZFS replication
|
zrepl - ZFS replication
|
||||||
@@ -25,10 +25,10 @@ zrepl - ZFS replication
|
|||||||
Progress: [=========================\----] 246.7 MiB / 264.7 MiB @ 11.5 MiB/s
|
Progress: [=========================\----] 246.7 MiB / 264.7 MiB @ 11.5 MiB/s
|
||||||
zroot STEPPING (step 1/2, 624 B/1.2 KiB) next: @a => @b
|
zroot STEPPING (step 1/2, 624 B/1.2 KiB) next: @a => @b
|
||||||
zroot/ROOT DONE (step 2/2, 1.2 KiB/1.2 KiB)
|
zroot/ROOT DONE (step 2/2, 1.2 KiB/1.2 KiB)
|
||||||
* zroot/ROOT/default STEPPING (step 1/2, 123.4 MiB/129.3 MiB) next: @a => @b
|
zroot/ROOT/default STEPPING (step 1/2, 123.4 MiB/129.3 MiB) next: @a => @b
|
||||||
zroot/tmp STEPPING (step 1/2, 29.9 KiB/44.2 KiB) next: @a => @b
|
zroot/tmp STEPPING (step 1/2, 29.9 KiB/44.2 KiB) next: @a => @b
|
||||||
zroot/usr STEPPING (step 1/2, 624 B/1.2 KiB) next: @a => @b
|
zroot/usr STEPPING (step 1/2, 624 B/1.2 KiB) next: @a => @b
|
||||||
* zroot/usr/home STEPPING (step 1/2, 123.3 MiB/135.3 MiB) next: @a => @b
|
zroot/usr/home STEPPING (step 1/2, 123.3 MiB/135.3 MiB) next: @a => @b
|
||||||
zroot/var STEPPING (step 1/2, 624 B/1.2 KiB) next: @a => @b
|
zroot/var STEPPING (step 1/2, 624 B/1.2 KiB) next: @a => @b
|
||||||
zroot/var/audit DONE (step 2/2, 1.2 KiB/1.2 KiB)
|
zroot/var/audit DONE (step 2/2, 1.2 KiB/1.2 KiB)
|
||||||
zroot/var/crash DONE (step 2/2, 1.2 KiB/1.2 KiB)
|
zroot/var/crash DONE (step 2/2, 1.2 KiB/1.2 KiB)
|
||||||
@@ -66,7 +66,6 @@ Main Features
|
|||||||
* [x] Large blocks send & receive
|
* [x] Large blocks send & receive
|
||||||
* [x] Embedded data send & receive
|
* [x] Embedded data send & receive
|
||||||
* [x] Resume state send & receive
|
* [x] Resume state send & receive
|
||||||
* [x] Bandwidth limiting
|
|
||||||
|
|
||||||
* **Automatic snapshot management**
|
* **Automatic snapshot management**
|
||||||
|
|
||||||
@@ -137,6 +136,5 @@ Table of Contents
|
|||||||
pr
|
pr
|
||||||
changelog
|
changelog
|
||||||
GitHub Repository & Issue Tracker <https://github.com/zrepl/zrepl>
|
GitHub Repository & Issue Tracker <https://github.com/zrepl/zrepl>
|
||||||
Chat: Matrix <https://matrix.to/#/#zrepl:matrix.org>
|
|
||||||
supporters
|
supporters
|
||||||
|
|
||||||
|
|||||||
@@ -9,29 +9,18 @@ The fingerprint of the signing key is ``E101 418F D3D6 FBCB 9D65 A62D 7086 99FC
|
|||||||
It is available at `<https://zrepl.cschwarz.com/apt/apt-key.asc>`_ .
|
It is available at `<https://zrepl.cschwarz.com/apt/apt-key.asc>`_ .
|
||||||
Please open an issue in on GitHub if you encounter any issues with the repository.
|
Please open an issue in on GitHub if you encounter any issues with the repository.
|
||||||
|
|
||||||
|
The following snippet configure the repository for your Debian or Ubuntu release:
|
||||||
|
|
||||||
::
|
::
|
||||||
|
|
||||||
(
|
apt update && apt install curl gnupg lsb-release; \
|
||||||
set -ex
|
ARCH="$(dpkg --print-architecture)"; \
|
||||||
zrepl_apt_key_url=https://zrepl.cschwarz.com/apt/apt-key.asc
|
CODENAME="$(lsb_release -i -s | tr '[:upper:]' '[:lower:]') $(lsb_release -c -s | tr '[:upper:]' '[:lower:]')"; \
|
||||||
zrepl_apt_key_dst=/usr/share/keyrings/zrepl.gpg
|
echo "Using Distro and Codename: $CODENAME"; \
|
||||||
zrepl_apt_repo_file=/etc/apt/sources.list.d/zrepl.list
|
(curl https://zrepl.cschwarz.com/apt/apt-key.asc | apt-key add -) && \
|
||||||
|
(echo "deb [arch=$ARCH] https://zrepl.cschwarz.com/apt/$CODENAME main" > /etc/apt/sources.list.d/zrepl.list) && \
|
||||||
|
apt update
|
||||||
|
|
||||||
# Install dependencies for subsequent commands
|
|
||||||
sudo apt update && sudo apt install curl gnupg lsb-release
|
|
||||||
|
|
||||||
# Deploy the zrepl apt key.
|
|
||||||
curl -fsSL "$zrepl_apt_key_url" | tee | gpg --dearmor | sudo tee "$zrepl_apt_key_dst" > /dev/null
|
|
||||||
|
|
||||||
# Add the zrepl apt repo.
|
|
||||||
ARCH="$(dpkg --print-architecture)"
|
|
||||||
CODENAME="$(lsb_release -i -s | tr '[:upper:]' '[:lower:]') $(lsb_release -c -s | tr '[:upper:]' '[:lower:]')"
|
|
||||||
echo "Using Distro and Codename: $CODENAME"
|
|
||||||
echo "deb [arch=$ARCH signed-by=$zrepl_apt_key_dst] https://zrepl.cschwarz.com/apt/$CODENAME main" | sudo tee /etc/apt/sources.list.d/zrepl.list
|
|
||||||
|
|
||||||
# Update apt repos.
|
|
||||||
sudo apt update
|
|
||||||
)
|
|
||||||
|
|
||||||
.. NOTE::
|
.. NOTE::
|
||||||
|
|
||||||
|
|||||||
@@ -93,7 +93,6 @@ Enable the zrepl daemon to start automatically at boot:
|
|||||||
|
|
||||||
sysrc zrepl_enable="YES"
|
sysrc zrepl_enable="YES"
|
||||||
|
|
||||||
Now jump to :ref:`the summary <installation-freebsd-jail-summary>` below.
|
|
||||||
|
|
||||||
Plugin
|
Plugin
|
||||||
######
|
######
|
||||||
@@ -135,18 +134,7 @@ Now ``zrepl`` can be started.
|
|||||||
|
|
||||||
service zrepl start
|
service zrepl start
|
||||||
|
|
||||||
Now jump to :ref:`the summary <installation-freebsd-jail-summary>` below.
|
|
||||||
|
|
||||||
.. _installation-freebsd-jail-summary:
|
|
||||||
|
|
||||||
Summary
|
Summary
|
||||||
-------
|
-------
|
||||||
|
|
||||||
Congratulations, you have a working jail!
|
Congratulations, you have a working jail!
|
||||||
|
|
||||||
.. NOTE::
|
|
||||||
|
|
||||||
With FreeBSD 13's transition to OpenZFS 2.0, please ensure that your jail's FreeBSD version matches the one in the kernel module.
|
|
||||||
If you are getting cryptic errors such as
|
|
||||||
``cannot receive new filesystem stream: invalid backup stream``
|
|
||||||
the instructions posted `here <https://github.com/zrepl/zrepl/issues/500#issuecomment-966215205>`_ might help.
|
|
||||||
|
|||||||
@@ -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.
|
|
||||||
|
|||||||
+23
-28
@@ -1,10 +1,10 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -euo pipefail
|
set -eo 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,11 +12,8 @@ while getopts "caP" arg; do
|
|||||||
"c")
|
"c")
|
||||||
DO_CLONE=true
|
DO_CLONE=true
|
||||||
;;
|
;;
|
||||||
"P")
|
|
||||||
PUSH=true
|
|
||||||
;;
|
|
||||||
*)
|
*)
|
||||||
echo "invalid option '-$arg'"
|
echo invalid option
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
@@ -30,8 +27,13 @@ checkout_repo_msg() {
|
|||||||
echo "clone ${GHPAGESREPO} to ${PUBLICDIR}:"
|
echo "clone ${GHPAGESREPO} to ${PUBLICDIR}:"
|
||||||
}
|
}
|
||||||
|
|
||||||
if ! type sphinx-multiversion >/dev/null; then
|
exit_msg() {
|
||||||
echo "install sphinx-multiversion and come back"
|
echo "error, exiting..."
|
||||||
|
}
|
||||||
|
trap exit_msg EXIT
|
||||||
|
|
||||||
|
if ! type sphinx-versioning >/dev/null; then
|
||||||
|
echo "install sphinx-versioning and come back"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -69,39 +71,32 @@ git reset --hard origin/master
|
|||||||
|
|
||||||
echo "cleaning GitHub pages repo"
|
echo "cleaning GitHub pages repo"
|
||||||
git rm -rf .
|
git rm -rf .
|
||||||
cat > .gitignore <<EOF
|
|
||||||
**/.doctrees
|
|
||||||
EOF
|
|
||||||
|
|
||||||
popd
|
popd
|
||||||
|
|
||||||
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"
|
||||||
|
|
||||||
echo "adding and commiting all changes in GitHub pages repo"
|
echo "adding and commiting all changes in GitHub pages repo"
|
||||||
git add .gitignore
|
|
||||||
git add -A
|
git add -A
|
||||||
if [ "$(git status --porcelain)" != "" ]; then
|
git commit -m "$COMMIT_MSG"
|
||||||
git commit -m "$COMMIT_MSG"
|
git push origin master
|
||||||
else
|
|
||||||
echo "nothing to commit"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if $PUSH; then
|
|
||||||
echo "pushing to GitHub pages repo"
|
|
||||||
git push origin master
|
|
||||||
else
|
|
||||||
echo "not pushing to GitHub pages repo, set -P flag to push"
|
|
||||||
fi
|
|
||||||
|
|
||||||
|
|||||||
+2
-16
@@ -33,7 +33,6 @@ Keep the :ref:`full config documentation <configuration_toc>` handy if a config
|
|||||||
|
|
||||||
quickstart/continuous_server_backup
|
quickstart/continuous_server_backup
|
||||||
quickstart/backup_to_external_disk
|
quickstart/backup_to_external_disk
|
||||||
quickstart/fan_out_replication
|
|
||||||
|
|
||||||
Use ``zrepl configcheck`` to validate your configuration.
|
Use ``zrepl configcheck`` to validate your configuration.
|
||||||
No output indicates that everything is fine.
|
No output indicates that everything is fine.
|
||||||
@@ -52,25 +51,12 @@ We hope that you have found a configuration that fits your use case.
|
|||||||
Use ``zrepl configcheck`` once again to make sure the config is correct (output indicates that everything is fine).
|
Use ``zrepl configcheck`` once again to make sure the config is correct (output indicates that everything is fine).
|
||||||
Then restart the zrepl daemon on all systems involved in the replication, likely using ``service zrepl restart`` or ``systemctl restart zrepl``.
|
Then restart the zrepl daemon on all systems involved in the replication, likely using ``service zrepl restart`` or ``systemctl restart zrepl``.
|
||||||
|
|
||||||
.. WARNING::
|
|
||||||
|
|
||||||
Please :ref:`read up carefully <prune>` on the pruning rules before applying the config.
|
|
||||||
In particular, note that most example configs apply to all snapshots, not just zrepl-created snapshots.
|
|
||||||
Use the following keep rule on sender and receiver to prevent this:
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
- type: regex
|
|
||||||
negate: true
|
|
||||||
regex: "^zrepl_.*" # <- the 'prefix' specified in snapshotting.prefix
|
|
||||||
|
|
||||||
|
|
||||||
Watch it Work
|
Watch it Work
|
||||||
=============
|
=============
|
||||||
|
|
||||||
Run ``zrepl status`` on the active side of the replication setup to monitor snaphotting, replication and pruning activity.
|
Run ``zrepl status`` on the active side of the replication setup to monitor snaphotting, replication and pruning activity.
|
||||||
To re-trigger replication (snapshots are separate!), use ``zrepl signal wakeup JOBNAME``.
|
To re-trigger replication (snapshots are separate!), use ``zrepl signal replication JOBNAME``.
|
||||||
(refer to the example use case document if you are uncertain which job you want to wake up).
|
(refer to the example use case document if you are uncertain which job you want to start replication).
|
||||||
|
|
||||||
You can also use basic UNIX tools to inspect see what's going on.
|
You can also use basic UNIX tools to inspect see what's going on.
|
||||||
If you like tmux, here is a handy script that works on FreeBSD: ::
|
If you like tmux, here is a handy script that works on FreeBSD: ::
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ Thus, we only keep one hour worth of high-resolution snapshots, then fade them o
|
|||||||
|
|
||||||
At the end of each work day, we connect our external disk that serves as our workstation's local offline backup.
|
At the end of each work day, we connect our external disk that serves as our workstation's local offline backup.
|
||||||
We want zrepl to inspect the filesystems and snapshots on the external pool, figure out which snapshots were created since the last time we connected the external disk, and use incremental replication to efficiently mirror our workstation to our backup disk.
|
We want zrepl to inspect the filesystems and snapshots on the external pool, figure out which snapshots were created since the last time we connected the external disk, and use incremental replication to efficiently mirror our workstation to our backup disk.
|
||||||
Afterwards, we want to clean up old snapshots on the backup pool: we want to keep all snapshots younger than one hour, 24 for each hour of the first day, then 360 daily backups.
|
Afterwards, we want to clean up old snapshots on the backup pool: we want to keep all snapshots younger than one hour, 24 for each hor of the first day, then 360 daily backups.
|
||||||
|
|
||||||
A few additional requirements:
|
A few additional requirements:
|
||||||
|
|
||||||
@@ -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.
|
||||||
@@ -9,13 +9,13 @@ This config example shows how we can backup our ZFS-based server to another mach
|
|||||||
|
|
||||||
* Production server ``prod`` with filesystems to back up:
|
* Production server ``prod`` with filesystems to back up:
|
||||||
|
|
||||||
* The entire pool ``zroot``
|
* ``zroot/var/db``
|
||||||
* except ``zroot/var/tmp`` and all child datasets of it
|
* ``zroot/usr/home`` and all its child filesystems
|
||||||
* and except ``zroot/usr/home/paranoid`` which belongs to a user doing backups themselves.
|
* **except** ``zroot/usr/home/paranoid`` belonging to a user doing backups themselves
|
||||||
|
|
||||||
* Backup server ``backups`` with a dataset sub-tree for use by zrepl:
|
* Backup server ``backups`` with
|
||||||
|
|
||||||
* In our example, that will be ``storage/zrepl/sink/prod``.
|
* Filesystem ``storage/zrepl/sink/prod`` + children dedicated to backups of ``prod``
|
||||||
|
|
||||||
Our backup solution should fulfill the following requirements:
|
Our backup solution should fulfill the following requirements:
|
||||||
|
|
||||||
|
|||||||
@@ -1,93 +0,0 @@
|
|||||||
.. include:: ../global.rst.inc
|
|
||||||
|
|
||||||
.. _quickstart-fan-out-replication:
|
|
||||||
|
|
||||||
Fan-out replication
|
|
||||||
===================
|
|
||||||
|
|
||||||
This quick-start example demonstrates how to implement a fan-out replication setup where datasets on a server (A) are replicated to multiple targets (B, C, etc.).
|
|
||||||
|
|
||||||
This example uses multiple ``source`` jobs on server A and ``pull`` jobs on the target servers.
|
|
||||||
|
|
||||||
.. WARNING::
|
|
||||||
|
|
||||||
Before implementing this setup, please see the caveats listed in the :ref:`fan-out replication configuration overview <fan-out-replication>`.
|
|
||||||
|
|
||||||
Overview
|
|
||||||
--------
|
|
||||||
|
|
||||||
On the source server (A), there should be:
|
|
||||||
|
|
||||||
* A ``snap`` job
|
|
||||||
|
|
||||||
* Creates the snapshots
|
|
||||||
* Handles the pruning of snapshots
|
|
||||||
|
|
||||||
* A ``source`` job for target B
|
|
||||||
|
|
||||||
* Accepts connections from server B and B only
|
|
||||||
|
|
||||||
* Further ``source`` jobs for each additional target (C, D, etc.)
|
|
||||||
|
|
||||||
* Listens on a unique port
|
|
||||||
* Only accepts connections from the specific target
|
|
||||||
|
|
||||||
On each target server, there should be:
|
|
||||||
|
|
||||||
* A ``pull`` job that connects to the corresponding ``source`` job on A
|
|
||||||
|
|
||||||
* ``prune_sender`` should keep all snapshots since A's ``snap`` job handles the pruning
|
|
||||||
* ``prune_receiver`` can be configured as appropriate on each target server
|
|
||||||
|
|
||||||
Generate TLS Certificates
|
|
||||||
-------------------------
|
|
||||||
|
|
||||||
Mutual TLS via the :ref:`TLS client authentication transport <transport-tcp+tlsclientauth>` can be used to secure the connections between the servers. In this example, a self-signed certificate is created for each server without setting up a CA.
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
source=a.example.com
|
|
||||||
targets=(
|
|
||||||
b.example.com
|
|
||||||
c.example.com
|
|
||||||
# ...
|
|
||||||
)
|
|
||||||
|
|
||||||
for server in "${source}" "${targets[@]}"; do
|
|
||||||
openssl req -x509 -sha256 -nodes \
|
|
||||||
-newkey rsa:4096 \
|
|
||||||
-days 365 \
|
|
||||||
-keyout "${server}.key" \
|
|
||||||
-out "${server}.crt" \
|
|
||||||
-addext "subjectAltName = DNS:${server}" \
|
|
||||||
-subj "/CN=${server}"
|
|
||||||
done
|
|
||||||
|
|
||||||
# Distribute each host's keypair
|
|
||||||
for server in "${source}" "${targets[@]}"; do
|
|
||||||
ssh root@"${server}" mkdir /etc/zrepl
|
|
||||||
scp "${server}".{crt,key} root@"${server}":/etc/zrepl/
|
|
||||||
done
|
|
||||||
|
|
||||||
# Distribute target certificates to the source
|
|
||||||
scp "${targets[@]/%/.crt}" root@"${source}":/etc/zrepl/
|
|
||||||
|
|
||||||
# Distribute source certificate to the targets
|
|
||||||
for server in "${targets[@]}"; do
|
|
||||||
scp "${source}.crt" root@"${server}":/etc/zrepl/
|
|
||||||
done
|
|
||||||
|
|
||||||
Configure source server A
|
|
||||||
-------------------------
|
|
||||||
|
|
||||||
.. literalinclude:: ../../config/samples/quickstart_fan_out_replication_source.yml
|
|
||||||
|
|
||||||
Configure each target server
|
|
||||||
----------------------------
|
|
||||||
|
|
||||||
.. literalinclude:: ../../config/samples/quickstart_fan_out_replication_target.yml
|
|
||||||
|
|
||||||
Go Back To Quickstart Guide
|
|
||||||
---------------------------
|
|
||||||
|
|
||||||
:ref:`Click here <quickstart-apply-config>` to go back to the quickstart guide.
|
|
||||||
+29
-23
@@ -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
|
-e git://github.com/rwblair/sphinxcontrib-versioning.git@7e3885a389a809e17ea55261316b7b0e98dbf98f#egg=sphinxcontrib-versioning
|
||||||
|
sphinxcontrib-websupport==1.1.2
|
||||||
|
urllib3==1.25.3
|
||||||
|
|||||||
@@ -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 '),
|
||||||
|
}
|
||||||
|
|
||||||
@@ -32,9 +32,6 @@ We would like to thank the following people and organizations for supporting zre
|
|||||||
|
|
||||||
<div class="fa fa-code" style="width: 1em;"></div>
|
<div class="fa fa-code" style="width: 1em;"></div>
|
||||||
|
|
||||||
* |supporter-std| `Max Christian Pohle <https://coderonline.de>`_
|
|
||||||
* |supporter-gold| Prominic.NET, Inc.
|
|
||||||
* |supporter-std| Torsten Blum
|
|
||||||
* |supporter-gold| Cyberiada GmbH
|
* |supporter-gold| Cyberiada GmbH
|
||||||
* |supporter-std| `Gordon Schulz <https://github.com/azmodude>`_
|
* |supporter-std| `Gordon Schulz <https://github.com/azmodude>`_
|
||||||
* |supporter-std| `@jwittlincohen <https://github.com/jwittlincohen>`_
|
* |supporter-std| `@jwittlincohen <https://github.com/jwittlincohen>`_
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user