Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 893d686eef |
+119
-343
@@ -1,79 +1,74 @@
|
|||||||
version: 2.1
|
version: 2.0
|
||||||
|
workflows:
|
||||||
|
version: 2
|
||||||
|
build:
|
||||||
|
jobs:
|
||||||
|
- build-1.11
|
||||||
|
- build-1.12
|
||||||
|
- build-1.13
|
||||||
|
- build-1.14
|
||||||
|
- build-latest
|
||||||
|
- test-build-in-docker
|
||||||
|
jobs:
|
||||||
|
|
||||||
commands:
|
# build-latest serves as the template
|
||||||
setup-home-local-bin:
|
# we use YAML anchors & aliases to exchange the docker image (and hence Go version used for the build)
|
||||||
steps:
|
build-latest: &build-latest
|
||||||
- run:
|
description: Builds zrepl
|
||||||
shell: /bin/bash -euo pipefail
|
|
||||||
command: |
|
|
||||||
mkdir -p "$HOME/.local/bin"
|
|
||||||
line='export PATH="$HOME/.local/bin:$PATH"'
|
|
||||||
if grep "$line" $BASH_ENV >/dev/null; then
|
|
||||||
echo "$line" >> $BASH_ENV
|
|
||||||
fi
|
|
||||||
|
|
||||||
invoke-lazy-sh:
|
|
||||||
parameters:
|
parameters:
|
||||||
subcommand:
|
image:
|
||||||
|
description: "the docker image that the job should use"
|
||||||
type: string
|
type: string
|
||||||
|
docker:
|
||||||
|
- image: circleci/golang:latest
|
||||||
|
environment:
|
||||||
|
# required by lazy.sh
|
||||||
|
TERM: xterm
|
||||||
|
working_directory: /go/src/github.com/zrepl/zrepl
|
||||||
steps:
|
steps:
|
||||||
- run:
|
- run:
|
||||||
environment:
|
name: Setup environment variables
|
||||||
TERM: xterm
|
|
||||||
command: ./lazy.sh <<parameters.subcommand>>
|
|
||||||
|
|
||||||
apt-update-and-install-common-deps:
|
|
||||||
steps:
|
|
||||||
- run: sudo apt update && sudo apt install gawk make
|
|
||||||
|
|
||||||
restore-cache-gomod:
|
|
||||||
steps:
|
|
||||||
- restore_cache:
|
|
||||||
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:
|
|
||||||
steps:
|
|
||||||
- apt-update-and-install-common-deps
|
|
||||||
- invoke-lazy-sh:
|
|
||||||
subcommand: godep
|
|
||||||
|
|
||||||
install-docdep:
|
|
||||||
steps:
|
|
||||||
- apt-update-and-install-common-deps
|
|
||||||
- run: sudo apt install python3 python3-pip libgirepository1.0-dev
|
|
||||||
- invoke-lazy-sh:
|
|
||||||
subcommand: docdep
|
|
||||||
|
|
||||||
download-and-install-minio-client:
|
|
||||||
steps:
|
|
||||||
- setup-home-local-bin
|
|
||||||
- restore_cache:
|
|
||||||
key: minio-client-v2
|
|
||||||
- run:
|
|
||||||
shell: /bin/bash -eo pipefail
|
|
||||||
command: |
|
command: |
|
||||||
if which mc; then exit 0; fi
|
# used by pip (for docs)
|
||||||
sudo curl -sSL https://dl.min.io/client/mc/release/linux-amd64/archive/mc.RELEASE.2020-08-20T00-23-01Z \
|
echo 'export PATH="$HOME/.local/bin:$PATH"' >> $BASH_ENV
|
||||||
-o "$HOME/.local/bin/mc"
|
# we use modules
|
||||||
sudo chmod +x "$HOME/.local/bin/mc"
|
echo 'export GO111MODULE=on' >> $BASH_ENV
|
||||||
- save_cache:
|
|
||||||
key: minio-client-v2
|
- restore_cache:
|
||||||
paths:
|
keys:
|
||||||
- "$HOME/.local/bin/mc"
|
- source
|
||||||
|
- protobuf
|
||||||
|
|
||||||
|
- checkout
|
||||||
|
|
||||||
|
- save_cache:
|
||||||
|
key: source
|
||||||
|
paths:
|
||||||
|
- ".git"
|
||||||
|
|
||||||
|
# install deps
|
||||||
|
- run: wget https://github.com/protocolbuffers/protobuf/releases/download/v3.6.1/protoc-3.6.1-linux-x86_64.zip
|
||||||
|
- run: echo "6003de742ea3fcf703cfec1cd4a3380fd143081a2eb0e559065563496af27807 protoc-3.6.1-linux-x86_64.zip" | sha256sum -c
|
||||||
|
- run: sudo unzip -d /usr protoc-3.6.1-linux-x86_64.zip
|
||||||
|
- save_cache:
|
||||||
|
key: protobuf
|
||||||
|
paths:
|
||||||
|
- "/usr/include/google/protobuf"
|
||||||
|
|
||||||
|
- run: sudo apt update && sudo apt install python3 python3-pip libgirepository1.0-dev gawk
|
||||||
|
- run: ./lazy.sh devsetup
|
||||||
|
|
||||||
|
- run: make zrepl-bin
|
||||||
|
- run: make vet
|
||||||
|
- run: make lint
|
||||||
|
- run: make release
|
||||||
|
- run: make test-go
|
||||||
|
# cannot run test-platform because circle-ci runs in linux containers
|
||||||
|
|
||||||
|
- store_artifacts:
|
||||||
|
path: ./artifacts/release
|
||||||
|
when: always
|
||||||
|
|
||||||
upload-minio:
|
|
||||||
parameters:
|
|
||||||
src:
|
|
||||||
type: string
|
|
||||||
dst:
|
|
||||||
type: string
|
|
||||||
steps:
|
|
||||||
- run:
|
- run:
|
||||||
shell: /bin/bash -eo pipefail
|
shell: /bin/bash -eo pipefail
|
||||||
when: always
|
when: always
|
||||||
@@ -84,296 +79,77 @@ commands:
|
|||||||
fi
|
fi
|
||||||
set -u # from now on
|
set -u # from now on
|
||||||
|
|
||||||
|
# Download and install minio
|
||||||
|
curl -sSL https://dl.minio.io/client/mc/release/linux-amd64/mc -o ${GOPATH}/bin/mc
|
||||||
|
chmod +x ${GOPATH}/bin/mc
|
||||||
mc config host add --api s3v4 zrepl-minio https://minio.cschwarz.com ${MINIO_ACCESS_KEY} ${MINIO_SECRET_KEY}
|
mc config host add --api s3v4 zrepl-minio https://minio.cschwarz.com ${MINIO_ACCESS_KEY} ${MINIO_SECRET_KEY}
|
||||||
|
|
||||||
# keep in sync with set-github-minio-status
|
|
||||||
jobprefix=zrepl-ci-artifacts/${CIRCLE_SHA1}-pipeline-<<pipeline.number>>/${CIRCLE_JOB}
|
|
||||||
|
|
||||||
# Upload artifacts
|
# Upload artifacts
|
||||||
mkdir -p ./artifacts
|
echo "$CIRCLE_BUILD_URL" > ./artifacts/release/cirlceci_build_url
|
||||||
mc cp -r <<parameters.src>> "zrepl-minio/$jobprefix/<<parameters.dst>>"
|
mc cp -r artifacts/release "zrepl-minio/zrepl-ci-artifacts/${CIRCLE_SHA1}/${CIRCLE_JOB}/"
|
||||||
|
|
||||||
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
|
# Push Artifact Link to GitHub
|
||||||
REPO="zrepl/zrepl"
|
REPO="zrepl/zrepl"
|
||||||
COMMIT="${CIRCLE_SHA1}"
|
COMMIT="${CIRCLE_SHA1}"
|
||||||
JOB_NAME="${CIRCLE_JOB}"
|
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" \
|
curl "https://api.github.com/repos/$REPO/statuses/$COMMIT" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-H "Authorization: token $GITHUB_COMMIT_STATUS_TOKEN" \
|
-H "Authorization: token $GITHUB_COMMIT_STATUS_TOKEN" \
|
||||||
-X POST \
|
-X POST \
|
||||||
-d '{"context":"'"$CONTEXT"'", "state": "success", "description":"'"$DESCRIPTION"'", "target_url":"'"$TARGETURL"'"}'
|
-d '{"context":"zrepl/publish-ci-artifacts", "state": "success", "description":"CI Build Artifacts for '"$JOB_NAME"'", "target_url":"https://minio.cschwarz.com/minio/zrepl-ci-artifacts/'"$COMMIT"'/"}'
|
||||||
|
|
||||||
|
- run:
|
||||||
|
shell: /bin/bash -euo pipefail
|
||||||
|
command: |
|
||||||
|
# Trigger Debian Package Build
|
||||||
|
curl -v -X POST https://api.github.com/repos/zrepl/debian-binary-packaging/dispatches \
|
||||||
|
-H 'Accept: application/vnd.github.v3+json' \
|
||||||
|
-H "Authorization: token $ZREPL_DEBIAN_BINARYPACKAGIN_TRIGGER_BUILD_GITHUB_TOKEN" \
|
||||||
|
--data '{"event_type": "push", "client_payload": { "zrepl_main_repo_commit": "'"$CIRCLE_SHA1"'", "go_version": "'"${CIRCLE_JOB##build-}"'" }}'
|
||||||
|
|
||||||
trigger-pipeline:
|
build-1.11:
|
||||||
parameters:
|
<<: *build-latest
|
||||||
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:
|
|
||||||
do_ci:
|
|
||||||
type: boolean
|
|
||||||
default: true
|
|
||||||
|
|
||||||
do_release:
|
|
||||||
type: boolean
|
|
||||||
default: false
|
|
||||||
|
|
||||||
release_docker_baseimage_tag:
|
|
||||||
type: string
|
|
||||||
default: "1.19"
|
|
||||||
|
|
||||||
workflows:
|
|
||||||
version: 2
|
|
||||||
|
|
||||||
ci:
|
|
||||||
when: << pipeline.parameters.do_ci >>
|
|
||||||
jobs:
|
|
||||||
- quickcheck-docs
|
|
||||||
- quickcheck-go: &quickcheck-go-smoketest
|
|
||||||
name: quickcheck-go-amd64-linux-1.19
|
|
||||||
goversion: &latest-go-release "1.19"
|
|
||||||
goos: linux
|
|
||||||
goarch: amd64
|
|
||||||
- test-go-on-latest-go-release:
|
|
||||||
goversion: *latest-go-release
|
|
||||||
- quickcheck-go:
|
|
||||||
requires:
|
|
||||||
- quickcheck-go-amd64-linux-1.19 #quickcheck-go-smoketest.name
|
|
||||||
matrix: &quickcheck-go-matrix
|
|
||||||
alias: quickcheck-go-matrix
|
|
||||||
parameters:
|
|
||||||
goversion: [*latest-go-release, "1.12"]
|
|
||||||
goos: ["linux", "freebsd"]
|
|
||||||
goarch: ["amd64", "arm64"]
|
|
||||||
exclude:
|
|
||||||
# don't re-do quickcheck-go-smoketest
|
|
||||||
- goversion: *latest-go-release
|
|
||||||
goos: linux
|
|
||||||
goarch: amd64
|
|
||||||
# not supported by Go 1.12
|
|
||||||
- goversion: "1.12"
|
|
||||||
goos: freebsd
|
|
||||||
goarch: arm64
|
|
||||||
|
|
||||||
release:
|
|
||||||
when: << pipeline.parameters.do_release >>
|
|
||||||
jobs:
|
|
||||||
- release-build
|
|
||||||
- release-deb:
|
|
||||||
requires:
|
|
||||||
- release-build
|
|
||||||
- release-rpm:
|
|
||||||
requires:
|
|
||||||
- release-build
|
|
||||||
- release-upload:
|
|
||||||
requires:
|
|
||||||
- release-build
|
|
||||||
- release-deb
|
|
||||||
- release-rpm
|
|
||||||
|
|
||||||
periodic:
|
|
||||||
triggers:
|
|
||||||
- schedule:
|
|
||||||
cron: "00 17 * * *"
|
|
||||||
filters:
|
|
||||||
branches:
|
|
||||||
only:
|
|
||||||
- master
|
|
||||||
- stable
|
|
||||||
- problame/circleci-build
|
|
||||||
jobs:
|
|
||||||
- periodic-full-pipeline-run
|
|
||||||
|
|
||||||
zrepl.github.io:
|
|
||||||
jobs:
|
|
||||||
- publish-zrepl-github-io:
|
|
||||||
filters:
|
|
||||||
branches:
|
|
||||||
only:
|
|
||||||
- stable
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
quickcheck-docs:
|
|
||||||
docker:
|
docker:
|
||||||
- image: cimg/base:2020.08
|
- image: circleci/golang:1.11
|
||||||
steps:
|
|
||||||
- checkout
|
|
||||||
- install-docdep
|
|
||||||
- run: make docs
|
|
||||||
|
|
||||||
- download-and-install-minio-client
|
build-1.12:
|
||||||
- upload-minio:
|
<<: *build-latest
|
||||||
src: artifacts
|
|
||||||
dst: ""
|
|
||||||
- set-github-minio-status:
|
|
||||||
context: artifacts/${CIRCLE_JOB}
|
|
||||||
description: artifacts of CI job ${CIRCLE_JOB}
|
|
||||||
minio-dst: ""
|
|
||||||
|
|
||||||
quickcheck-go:
|
|
||||||
parameters:
|
|
||||||
goversion:
|
|
||||||
type: string
|
|
||||||
goos:
|
|
||||||
type: string
|
|
||||||
goarch:
|
|
||||||
type: string
|
|
||||||
docker:
|
docker:
|
||||||
- image: cimg/go:<<parameters.goversion>>
|
- image: circleci/golang:1.12
|
||||||
|
|
||||||
|
build-1.13:
|
||||||
|
<<: *build-latest
|
||||||
|
docker:
|
||||||
|
- image: circleci/golang:1.13
|
||||||
|
|
||||||
|
build-1.14:
|
||||||
|
<<: *build-latest
|
||||||
|
docker:
|
||||||
|
- image: circleci/golang:1.14
|
||||||
|
|
||||||
|
# this job tries to mimic the build-in-docker instructions
|
||||||
|
# given in docs/installation.rst
|
||||||
|
#
|
||||||
|
# However, CircleCI doesn't support volume mounts, so we have to copy
|
||||||
|
# the source into the build-container by modifying the Dockerfile
|
||||||
|
test-build-in-docker:
|
||||||
|
description: Check that build-in-docker works
|
||||||
|
docker:
|
||||||
|
- image: circleci/golang:latest
|
||||||
environment:
|
environment:
|
||||||
GOOS: <<parameters.goos>>
|
working_directory: /go/src/github.com/zrepl/zrepl
|
||||||
GOARCH: <<parameters.goarch>>
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- checkout
|
- checkout
|
||||||
|
- setup_remote_docker
|
||||||
- restore-cache-gomod
|
- run:
|
||||||
- run: go mod download
|
name: (hacky) circleci doesn't allow volume mounts, so copy src to container
|
||||||
- run: cd build && go mod download
|
command: echo "ADD . /src" >> build.Dockerfile
|
||||||
- save-cache-gomod
|
- run:
|
||||||
|
name: (hacky) commit modified Dockerfile to avoid failing git clean check in Makefile
|
||||||
- install-godep
|
command: git -c user.name='circleci' -c user.email='circleci@localhost' commit -m 'CIRCLECI modified Dockerfile with zrepl src' --author 'autoauthor <circleci@localhost>' -- build.Dockerfile
|
||||||
- run: make formatcheck
|
- run:
|
||||||
- run: make generate-platform-test-list
|
name: build the build image (build deps)
|
||||||
- run: make zrepl-bin test-platform-bin
|
command: docker build -t zrepl_build -f build.Dockerfile .
|
||||||
- run: make vet
|
- run:
|
||||||
- run: make lint
|
name: try compiling
|
||||||
|
command: docker run -it zrepl_build make release
|
||||||
- download-and-install-minio-client
|
|
||||||
- run: rm -f artifacts/generate-platform-test-list
|
|
||||||
- store_artifacts:
|
|
||||||
path: artifacts
|
|
||||||
- upload-minio:
|
|
||||||
src: artifacts
|
|
||||||
dst: ""
|
|
||||||
- set-github-minio-status:
|
|
||||||
context: artifacts/${CIRCLE_JOB}
|
|
||||||
description: artifacts of CI job ${CIRCLE_JOB}
|
|
||||||
minio-dst: ""
|
|
||||||
|
|
||||||
test-go-on-latest-go-release:
|
|
||||||
parameters:
|
|
||||||
goversion:
|
|
||||||
type: string
|
|
||||||
docker:
|
|
||||||
- image: cimg/go:<<parameters.goversion>>
|
|
||||||
steps:
|
|
||||||
- checkout
|
|
||||||
- restore-cache-gomod
|
|
||||||
- run: make test-go
|
|
||||||
# don't save-cache-gomod here, test-go doesn't pull all the dependencies
|
|
||||||
|
|
||||||
release-build:
|
|
||||||
machine:
|
|
||||||
image: ubuntu-2004:202201-02
|
|
||||||
steps:
|
|
||||||
- checkout
|
|
||||||
- run: make release-docker RELEASE_DOCKER_BASEIMAGE_TAG=<<pipeline.parameters.release_docker_baseimage_tag>>
|
|
||||||
- persist_to_workspace:
|
|
||||||
root: .
|
|
||||||
paths: [.]
|
|
||||||
release-deb:
|
|
||||||
machine:
|
|
||||||
image: ubuntu-2004:202201-02
|
|
||||||
steps:
|
|
||||||
- attach_workspace:
|
|
||||||
at: .
|
|
||||||
- run: make debs-docker
|
|
||||||
- persist_to_workspace:
|
|
||||||
root: .
|
|
||||||
paths:
|
|
||||||
- "artifacts/*.deb"
|
|
||||||
|
|
||||||
release-rpm:
|
|
||||||
machine:
|
|
||||||
image: ubuntu-2004:202201-02
|
|
||||||
steps:
|
|
||||||
- attach_workspace:
|
|
||||||
at: .
|
|
||||||
- run: make rpms-docker
|
|
||||||
- persist_to_workspace:
|
|
||||||
root: .
|
|
||||||
paths:
|
|
||||||
- "artifacts/*.rpm"
|
|
||||||
|
|
||||||
release-upload:
|
|
||||||
docker:
|
|
||||||
- image: cimg/base:2020.08
|
|
||||||
steps:
|
|
||||||
- attach_workspace:
|
|
||||||
at: .
|
|
||||||
- store_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:
|
|
||||||
docker:
|
|
||||||
- image: cimg/python:3.7
|
|
||||||
steps:
|
|
||||||
- checkout
|
|
||||||
- invoke-lazy-sh:
|
|
||||||
subcommand: docdep
|
|
||||||
- run:
|
|
||||||
command: |
|
|
||||||
git config --global user.email "zreplbot@cschwarz.com"
|
|
||||||
git config --global user.name "zrepl-github-io-ci"
|
|
||||||
|
|
||||||
# https://circleci.com/docs/2.0/add-ssh-key/#adding-multiple-keys-with-blank-hostnames
|
|
||||||
- run: ssh-add -D
|
|
||||||
# the default circleci ssh config only additional ssh keys for Host !github.com
|
|
||||||
- run:
|
|
||||||
command: |
|
|
||||||
cat > ~/.ssh/config \<<EOF
|
|
||||||
Host *
|
|
||||||
IdentityFile /home/circleci/.ssh/id_rsa_458e62c517f6c480e40452126ce47421
|
|
||||||
EOF
|
|
||||||
- add_ssh_keys:
|
|
||||||
fingerprints:
|
|
||||||
# deploy key for zrepl.github.io
|
|
||||||
- "45:8e:62:c5:17:f6:c4:80:e4:04:52:12:6c:e4:74:21"
|
|
||||||
|
|
||||||
- run: bash -x docs/publish.sh -c -a
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
COMMIT="$1"
|
|
||||||
GO_VERSION="$2"
|
|
||||||
|
|
||||||
curl -v -X POST https://api.github.com/repos/zrepl/debian-binary-packaging/dispatches \
|
|
||||||
-H 'Accept: application/vnd.github.v3+json' \
|
|
||||||
-H "Authorization: token $GITHUB_ACCESS_TOKEN" \
|
|
||||||
--data '{"event_type": "push", "client_payload": { "zrepl_main_repo_commit": "'"$COMMIT"'", "go_version": "'"$GO_VERSION"'" }}'
|
|
||||||
@@ -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:"
|
|
||||||
|
|
||||||
|
|||||||
+70
@@ -0,0 +1,70 @@
|
|||||||
|
dist: xenial
|
||||||
|
services:
|
||||||
|
- docker
|
||||||
|
|
||||||
|
env: # for allow_failures: https://docs.travis-ci.com/user/customizing-the-build/
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
|
||||||
|
- language: go
|
||||||
|
name: "Build in Docker (docs/installation.rst)"
|
||||||
|
script:
|
||||||
|
- sudo docker build -t zrepl_build -f build.Dockerfile .
|
||||||
|
- |
|
||||||
|
sudo docker run -it --rm \
|
||||||
|
-v "${PWD}:/go/src/github.com/zrepl/zrepl" \
|
||||||
|
--user "$(id -u):$(id -g)" \
|
||||||
|
zrepl_build make vendordeps release
|
||||||
|
|
||||||
|
- &zrepl_build_template
|
||||||
|
language: go
|
||||||
|
go_import_path: github.com/zrepl/zrepl
|
||||||
|
before_install:
|
||||||
|
- wget https://github.com/protocolbuffers/protobuf/releases/download/v3.6.1/protoc-3.6.1-linux-x86_64.zip
|
||||||
|
- echo "6003de742ea3fcf703cfec1cd4a3380fd143081a2eb0e559065563496af27807 protoc-3.6.1-linux-x86_64.zip" | sha256sum -c
|
||||||
|
- sudo unzip -d /usr protoc-3.6.1-linux-x86_64.zip
|
||||||
|
- ./lazy.sh godep
|
||||||
|
- make vendordeps
|
||||||
|
script:
|
||||||
|
- make
|
||||||
|
- make vet
|
||||||
|
- make test
|
||||||
|
- make lint
|
||||||
|
- make artifacts/zrepl-freebsd-amd64
|
||||||
|
- make artifacts/zrepl-linux-amd64
|
||||||
|
- make artifacts/zrepl-darwin-amd64
|
||||||
|
go:
|
||||||
|
- "1.11"
|
||||||
|
|
||||||
|
- <<: *zrepl_build_template
|
||||||
|
go:
|
||||||
|
- "1.12"
|
||||||
|
|
||||||
|
- <<: *zrepl_build_template
|
||||||
|
go:
|
||||||
|
- "master"
|
||||||
|
|
||||||
|
- &zrepl_docs_template
|
||||||
|
language: python
|
||||||
|
python:
|
||||||
|
- "3.4"
|
||||||
|
install:
|
||||||
|
- sudo apt-get install libgirepository1.0-dev
|
||||||
|
- pip install -r docs/requirements.txt
|
||||||
|
script:
|
||||||
|
- make docs
|
||||||
|
- <<: *zrepl_docs_template
|
||||||
|
python:
|
||||||
|
- "3.5"
|
||||||
|
- <<: *zrepl_docs_template
|
||||||
|
python:
|
||||||
|
- "3.6"
|
||||||
|
- <<: *zrepl_docs_template
|
||||||
|
python:
|
||||||
|
- "3.7"
|
||||||
|
|
||||||
|
|
||||||
|
allow_failures:
|
||||||
|
- <<: *zrepl_build_template
|
||||||
|
go:
|
||||||
|
- "master"
|
||||||
@@ -28,8 +28,6 @@ GO_BUILDFLAGS := $(GO_MOD_READONLY) $(GO_EXTRA_BUILDFLAGS)
|
|||||||
GO_BUILD := $(GO_ENV_VARS) $(GO) build $(GO_BUILDFLAGS) -ldflags $(GO_LDFLAGS)
|
GO_BUILD := $(GO_ENV_VARS) $(GO) build $(GO_BUILDFLAGS) -ldflags $(GO_LDFLAGS)
|
||||||
GOLANGCI_LINT := golangci-lint
|
GOLANGCI_LINT := golangci-lint
|
||||||
GOCOVMERGE := gocovmerge
|
GOCOVMERGE := gocovmerge
|
||||||
RELEASE_DOCKER_BASEIMAGE_TAG ?= 1.19
|
|
||||||
RELEASE_DOCKER_BASEIMAGE ?= golang:$(RELEASE_DOCKER_BASEIMAGE_TAG)
|
|
||||||
|
|
||||||
ifneq ($(GOARM),)
|
ifneq ($(GOARM),)
|
||||||
ZREPL_TARGET_TUPLE := $(GOOS)-$(GOARCH)v$(GOARM)
|
ZREPL_TARGET_TUPLE := $(GOOS)-$(GOARCH)v$(GOARM)
|
||||||
@@ -55,89 +53,10 @@ 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"
|
||||||
|
|
||||||
release-docker: $(ARTIFACTDIR)
|
|
||||||
sed 's/FROM.*!SUBSTITUTED_BY_MAKEFILE/FROM $(RELEASE_DOCKER_BASEIMAGE)/' build.Dockerfile > artifacts/release-docker.Dockerfile
|
|
||||||
docker build -t zrepl_release --pull -f artifacts/release-docker.Dockerfile .
|
|
||||||
docker run --rm -i -v $(CURDIR):/src -u $$(id -u):$$(id -g) \
|
|
||||||
zrepl_release \
|
|
||||||
make release GOOS=$(GOOS) GOARCH=$(GOARCH) GOARM=$(GOARM)
|
|
||||||
|
|
||||||
debs-docker:
|
|
||||||
$(MAKE) _debs_or_rpms_docker _DEB_OR_RPM=deb
|
|
||||||
rpms-docker:
|
|
||||||
$(MAKE) _debs_or_rpms_docker _DEB_OR_RPM=rpm
|
|
||||||
_debs_or_rpms_docker: # artifacts/_zrepl.zsh_completion artifacts/bash_completion docs zrepl-bin
|
|
||||||
$(MAKE) $(_DEB_OR_RPM)-docker GOOS=linux GOARCH=amd64
|
|
||||||
$(MAKE) $(_DEB_OR_RPM)-docker GOOS=linux GOARCH=arm64
|
|
||||||
$(MAKE) $(_DEB_OR_RPM)-docker GOOS=linux GOARCH=arm GOARM=7
|
|
||||||
$(MAKE) $(_DEB_OR_RPM)-docker GOOS=linux GOARCH=386
|
|
||||||
|
|
||||||
rpm: $(ARTIFACTDIR) # artifacts/_zrepl.zsh_completion artifacts/bash_completion docs zrepl-bin
|
|
||||||
$(eval _ZREPL_RPM_VERSION := $(subst -,.,$(_ZREPL_VERSION)))
|
|
||||||
$(eval _ZREPL_RPM_TOPDIR_ABS := $(CURDIR)/$(ARTIFACTDIR)/rpmbuild)
|
|
||||||
rm -rf "$(_ZREPL_RPM_TOPDIR_ABS)"
|
|
||||||
mkdir "$(_ZREPL_RPM_TOPDIR_ABS)"
|
|
||||||
mkdir -p "$(_ZREPL_RPM_TOPDIR_ABS)"/{SPECS,RPMS,BUILD,BUILDROOT}
|
|
||||||
sed "s/^Version:.*/Version: $(_ZREPL_RPM_VERSION)/g" \
|
|
||||||
packaging/rpm/zrepl.spec > $(_ZREPL_RPM_TOPDIR_ABS)/SPECS/zrepl.spec
|
|
||||||
|
|
||||||
# see /usr/lib/rpm/platform
|
|
||||||
ifeq ($(GOARCH),amd64)
|
|
||||||
$(eval _ZREPL_RPMBUILD_TARGET := x86_64)
|
|
||||||
else ifeq ($(GOARCH), 386)
|
|
||||||
$(eval _ZREPL_RPMBUILD_TARGET := i386)
|
|
||||||
else ifeq ($(GOARCH), arm64)
|
|
||||||
$(eval _ZREPL_RPMBUILD_TARGET := aarch64)
|
|
||||||
else ifeq ($(GOARCH), arm)
|
|
||||||
$(eval _ZREPL_RPMBUILD_TARGET := armv7hl)
|
|
||||||
else
|
|
||||||
$(eval _ZREPL_RPMBUILD_TARGET := $(GOARCH))
|
|
||||||
endif
|
|
||||||
rpmbuild \
|
|
||||||
--build-in-place \
|
|
||||||
--define "_sourcedir $(CURDIR)" \
|
|
||||||
--define "_topdir $(_ZREPL_RPM_TOPDIR_ABS)" \
|
|
||||||
--define "_zrepl_binary_filename zrepl-$(ZREPL_TARGET_TUPLE)" \
|
|
||||||
--target $(_ZREPL_RPMBUILD_TARGET) \
|
|
||||||
-bb "$(_ZREPL_RPM_TOPDIR_ABS)"/SPECS/zrepl.spec
|
|
||||||
cp "$(_ZREPL_RPM_TOPDIR_ABS)"/RPMS/$(_ZREPL_RPMBUILD_TARGET)/zrepl-$(_ZREPL_RPM_VERSION)*.rpm $(ARTIFACTDIR)/
|
|
||||||
|
|
||||||
rpm-docker:
|
|
||||||
docker build -t zrepl_rpm_pkg --pull -f packaging/rpm/Dockerfile .
|
|
||||||
docker run --rm -i -v $(CURDIR):/build/src -u $$(id -u):$$(id -g) \
|
|
||||||
zrepl_rpm_pkg \
|
|
||||||
make rpm GOOS=$(GOOS) GOARCH=$(GOARCH) GOARM=$(GOARM)
|
|
||||||
|
|
||||||
deb: $(ARTIFACTDIR) # artifacts/_zrepl.zsh_completion artifacts/bash_completion docs zrepl-bin
|
|
||||||
|
|
||||||
cp packaging/deb/debian/changelog.template packaging/deb/debian/changelog
|
|
||||||
sed -i 's/DATE_DASH_R_OUTPUT/$(shell date -R)/' packaging/deb/debian/changelog
|
|
||||||
VERSION="$(subst -,.,$(_ZREPL_VERSION))"; \
|
|
||||||
export VERSION="$${VERSION#v}"; \
|
|
||||||
sed -i 's/VERSION/'"$$VERSION"'/' packaging/deb/debian/changelog
|
|
||||||
|
|
||||||
ifeq ($(GOARCH), arm)
|
|
||||||
$(eval DEB_HOST_ARCH := armhf)
|
|
||||||
else ifeq ($(GOARCH), 386)
|
|
||||||
$(eval DEB_HOST_ARCH := i386)
|
|
||||||
else
|
|
||||||
$(eval DEB_HOST_ARCH := $(GOARCH))
|
|
||||||
endif
|
|
||||||
|
|
||||||
export ZREPL_DPKG_ZREPL_BINARY_FILENAME=zrepl-$(ZREPL_TARGET_TUPLE); \
|
|
||||||
dpkg-buildpackage -b --no-sign --host-arch $(DEB_HOST_ARCH)
|
|
||||||
cp ../*.deb artifacts/
|
|
||||||
|
|
||||||
deb-docker:
|
|
||||||
docker build -t zrepl_debian_pkg --pull -f packaging/deb/Dockerfile .
|
|
||||||
docker run --rm -i -v $(CURDIR):/build/src -u $$(id -u):$$(id -g) \
|
|
||||||
zrepl_debian_pkg \
|
|
||||||
make deb GOOS=$(GOOS) GOARCH=$(GOARCH) GOARM=$(GOARM)
|
|
||||||
|
|
||||||
# expects `release` target to have run before
|
# expects `release` target to have run before
|
||||||
NOARCH_TARBALL := $(ARTIFACTDIR)/zrepl-noarch.tar
|
NOARCH_TARBALL := $(ARTIFACTDIR)/zrepl-noarch.tar
|
||||||
wrapup-and-checksum:
|
wrapup-and-checksum:
|
||||||
@@ -173,10 +92,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 \
|
||||||
@@ -193,8 +108,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
|
||||||
@@ -234,26 +147,11 @@ zrepl-bin:
|
|||||||
generate-platform-test-list:
|
generate-platform-test-list:
|
||||||
$(GO_BUILD) -o $(ARTIFACTDIR)/generate-platform-test-list ./platformtest/tests/gen
|
$(GO_BUILD) -o $(ARTIFACTDIR)/generate-platform-test-list ./platformtest/tests/gen
|
||||||
|
|
||||||
COVER_PLATFORM_BIN_PATH := $(ARTIFACTDIR)/platformtest-cover-$(ZREPL_TARGET_TUPLE)
|
test-platform-bin:
|
||||||
cover-platform-bin:
|
|
||||||
$(GO_ENV_VARS) $(GO) test $(GO_BUILDFLAGS) \
|
$(GO_ENV_VARS) $(GO) test $(GO_BUILDFLAGS) \
|
||||||
-c -o "$(COVER_PLATFORM_BIN_PATH)" \
|
-c -o "$(ARTIFACTDIR)/platformtest-$(ZREPL_TARGET_TUPLE)" \
|
||||||
-covermode=atomic -cover -coverpkg github.com/zrepl/zrepl/... \
|
-covermode=atomic -cover -coverpkg github.com/zrepl/zrepl/... \
|
||||||
./platformtest/harness
|
./platformtest/harness
|
||||||
cover-platform:
|
|
||||||
# do not track dependency on cover-platform-bin to allow build of binary outside of test VM
|
|
||||||
export _TEST_PLATFORM_CMD="$(COVER_PLATFORM_BIN_PATH) \
|
|
||||||
-test.coverprofile \"$(ARTIFACTDIR)/platformtest.cover\" \
|
|
||||||
-test.v \
|
|
||||||
__DEVEL--i-heard-you-like-tests"; \
|
|
||||||
$(MAKE) _test-or-cover-platform-impl
|
|
||||||
|
|
||||||
TEST_PLATFORM_BIN_PATH := $(ARTIFACTDIR)/platformtest-$(ZREPL_TARGET_TUPLE)
|
|
||||||
test-platform-bin:
|
|
||||||
$(GO_BUILD) -o "$(TEST_PLATFORM_BIN_PATH)" ./platformtest/harness
|
|
||||||
test-platform:
|
|
||||||
export _TEST_PLATFORM_CMD="\"$(TEST_PLATFORM_BIN_PATH)\""; \
|
|
||||||
$(MAKE) _test-or-cover-platform-impl
|
|
||||||
|
|
||||||
ZREPL_PLATFORMTEST_POOLNAME := zreplplatformtest
|
ZREPL_PLATFORMTEST_POOLNAME := zreplplatformtest
|
||||||
ZREPL_PLATFORMTEST_IMAGEPATH := /tmp/zreplplatformtest.pool.img
|
ZREPL_PLATFORMTEST_IMAGEPATH := /tmp/zreplplatformtest.pool.img
|
||||||
@@ -261,14 +159,14 @@ ZREPL_PLATFORMTEST_MOUNTPOINT := /tmp/zreplplatformtest.pool
|
|||||||
ZREPL_PLATFORMTEST_ZFS_LOG := /tmp/zreplplatformtest.zfs.log
|
ZREPL_PLATFORMTEST_ZFS_LOG := /tmp/zreplplatformtest.zfs.log
|
||||||
# ZREPL_PLATFORMTEST_STOP_AND_KEEP := -failure.stop-and-keep-pool
|
# ZREPL_PLATFORMTEST_STOP_AND_KEEP := -failure.stop-and-keep-pool
|
||||||
ZREPL_PLATFORMTEST_ARGS :=
|
ZREPL_PLATFORMTEST_ARGS :=
|
||||||
_test-or-cover-platform-impl: $(ARTIFACTDIR)
|
test-platform: $(ARTIFACTDIR) # do not track dependency on test-platform-bin to allow build of platformtest outside of test VM
|
||||||
ifndef _TEST_PLATFORM_CMD
|
|
||||||
$(error _TEST_PLATFORM_CMD is undefined, caller 'cover-platform' or 'test-platform' should have defined it)
|
|
||||||
endif
|
|
||||||
rm -f "$(ZREPL_PLATFORMTEST_ZFS_LOG)"
|
rm -f "$(ZREPL_PLATFORMTEST_ZFS_LOG)"
|
||||||
rm -f "$(ARTIFACTDIR)/platformtest.cover"
|
rm -f "$(ARTIFACTDIR)/platformtest.cover"
|
||||||
platformtest/logmockzfs/logzfsenv "$(ZREPL_PLATFORMTEST_ZFS_LOG)" `which zfs` \
|
platformtest/logmockzfs/logzfsenv "$(ZREPL_PLATFORMTEST_ZFS_LOG)" `which zfs` \
|
||||||
$(_TEST_PLATFORM_CMD) \
|
"$(ARTIFACTDIR)/platformtest-$(ZREPL_TARGET_TUPLE)" \
|
||||||
|
-test.coverprofile "$(ARTIFACTDIR)/platformtest.cover" \
|
||||||
|
-test.v \
|
||||||
|
__DEVEL--i-heard-you-like-tests \
|
||||||
-poolname "$(ZREPL_PLATFORMTEST_POOLNAME)" \
|
-poolname "$(ZREPL_PLATFORMTEST_POOLNAME)" \
|
||||||
-imagepath "$(ZREPL_PLATFORMTEST_IMAGEPATH)" \
|
-imagepath "$(ZREPL_PLATFORMTEST_IMAGEPATH)" \
|
||||||
-mountpoint "$(ZREPL_PLATFORMTEST_MOUNTPOINT)" \
|
-mountpoint "$(ZREPL_PLATFORMTEST_MOUNTPOINT)" \
|
||||||
@@ -280,32 +178,22 @@ cover-merge: $(ARTIFACTDIR)
|
|||||||
cover-html: cover-merge
|
cover-html: cover-merge
|
||||||
$(GO) tool cover -html "$(ARTIFACTDIR)/merged.cover" -o "$(ARTIFACTDIR)/merged.cover.html"
|
$(GO) tool cover -html "$(ARTIFACTDIR)/merged.cover" -o "$(ARTIFACTDIR)/merged.cover.html"
|
||||||
|
|
||||||
cover-full:
|
test-full:
|
||||||
test "$$(id -u)" = "0" || echo "MUST RUN AS ROOT" 1>&2
|
test "$$(id -u)" = "0" || echo "MUST RUN AS ROOT" 1>&2
|
||||||
$(MAKE) test-go COVER=1
|
$(MAKE) test-go COVER=1
|
||||||
$(MAKE) cover-platform-bin
|
$(MAKE) test-platform
|
||||||
$(MAKE) cover-platform
|
|
||||||
$(MAKE) cover-html
|
$(MAKE) cover-html
|
||||||
|
|
||||||
##################### DEV TARGETS #####################
|
##################### DEV TARGETS #####################
|
||||||
# not part of the build, must do that manually
|
# not part of the build, must do that manually
|
||||||
.PHONY: generate formatcheck format
|
.PHONY: generate format
|
||||||
|
|
||||||
generate: generate-platform-test-list
|
generate: generate-platform-test-list
|
||||||
protoc -I=replication/logic/pdu --go_out=replication/logic/pdu --go-grpc_out=replication/logic/pdu replication/logic/pdu/pdu.proto
|
protoc -I=replication/logic/pdu --go_out=plugins=grpc:replication/logic/pdu replication/logic/pdu/pdu.proto
|
||||||
protoc -I=rpc/grpcclientidentity/example --go_out=rpc/grpcclientidentity/example/pdu --go-grpc_out=rpc/grpcclientidentity/example/pdu rpc/grpcclientidentity/example/grpcauth.proto
|
|
||||||
$(GO_ENV_VARS) $(GO) generate $(GO_BUILDFLAGS) -x ./...
|
$(GO_ENV_VARS) $(GO) generate $(GO_BUILDFLAGS) -x ./...
|
||||||
|
|
||||||
GOIMPORTS := goimports -srcdir . -local 'github.com/zrepl/zrepl'
|
|
||||||
FINDSRCFILES := find . -type f -name '*.go' -not -path "./vendor/*" -not -name '*.pb.go' -not -name '*_enumer.go'
|
|
||||||
|
|
||||||
formatcheck:
|
|
||||||
@# goimports doesn't have a knob to exit with non-zero status code if formatting is needed
|
|
||||||
@# see https://go-review.googlesource.com/c/tools/+/237378
|
|
||||||
@ affectedfiles=$$($(GOIMPORTS) -l $(shell $(FINDSRCFILES)) | tee /dev/stderr | wc -l); test "$$affectedfiles" = 0
|
|
||||||
|
|
||||||
format:
|
format:
|
||||||
@ $(GOIMPORTS) -w -d $(shell $(FINDSRCFILES))
|
goimports -srcdir . -local 'github.com/zrepl/zrepl' -w $(shell find . -type f -name '*.go' -not -path "./vendor/*" -not -name '*.pb.go' -not -name '*_enumer.go')
|
||||||
|
|
||||||
##################### NOARCH #####################
|
##################### NOARCH #####################
|
||||||
.PHONY: noarch $(ARTIFACTDIR)/bash_completion $(ARTIFACTDIR)/_zrepl.zsh_completion $(ARTIFACTDIR)/go_env.txt docs docs-clean
|
.PHONY: noarch $(ARTIFACTDIR)/bash_completion $(ARTIFACTDIR)/_zrepl.zsh_completion $(ARTIFACTDIR)/go_env.txt docs docs-clean
|
||||||
@@ -329,16 +217,13 @@ $(ARTIFACTDIR)/_zrepl.zsh_completion:
|
|||||||
|
|
||||||
$(ARTIFACTDIR)/go_env.txt:
|
$(ARTIFACTDIR)/go_env.txt:
|
||||||
$(GO_ENV_VARS) $(GO) env > $@
|
$(GO_ENV_VARS) $(GO) env > $@
|
||||||
$(GO) version >> $@
|
|
||||||
|
|
||||||
docs: $(ARTIFACTDIR)/docs
|
docs: $(ARTIFACTDIR)/docs
|
||||||
# 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"
|
|
||||||
|
|
||||||
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://www.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.
|
||||||
@@ -22,7 +21,7 @@ zrepl is a one-stop ZFS backup & replication solution.
|
|||||||
|
|
||||||
## Feature Requests
|
## Feature Requests
|
||||||
|
|
||||||
1. Does your feature request require default values / some kind of configuration?
|
1. Does you feature request require default values / some kind of configuration?
|
||||||
If so, think of an expressive configuration example.
|
If so, think of an expressive configuration example.
|
||||||
2. Think of at least one use case that generalizes from your concrete application.
|
2. Think of at least one use case that generalizes from your concrete application.
|
||||||
3. Open an issue on GitHub with example conf & use case attached.
|
3. Open an issue on GitHub with example conf & use case attached.
|
||||||
@@ -31,12 +30,20 @@ zrepl is a one-stop ZFS backup & replication solution.
|
|||||||
The above does not apply if you already implemented everything.
|
The above does not apply if you already implemented everything.
|
||||||
Check out the *Coding Workflow* section below for details.
|
Check out the *Coding Workflow* section below for details.
|
||||||
|
|
||||||
## Building, Releasing, Downstream-Packaging
|
## Package Maintainer Information
|
||||||
|
|
||||||
This section provides an overview of the zrepl build & release process.
|
* Follow the steps in `docs/installation.rst -> Compiling from Source` and read the Makefile / shell scripts used in this process.
|
||||||
Check out `docs/installation/compile-from-source.rst` for build-from-source instructions.
|
* Make sure your distro is compatible with the paths in `docs/installation.rst`.
|
||||||
|
* 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.
|
||||||
|
* `dist/systemd` contains a Systemd unit template.
|
||||||
|
* Ship other material provided in `./dist`, e.g. in `/usr/share/zrepl/`.
|
||||||
|
* Use `make release ZREPL_VERSION='mydistro-1.2.3_1'`
|
||||||
|
* Your distro's name and any versioning supplemental to zrepl's (e.g. package revision) should be in this string
|
||||||
|
* Use `sudo make test-platform` **on a test system** to validate that zrepl's abstractions on top of ZFS work with the system ZFS.
|
||||||
|
* Make sure you are informed about new zrepl versions, e.g. by subscribing to GitHub's release RSS feed.
|
||||||
|
|
||||||
### Overview
|
## Developer Documentation
|
||||||
|
|
||||||
zrepl is written in [Go](https://golang.org) and uses [Go modules](https://github.com/golang/go/wiki/Modules) to manage dependencies.
|
zrepl is written in [Go](https://golang.org) and uses [Go modules](https://github.com/golang/go/wiki/Modules) to manage dependencies.
|
||||||
The documentation is written in [ReStructured Text](http://docutils.sourceforge.net/rst.html) using the [Sphinx](https://www.sphinx-doc.org) framework.
|
The documentation is written in [ReStructured Text](http://docutils.sourceforge.net/rst.html) using the [Sphinx](https://www.sphinx-doc.org) framework.
|
||||||
@@ -53,61 +60,60 @@ An HTML report can be generated using `make cover-html`.
|
|||||||
|
|
||||||
**Code generation** is triggered by `make generate`. Generated code is committed to the source tree.
|
**Code generation** is triggered by `make generate`. Generated code is committed to the source tree.
|
||||||
|
|
||||||
### Build & Release Process
|
### Project Structure
|
||||||
|
|
||||||
**The `Makefile` is catering to the needs of developers & CI, not distro packagers**.
|
```
|
||||||
It provides phony targets for
|
├── artifacts # build artifcats generate by make
|
||||||
* local development (building, running tests, etc)
|
├── cli # wrapper around CLI package cobra
|
||||||
* building a release in Docker (used by the CI & release management)
|
├── client # all subcommands that are not `daemon`
|
||||||
* building .deb and .rpm packages out of the release artifacts.
|
├── config # config data types (=> package yaml-config)
|
||||||
|
│ └── samples
|
||||||
|
├── daemon # the implementation of `zrepl daemon` subcommand
|
||||||
|
│ ├── filters
|
||||||
|
│ ├── hooks # snapshot hooks
|
||||||
|
│ ├── job # job implementations
|
||||||
|
│ ├── logging # logging outlets + formatters
|
||||||
|
│ ├── nethelpers
|
||||||
|
│ ├── prometheus
|
||||||
|
│ ├── pruner # pruner implementation
|
||||||
|
│ ├── snapper # snapshotter implementation
|
||||||
|
├── dist # supplemental material for users & package maintainers
|
||||||
|
├── docs # sphinx-based documentation
|
||||||
|
│ ├── **/*.rst # documentation in reStructuredText
|
||||||
|
│ ├── sphinxconf
|
||||||
|
│ │ └── conf.py # sphinx config (see commit 445a280 why its not in docs/)
|
||||||
|
│ ├── requirements.txt # pip3 requirements to build documentation
|
||||||
|
│ ├── publish.sh # shell script for automated rendering & deploy to zrepl.github.io repo
|
||||||
|
│ └── public_git # checkout of zrepl.github.io managed by above shell script
|
||||||
|
├── endpoint # implementation of replication endpoints (=> package replication)
|
||||||
|
├── logger # our own logger package
|
||||||
|
├── platformtest # test suite for our zfs abstractions (error classification, etc)
|
||||||
|
├── pruning # pruning rules (the logic, not the actual execution)
|
||||||
|
│ └── retentiongrid
|
||||||
|
├── replication
|
||||||
|
│ ├── driver # the driver of the replication logic (status reporting, error handling)
|
||||||
|
│ ├── logic # planning & executing replication steps via rpc
|
||||||
|
| | └── pdu # the generated gRPC & protobuf code used in replication (and endpoints)
|
||||||
|
│ └── report # the JSON-serializable report datastructures exposed to the client
|
||||||
|
├── rpc # the hybrid gRPC + ./dataconn RPC client: connects to a remote replication.Endpoint
|
||||||
|
│ ├── dataconn # Bulk data-transfer RPC protocol
|
||||||
|
│ ├── grpcclientidentity # adaptor to inject package transport's 'client identity' concept into gRPC contexts
|
||||||
|
│ ├── netadaptor # adaptor to convert a package transport's Connecter and Listener into net.* primitives
|
||||||
|
│ ├── transportmux # TCP connecter and listener used to split control & data traffic
|
||||||
|
│ └── versionhandshake # replication protocol version handshake perfomed on newly established connections
|
||||||
|
├── tlsconf # abstraction for Go TLS server + client config
|
||||||
|
├── transport # transport implementations
|
||||||
|
│ ├── fromconfig
|
||||||
|
│ ├── local
|
||||||
|
│ ├── ssh
|
||||||
|
│ ├── tcp
|
||||||
|
│ └── tls
|
||||||
|
├── util
|
||||||
|
├── version # abstraction for versions (filled during build by Makefile)
|
||||||
|
└── zfs # zfs(8) wrappers
|
||||||
|
```
|
||||||
|
|
||||||
**Build tooling & dependencies** are documented as code in `lazy.sh`.
|
### Coding Workflow
|
||||||
Go dependencies are then fetched by the go command and pip dependencies are pinned through a `requirements.txt`.
|
|
||||||
|
|
||||||
**We use CircleCI for continuous integration**.
|
|
||||||
There are two workflows:
|
|
||||||
|
|
||||||
* `ci` runs for every commit / branch / tag pushed to GitHub.
|
|
||||||
It is supposed to run very fast (<5min and provides quick feedback to developers).
|
|
||||||
It runs formatting checks, lints and tests on the most important OSes / architectures.
|
|
||||||
Artifacts are published to minio.cschwarz.com (see GitHub Commit Status).
|
|
||||||
|
|
||||||
* `release` runs
|
|
||||||
* on manual triggers through the CircleCI API (in order to produce a release)
|
|
||||||
* periodically on `master`
|
|
||||||
Artifacts are published to minio.cschwarz.com (see GitHub Commit Status).
|
|
||||||
|
|
||||||
**Releases** are issued via Git tags + GitHub Releases feature.
|
|
||||||
The procedure to issue a release is as follows:
|
|
||||||
* Issue the source release:
|
|
||||||
* Git tag the release on the `master` branch.
|
|
||||||
* Push the tag.
|
|
||||||
* Run `./docs/publish.sh` to re-build & push zrepl.github.io.
|
|
||||||
* Issue the official binary release:
|
|
||||||
* Run the `release` pipeline (triggered via CircleCI API)
|
|
||||||
* Download the artifacts to the release manager's machine.
|
|
||||||
* Create a GitHub release, edit the changelog, upload all the release artifacts, including .rpm and .deb files.
|
|
||||||
* Issue the GitHub release.
|
|
||||||
* Add the .rpm and .deb files to the official zrepl repos, publish those.
|
|
||||||
|
|
||||||
**Official binary releases are not re-built when Go receives an update. If the Go update is critical to zrepl (e.g. a Go security update that affects zrepl), we'd issue a new source release**.
|
|
||||||
The rationale for this is that whereas distros provide a mechanism for this (`$zrepl_source_release-$distro_package_revision`), GitHub Releases doesn't which means we'd need to update the existing GitHub release's assets, which nobody would notice (no RSS feed updates, etc.).
|
|
||||||
Downstream packagers can read the changelog to determine whether they want to push that minor release into their distro or simply skip it.
|
|
||||||
|
|
||||||
### 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.
|
|
||||||
* 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.
|
|
||||||
* `dist/systemd` contains a Systemd unit template.
|
|
||||||
* Ship other material provided in `./dist`, e.g. in `/usr/share/zrepl/`.
|
|
||||||
* Have a look at the `Makefile`'s `ZREPL_VERSION` variable and how it passed to Go's `ldFlags`.
|
|
||||||
This is how `zrepl version` knows what version number to show.
|
|
||||||
Your build system should set the `ldFlags` flags appropriately and add a prefix or suffix that indicates that the given zrepl binary is a distro build, not an official one.
|
|
||||||
* Make sure you are informed about new zrepl versions, e.g. by subscribing to GitHub's release RSS feed.
|
|
||||||
|
|
||||||
|
|
||||||
## Contributing Code
|
|
||||||
|
|
||||||
* Open an issue when starting to hack on a new feature
|
* Open an issue when starting to hack on a new feature
|
||||||
* Commits should reference the issue they are related to
|
* Commits should reference the issue they are related to
|
||||||
@@ -117,6 +123,9 @@ Downstream packagers can read the changelog to determine whether they want to pu
|
|||||||
|
|
||||||
Backward-incompatible changes must be documented in the git commit message and are listed in `docs/changelog.rst`.
|
Backward-incompatible changes must be documented in the git commit message and are listed in `docs/changelog.rst`.
|
||||||
|
|
||||||
|
* Config-breaking changes must contain a line `BREAK CONFIG` in the commit message
|
||||||
|
* Other breaking changes must contain a line `BREAK` in the commit message
|
||||||
|
|
||||||
### Glossary & Naming Inconsistencies
|
### Glossary & Naming Inconsistencies
|
||||||
|
|
||||||
In ZFS, *dataset* refers to the objects *filesystem*, *ZVOL* and *snapshot*. <br />
|
In ZFS, *dataset* refers to the objects *filesystem*, *ZVOL* and *snapshot*. <br />
|
||||||
@@ -133,3 +142,16 @@ variables and types are often named *dataset* when they in fact refer to a *file
|
|||||||
There will not be a big refactoring (an attempt was made, but it's destroying too much history without much gain).
|
There will not be a big refactoring (an attempt was made, but it's destroying too much history without much gain).
|
||||||
|
|
||||||
However, new contributions & patches should fix naming without further notice in the commit message.
|
However, new contributions & patches should fix naming without further notice in the commit message.
|
||||||
|
|
||||||
|
### RPC debugging
|
||||||
|
|
||||||
|
Optionally, there are various RPC-related environment variables, that if set to something != `""` will produce additional debug output on stderr:
|
||||||
|
|
||||||
|
https://github.com/zrepl/zrepl/blob/master/rpc/rpc_debug.go#L11
|
||||||
|
|
||||||
|
https://github.com/zrepl/zrepl/blob/master/rpc/dataconn/dataconn_debug.go#L11
|
||||||
|
|
||||||
|
https://github.com/zrepl/zrepl/blob/master/rpc/dataconn/stream/stream_debug.go#L11
|
||||||
|
|
||||||
|
https://github.com/zrepl/zrepl/blob/master/rpc/dataconn/heartbeatconn/heartbeatconn_debug.go#L11
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
FROM !SUBSTITUTED_BY_MAKEFILE
|
FROM golang:latest
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y \
|
RUN apt-get update && apt-get install -y \
|
||||||
python3-pip \
|
python3-pip \
|
||||||
|
|||||||
+44
-5
@@ -3,11 +3,50 @@ module github.com/zrepl/zrepl/build
|
|||||||
go 1.12
|
go 1.12
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/OpenPeeDeeP/depguard v0.0.0-20181229194401-1f388ab2d810 // indirect
|
||||||
github.com/alvaroloes/enumer v1.1.1
|
github.com/alvaroloes/enumer v1.1.1
|
||||||
github.com/golangci/golangci-lint v1.50.1
|
github.com/fatih/color v1.7.0 // indirect
|
||||||
|
github.com/go-critic/go-critic v0.3.4 // indirect
|
||||||
|
github.com/gogo/protobuf v1.2.1 // indirect
|
||||||
|
github.com/golang/mock v1.2.0 // indirect
|
||||||
|
github.com/golang/protobuf v1.2.0
|
||||||
|
github.com/golangci/errcheck v0.0.0-20181223084120-ef45e06d44b6 // indirect
|
||||||
|
github.com/golangci/go-tools v0.0.0-20190124090046-35a9f45a5db0 // indirect
|
||||||
|
github.com/golangci/gocyclo v0.0.0-20180528144436-0a533e8fa43d // indirect
|
||||||
|
github.com/golangci/gofmt v0.0.0-20181222123516-0b8337e80d98 // indirect
|
||||||
|
github.com/golangci/golangci-lint v1.17.1
|
||||||
|
github.com/golangci/gosec v0.0.0-20180901114220-8afd9cbb6cfb // indirect
|
||||||
|
github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219 // indirect
|
||||||
|
github.com/golangci/misspell v0.3.4 // indirect
|
||||||
|
github.com/golangci/revgrep v0.0.0-20180812185044-276a5c0a1039 // indirect
|
||||||
|
github.com/google/go-cmp v0.3.0 // indirect
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
|
||||||
|
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||||
|
github.com/pkg/errors v0.8.1 // indirect
|
||||||
|
github.com/sirupsen/logrus v1.4.2 // indirect
|
||||||
|
github.com/spf13/afero v1.2.2 // indirect
|
||||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||||
github.com/wadey/gocovmerge v0.0.0-20160331181800-b5bfa59ec0ad
|
github.com/spf13/viper v1.3.2 // indirect
|
||||||
golang.org/x/tools v0.2.0
|
github.com/stretchr/testify v1.4.0 // indirect
|
||||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0 // indirect
|
github.com/wadey/gocovmerge v0.0.0-20160331181800-b5bfa59ec0ad // indirect
|
||||||
google.golang.org/protobuf v1.28.0
|
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980 // indirect
|
||||||
|
golang.org/x/sys v0.0.0-20191010194322-b09406accb47 // indirect
|
||||||
|
golang.org/x/tools v0.0.0-20190524210228-3d17549cdc6b
|
||||||
|
mvdan.cc/unparam v0.0.0-20190310220240-1b9ccfa71afe // indirect
|
||||||
|
sourcegraph.com/sqs/pbtypes v1.0.0 // indirect
|
||||||
|
)
|
||||||
|
|
||||||
|
// invalid dates in transitive dependencies (first validated in Go 1.13, didn't fail in earlier Go versions)
|
||||||
|
replace (
|
||||||
|
github.com/go-critic/go-critic v0.0.0-20181204210945-1df300866540 => github.com/go-critic/go-critic v0.3.5-0.20190526074819-1df300866540
|
||||||
|
github.com/go-critic/go-critic v0.0.0-20181204210945-ee9bf5809ead => github.com/go-critic/go-critic v0.3.5-0.20190210220443-ee9bf5809ead
|
||||||
|
github.com/golangci/errcheck v0.0.0-20181003203344-ef45e06d44b6 => github.com/golangci/errcheck v0.0.0-20181223084120-ef45e06d44b6
|
||||||
|
github.com/golangci/go-tools v0.0.0-20180109140146-35a9f45a5db0 => github.com/golangci/go-tools v0.0.0-20190124090046-35a9f45a5db0
|
||||||
|
github.com/golangci/go-tools v0.0.0-20180109140146-af6baa5dc196 => github.com/golangci/go-tools v0.0.0-20190318060251-af6baa5dc196
|
||||||
|
github.com/golangci/gofmt v0.0.0-20181105071733-0b8337e80d98 => github.com/golangci/gofmt v0.0.0-20181222123516-0b8337e80d98
|
||||||
|
github.com/golangci/gosec v0.0.0-20180901114220-66fb7fc33547 => github.com/golangci/gosec v0.0.0-20190211064107-66fb7fc33547
|
||||||
|
github.com/golangci/ineffassign v0.0.0-20180808204949-42439a7714cc => github.com/golangci/ineffassign v0.0.0-20190609212857-42439a7714cc
|
||||||
|
github.com/golangci/lint-1 v0.0.0-20180610141402-ee948d087217 => github.com/golangci/lint-1 v0.0.0-20190420132249-ee948d087217
|
||||||
|
golang.org/x/tools v0.0.0-20190125232054-379209517ffe => golang.org/x/tools v0.0.0-20190205201329-379209517ffe
|
||||||
|
mvdan.cc/unparam v0.0.0-20190124213536-fbb59629db34 => mvdan.cc/unparam v0.0.0-20190209190245-fbb59629db34
|
||||||
)
|
)
|
||||||
|
|||||||
+100
-1644
File diff suppressed because it is too large
Load Diff
+1
-4
@@ -1,15 +1,12 @@
|
|||||||
//go:build tools
|
|
||||||
// +build tools
|
// +build tools
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
// the lines are parsed by lazy.sh, do not edit
|
|
||||||
import (
|
import (
|
||||||
_ "github.com/alvaroloes/enumer"
|
_ "github.com/alvaroloes/enumer"
|
||||||
|
_ "github.com/golang/protobuf/protoc-gen-go"
|
||||||
_ "github.com/golangci/golangci-lint/cmd/golangci-lint"
|
_ "github.com/golangci/golangci-lint/cmd/golangci-lint"
|
||||||
_ "github.com/wadey/gocovmerge"
|
_ "github.com/wadey/gocovmerge"
|
||||||
_ "golang.org/x/tools/cmd/goimports"
|
_ "golang.org/x/tools/cmd/goimports"
|
||||||
_ "golang.org/x/tools/cmd/stringer"
|
_ "golang.org/x/tools/cmd/stringer"
|
||||||
_ "google.golang.org/grpc/cmd/protoc-gen-go-grpc"
|
|
||||||
_ "google.golang.org/protobuf/cmd/protoc-gen-go"
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/daemon/logging/trace"
|
"github.com/zrepl/zrepl/daemon/logging/trace"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/config"
|
"github.com/zrepl/zrepl/config"
|
||||||
|
|||||||
+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,811 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"math"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
// tcell is the termbox-compatible library for abstracting away escape sequences, etc.
|
||||||
|
// as of tcell#252, the number of default distributed terminals is relatively limited
|
||||||
|
// additional terminal definitions can be included via side-effect import
|
||||||
|
// See https://github.com/gdamore/tcell/blob/master/terminfo/base/base.go
|
||||||
|
// See https://github.com/gdamore/tcell/issues/252#issuecomment-533836078
|
||||||
|
"github.com/gdamore/tcell/termbox"
|
||||||
|
_ "github.com/gdamore/tcell/terminfo/s/screen" // tmux on FreeBSD 11 & 12 without ncurses
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/spf13/pflag"
|
||||||
|
"github.com/zrepl/yaml-config"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/cli"
|
||||||
|
"github.com/zrepl/zrepl/daemon"
|
||||||
|
"github.com/zrepl/zrepl/daemon/job"
|
||||||
|
"github.com/zrepl/zrepl/daemon/pruner"
|
||||||
|
"github.com/zrepl/zrepl/daemon/snapper"
|
||||||
|
"github.com/zrepl/zrepl/replication/report"
|
||||||
|
)
|
||||||
|
|
||||||
|
type byteProgressMeasurement struct {
|
||||||
|
time time.Time
|
||||||
|
val int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type bytesProgressHistory struct {
|
||||||
|
last *byteProgressMeasurement // pointer as poor man's optional
|
||||||
|
changeCount int
|
||||||
|
lastChange time.Time
|
||||||
|
bpsAvg float64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *bytesProgressHistory) Update(currentVal int64) (bytesPerSecondAvg int64, changeCount int) {
|
||||||
|
|
||||||
|
if p.last == nil {
|
||||||
|
p.last = &byteProgressMeasurement{
|
||||||
|
time: time.Now(),
|
||||||
|
val: currentVal,
|
||||||
|
}
|
||||||
|
return 0, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.last.val != currentVal {
|
||||||
|
p.changeCount++
|
||||||
|
p.lastChange = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
if time.Since(p.lastChange) > 3*time.Second {
|
||||||
|
p.last = nil
|
||||||
|
return 0, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
deltaV := currentVal - p.last.val
|
||||||
|
deltaT := time.Since(p.last.time)
|
||||||
|
rate := float64(deltaV) / deltaT.Seconds()
|
||||||
|
|
||||||
|
factor := 0.3
|
||||||
|
p.bpsAvg = (1-factor)*p.bpsAvg + factor*rate
|
||||||
|
|
||||||
|
p.last.time = time.Now()
|
||||||
|
p.last.val = currentVal
|
||||||
|
|
||||||
|
return int64(p.bpsAvg), p.changeCount
|
||||||
|
}
|
||||||
|
|
||||||
|
type tui struct {
|
||||||
|
x, y int
|
||||||
|
indent int
|
||||||
|
|
||||||
|
lock sync.Mutex //For report and error
|
||||||
|
report map[string]*job.Status
|
||||||
|
err error
|
||||||
|
|
||||||
|
jobFilter string
|
||||||
|
|
||||||
|
replicationProgress map[string]*bytesProgressHistory // by job name
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTui() tui {
|
||||||
|
return tui{
|
||||||
|
replicationProgress: make(map[string]*bytesProgressHistory),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const INDENT_MULTIPLIER = 4
|
||||||
|
|
||||||
|
func (t *tui) moveLine(dl int, col int) {
|
||||||
|
t.y += dl
|
||||||
|
t.x = t.indent*INDENT_MULTIPLIER + col
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *tui) write(text string) {
|
||||||
|
for _, c := range text {
|
||||||
|
if c == '\n' {
|
||||||
|
t.newline()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
termbox.SetCell(t.x, t.y, c, termbox.ColorDefault, termbox.ColorDefault)
|
||||||
|
t.x += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *tui) printf(text string, a ...interface{}) {
|
||||||
|
t.write(fmt.Sprintf(text, a...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func wrap(s string, width int) string {
|
||||||
|
var b strings.Builder
|
||||||
|
for len(s) > 0 {
|
||||||
|
rem := width
|
||||||
|
if rem > len(s) {
|
||||||
|
rem = len(s)
|
||||||
|
}
|
||||||
|
if idx := strings.IndexAny(s, "\n\r"); idx != -1 && idx < rem {
|
||||||
|
rem = idx + 1
|
||||||
|
}
|
||||||
|
untilNewline := strings.TrimRight(s[:rem], "\n\r")
|
||||||
|
s = s[rem:]
|
||||||
|
if len(untilNewline) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
b.WriteString(untilNewline)
|
||||||
|
b.WriteString("\n")
|
||||||
|
}
|
||||||
|
return strings.TrimRight(b.String(), "\n\r")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *tui) printfDrawIndentedAndWrappedIfMultiline(format string, a ...interface{}) {
|
||||||
|
whole := fmt.Sprintf(format, a...)
|
||||||
|
width, _ := termbox.Size()
|
||||||
|
if !strings.ContainsAny(whole, "\n\r") && t.x+len(whole) <= width {
|
||||||
|
t.printf(format, a...)
|
||||||
|
} else {
|
||||||
|
t.addIndent(1)
|
||||||
|
t.newline()
|
||||||
|
t.write(wrap(whole, width-INDENT_MULTIPLIER*t.indent))
|
||||||
|
t.addIndent(-1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *tui) newline() {
|
||||||
|
t.moveLine(1, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *tui) setIndent(indent int) {
|
||||||
|
t.indent = indent
|
||||||
|
t.moveLine(0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *tui) addIndent(indent int) {
|
||||||
|
t.indent += indent
|
||||||
|
t.moveLine(0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
var statusFlags struct {
|
||||||
|
Raw bool
|
||||||
|
Job string
|
||||||
|
}
|
||||||
|
|
||||||
|
var StatusCmd = &cli.Subcommand{
|
||||||
|
Use: "status",
|
||||||
|
Short: "show job activity or dump as JSON for monitoring",
|
||||||
|
SetupFlags: func(f *pflag.FlagSet) {
|
||||||
|
f.BoolVar(&statusFlags.Raw, "raw", false, "dump raw status description from zrepl daemon")
|
||||||
|
f.StringVar(&statusFlags.Job, "job", "", "only dump specified job")
|
||||||
|
},
|
||||||
|
Run: runStatus,
|
||||||
|
}
|
||||||
|
|
||||||
|
func runStatus(ctx context.Context, s *cli.Subcommand, args []string) error {
|
||||||
|
httpc, err := controlHttpClient(s.Config().Global.Control.SockPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if statusFlags.Raw {
|
||||||
|
resp, err := httpc.Get("http://unix" + daemon.ControlJobEndpointStatus)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
fmt.Fprintf(os.Stderr, "Received error response:\n")
|
||||||
|
_, err := io.CopyN(os.Stderr, resp.Body, 4096)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return errors.Errorf("exit")
|
||||||
|
}
|
||||||
|
if _, err := io.Copy(os.Stdout, resp.Body); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
t := newTui()
|
||||||
|
t.lock.Lock()
|
||||||
|
t.err = errors.New("Got no report yet")
|
||||||
|
t.lock.Unlock()
|
||||||
|
t.jobFilter = statusFlags.Job
|
||||||
|
|
||||||
|
err = termbox.Init()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer termbox.Close()
|
||||||
|
|
||||||
|
update := func() {
|
||||||
|
var m daemon.Status
|
||||||
|
|
||||||
|
err2 := jsonRequestResponse(httpc, daemon.ControlJobEndpointStatus,
|
||||||
|
struct{}{},
|
||||||
|
&m,
|
||||||
|
)
|
||||||
|
|
||||||
|
t.lock.Lock()
|
||||||
|
t.err = err2
|
||||||
|
t.report = m.Jobs
|
||||||
|
t.lock.Unlock()
|
||||||
|
t.draw()
|
||||||
|
}
|
||||||
|
update()
|
||||||
|
|
||||||
|
ticker := time.NewTicker(500 * time.Millisecond)
|
||||||
|
defer ticker.Stop()
|
||||||
|
go func() {
|
||||||
|
for range ticker.C {
|
||||||
|
update()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
termbox.HideCursor()
|
||||||
|
termbox.Clear(termbox.ColorDefault, termbox.ColorDefault)
|
||||||
|
|
||||||
|
loop:
|
||||||
|
for {
|
||||||
|
switch ev := termbox.PollEvent(); ev.Type {
|
||||||
|
case termbox.EventKey:
|
||||||
|
switch ev.Key {
|
||||||
|
case termbox.KeyEsc:
|
||||||
|
break loop
|
||||||
|
case termbox.KeyCtrlC:
|
||||||
|
break loop
|
||||||
|
}
|
||||||
|
case termbox.EventResize:
|
||||||
|
t.draw()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *tui) getReplicationProgressHistory(jobName string) *bytesProgressHistory {
|
||||||
|
p, ok := t.replicationProgress[jobName]
|
||||||
|
if !ok {
|
||||||
|
p = &bytesProgressHistory{}
|
||||||
|
t.replicationProgress[jobName] = p
|
||||||
|
}
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *tui) draw() {
|
||||||
|
t.lock.Lock()
|
||||||
|
defer t.lock.Unlock()
|
||||||
|
|
||||||
|
termbox.Clear(termbox.ColorDefault, termbox.ColorDefault)
|
||||||
|
t.x = 0
|
||||||
|
t.y = 0
|
||||||
|
t.indent = 0
|
||||||
|
|
||||||
|
if t.err != nil {
|
||||||
|
t.write(t.err.Error())
|
||||||
|
} else {
|
||||||
|
//Iterate over map in alphabetical order
|
||||||
|
keys := make([]string, 0, len(t.report))
|
||||||
|
for k := range t.report {
|
||||||
|
if len(k) == 0 || daemon.IsInternalJobName(k) { //Internal job
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if t.jobFilter != "" && k != t.jobFilter {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
sort.Strings(keys)
|
||||||
|
|
||||||
|
if len(keys) == 0 {
|
||||||
|
t.setIndent(0)
|
||||||
|
t.printf("no jobs to display")
|
||||||
|
t.newline()
|
||||||
|
termbox.Flush()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, k := range keys {
|
||||||
|
v := t.report[k]
|
||||||
|
|
||||||
|
t.setIndent(0)
|
||||||
|
|
||||||
|
t.printf("Job: %s", k)
|
||||||
|
t.setIndent(1)
|
||||||
|
t.newline()
|
||||||
|
t.printf("Type: %s", v.Type)
|
||||||
|
t.setIndent(1)
|
||||||
|
t.newline()
|
||||||
|
|
||||||
|
if v.Type == job.TypePush || v.Type == job.TypePull {
|
||||||
|
activeStatus, ok := v.JobSpecific.(*job.ActiveSideStatus)
|
||||||
|
if !ok || activeStatus == nil {
|
||||||
|
t.printf("ActiveSideStatus is null")
|
||||||
|
t.newline()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
t.printf("Replication:")
|
||||||
|
t.newline()
|
||||||
|
t.addIndent(1)
|
||||||
|
t.renderReplicationReport(activeStatus.Replication, t.getReplicationProgressHistory(k))
|
||||||
|
t.addIndent(-1)
|
||||||
|
|
||||||
|
t.printf("Pruning Sender:")
|
||||||
|
t.newline()
|
||||||
|
t.addIndent(1)
|
||||||
|
t.renderPrunerReport(activeStatus.PruningSender)
|
||||||
|
t.addIndent(-1)
|
||||||
|
|
||||||
|
t.printf("Pruning Receiver:")
|
||||||
|
t.newline()
|
||||||
|
t.addIndent(1)
|
||||||
|
t.renderPrunerReport(activeStatus.PruningReceiver)
|
||||||
|
t.addIndent(-1)
|
||||||
|
|
||||||
|
if v.Type == job.TypePush {
|
||||||
|
t.printf("Snapshotting:")
|
||||||
|
t.newline()
|
||||||
|
t.addIndent(1)
|
||||||
|
t.renderSnapperReport(activeStatus.Snapshotting)
|
||||||
|
t.addIndent(-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if v.Type == job.TypeSnap {
|
||||||
|
snapStatus, ok := v.JobSpecific.(*job.SnapJobStatus)
|
||||||
|
if !ok || snapStatus == nil {
|
||||||
|
t.printf("SnapJobStatus is null")
|
||||||
|
t.newline()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
t.printf("Pruning snapshots:")
|
||||||
|
t.newline()
|
||||||
|
t.addIndent(1)
|
||||||
|
t.renderPrunerReport(snapStatus.Pruning)
|
||||||
|
t.addIndent(-1)
|
||||||
|
t.printf("Snapshotting:")
|
||||||
|
t.newline()
|
||||||
|
t.addIndent(1)
|
||||||
|
t.renderSnapperReport(snapStatus.Snapshotting)
|
||||||
|
t.addIndent(-1)
|
||||||
|
} else if v.Type == job.TypeSource {
|
||||||
|
|
||||||
|
st := v.JobSpecific.(*job.PassiveStatus)
|
||||||
|
t.printf("Snapshotting:\n")
|
||||||
|
t.addIndent(1)
|
||||||
|
t.renderSnapperReport(st.Snapper)
|
||||||
|
t.addIndent(-1)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
t.printf("No status representation for job type '%s', dumping as YAML", v.Type)
|
||||||
|
t.newline()
|
||||||
|
asYaml, err := yaml.Marshal(v.JobSpecific)
|
||||||
|
if err != nil {
|
||||||
|
t.printf("Error marshaling status to YAML: %s", err)
|
||||||
|
t.newline()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
t.write(string(asYaml))
|
||||||
|
t.newline()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
termbox.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *tui) renderReplicationReport(rep *report.Report, history *bytesProgressHistory) {
|
||||||
|
if rep == nil {
|
||||||
|
t.printf("...\n")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if rep.WaitReconnectError != nil {
|
||||||
|
t.printfDrawIndentedAndWrappedIfMultiline("Connectivity: %s", rep.WaitReconnectError)
|
||||||
|
t.newline()
|
||||||
|
}
|
||||||
|
if !rep.WaitReconnectSince.IsZero() {
|
||||||
|
delta := time.Until(rep.WaitReconnectUntil).Round(time.Second)
|
||||||
|
if rep.WaitReconnectUntil.IsZero() || delta > 0 {
|
||||||
|
var until string
|
||||||
|
if rep.WaitReconnectUntil.IsZero() {
|
||||||
|
until = "waiting indefinitely"
|
||||||
|
} else {
|
||||||
|
until = fmt.Sprintf("hard fail in %s @ %s", delta, rep.WaitReconnectUntil)
|
||||||
|
}
|
||||||
|
t.printfDrawIndentedAndWrappedIfMultiline("Connectivity: reconnecting with exponential backoff (since %s) (%s)",
|
||||||
|
rep.WaitReconnectSince, until)
|
||||||
|
} else {
|
||||||
|
t.printfDrawIndentedAndWrappedIfMultiline("Connectivity: reconnects reached hard-fail timeout @ %s", rep.WaitReconnectUntil)
|
||||||
|
}
|
||||||
|
t.newline()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO visualize more than the latest attempt by folding all attempts into one
|
||||||
|
if len(rep.Attempts) == 0 {
|
||||||
|
t.printf("no attempts made yet")
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
t.printf("Attempt #%d", len(rep.Attempts))
|
||||||
|
if len(rep.Attempts) > 1 {
|
||||||
|
t.printf(". Previous attempts failed with the following statuses:")
|
||||||
|
t.newline()
|
||||||
|
t.addIndent(1)
|
||||||
|
for i, a := range rep.Attempts[:len(rep.Attempts)-1] {
|
||||||
|
t.printfDrawIndentedAndWrappedIfMultiline("#%d: %s (failed at %s) (ran %s)", i+1, a.State, a.FinishAt, a.FinishAt.Sub(a.StartAt))
|
||||||
|
t.newline()
|
||||||
|
}
|
||||||
|
t.addIndent(-1)
|
||||||
|
} else {
|
||||||
|
t.newline()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
latest := rep.Attempts[len(rep.Attempts)-1]
|
||||||
|
sort.Slice(latest.Filesystems, func(i, j int) bool {
|
||||||
|
return latest.Filesystems[i].Info.Name < latest.Filesystems[j].Info.Name
|
||||||
|
})
|
||||||
|
|
||||||
|
t.printf("Status: %s", latest.State)
|
||||||
|
t.newline()
|
||||||
|
if latest.State == report.AttemptPlanningError {
|
||||||
|
t.printf("Problem: ")
|
||||||
|
t.printfDrawIndentedAndWrappedIfMultiline("%s", latest.PlanError)
|
||||||
|
t.newline()
|
||||||
|
} else if latest.State == report.AttemptFanOutError {
|
||||||
|
t.printf("Problem: one or more of the filesystems encountered errors")
|
||||||
|
t.newline()
|
||||||
|
}
|
||||||
|
|
||||||
|
if latest.State != report.AttemptPlanning && latest.State != report.AttemptPlanningError {
|
||||||
|
// Draw global progress bar
|
||||||
|
// Progress: [---------------]
|
||||||
|
expected, replicated, containsInvalidSizeEstimates := latest.BytesSum()
|
||||||
|
rate, changeCount := history.Update(replicated)
|
||||||
|
eta := time.Duration(0)
|
||||||
|
if rate > 0 {
|
||||||
|
eta = time.Duration((expected-replicated)/rate) * time.Second
|
||||||
|
}
|
||||||
|
t.write("Progress: ")
|
||||||
|
t.drawBar(50, replicated, expected, changeCount)
|
||||||
|
t.write(fmt.Sprintf(" %s / %s @ %s/s", ByteCountBinary(replicated), ByteCountBinary(expected), ByteCountBinary(rate)))
|
||||||
|
if eta != 0 {
|
||||||
|
t.write(fmt.Sprintf(" (%s remaining)", humanizeDuration(eta)))
|
||||||
|
}
|
||||||
|
t.newline()
|
||||||
|
if containsInvalidSizeEstimates {
|
||||||
|
t.write("NOTE: not all steps could be size-estimated, total estimate is likely imprecise!")
|
||||||
|
t.newline()
|
||||||
|
}
|
||||||
|
|
||||||
|
var maxFSLen int
|
||||||
|
for _, fs := range latest.Filesystems {
|
||||||
|
if len(fs.Info.Name) > maxFSLen {
|
||||||
|
maxFSLen = len(fs.Info.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, fs := range latest.Filesystems {
|
||||||
|
t.printFilesystemStatus(fs, false, maxFSLen) // FIXME bring 'active' flag back
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *tui) renderPrunerReport(r *pruner.Report) {
|
||||||
|
if r == nil {
|
||||||
|
t.printf("...\n")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
state, err := pruner.StateString(r.State)
|
||||||
|
if err != nil {
|
||||||
|
t.printf("Status: %q (parse error: %q)\n", r.State, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
t.printf("Status: %s", state)
|
||||||
|
t.newline()
|
||||||
|
|
||||||
|
if r.Error != "" {
|
||||||
|
t.printf("Error: %s\n", r.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type commonFS struct {
|
||||||
|
*pruner.FSReport
|
||||||
|
completed bool
|
||||||
|
}
|
||||||
|
all := make([]commonFS, 0, len(r.Pending)+len(r.Completed))
|
||||||
|
for i := range r.Pending {
|
||||||
|
all = append(all, commonFS{&r.Pending[i], false})
|
||||||
|
}
|
||||||
|
for i := range r.Completed {
|
||||||
|
all = append(all, commonFS{&r.Completed[i], true})
|
||||||
|
}
|
||||||
|
|
||||||
|
switch state {
|
||||||
|
case pruner.Plan:
|
||||||
|
fallthrough
|
||||||
|
case pruner.PlanErr:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(all) == 0 {
|
||||||
|
t.printf("nothing to do\n")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var totalDestroyCount, completedDestroyCount int
|
||||||
|
var maxFSname int
|
||||||
|
for _, fs := range all {
|
||||||
|
totalDestroyCount += len(fs.DestroyList)
|
||||||
|
if fs.completed {
|
||||||
|
completedDestroyCount += len(fs.DestroyList)
|
||||||
|
}
|
||||||
|
if maxFSname < len(fs.Filesystem) {
|
||||||
|
maxFSname = len(fs.Filesystem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// global progress bar
|
||||||
|
progress := int(math.Round(80 * float64(completedDestroyCount) / float64(totalDestroyCount)))
|
||||||
|
t.write("Progress: ")
|
||||||
|
t.write("[")
|
||||||
|
t.write(times("=", progress))
|
||||||
|
t.write(">")
|
||||||
|
t.write(times("-", 80-progress))
|
||||||
|
t.write("]")
|
||||||
|
t.printf(" %d/%d snapshots", completedDestroyCount, totalDestroyCount)
|
||||||
|
t.newline()
|
||||||
|
|
||||||
|
sort.SliceStable(all, func(i, j int) bool {
|
||||||
|
return strings.Compare(all[i].Filesystem, all[j].Filesystem) == -1
|
||||||
|
})
|
||||||
|
|
||||||
|
// Draw a table-like representation of 'all'
|
||||||
|
for _, fs := range all {
|
||||||
|
t.write(rightPad(fs.Filesystem, maxFSname, " "))
|
||||||
|
t.write(" ")
|
||||||
|
if !fs.SkipReason.NotSkipped() {
|
||||||
|
t.printf("skipped: %s\n", fs.SkipReason)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if fs.LastError != "" {
|
||||||
|
if strings.ContainsAny(fs.LastError, "\r\n") {
|
||||||
|
t.printf("ERROR:")
|
||||||
|
t.printfDrawIndentedAndWrappedIfMultiline("%s\n", fs.LastError)
|
||||||
|
} else {
|
||||||
|
t.printfDrawIndentedAndWrappedIfMultiline("ERROR: %s\n", fs.LastError)
|
||||||
|
}
|
||||||
|
t.newline()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
pruneRuleActionStr := fmt.Sprintf("(destroy %d of %d snapshots)",
|
||||||
|
len(fs.DestroyList), len(fs.SnapshotList))
|
||||||
|
|
||||||
|
if fs.completed {
|
||||||
|
t.printf("Completed %s\n", pruneRuleActionStr)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
t.write("Pending ") // whitespace is padding 10
|
||||||
|
if len(fs.DestroyList) == 1 {
|
||||||
|
t.write(fs.DestroyList[0].Name)
|
||||||
|
} else {
|
||||||
|
t.write(pruneRuleActionStr)
|
||||||
|
}
|
||||||
|
t.newline()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *tui) renderSnapperReport(r *snapper.Report) {
|
||||||
|
if r == nil {
|
||||||
|
t.printf("<snapshot type does not have a report>\n")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
t.printf("Status: %s", r.State)
|
||||||
|
t.newline()
|
||||||
|
|
||||||
|
if r.Error != "" {
|
||||||
|
t.printf("Error: %s\n", r.Error)
|
||||||
|
}
|
||||||
|
if !r.SleepUntil.IsZero() {
|
||||||
|
t.printf("Sleep until: %s\n", r.SleepUntil)
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(r.Progress, func(i, j int) bool {
|
||||||
|
return strings.Compare(r.Progress[i].Path, r.Progress[j].Path) == -1
|
||||||
|
})
|
||||||
|
|
||||||
|
t.addIndent(1)
|
||||||
|
defer t.addIndent(-1)
|
||||||
|
dur := func(d time.Duration) string {
|
||||||
|
return d.Round(100 * time.Millisecond).String()
|
||||||
|
}
|
||||||
|
|
||||||
|
type row struct {
|
||||||
|
path, state, duration, remainder, hookReport string
|
||||||
|
}
|
||||||
|
var widths struct {
|
||||||
|
path, state, duration int
|
||||||
|
}
|
||||||
|
rows := make([]*row, len(r.Progress))
|
||||||
|
for i, fs := range r.Progress {
|
||||||
|
r := &row{
|
||||||
|
path: fs.Path,
|
||||||
|
state: fs.State.String(),
|
||||||
|
}
|
||||||
|
if fs.HooksHadError {
|
||||||
|
r.hookReport = fs.Hooks // FIXME render here, not in daemon
|
||||||
|
}
|
||||||
|
switch fs.State {
|
||||||
|
case snapper.SnapPending:
|
||||||
|
r.duration = "..."
|
||||||
|
r.remainder = ""
|
||||||
|
case snapper.SnapStarted:
|
||||||
|
r.duration = dur(time.Since(fs.StartAt))
|
||||||
|
r.remainder = fmt.Sprintf("snap name: %q", fs.SnapName)
|
||||||
|
case snapper.SnapDone:
|
||||||
|
fallthrough
|
||||||
|
case snapper.SnapError:
|
||||||
|
r.duration = dur(fs.DoneAt.Sub(fs.StartAt))
|
||||||
|
r.remainder = fmt.Sprintf("snap name: %q", fs.SnapName)
|
||||||
|
}
|
||||||
|
rows[i] = r
|
||||||
|
if len(r.path) > widths.path {
|
||||||
|
widths.path = len(r.path)
|
||||||
|
}
|
||||||
|
if len(r.state) > widths.state {
|
||||||
|
widths.state = len(r.state)
|
||||||
|
}
|
||||||
|
if len(r.duration) > widths.duration {
|
||||||
|
widths.duration = len(r.duration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, r := range rows {
|
||||||
|
path := rightPad(r.path, widths.path, " ")
|
||||||
|
state := rightPad(r.state, widths.state, " ")
|
||||||
|
duration := rightPad(r.duration, widths.duration, " ")
|
||||||
|
t.printf("%s %s %s", path, state, duration)
|
||||||
|
t.printfDrawIndentedAndWrappedIfMultiline(" %s", r.remainder)
|
||||||
|
if r.hookReport != "" {
|
||||||
|
t.printfDrawIndentedAndWrappedIfMultiline("%s", r.hookReport)
|
||||||
|
}
|
||||||
|
t.newline()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func times(str string, n int) (out string) {
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
out += str
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func rightPad(str string, length int, pad string) string {
|
||||||
|
if len(str) > length {
|
||||||
|
return str[:length]
|
||||||
|
}
|
||||||
|
return str + strings.Repeat(pad, length-len(str))
|
||||||
|
}
|
||||||
|
|
||||||
|
var arrowPositions = `>\|/`
|
||||||
|
|
||||||
|
// changeCount = 0 indicates stall / no progress
|
||||||
|
func (t *tui) drawBar(length int, bytes, totalBytes int64, changeCount int) {
|
||||||
|
var completedLength int
|
||||||
|
if totalBytes > 0 {
|
||||||
|
completedLength = int(int64(length) * bytes / totalBytes)
|
||||||
|
if completedLength > length {
|
||||||
|
completedLength = length
|
||||||
|
}
|
||||||
|
} else if totalBytes == bytes {
|
||||||
|
completedLength = length
|
||||||
|
}
|
||||||
|
|
||||||
|
t.write("[")
|
||||||
|
t.write(times("=", completedLength))
|
||||||
|
t.write(string(arrowPositions[changeCount%len(arrowPositions)]))
|
||||||
|
t.write(times("-", length-completedLength))
|
||||||
|
t.write("]")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *tui) printFilesystemStatus(rep *report.FilesystemReport, active bool, maxFS int) {
|
||||||
|
|
||||||
|
expected, replicated, containsInvalidSizeEstimates := rep.BytesSum()
|
||||||
|
sizeEstimationImpreciseNotice := ""
|
||||||
|
if containsInvalidSizeEstimates {
|
||||||
|
sizeEstimationImpreciseNotice = " (some steps lack size estimation)"
|
||||||
|
}
|
||||||
|
if rep.CurrentStep < len(rep.Steps) && rep.Steps[rep.CurrentStep].Info.BytesExpected == 0 {
|
||||||
|
sizeEstimationImpreciseNotice = " (step lacks size estimation)"
|
||||||
|
}
|
||||||
|
|
||||||
|
status := fmt.Sprintf("%s (step %d/%d, %s/%s)%s",
|
||||||
|
strings.ToUpper(string(rep.State)),
|
||||||
|
rep.CurrentStep, len(rep.Steps),
|
||||||
|
ByteCountBinary(replicated), ByteCountBinary(expected),
|
||||||
|
sizeEstimationImpreciseNotice,
|
||||||
|
)
|
||||||
|
|
||||||
|
activeIndicator := " "
|
||||||
|
if active {
|
||||||
|
activeIndicator = "*"
|
||||||
|
}
|
||||||
|
t.printf("%s %s %s ",
|
||||||
|
activeIndicator,
|
||||||
|
rightPad(rep.Info.Name, maxFS, " "),
|
||||||
|
status)
|
||||||
|
|
||||||
|
next := ""
|
||||||
|
if err := rep.Error(); err != nil {
|
||||||
|
next = err.Err
|
||||||
|
} else if rep.State != report.FilesystemDone {
|
||||||
|
if nextStep := rep.NextStep(); nextStep != nil {
|
||||||
|
if nextStep.IsIncremental() {
|
||||||
|
next = fmt.Sprintf("next: %s => %s", nextStep.Info.From, nextStep.Info.To)
|
||||||
|
} else {
|
||||||
|
next = fmt.Sprintf("next: full send %s", nextStep.Info.To)
|
||||||
|
}
|
||||||
|
attribs := []string{}
|
||||||
|
|
||||||
|
if nextStep.Info.Resumed {
|
||||||
|
attribs = append(attribs, "resumed")
|
||||||
|
}
|
||||||
|
|
||||||
|
attribs = append(attribs, fmt.Sprintf("encrypted=%s", nextStep.Info.Encrypted))
|
||||||
|
|
||||||
|
next += fmt.Sprintf(" (%s)", strings.Join(attribs, ", "))
|
||||||
|
} else {
|
||||||
|
next = "" // individual FSes may still be in planning state
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
t.printfDrawIndentedAndWrappedIfMultiline("%s", next)
|
||||||
|
|
||||||
|
t.newline()
|
||||||
|
}
|
||||||
|
|
||||||
|
func ByteCountBinary(b int64) string {
|
||||||
|
const unit = 1024
|
||||||
|
if b < unit {
|
||||||
|
return fmt.Sprintf("%d B", b)
|
||||||
|
}
|
||||||
|
div, exp := int64(unit), 0
|
||||||
|
for n := b / unit; n >= unit; n /= unit {
|
||||||
|
div *= unit
|
||||||
|
exp++
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%.1f %ciB", float64(b)/float64(div), "KMGTPE"[exp])
|
||||||
|
}
|
||||||
|
|
||||||
|
func humanizeDuration(duration time.Duration) string {
|
||||||
|
days := int64(duration.Hours() / 24)
|
||||||
|
hours := int64(math.Mod(duration.Hours(), 24))
|
||||||
|
minutes := int64(math.Mod(duration.Minutes(), 60))
|
||||||
|
seconds := int64(math.Mod(duration.Seconds(), 60))
|
||||||
|
|
||||||
|
var parts []string
|
||||||
|
|
||||||
|
force := false
|
||||||
|
chunks := []int64{days, hours, minutes, seconds}
|
||||||
|
for i, chunk := range chunks {
|
||||||
|
if force || chunk > 0 {
|
||||||
|
padding := 0
|
||||||
|
if force {
|
||||||
|
padding = 2
|
||||||
|
}
|
||||||
|
parts = append(parts, fmt.Sprintf("%*d%c", padding, chunk, "dhms"[i]))
|
||||||
|
force = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(parts, " ")
|
||||||
|
}
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/daemon"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Client struct {
|
|
||||||
h *http.Client
|
|
||||||
}
|
|
||||||
|
|
||||||
func New(network, addr string) (*Client, error) {
|
|
||||||
httpc, err := makeControlHttpClient(func(_ context.Context) (net.Conn, error) { return net.Dial(network, addr) })
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &Client{httpc}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) Status() (s daemon.Status, _ error) {
|
|
||||||
err := jsonRequestResponse(c.h, daemon.ControlJobEndpointStatus,
|
|
||||||
struct{}{},
|
|
||||||
&s,
|
|
||||||
)
|
|
||||||
return s, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) StatusRaw() ([]byte, error) {
|
|
||||||
var r json.RawMessage
|
|
||||||
err := jsonRequestResponse(c.h, daemon.ControlJobEndpointStatus, struct{}{}, &r)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return r, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) signal(job, sig string) error {
|
|
||||||
return jsonRequestResponse(c.h, daemon.ControlJobEndpointSignal,
|
|
||||||
struct {
|
|
||||||
Name string
|
|
||||||
Op string
|
|
||||||
}{
|
|
||||||
Name: job,
|
|
||||||
Op: sig,
|
|
||||||
},
|
|
||||||
struct{}{},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) SignalWakeup(job string) error {
|
|
||||||
return c.signal(job, "wakeup")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) SignalReset(job string) error {
|
|
||||||
return c.signal(job, "reset")
|
|
||||||
}
|
|
||||||
|
|
||||||
func makeControlHttpClient(dialfunc func(context.Context) (net.Conn, error)) (client *http.Client, err error) {
|
|
||||||
return &http.Client{
|
|
||||||
Transport: &http.Transport{
|
|
||||||
DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
|
|
||||||
return dialfunc(ctx)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func jsonRequestResponse(c *http.Client, endpoint string, req interface{}, res interface{}) error {
|
|
||||||
var buf bytes.Buffer
|
|
||||||
encodeErr := json.NewEncoder(&buf).Encode(req)
|
|
||||||
if encodeErr != nil {
|
|
||||||
return encodeErr
|
|
||||||
}
|
|
||||||
|
|
||||||
hreq, err := http.NewRequest("POST", "http://unix"+endpoint, &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 {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
var msg bytes.Buffer
|
|
||||||
_, _ = io.CopyN(&msg, resp.Body, 4096) // ignore error, just display what we got
|
|
||||||
return errors.Errorf("%s", msg.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
decodeError := json.NewDecoder(resp.Body).Decode(&res)
|
|
||||||
if decodeError != nil {
|
|
||||||
return decodeError
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
package status
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"os"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/mattn/go-isatty"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/spf13/pflag"
|
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/cli"
|
|
||||||
"github.com/zrepl/zrepl/client/status/client"
|
|
||||||
"github.com/zrepl/zrepl/config"
|
|
||||||
"github.com/zrepl/zrepl/daemon"
|
|
||||||
"github.com/zrepl/zrepl/util/choices"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Client interface {
|
|
||||||
Status() (daemon.Status, error)
|
|
||||||
StatusRaw() ([]byte, error)
|
|
||||||
SignalWakeup(job string) error
|
|
||||||
SignalReset(job string) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type statusFlags struct {
|
|
||||||
Mode choices.Choices
|
|
||||||
Job string
|
|
||||||
Delay time.Duration
|
|
||||||
}
|
|
||||||
|
|
||||||
var statusv2Flags statusFlags
|
|
||||||
|
|
||||||
type statusv2Mode int
|
|
||||||
|
|
||||||
const (
|
|
||||||
StatusV2ModeInteractive statusv2Mode = 1 + iota
|
|
||||||
StatusV2ModeDump
|
|
||||||
StatusV2ModeRaw
|
|
||||||
StatusV2ModeLegacy
|
|
||||||
)
|
|
||||||
|
|
||||||
var Subcommand = &cli.Subcommand{
|
|
||||||
Use: "status",
|
|
||||||
Short: "retrieve & display daemon status information",
|
|
||||||
SetupFlags: func(f *pflag.FlagSet) {
|
|
||||||
statusv2Flags.Mode.Init(
|
|
||||||
"interactive", StatusV2ModeInteractive,
|
|
||||||
"dump", StatusV2ModeDump,
|
|
||||||
"raw", StatusV2ModeRaw,
|
|
||||||
"legacy", StatusV2ModeLegacy,
|
|
||||||
)
|
|
||||||
statusv2Flags.Mode.SetTypeString("mode")
|
|
||||||
statusv2Flags.Mode.SetDefaultValue(StatusV2ModeInteractive)
|
|
||||||
f.Var(&statusv2Flags.Mode, "mode", statusv2Flags.Mode.Usage())
|
|
||||||
f.StringVar(&statusv2Flags.Job, "job", "", "only show specified job (works in \"dump\" and \"interactive\" mode)")
|
|
||||||
f.DurationVarP(&statusv2Flags.Delay, "delay", "d", 1*time.Second, "use -d 3s for 3 seconds delay (minimum delay is 1s)")
|
|
||||||
},
|
|
||||||
Run: func(ctx context.Context, subcommand *cli.Subcommand, args []string) error {
|
|
||||||
return runStatusV2Command(ctx, subcommand.Config(), args)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func runStatusV2Command(ctx context.Context, config *config.Config, args []string) error {
|
|
||||||
|
|
||||||
c, err := client.New("unix", config.Global.Control.SockPath)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrapf(err, "connect to daemon socket at %q", config.Global.Control.SockPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
mode := statusv2Flags.Mode.Value().(statusv2Mode)
|
|
||||||
|
|
||||||
if !isatty.IsTerminal(os.Stdout.Fd()) && mode != StatusV2ModeDump && mode != StatusV2ModeRaw {
|
|
||||||
dumpmode, err := statusv2Flags.Mode.InputForChoice(StatusV2ModeDump)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
rawmode, err := statusv2Flags.Mode.InputForChoice(StatusV2ModeRaw)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return errors.Errorf("error: stdout is not a tty, please use --mode %s or --mode %s", dumpmode, rawmode)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch mode {
|
|
||||||
case StatusV2ModeInteractive:
|
|
||||||
return interactive(c, statusv2Flags)
|
|
||||||
case StatusV2ModeDump:
|
|
||||||
return dump(c, statusv2Flags.Job)
|
|
||||||
case StatusV2ModeRaw:
|
|
||||||
return raw(c)
|
|
||||||
case StatusV2ModeLegacy:
|
|
||||||
return legacy(c, statusv2Flags)
|
|
||||||
default:
|
|
||||||
panic("unreachable")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
package status
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/gdamore/tcell"
|
|
||||||
"github.com/mattn/go-isatty"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/client/status/viewmodel"
|
|
||||||
)
|
|
||||||
|
|
||||||
func dump(c Client, job string) error {
|
|
||||||
s, err := c.Status()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if job != "" {
|
|
||||||
if _, ok := s.Jobs[job]; !ok {
|
|
||||||
return errors.Errorf("job %q not found", job)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
width := (1 << 31) - 1
|
|
||||||
wrap := false
|
|
||||||
hline := strings.Repeat("-", 80)
|
|
||||||
if isatty.IsTerminal(os.Stdout.Fd()) {
|
|
||||||
wrap = true
|
|
||||||
screen, err := tcell.NewScreen()
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "get terminal dimensions")
|
|
||||||
}
|
|
||||||
if err := screen.Init(); err != nil {
|
|
||||||
return errors.Wrap(err, "init screen")
|
|
||||||
}
|
|
||||||
width, _ = screen.Size()
|
|
||||||
screen.Fini()
|
|
||||||
hline = strings.Repeat("-", width)
|
|
||||||
}
|
|
||||||
|
|
||||||
m := viewmodel.New()
|
|
||||||
params := viewmodel.Params{
|
|
||||||
Report: s.Jobs,
|
|
||||||
ReportFetchError: nil,
|
|
||||||
SelectedJob: nil,
|
|
||||||
FSFilter: func(s string) bool { return true },
|
|
||||||
DetailViewWidth: width,
|
|
||||||
DetailViewWrap: wrap,
|
|
||||||
ShortKeybindingOverview: "",
|
|
||||||
}
|
|
||||||
m.Update(params)
|
|
||||||
for _, j := range m.Jobs() {
|
|
||||||
if job != "" && j.Name() != job {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
params.SelectedJob = j
|
|
||||||
m.Update(params)
|
|
||||||
fmt.Println(m.SelectedJob().FullDescription())
|
|
||||||
if job != "" {
|
|
||||||
return nil
|
|
||||||
} else {
|
|
||||||
fmt.Println(hline)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,347 +0,0 @@
|
|||||||
package status
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gdamore/tcell/v2"
|
|
||||||
tview "gitlab.com/tslocum/cview"
|
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/client/status/viewmodel"
|
|
||||||
)
|
|
||||||
|
|
||||||
func interactive(c Client, flag statusFlags) error {
|
|
||||||
|
|
||||||
// Set this so we don't overwrite the default terminal colors
|
|
||||||
// See https://github.com/rivo/tview/blob/master/styles.go
|
|
||||||
tview.Styles.PrimitiveBackgroundColor = tcell.ColorDefault
|
|
||||||
tview.Styles.ContrastBackgroundColor = tcell.ColorDefault
|
|
||||||
tview.Styles.PrimaryTextColor = tcell.ColorDefault
|
|
||||||
tview.Styles.BorderColor = tcell.ColorDefault
|
|
||||||
app := tview.NewApplication()
|
|
||||||
|
|
||||||
jobDetailSplit := tview.NewFlex()
|
|
||||||
jobMenu := tview.NewTreeView()
|
|
||||||
jobMenuRoot := tview.NewTreeNode("jobs")
|
|
||||||
jobMenuRoot.SetSelectable(true)
|
|
||||||
jobMenu.SetRoot(jobMenuRoot)
|
|
||||||
jobMenu.SetCurrentNode(jobMenuRoot)
|
|
||||||
jobMenu.SetSelectedTextColor(tcell.ColorGreen)
|
|
||||||
jobTextDetail := tview.NewTextView()
|
|
||||||
jobTextDetail.SetWrap(false)
|
|
||||||
|
|
||||||
jobMenu.SetBorder(true)
|
|
||||||
jobTextDetail.SetBorder(true)
|
|
||||||
|
|
||||||
toolbarSplit := tview.NewFlex()
|
|
||||||
toolbarSplit.SetDirection(tview.FlexRow)
|
|
||||||
inputBarContainer := tview.NewFlex()
|
|
||||||
fsFilterInput := tview.NewInputField()
|
|
||||||
fsFilterInput.SetBorder(false)
|
|
||||||
fsFilterInput.SetFieldBackgroundColor(tcell.ColorDefault)
|
|
||||||
inputBarLabel := tview.NewTextView()
|
|
||||||
inputBarLabel.SetText("[::b]FILTER ")
|
|
||||||
inputBarLabel.SetDynamicColors(true)
|
|
||||||
inputBarContainer.AddItem(inputBarLabel, 7, 1, false)
|
|
||||||
inputBarContainer.AddItem(fsFilterInput, 0, 10, false)
|
|
||||||
toolbarSplit.AddItem(inputBarContainer, 1, 0, false)
|
|
||||||
toolbarSplit.AddItem(jobDetailSplit, 0, 10, false)
|
|
||||||
|
|
||||||
bottombar := tview.NewFlex()
|
|
||||||
bottombar.SetDirection(tview.FlexColumn)
|
|
||||||
bottombarDateView := tview.NewTextView()
|
|
||||||
bottombar.AddItem(bottombarDateView, len(time.Now().String()), 0, false)
|
|
||||||
bottomBarStatus := tview.NewTextView()
|
|
||||||
bottomBarStatus.SetDynamicColors(true)
|
|
||||||
bottomBarStatus.SetTextAlign(tview.AlignRight)
|
|
||||||
bottombar.AddItem(bottomBarStatus, 0, 10, false)
|
|
||||||
toolbarSplit.AddItem(bottombar, 1, 0, false)
|
|
||||||
|
|
||||||
tabbableWithJobMenu := []tview.Primitive{jobMenu, jobTextDetail, fsFilterInput}
|
|
||||||
tabbableWithoutJobMenu := []tview.Primitive{jobTextDetail, fsFilterInput}
|
|
||||||
var tabbable []tview.Primitive
|
|
||||||
tabbableActiveIndex := 0
|
|
||||||
tabbableRedraw := func() {
|
|
||||||
if len(tabbable) == 0 {
|
|
||||||
app.SetFocus(nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if tabbableActiveIndex >= len(tabbable) {
|
|
||||||
app.SetFocus(tabbable[0])
|
|
||||||
return
|
|
||||||
}
|
|
||||||
app.SetFocus(tabbable[tabbableActiveIndex])
|
|
||||||
}
|
|
||||||
tabbableCycle := func() {
|
|
||||||
if len(tabbable) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
tabbableActiveIndex = (tabbableActiveIndex + 1) % len(tabbable)
|
|
||||||
app.SetFocus(tabbable[tabbableActiveIndex])
|
|
||||||
tabbableRedraw()
|
|
||||||
}
|
|
||||||
|
|
||||||
jobMenuVisisble := false
|
|
||||||
reconfigureJobDetailSplit := func(setJobMenuVisible bool) {
|
|
||||||
if jobMenuVisisble == setJobMenuVisible {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
jobMenuVisisble = setJobMenuVisible
|
|
||||||
if setJobMenuVisible {
|
|
||||||
jobDetailSplit.RemoveItem(jobTextDetail)
|
|
||||||
jobDetailSplit.AddItem(jobMenu, 0, 1, true)
|
|
||||||
jobDetailSplit.AddItem(jobTextDetail, 0, 5, false)
|
|
||||||
tabbable = tabbableWithJobMenu
|
|
||||||
} else {
|
|
||||||
jobDetailSplit.RemoveItem(jobMenu)
|
|
||||||
tabbable = tabbableWithoutJobMenu
|
|
||||||
}
|
|
||||||
tabbableRedraw()
|
|
||||||
}
|
|
||||||
|
|
||||||
showModal := func(m *tview.Modal, modalDoneFunc func(idx int, label string)) {
|
|
||||||
preModalFocus := app.GetFocus()
|
|
||||||
m.SetDoneFunc(func(idx int, label string) {
|
|
||||||
if modalDoneFunc != nil {
|
|
||||||
modalDoneFunc(idx, label)
|
|
||||||
}
|
|
||||||
app.SetRoot(toolbarSplit, true)
|
|
||||||
app.SetFocus(preModalFocus)
|
|
||||||
app.Draw()
|
|
||||||
})
|
|
||||||
app.SetRoot(m, true)
|
|
||||||
app.Draw()
|
|
||||||
}
|
|
||||||
|
|
||||||
app.SetRoot(toolbarSplit, true)
|
|
||||||
// initial focus
|
|
||||||
tabbableActiveIndex = len(tabbable)
|
|
||||||
tabbableCycle()
|
|
||||||
reconfigureJobDetailSplit(true)
|
|
||||||
|
|
||||||
m := viewmodel.New()
|
|
||||||
params := &viewmodel.Params{
|
|
||||||
Report: nil,
|
|
||||||
SelectedJob: nil,
|
|
||||||
FSFilter: func(_ string) bool { return true },
|
|
||||||
DetailViewWidth: 100,
|
|
||||||
DetailViewWrap: false,
|
|
||||||
ShortKeybindingOverview: "[::b]Q[::-] quit [::b]<TAB>[::-] switch panes [::b]W[::-] wrap lines [::b]Shift+M[::-] toggle navbar [::b]Shift+S[::-] signal job [::b]</>[::-] filter filesystems",
|
|
||||||
}
|
|
||||||
paramsMtx := &sync.Mutex{}
|
|
||||||
var redraw func()
|
|
||||||
viewmodelupdate := func(cb func(*viewmodel.Params)) {
|
|
||||||
paramsMtx.Lock()
|
|
||||||
defer paramsMtx.Unlock()
|
|
||||||
cb(params)
|
|
||||||
m.Update(*params)
|
|
||||||
}
|
|
||||||
redraw = func() {
|
|
||||||
jobs := m.Jobs()
|
|
||||||
if flag.Job != "" {
|
|
||||||
job_found := false
|
|
||||||
for _, job := range jobs {
|
|
||||||
if strings.Compare(flag.Job, job.Name()) == 0 {
|
|
||||||
jobs = []*viewmodel.Job{job}
|
|
||||||
job_found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !job_found {
|
|
||||||
jobs = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
redrawJobsList := false
|
|
||||||
var selectedJobN *tview.TreeNode
|
|
||||||
if len(jobMenuRoot.GetChildren()) == len(jobs) {
|
|
||||||
for i, jobN := range jobMenuRoot.GetChildren() {
|
|
||||||
if jobN.GetReference().(*viewmodel.Job) != jobs[i] {
|
|
||||||
redrawJobsList = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if jobN.GetReference().(*viewmodel.Job) == m.SelectedJob() {
|
|
||||||
selectedJobN = jobN
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
redrawJobsList = true
|
|
||||||
}
|
|
||||||
if redrawJobsList {
|
|
||||||
selectedJobN = nil
|
|
||||||
children := make([]*tview.TreeNode, len(jobs))
|
|
||||||
for i := range jobs {
|
|
||||||
jobN := tview.NewTreeNode(jobs[i].JobTreeTitle())
|
|
||||||
jobN.SetReference(jobs[i])
|
|
||||||
jobN.SetSelectable(true)
|
|
||||||
children[i] = jobN
|
|
||||||
jobN.SetSelectedFunc(func() {
|
|
||||||
viewmodelupdate(func(p *viewmodel.Params) {
|
|
||||||
p.SelectedJob = jobN.GetReference().(*viewmodel.Job)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
if jobs[i] == m.SelectedJob() {
|
|
||||||
selectedJobN = jobN
|
|
||||||
}
|
|
||||||
}
|
|
||||||
jobMenuRoot.SetChildren(children)
|
|
||||||
}
|
|
||||||
|
|
||||||
if selectedJobN != nil && jobMenu.GetCurrentNode() != selectedJobN {
|
|
||||||
jobMenu.SetCurrentNode(selectedJobN)
|
|
||||||
} else if selectedJobN == nil {
|
|
||||||
// select something, otherwise selection breaks (likely bug in tview)
|
|
||||||
jobMenu.SetCurrentNode(jobMenuRoot)
|
|
||||||
}
|
|
||||||
|
|
||||||
if selJ := m.SelectedJob(); selJ != nil {
|
|
||||||
jobTextDetail.SetText(selJ.FullDescription())
|
|
||||||
} else {
|
|
||||||
jobTextDetail.SetText("please select a job")
|
|
||||||
}
|
|
||||||
|
|
||||||
bottombardatestring := m.DateString()
|
|
||||||
bottombarDateView.SetText(bottombardatestring)
|
|
||||||
bottombar.ResizeItem(bottombarDateView, len(bottombardatestring), 0)
|
|
||||||
|
|
||||||
bottomBarStatus.SetText(m.BottomBarStatus())
|
|
||||||
|
|
||||||
app.Draw()
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
defer func() {
|
|
||||||
if err := recover(); err != nil {
|
|
||||||
app.Suspend(func() {
|
|
||||||
panic(err)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
for {
|
|
||||||
st, err := c.Status()
|
|
||||||
viewmodelupdate(func(p *viewmodel.Params) {
|
|
||||||
p.Report = st.Jobs
|
|
||||||
p.ReportFetchError = err
|
|
||||||
})
|
|
||||||
app.QueueUpdateDraw(redraw)
|
|
||||||
|
|
||||||
time.Sleep(flag.Delay)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
jobMenu.SetChangedFunc(func(jobN *tview.TreeNode) {
|
|
||||||
viewmodelupdate(func(p *viewmodel.Params) {
|
|
||||||
p.SelectedJob, _ = jobN.GetReference().(*viewmodel.Job)
|
|
||||||
})
|
|
||||||
redraw()
|
|
||||||
jobTextDetail.ScrollToBeginning()
|
|
||||||
})
|
|
||||||
jobMenu.SetSelectedFunc(func(jobN *tview.TreeNode) {
|
|
||||||
app.SetFocus(jobTextDetail)
|
|
||||||
})
|
|
||||||
|
|
||||||
app.SetBeforeDrawFunc(func(screen tcell.Screen) bool {
|
|
||||||
viewmodelupdate(func(p *viewmodel.Params) {
|
|
||||||
_, _, p.DetailViewWidth, _ = jobTextDetail.GetInnerRect()
|
|
||||||
})
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
|
|
||||||
app.SetInputCapture(func(e *tcell.EventKey) *tcell.EventKey {
|
|
||||||
if e.Key() == tcell.KeyTab {
|
|
||||||
tabbableCycle()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if e.Key() == tcell.KeyRune && app.GetFocus() == fsFilterInput {
|
|
||||||
return e
|
|
||||||
}
|
|
||||||
|
|
||||||
if e.Key() == tcell.KeyRune && e.Rune() == '/' {
|
|
||||||
if app.GetFocus() != fsFilterInput {
|
|
||||||
app.SetFocus(fsFilterInput)
|
|
||||||
}
|
|
||||||
return e
|
|
||||||
}
|
|
||||||
|
|
||||||
if e.Key() == tcell.KeyRune && e.Rune() == 'M' {
|
|
||||||
reconfigureJobDetailSplit(!jobMenuVisisble)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if e.Key() == tcell.KeyRune && e.Rune() == 'q' {
|
|
||||||
app.Stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
if e.Key() == tcell.KeyRune && e.Rune() == 'S' {
|
|
||||||
job, ok := jobMenu.GetCurrentNode().GetReference().(*viewmodel.Job)
|
|
||||||
if !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
signals := []string{"wakeup", "reset"}
|
|
||||||
clientFuncs := []func(job string) error{c.SignalWakeup, c.SignalReset}
|
|
||||||
sigMod := tview.NewModal()
|
|
||||||
sigMod.SetBackgroundColor(tcell.ColorDefault)
|
|
||||||
sigMod.SetBorder(true)
|
|
||||||
sigMod.GetForm().SetButtonTextColorFocused(tcell.ColorGreen)
|
|
||||||
sigMod.AddButtons(signals)
|
|
||||||
sigMod.SetText(fmt.Sprintf("Send a signal to job %q", job.Name()))
|
|
||||||
showModal(sigMod, func(idx int, _ string) {
|
|
||||||
go func() {
|
|
||||||
if idx == -1 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
err := clientFuncs[idx](job.Name())
|
|
||||||
if err != nil {
|
|
||||||
app.QueueUpdate(func() {
|
|
||||||
me := tview.NewModal()
|
|
||||||
me.SetText(fmt.Sprintf("signal error: %s", err))
|
|
||||||
me.AddButtons([]string{"Close"})
|
|
||||||
showModal(me, nil)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return e
|
|
||||||
})
|
|
||||||
|
|
||||||
fsFilterInput.SetChangedFunc(func(searchterm string) {
|
|
||||||
viewmodelupdate(func(p *viewmodel.Params) {
|
|
||||||
p.FSFilter = func(fs string) bool {
|
|
||||||
r, err := regexp.Compile(searchterm)
|
|
||||||
if err != nil {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return r.MatchString(fs)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
redraw()
|
|
||||||
jobTextDetail.ScrollToBeginning()
|
|
||||||
})
|
|
||||||
fsFilterInput.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
|
||||||
if event.Key() == tcell.KeyEnter {
|
|
||||||
app.SetFocus(jobTextDetail)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return event
|
|
||||||
})
|
|
||||||
|
|
||||||
jobTextDetail.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
|
||||||
if event.Key() == tcell.KeyRune && event.Rune() == 'w' {
|
|
||||||
// toggle wrapping
|
|
||||||
viewmodelupdate(func(p *viewmodel.Params) {
|
|
||||||
p.DetailViewWrap = !p.DetailViewWrap
|
|
||||||
})
|
|
||||||
redraw()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return event
|
|
||||||
})
|
|
||||||
|
|
||||||
return app.Run()
|
|
||||||
}
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
package status
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gdamore/tcell/v2"
|
|
||||||
"github.com/mattn/go-isatty"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
tview "gitlab.com/tslocum/cview"
|
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/client/status/viewmodel"
|
|
||||||
)
|
|
||||||
|
|
||||||
func legacy(c Client, flag statusFlags) error {
|
|
||||||
|
|
||||||
// Set this so we don't overwrite the default terminal colors
|
|
||||||
// See https://github.com/rivo/tview/blob/master/styles.go
|
|
||||||
tview.Styles.PrimitiveBackgroundColor = tcell.ColorDefault
|
|
||||||
tview.Styles.ContrastBackgroundColor = tcell.ColorDefault
|
|
||||||
tview.Styles.PrimaryTextColor = tcell.ColorDefault
|
|
||||||
tview.Styles.BorderColor = tcell.ColorDefault
|
|
||||||
app := tview.NewApplication()
|
|
||||||
|
|
||||||
textView := tview.NewTextView()
|
|
||||||
textView.SetWrap(true)
|
|
||||||
textView.SetScrollable(true) // so that it allows us to set scroll position
|
|
||||||
textView.SetScrollBarVisibility(tview.ScrollBarNever)
|
|
||||||
|
|
||||||
app.SetRoot(textView, true)
|
|
||||||
|
|
||||||
width := (1 << 31) - 1
|
|
||||||
wrap := false
|
|
||||||
if isatty.IsTerminal(os.Stdout.Fd()) {
|
|
||||||
wrap = true
|
|
||||||
screen, err := tcell.NewScreen()
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "get terminal dimensions")
|
|
||||||
}
|
|
||||||
if err := screen.Init(); err != nil {
|
|
||||||
return errors.Wrap(err, "init screen")
|
|
||||||
}
|
|
||||||
width, _ = screen.Size()
|
|
||||||
screen.Fini()
|
|
||||||
}
|
|
||||||
|
|
||||||
paramsMtx := &sync.Mutex{}
|
|
||||||
params := viewmodel.Params{
|
|
||||||
Report: nil,
|
|
||||||
ReportFetchError: nil,
|
|
||||||
SelectedJob: nil,
|
|
||||||
FSFilter: func(s string) bool { return true },
|
|
||||||
DetailViewWidth: width,
|
|
||||||
DetailViewWrap: wrap,
|
|
||||||
ShortKeybindingOverview: "",
|
|
||||||
}
|
|
||||||
|
|
||||||
redraw := func() {
|
|
||||||
textView.Clear()
|
|
||||||
|
|
||||||
paramsMtx.Lock()
|
|
||||||
defer paramsMtx.Unlock()
|
|
||||||
|
|
||||||
if params.ReportFetchError != nil {
|
|
||||||
fmt.Fprintln(textView, params.ReportFetchError.Error())
|
|
||||||
} else if params.Report != nil {
|
|
||||||
m := viewmodel.New()
|
|
||||||
m.Update(params)
|
|
||||||
for _, j := range m.Jobs() {
|
|
||||||
if flag.Job != "" && j.Name() != flag.Job {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
params.SelectedJob = j
|
|
||||||
m.Update(params)
|
|
||||||
fmt.Fprintln(textView, m.SelectedJob().FullDescription())
|
|
||||||
if flag.Job != "" {
|
|
||||||
break
|
|
||||||
} else {
|
|
||||||
hline := strings.Repeat("-", params.DetailViewWidth)
|
|
||||||
fmt.Fprintln(textView, hline)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
fmt.Fprintln(textView, "waiting for request results")
|
|
||||||
}
|
|
||||||
textView.ScrollToBeginning()
|
|
||||||
}
|
|
||||||
|
|
||||||
app.SetBeforeDrawFunc(func(screen tcell.Screen) bool {
|
|
||||||
// sync resizes to `params`
|
|
||||||
paramsMtx.Lock()
|
|
||||||
_, _, newWidth, _ := textView.GetInnerRect()
|
|
||||||
if newWidth != params.DetailViewWidth {
|
|
||||||
params.DetailViewWidth = newWidth
|
|
||||||
app.QueueUpdateDraw(redraw)
|
|
||||||
}
|
|
||||||
paramsMtx.Unlock()
|
|
||||||
|
|
||||||
textView.ScrollToBeginning() // has the effect of inhibiting user scrolls
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
defer func() {
|
|
||||||
if err := recover(); err != nil {
|
|
||||||
app.Suspend(func() {
|
|
||||||
panic(err)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
for {
|
|
||||||
st, err := c.Status()
|
|
||||||
paramsMtx.Lock()
|
|
||||||
params.Report = st.Jobs
|
|
||||||
params.ReportFetchError = err
|
|
||||||
paramsMtx.Unlock()
|
|
||||||
|
|
||||||
app.QueueUpdateDraw(redraw)
|
|
||||||
|
|
||||||
time.Sleep(flag.Delay)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
return app.Run()
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
package status
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
func raw(c Client) error {
|
|
||||||
b, err := c.StatusRaw()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if _, err := io.Copy(os.Stdout, bytes.NewReader(b)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
package viewmodel
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"math"
|
|
||||||
)
|
|
||||||
|
|
||||||
func ByteCountBinaryUint(b uint64) string {
|
|
||||||
if b > math.MaxInt64 {
|
|
||||||
panic(b)
|
|
||||||
}
|
|
||||||
return ByteCountBinary(int64(b))
|
|
||||||
}
|
|
||||||
|
|
||||||
func ByteCountBinary(b int64) string {
|
|
||||||
const unit = 1024
|
|
||||||
if b < unit {
|
|
||||||
return fmt.Sprintf("%d B", b)
|
|
||||||
}
|
|
||||||
div, exp := unit, 0
|
|
||||||
for n := b / unit; n >= unit; n /= unit {
|
|
||||||
div *= unit
|
|
||||||
exp++
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("%.1f %ciB", float64(b)/float64(div), "KMGTPE"[exp])
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
package viewmodel
|
|
||||||
|
|
||||||
import "time"
|
|
||||||
|
|
||||||
type byteProgressMeasurement struct {
|
|
||||||
time time.Time
|
|
||||||
val uint64
|
|
||||||
}
|
|
||||||
|
|
||||||
type bytesProgressHistory struct {
|
|
||||||
last *byteProgressMeasurement // pointer as poor man's optional
|
|
||||||
changeCount int
|
|
||||||
lastChange time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *bytesProgressHistory) Update(currentVal uint64) (bytesPerSecondAvg int64, changeCount int) {
|
|
||||||
|
|
||||||
if p.last == nil {
|
|
||||||
p.last = &byteProgressMeasurement{
|
|
||||||
time: time.Now(),
|
|
||||||
val: currentVal,
|
|
||||||
}
|
|
||||||
return 0, 0
|
|
||||||
}
|
|
||||||
|
|
||||||
if p.last.val != currentVal {
|
|
||||||
p.changeCount++
|
|
||||||
p.lastChange = time.Now()
|
|
||||||
}
|
|
||||||
|
|
||||||
if time.Since(p.lastChange) > 3*time.Second {
|
|
||||||
p.last = nil
|
|
||||||
return 0, 0
|
|
||||||
}
|
|
||||||
|
|
||||||
var deltaV int64
|
|
||||||
if currentVal >= p.last.val {
|
|
||||||
deltaV = int64(currentVal - p.last.val)
|
|
||||||
} else {
|
|
||||||
deltaV = -int64(p.last.val - currentVal)
|
|
||||||
}
|
|
||||||
deltaT := time.Since(p.last.time)
|
|
||||||
rate := float64(deltaV) / deltaT.Seconds()
|
|
||||||
|
|
||||||
p.last.time = time.Now()
|
|
||||||
p.last.val = currentVal
|
|
||||||
|
|
||||||
return int64(rate), p.changeCount
|
|
||||||
}
|
|
||||||
@@ -1,656 +0,0 @@
|
|||||||
package viewmodel
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"math"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/go-playground/validator/v10"
|
|
||||||
yaml "github.com/zrepl/yaml-config"
|
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/client/status/viewmodel/stringbuilder"
|
|
||||||
"github.com/zrepl/zrepl/daemon"
|
|
||||||
"github.com/zrepl/zrepl/daemon/job"
|
|
||||||
"github.com/zrepl/zrepl/daemon/pruner"
|
|
||||||
"github.com/zrepl/zrepl/daemon/snapper"
|
|
||||||
"github.com/zrepl/zrepl/replication/report"
|
|
||||||
)
|
|
||||||
|
|
||||||
type M struct {
|
|
||||||
jobs map[string]*Job
|
|
||||||
jobsList []*Job
|
|
||||||
selectedJob *Job
|
|
||||||
dateString string
|
|
||||||
bottomBarStatus string
|
|
||||||
}
|
|
||||||
|
|
||||||
type Job struct {
|
|
||||||
// long-lived
|
|
||||||
name string
|
|
||||||
byteProgress *bytesProgressHistory
|
|
||||||
|
|
||||||
lastStatus *job.Status
|
|
||||||
fulldescription string
|
|
||||||
}
|
|
||||||
|
|
||||||
func New() *M {
|
|
||||||
return &M{
|
|
||||||
jobs: make(map[string]*Job),
|
|
||||||
jobsList: make([]*Job, 0),
|
|
||||||
selectedJob: nil,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type FilterFunc func(string) bool
|
|
||||||
|
|
||||||
type Params struct {
|
|
||||||
Report map[string]*job.Status
|
|
||||||
ReportFetchError error
|
|
||||||
SelectedJob *Job
|
|
||||||
FSFilter FilterFunc `validate:"required"`
|
|
||||||
DetailViewWidth int `validate:"gte=1"`
|
|
||||||
DetailViewWrap bool
|
|
||||||
ShortKeybindingOverview string
|
|
||||||
}
|
|
||||||
|
|
||||||
var validate = validator.New()
|
|
||||||
|
|
||||||
func (m *M) Update(p Params) {
|
|
||||||
|
|
||||||
if err := validate.Struct(p); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if p.ReportFetchError != nil {
|
|
||||||
m.bottomBarStatus = fmt.Sprintf("[red::]status fetch: %s", p.ReportFetchError)
|
|
||||||
} else {
|
|
||||||
m.bottomBarStatus = p.ShortKeybindingOverview
|
|
||||||
for jobname, st := range p.Report {
|
|
||||||
// TODO handle job renames & deletions
|
|
||||||
j, ok := m.jobs[jobname]
|
|
||||||
if !ok {
|
|
||||||
j = &Job{
|
|
||||||
name: jobname,
|
|
||||||
byteProgress: &bytesProgressHistory{},
|
|
||||||
}
|
|
||||||
m.jobs[jobname] = j
|
|
||||||
m.jobsList = append(m.jobsList, j)
|
|
||||||
}
|
|
||||||
j.lastStatus = st
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// filter out internal jobs
|
|
||||||
var jobsList []*Job
|
|
||||||
for _, j := range m.jobsList {
|
|
||||||
if daemon.IsInternalJobName(j.name) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
jobsList = append(jobsList, j)
|
|
||||||
}
|
|
||||||
m.jobsList = jobsList
|
|
||||||
|
|
||||||
// determinism!
|
|
||||||
sort.Slice(m.jobsList, func(i, j int) bool {
|
|
||||||
return strings.Compare(m.jobsList[i].name, m.jobsList[j].name) < 0
|
|
||||||
})
|
|
||||||
|
|
||||||
// try to not lose the selected job
|
|
||||||
m.selectedJob = nil
|
|
||||||
for _, j := range m.jobsList {
|
|
||||||
j.updateFullDescription(p)
|
|
||||||
if j == p.SelectedJob {
|
|
||||||
m.selectedJob = j
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
m.dateString = time.Now().Format(time.RFC3339)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *M) BottomBarStatus() string { return m.bottomBarStatus }
|
|
||||||
|
|
||||||
func (m *M) Jobs() []*Job { return m.jobsList }
|
|
||||||
|
|
||||||
// may be nil
|
|
||||||
func (m *M) SelectedJob() *Job { return m.selectedJob }
|
|
||||||
|
|
||||||
func (m *M) DateString() string { return m.dateString }
|
|
||||||
|
|
||||||
func (j *Job) updateFullDescription(p Params) {
|
|
||||||
width := p.DetailViewWidth
|
|
||||||
if !p.DetailViewWrap {
|
|
||||||
width = 10000000 // FIXME
|
|
||||||
}
|
|
||||||
b := stringbuilder.New(stringbuilder.Config{
|
|
||||||
IndentMultiplier: 3,
|
|
||||||
Width: width,
|
|
||||||
})
|
|
||||||
drawJob(b, j.name, j.lastStatus, j.byteProgress, p.FSFilter)
|
|
||||||
j.fulldescription = b.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (j *Job) JobTreeTitle() string {
|
|
||||||
return j.name
|
|
||||||
}
|
|
||||||
|
|
||||||
func (j *Job) FullDescription() string {
|
|
||||||
return j.fulldescription
|
|
||||||
}
|
|
||||||
|
|
||||||
func (j *Job) Name() string {
|
|
||||||
return j.name
|
|
||||||
}
|
|
||||||
|
|
||||||
func drawJob(t *stringbuilder.B, name string, v *job.Status, history *bytesProgressHistory, fsfilter FilterFunc) {
|
|
||||||
|
|
||||||
t.Printf("Job: %s\n", name)
|
|
||||||
t.Printf("Type: %s\n\n", v.Type)
|
|
||||||
|
|
||||||
if v.Type == job.TypePush || v.Type == job.TypePull {
|
|
||||||
activeStatus, ok := v.JobSpecific.(*job.ActiveSideStatus)
|
|
||||||
if !ok || activeStatus == nil {
|
|
||||||
t.Printf("ActiveSideStatus is null")
|
|
||||||
t.Newline()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Printf("Replication:")
|
|
||||||
t.AddIndentAndNewline(1)
|
|
||||||
renderReplicationReport(t, activeStatus.Replication, history, fsfilter)
|
|
||||||
t.AddIndentAndNewline(-1)
|
|
||||||
|
|
||||||
t.Printf("Pruning Sender:")
|
|
||||||
t.AddIndentAndNewline(1)
|
|
||||||
renderPrunerReport(t, activeStatus.PruningSender, fsfilter)
|
|
||||||
t.AddIndentAndNewline(-1)
|
|
||||||
|
|
||||||
t.Printf("Pruning Receiver:")
|
|
||||||
t.AddIndentAndNewline(1)
|
|
||||||
renderPrunerReport(t, activeStatus.PruningReceiver, fsfilter)
|
|
||||||
t.AddIndentAndNewline(-1)
|
|
||||||
|
|
||||||
if v.Type == job.TypePush {
|
|
||||||
t.Printf("Snapshotting:")
|
|
||||||
t.AddIndentAndNewline(1)
|
|
||||||
renderSnapperReport(t, activeStatus.Snapshotting, fsfilter)
|
|
||||||
t.AddIndentAndNewline(-1)
|
|
||||||
}
|
|
||||||
|
|
||||||
} else if v.Type == job.TypeSnap {
|
|
||||||
snapStatus, ok := v.JobSpecific.(*job.SnapJobStatus)
|
|
||||||
if !ok || snapStatus == nil {
|
|
||||||
t.Printf("SnapJobStatus is null")
|
|
||||||
t.Newline()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
t.Printf("Pruning snapshots:")
|
|
||||||
t.AddIndentAndNewline(1)
|
|
||||||
renderPrunerReport(t, snapStatus.Pruning, fsfilter)
|
|
||||||
t.AddIndentAndNewline(-1)
|
|
||||||
t.Printf("Snapshotting:")
|
|
||||||
t.AddIndentAndNewline(1)
|
|
||||||
renderSnapperReport(t, snapStatus.Snapshotting, fsfilter)
|
|
||||||
t.AddIndentAndNewline(-1)
|
|
||||||
} else if v.Type == job.TypeSource {
|
|
||||||
|
|
||||||
st := v.JobSpecific.(*job.PassiveStatus)
|
|
||||||
t.Printf("Snapshotting:\n")
|
|
||||||
t.AddIndent(1)
|
|
||||||
renderSnapperReport(t, st.Snapper, fsfilter)
|
|
||||||
t.AddIndentAndNewline(-1)
|
|
||||||
|
|
||||||
} else {
|
|
||||||
t.Printf("No status representation for job type '%s', dumping as YAML", v.Type)
|
|
||||||
t.Newline()
|
|
||||||
asYaml, err := yaml.Marshal(v.JobSpecific)
|
|
||||||
if err != nil {
|
|
||||||
t.Printf("Error marshaling status to YAML: %s", err)
|
|
||||||
t.Newline()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
t.Write(string(asYaml))
|
|
||||||
t.Newline()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func printFilesystemStatus(t *stringbuilder.B, rep *report.FilesystemReport, maxFS int) {
|
|
||||||
|
|
||||||
expected, replicated, containsInvalidSizeEstimates := rep.BytesSum()
|
|
||||||
sizeEstimationImpreciseNotice := ""
|
|
||||||
if containsInvalidSizeEstimates {
|
|
||||||
sizeEstimationImpreciseNotice = " (some steps lack size estimation)"
|
|
||||||
}
|
|
||||||
if rep.CurrentStep < len(rep.Steps) && rep.Steps[rep.CurrentStep].Info.BytesExpected == 0 {
|
|
||||||
sizeEstimationImpreciseNotice = " (step lacks size estimation)"
|
|
||||||
}
|
|
||||||
|
|
||||||
userVisisbleCurrentStep, userVisibleTotalSteps := rep.CurrentStep, len(rep.Steps)
|
|
||||||
// `.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",
|
|
||||||
strings.ToUpper(string(rep.State)),
|
|
||||||
userVisisbleCurrentStep, userVisibleTotalSteps,
|
|
||||||
ByteCountBinaryUint(replicated), ByteCountBinaryUint(expected),
|
|
||||||
sizeEstimationImpreciseNotice,
|
|
||||||
)
|
|
||||||
|
|
||||||
activeIndicator := " "
|
|
||||||
if rep.BlockedOn == report.FsBlockedOnNothing &&
|
|
||||||
(rep.State == report.FilesystemPlanning || rep.State == report.FilesystemStepping) {
|
|
||||||
activeIndicator = "*"
|
|
||||||
}
|
|
||||||
t.AddIndent(1)
|
|
||||||
t.Printf("%s %s %s ",
|
|
||||||
activeIndicator,
|
|
||||||
stringbuilder.RightPad(rep.Info.Name, maxFS, " "),
|
|
||||||
status)
|
|
||||||
|
|
||||||
next := ""
|
|
||||||
if err := rep.Error(); err != nil {
|
|
||||||
next = err.Err
|
|
||||||
} else if rep.State != report.FilesystemDone {
|
|
||||||
if nextStep := rep.NextStep(); nextStep != nil {
|
|
||||||
if nextStep.IsIncremental() {
|
|
||||||
next = fmt.Sprintf("next: %s => %s", nextStep.Info.From, nextStep.Info.To)
|
|
||||||
} else {
|
|
||||||
next = fmt.Sprintf("next: full send %s", nextStep.Info.To)
|
|
||||||
}
|
|
||||||
attribs := []string{}
|
|
||||||
|
|
||||||
if nextStep.Info.Resumed {
|
|
||||||
attribs = append(attribs, "resumed")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(attribs) > 0 {
|
|
||||||
next += fmt.Sprintf(" (%s)", strings.Join(attribs, ", "))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
next = "" // individual FSes may still be in planning state
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
t.Printf("%s", next)
|
|
||||||
|
|
||||||
t.AddIndent(-1)
|
|
||||||
t.Newline()
|
|
||||||
}
|
|
||||||
|
|
||||||
func renderReplicationReport(t *stringbuilder.B, rep *report.Report, history *bytesProgressHistory, fsfilter FilterFunc) {
|
|
||||||
if rep == nil {
|
|
||||||
t.Printf("...\n")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if rep.WaitReconnectError != nil {
|
|
||||||
t.PrintfDrawIndentedAndWrappedIfMultiline("Connectivity: %s", rep.WaitReconnectError)
|
|
||||||
t.Newline()
|
|
||||||
}
|
|
||||||
if !rep.WaitReconnectSince.IsZero() {
|
|
||||||
delta := time.Until(rep.WaitReconnectUntil).Round(time.Second)
|
|
||||||
if rep.WaitReconnectUntil.IsZero() || delta > 0 {
|
|
||||||
var until string
|
|
||||||
if rep.WaitReconnectUntil.IsZero() {
|
|
||||||
until = "waiting indefinitely"
|
|
||||||
} else {
|
|
||||||
until = fmt.Sprintf("hard fail in %s @ %s", delta, rep.WaitReconnectUntil)
|
|
||||||
}
|
|
||||||
t.PrintfDrawIndentedAndWrappedIfMultiline("Connectivity: reconnecting with exponential backoff (since %s) (%s)",
|
|
||||||
rep.WaitReconnectSince, until)
|
|
||||||
} else {
|
|
||||||
t.PrintfDrawIndentedAndWrappedIfMultiline("Connectivity: reconnects reached hard-fail timeout @ %s", rep.WaitReconnectUntil)
|
|
||||||
}
|
|
||||||
t.Newline()
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO visualize more than the latest attempt by folding all attempts into one
|
|
||||||
if len(rep.Attempts) == 0 {
|
|
||||||
t.Printf("no attempts made yet")
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
t.Printf("Attempt #%d", len(rep.Attempts))
|
|
||||||
if len(rep.Attempts) > 1 {
|
|
||||||
t.Printf(". Previous attempts failed with the following statuses:")
|
|
||||||
t.AddIndentAndNewline(1)
|
|
||||||
for i, a := range rep.Attempts[:len(rep.Attempts)-1] {
|
|
||||||
t.PrintfDrawIndentedAndWrappedIfMultiline("#%d: %s (failed at %s) (ran %s)\n", i+1, a.State, a.FinishAt, a.FinishAt.Sub(a.StartAt))
|
|
||||||
}
|
|
||||||
t.AddIndentAndNewline(-1)
|
|
||||||
} else {
|
|
||||||
t.Newline()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
latest := rep.Attempts[len(rep.Attempts)-1]
|
|
||||||
sort.Slice(latest.Filesystems, func(i, j int) bool {
|
|
||||||
return latest.Filesystems[i].Info.Name < latest.Filesystems[j].Info.Name
|
|
||||||
})
|
|
||||||
|
|
||||||
// apply filter
|
|
||||||
filtered := make([]*report.FilesystemReport, 0, len(latest.Filesystems))
|
|
||||||
for _, fs := range latest.Filesystems {
|
|
||||||
if !fsfilter(fs.Info.Name) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
filtered = append(filtered, fs)
|
|
||||||
}
|
|
||||||
latest.Filesystems = filtered
|
|
||||||
|
|
||||||
t.Printf("Status: %s", latest.State)
|
|
||||||
t.Newline()
|
|
||||||
if !latest.FinishAt.IsZero() {
|
|
||||||
t.Printf("Last Run: %s (lasted %s)\n", latest.FinishAt.Round(time.Second), latest.FinishAt.Sub(latest.StartAt).Round(time.Second))
|
|
||||||
} else {
|
|
||||||
t.Printf("Started: %s (lasting %s)\n", latest.StartAt.Round(time.Second), time.Since(latest.StartAt).Round(time.Second))
|
|
||||||
}
|
|
||||||
|
|
||||||
if latest.State == report.AttemptPlanningError {
|
|
||||||
t.Printf("Problem: ")
|
|
||||||
t.PrintfDrawIndentedAndWrappedIfMultiline("%s", latest.PlanError)
|
|
||||||
t.Newline()
|
|
||||||
} else if latest.State == report.AttemptFanOutError {
|
|
||||||
t.Printf("Problem: one or more of the filesystems encountered errors")
|
|
||||||
t.Newline()
|
|
||||||
}
|
|
||||||
|
|
||||||
if latest.State != report.AttemptPlanning && latest.State != report.AttemptPlanningError {
|
|
||||||
// Draw global progress bar
|
|
||||||
// Progress: [---------------]
|
|
||||||
expected, replicated, containsInvalidSizeEstimates := latest.BytesSum()
|
|
||||||
rate, changeCount := history.Update(replicated)
|
|
||||||
eta := time.Duration(0)
|
|
||||||
if rate > 0 {
|
|
||||||
eta = time.Duration((float64(expected)-float64(replicated))/float64(rate)) * time.Second
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Write("Progress: ")
|
|
||||||
t.DrawBar(50, replicated, expected, changeCount)
|
|
||||||
t.Write(fmt.Sprintf(" %s / %s @ %s/s", ByteCountBinaryUint(replicated), ByteCountBinaryUint(expected), ByteCountBinary(rate)))
|
|
||||||
if eta != 0 {
|
|
||||||
t.Write(fmt.Sprintf(" (%s remaining)", humanizeDuration(eta)))
|
|
||||||
}
|
|
||||||
t.Newline()
|
|
||||||
if containsInvalidSizeEstimates {
|
|
||||||
t.Write("NOTE: not all steps could be size-estimated, total estimate is likely imprecise!")
|
|
||||||
t.Newline()
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(latest.Filesystems) == 0 {
|
|
||||||
t.Write("NOTE: no filesystems were considered for replication!")
|
|
||||||
t.Newline()
|
|
||||||
}
|
|
||||||
|
|
||||||
var maxFSLen int
|
|
||||||
for _, fs := range latest.Filesystems {
|
|
||||||
if len(fs.Info.Name) > maxFSLen {
|
|
||||||
maxFSLen = len(fs.Info.Name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, fs := range latest.Filesystems {
|
|
||||||
printFilesystemStatus(t, fs, maxFSLen)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func humanizeDuration(duration time.Duration) string {
|
|
||||||
days := int64(duration.Hours() / 24)
|
|
||||||
hours := int64(math.Mod(duration.Hours(), 24))
|
|
||||||
minutes := int64(math.Mod(duration.Minutes(), 60))
|
|
||||||
seconds := int64(math.Mod(duration.Seconds(), 60))
|
|
||||||
|
|
||||||
var parts []string
|
|
||||||
|
|
||||||
force := false
|
|
||||||
chunks := []int64{days, hours, minutes, seconds}
|
|
||||||
for i, chunk := range chunks {
|
|
||||||
if force || chunk > 0 {
|
|
||||||
padding := 0
|
|
||||||
if force {
|
|
||||||
padding = 2
|
|
||||||
}
|
|
||||||
parts = append(parts, fmt.Sprintf("%*d%c", padding, chunk, "dhms"[i]))
|
|
||||||
force = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return strings.Join(parts, " ")
|
|
||||||
}
|
|
||||||
|
|
||||||
func renderPrunerReport(t *stringbuilder.B, r *pruner.Report, fsfilter FilterFunc) {
|
|
||||||
if r == nil {
|
|
||||||
t.Printf("...\n")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
state, err := pruner.StateString(r.State)
|
|
||||||
if err != nil {
|
|
||||||
t.Printf("Status: %q (parse error: %q)\n", r.State, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Printf("Status: %s", state)
|
|
||||||
t.Newline()
|
|
||||||
|
|
||||||
if r.Error != "" {
|
|
||||||
t.Printf("Error: %s\n", r.Error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type commonFS struct {
|
|
||||||
*pruner.FSReport
|
|
||||||
completed bool
|
|
||||||
}
|
|
||||||
all := make([]commonFS, 0, len(r.Pending)+len(r.Completed))
|
|
||||||
for i := range r.Pending {
|
|
||||||
all = append(all, commonFS{&r.Pending[i], false})
|
|
||||||
}
|
|
||||||
for i := range r.Completed {
|
|
||||||
all = append(all, commonFS{&r.Completed[i], true})
|
|
||||||
}
|
|
||||||
|
|
||||||
// filter all
|
|
||||||
filtered := make([]commonFS, 0, len(all))
|
|
||||||
for _, fs := range all {
|
|
||||||
if fsfilter(fs.FSReport.Filesystem) {
|
|
||||||
filtered = append(filtered, fs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
all = filtered
|
|
||||||
|
|
||||||
switch state {
|
|
||||||
case pruner.Plan:
|
|
||||||
fallthrough
|
|
||||||
case pruner.PlanErr:
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(all) == 0 {
|
|
||||||
t.Printf("nothing to do\n")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var totalDestroyCount, completedDestroyCount int
|
|
||||||
var maxFSname int
|
|
||||||
for _, fs := range all {
|
|
||||||
totalDestroyCount += len(fs.DestroyList)
|
|
||||||
if fs.completed {
|
|
||||||
completedDestroyCount += len(fs.DestroyList)
|
|
||||||
}
|
|
||||||
if maxFSname < len(fs.Filesystem) {
|
|
||||||
maxFSname = len(fs.Filesystem)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// global progress bar
|
|
||||||
progress := int(math.Round(80 * float64(completedDestroyCount) / float64(totalDestroyCount)))
|
|
||||||
t.Write("Progress: ")
|
|
||||||
t.Write("[")
|
|
||||||
t.Write(stringbuilder.Times("=", progress))
|
|
||||||
t.Write(">")
|
|
||||||
t.Write(stringbuilder.Times("-", 80-progress))
|
|
||||||
t.Write("]")
|
|
||||||
t.Printf(" %d/%d snapshots", completedDestroyCount, totalDestroyCount)
|
|
||||||
t.Newline()
|
|
||||||
|
|
||||||
sort.SliceStable(all, func(i, j int) bool {
|
|
||||||
return strings.Compare(all[i].Filesystem, all[j].Filesystem) == -1
|
|
||||||
})
|
|
||||||
|
|
||||||
// Draw a table-like representation of 'all'
|
|
||||||
for _, fs := range all {
|
|
||||||
t.Write(stringbuilder.RightPad(fs.Filesystem, maxFSname, " "))
|
|
||||||
t.Write(" ")
|
|
||||||
if !fs.SkipReason.NotSkipped() {
|
|
||||||
t.Printf("skipped: %s\n", fs.SkipReason)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if fs.LastError != "" {
|
|
||||||
if strings.ContainsAny(fs.LastError, "\r\n") {
|
|
||||||
t.Printf("ERROR:")
|
|
||||||
t.PrintfDrawIndentedAndWrappedIfMultiline("%s\n", fs.LastError)
|
|
||||||
} else {
|
|
||||||
t.PrintfDrawIndentedAndWrappedIfMultiline("ERROR: %s\n", fs.LastError)
|
|
||||||
}
|
|
||||||
t.Newline()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
pruneRuleActionStr := fmt.Sprintf("(destroy %d of %d snapshots)",
|
|
||||||
len(fs.DestroyList), len(fs.SnapshotList))
|
|
||||||
|
|
||||||
if fs.completed {
|
|
||||||
t.Printf("Completed %s\n", pruneRuleActionStr)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Write("Pending ") // whitespace is padding 10
|
|
||||||
if len(fs.DestroyList) == 1 {
|
|
||||||
t.Write(fs.DestroyList[0].Name)
|
|
||||||
} else {
|
|
||||||
t.Write(pruneRuleActionStr)
|
|
||||||
}
|
|
||||||
t.Newline()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func renderSnapperReport(t *stringbuilder.B, r *snapper.Report, fsfilter FilterFunc) {
|
|
||||||
if r == nil {
|
|
||||||
t.Printf("<no snapshotting report available>\n")
|
|
||||||
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.Newline()
|
|
||||||
|
|
||||||
if r.Error != "" {
|
|
||||||
t.Printf("Error: %s\n", r.Error)
|
|
||||||
}
|
|
||||||
if !r.SleepUntil.IsZero() {
|
|
||||||
t.Printf("Sleep until: %s\n", r.SleepUntil)
|
|
||||||
}
|
|
||||||
|
|
||||||
renderSnapperPlanReportFilesystem(t, r.Progress, fsfilter)
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
return d.Round(100 * time.Millisecond).String()
|
|
||||||
}
|
|
||||||
|
|
||||||
type row struct {
|
|
||||||
path, state, duration, remainder, hookReport string
|
|
||||||
}
|
|
||||||
var widths struct {
|
|
||||||
path, state, duration int
|
|
||||||
}
|
|
||||||
rows := make([]*row, 0, len(fss))
|
|
||||||
for _, fs := range fss {
|
|
||||||
if !fsfilter(fs.Path) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
r := &row{
|
|
||||||
path: fs.Path,
|
|
||||||
state: fs.State.String(),
|
|
||||||
}
|
|
||||||
if fs.HooksHadError {
|
|
||||||
r.hookReport = fs.Hooks // FIXME render here, not in daemon
|
|
||||||
}
|
|
||||||
switch fs.State {
|
|
||||||
case snapper.SnapPending:
|
|
||||||
r.duration = "..."
|
|
||||||
r.remainder = ""
|
|
||||||
case snapper.SnapStarted:
|
|
||||||
r.duration = dur(time.Since(fs.StartAt))
|
|
||||||
r.remainder = fmt.Sprintf("snap name: %q", fs.SnapName)
|
|
||||||
case snapper.SnapDone:
|
|
||||||
fallthrough
|
|
||||||
case snapper.SnapError:
|
|
||||||
r.duration = dur(fs.DoneAt.Sub(fs.StartAt))
|
|
||||||
r.remainder = fmt.Sprintf("snap name: %q", fs.SnapName)
|
|
||||||
}
|
|
||||||
rows = append(rows, r)
|
|
||||||
if len(r.path) > widths.path {
|
|
||||||
widths.path = len(r.path)
|
|
||||||
}
|
|
||||||
if len(r.state) > widths.state {
|
|
||||||
widths.state = len(r.state)
|
|
||||||
}
|
|
||||||
if len(r.duration) > widths.duration {
|
|
||||||
widths.duration = len(r.duration)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, r := range rows {
|
|
||||||
path := stringbuilder.RightPad(r.path, widths.path, " ")
|
|
||||||
state := stringbuilder.RightPad(r.state, widths.state, " ")
|
|
||||||
duration := stringbuilder.RightPad(r.duration, widths.duration, " ")
|
|
||||||
t.Printf("%s %s %s", path, state, duration)
|
|
||||||
t.PrintfDrawIndentedAndWrappedIfMultiline(" %s", r.remainder)
|
|
||||||
if r.hookReport != "" {
|
|
||||||
t.AddIndent(1)
|
|
||||||
t.Newline()
|
|
||||||
t.Printf("%s", r.hookReport)
|
|
||||||
t.AddIndent(-1)
|
|
||||||
}
|
|
||||||
t.Newline()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
package stringbuilder
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/go-playground/validator/v10"
|
|
||||||
)
|
|
||||||
|
|
||||||
type B struct {
|
|
||||||
// const
|
|
||||||
indentMultiplier int
|
|
||||||
|
|
||||||
// mut
|
|
||||||
sb *strings.Builder
|
|
||||||
indent int
|
|
||||||
width int
|
|
||||||
x, y int
|
|
||||||
}
|
|
||||||
|
|
||||||
type Config struct {
|
|
||||||
IndentMultiplier int `validate:"gte=1"`
|
|
||||||
Width int `validate:"gte=1"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var validate = validator.New()
|
|
||||||
|
|
||||||
func New(config Config) *B {
|
|
||||||
|
|
||||||
if err := validate.Struct(config); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &B{sb: &strings.Builder{}, width: config.Width, indentMultiplier: config.IndentMultiplier}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *B) String() string { return b.sb.String() }
|
|
||||||
|
|
||||||
func (w *B) Newline() {
|
|
||||||
w.Write("\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *B) PrintfDrawIndentedAndWrappedIfMultiline(format string, args ...interface{}) {
|
|
||||||
whole := fmt.Sprintf(format, args...)
|
|
||||||
if strings.ContainsAny(whole, "\n\r") {
|
|
||||||
w.AddIndent(1)
|
|
||||||
defer w.AddIndent(-1)
|
|
||||||
}
|
|
||||||
w.Write(whole)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *B) Printf(format string, args ...interface{}) {
|
|
||||||
whole := fmt.Sprintf(format, args...)
|
|
||||||
w.Write(whole)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *B) AddIndent(delta int) {
|
|
||||||
t.indent += delta * t.indentMultiplier
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *B) AddIndentAndNewline(delta int) {
|
|
||||||
t.indent += delta * t.indentMultiplier
|
|
||||||
t.Write("\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *B) Write(s string) {
|
|
||||||
for _, c := range s {
|
|
||||||
if c == '\n' {
|
|
||||||
fmt.Fprint(w.sb, "\n")
|
|
||||||
w.x = 0
|
|
||||||
fmt.Fprint(w.sb, Times(" ", w.indent-w.x))
|
|
||||||
w.x = w.indent
|
|
||||||
w.y++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if w.x >= w.width {
|
|
||||||
fmt.Fprint(w.sb, "\n")
|
|
||||||
w.x = 0
|
|
||||||
fmt.Fprint(w.sb, Times(" ", w.indent-w.x))
|
|
||||||
w.x = w.indent
|
|
||||||
}
|
|
||||||
fmt.Fprintf(w.sb, "%c", c)
|
|
||||||
w.x++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Times(str string, n int) (out string) {
|
|
||||||
for i := 0; i < n; i++ {
|
|
||||||
out += str
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func RightPad(str string, length int, pad string) string {
|
|
||||||
if len(str) > length {
|
|
||||||
return str[:length]
|
|
||||||
}
|
|
||||||
return str + strings.Repeat(pad, length-len(str))
|
|
||||||
}
|
|
||||||
|
|
||||||
// changeCount = 0 indicates stall / no progress
|
|
||||||
func (w *B) DrawBar(length int, bytes, totalBytes uint64, changeCount int) {
|
|
||||||
const arrowPositions = `>\|/`
|
|
||||||
var completedLength int
|
|
||||||
if totalBytes > 0 {
|
|
||||||
completedLength = int(uint64(length) * bytes / totalBytes)
|
|
||||||
if completedLength > length {
|
|
||||||
completedLength = length
|
|
||||||
}
|
|
||||||
} else if totalBytes == bytes {
|
|
||||||
completedLength = length
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Write("[")
|
|
||||||
w.Write(Times("=", completedLength))
|
|
||||||
w.Write(string(arrowPositions[changeCount%len(arrowPositions)]))
|
|
||||||
w.Write(Times("-", length-completedLength))
|
|
||||||
w.Write("]")
|
|
||||||
}
|
|
||||||
+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 {
|
||||||
|
|||||||
@@ -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,18 +44,17 @@ 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
|
||||||
defer wg.Wait()
|
defer wg.Wait()
|
||||||
|
wg.Add(1)
|
||||||
|
|
||||||
// print results
|
// print results
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
enc := json.NewEncoder(os.Stdout)
|
enc := json.NewEncoder(os.Stdout)
|
||||||
@@ -64,7 +63,7 @@ func doZabsList(ctx context.Context, sc *cli.Subcommand, args []string) error {
|
|||||||
defer line.Lock().Unlock()
|
defer line.Lock().Unlock()
|
||||||
if zabsListFlags.Json {
|
if zabsListFlags.Json {
|
||||||
enc.SetIndent("", " ")
|
enc.SetIndent("", " ")
|
||||||
if err := enc.Encode(a); err != nil {
|
if err := enc.Encode(abstractions); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
|
|||||||
+40
-102
@@ -11,20 +11,11 @@ import (
|
|||||||
"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"`
|
||||||
@@ -63,17 +54,11 @@ 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"`
|
||||||
Debug JobDebugSettings `yaml:"debug,optional"`
|
Debug JobDebugSettings `yaml:"debug,optional"`
|
||||||
Replication *Replication `yaml:"replication,optional,fromdefaults"`
|
|
||||||
ConflictResolution *ConflictResolution `yaml:"conflict_resolution,optional,fromdefaults"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ConflictResolution struct {
|
|
||||||
InitialReplication string `yaml:"initial_replication,optional,default=most_recent"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type PassiveJob struct {
|
type PassiveJob struct {
|
||||||
@@ -93,16 +78,33 @@ type SnapJob struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type SendOptions struct {
|
type SendOptions struct {
|
||||||
Encrypted bool `yaml:"encrypted,optional,default=false"`
|
StepHolds SendOptionsStepHolds `yaml:"step_holds,optional"`
|
||||||
Raw bool `yaml:"raw,optional,default=false"`
|
|
||||||
SendProperties bool `yaml:"send_properties,optional,default=false"`
|
|
||||||
BackupProperties bool `yaml:"backup_properties,optional,default=false"`
|
|
||||||
LargeBlocks bool `yaml:"large_blocks,optional,default=false"`
|
|
||||||
Compressed bool `yaml:"compressed,optional,default=false"`
|
|
||||||
EmbeddedData bool `yaml:"embedded_data,optional,default=false"`
|
|
||||||
Saved bool `yaml:"saved,optional,default=false"`
|
|
||||||
|
|
||||||
BandwidthLimit *BandwidthLimit `yaml:"bandwidth_limit,optional,fromdefaults"`
|
Encrypted bool `yaml:"encrypted,optional"`
|
||||||
|
Raw bool `yaml:"raw,optional"`
|
||||||
|
SendProperties bool `yaml:"send_properties,optional"`
|
||||||
|
BackupProperties bool `yaml:"backup_properties,optional"`
|
||||||
|
LargeBlocks bool `yaml:"large_blocks,optional"`
|
||||||
|
Compressed bool `yaml:"compressed,optional"`
|
||||||
|
EmbeddedData bool `yaml:"embbeded_data,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SendOptionsStepHolds struct {
|
||||||
|
DisableIncremental bool `yaml:"disable_incremental,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ yaml.Defaulter = (*SendOptions)(nil)
|
||||||
|
|
||||||
|
func (l *SendOptions) SetDefault() {
|
||||||
|
*l = SendOptions{
|
||||||
|
Encrypted: false,
|
||||||
|
Raw: false,
|
||||||
|
SendProperties: false,
|
||||||
|
BackupProperties: false,
|
||||||
|
LargeBlocks: false,
|
||||||
|
Compressed: false,
|
||||||
|
EmbeddedData: false,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type RecvOptions struct {
|
type RecvOptions struct {
|
||||||
@@ -112,32 +114,12 @@ 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{}
|
var _ yaml.Defaulter = (*RecvOptions)(nil)
|
||||||
|
|
||||||
type BandwidthLimit struct {
|
func (l *RecvOptions) SetDefault() {
|
||||||
Max datasizeunit.Bits `yaml:"max,default=-1 B"`
|
*l = RecvOptions{Properties: &PropertyRecvOptions{}}
|
||||||
BucketCapacity datasizeunit.Bits `yaml:"bucket_capacity,default=128 KiB"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Replication struct {
|
|
||||||
Protection *ReplicationOptionsProtection `yaml:"protection,optional,fromdefaults"`
|
|
||||||
Concurrency *ReplicationOptionsConcurrency `yaml:"concurrency,optional,fromdefaults"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ReplicationOptionsProtection struct {
|
|
||||||
Initial string `yaml:"initial,optional,default=guarantee_resumability"`
|
|
||||||
Incremental string `yaml:"incremental,optional,default=guarantee_resumability"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ReplicationOptionsConcurrency struct {
|
|
||||||
Steps int `yaml:"steps,optional,default=1"`
|
|
||||||
SizeEstimates int `yaml:"size_estimates,optional,default=4"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type PropertyRecvOptions struct {
|
type PropertyRecvOptions struct {
|
||||||
@@ -145,8 +127,12 @@ type PropertyRecvOptions struct {
|
|||||||
Override map[zfsprop.Property]string `yaml:"override,optional"`
|
Override map[zfsprop.Property]string `yaml:"override,optional"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PlaceholderRecvOptions struct {
|
var _ yaml.Defaulter = (*PropertyRecvOptions)(nil)
|
||||||
Encryption string `yaml:"encryption,default=unspecified"`
|
|
||||||
|
func (l *PropertyRecvOptions) SetDefault() {
|
||||||
|
//*l = PropertyRecvOptions{}
|
||||||
|
//TODO: is below necessary?
|
||||||
|
*l = PropertyRecvOptions{Inherit: make([]zfsprop.Property, 0), Override: make(map[zfsprop.Property]string)}
|
||||||
}
|
}
|
||||||
|
|
||||||
type PushJob struct {
|
type PushJob struct {
|
||||||
@@ -156,9 +142,6 @@ type PushJob struct {
|
|||||||
Send *SendOptions `yaml:"send,fromdefaults,optional"`
|
Send *SendOptions `yaml:"send,fromdefaults,optional"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *PushJob) GetFilesystems() FilesystemsFilter { return j.Filesystems }
|
|
||||||
func (j *PushJob) GetSendOptions() *SendOptions { return j.Send }
|
|
||||||
|
|
||||||
type PullJob struct {
|
type PullJob struct {
|
||||||
ActiveJob `yaml:",inline"`
|
ActiveJob `yaml:",inline"`
|
||||||
RootFS string `yaml:"root_fs"`
|
RootFS string `yaml:"root_fs"`
|
||||||
@@ -166,10 +149,6 @@ type PullJob struct {
|
|||||||
Recv *RecvOptions `yaml:"recv,fromdefaults,optional"`
|
Recv *RecvOptions `yaml:"recv,fromdefaults,optional"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *PullJob) GetRootFS() string { return j.RootFS }
|
|
||||||
func (j *PullJob) GetAppendClientIdentity() bool { return false }
|
|
||||||
func (j *PullJob) GetRecvOptions() *RecvOptions { return j.Recv }
|
|
||||||
|
|
||||||
type PositiveDurationOrManual struct {
|
type PositiveDurationOrManual struct {
|
||||||
Interval time.Duration
|
Interval time.Duration
|
||||||
Manual bool
|
Manual bool
|
||||||
@@ -207,10 +186,6 @@ type SinkJob struct {
|
|||||||
Recv *RecvOptions `yaml:"recv,optional,fromdefaults"`
|
Recv *RecvOptions `yaml:"recv,optional,fromdefaults"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *SinkJob) GetRootFS() string { return j.RootFS }
|
|
||||||
func (j *SinkJob) GetAppendClientIdentity() bool { return true }
|
|
||||||
func (j *SinkJob) GetRecvOptions() *RecvOptions { return j.Recv }
|
|
||||||
|
|
||||||
type SourceJob struct {
|
type SourceJob struct {
|
||||||
PassiveJob `yaml:",inline"`
|
PassiveJob `yaml:",inline"`
|
||||||
Snapshotting SnapshottingEnum `yaml:"snapshotting"`
|
Snapshotting SnapshottingEnum `yaml:"snapshotting"`
|
||||||
@@ -218,9 +193,6 @@ type SourceJob struct {
|
|||||||
Send *SendOptions `yaml:"send,optional,fromdefaults"`
|
Send *SendOptions `yaml:"send,optional,fromdefaults"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *SourceJob) GetFilesystems() FilesystemsFilter { return j.Filesystems }
|
|
||||||
func (j *SourceJob) GetSendOptions() *SendOptions { return j.Send }
|
|
||||||
|
|
||||||
type FilesystemsFilter map[string]bool
|
type FilesystemsFilter map[string]bool
|
||||||
|
|
||||||
type SnapshottingEnum struct {
|
type SnapshottingEnum struct {
|
||||||
@@ -234,38 +206,6 @@ type SnapshottingPeriodic struct {
|
|||||||
Hooks HookList `yaml:"hooks,optional"`
|
Hooks HookList `yaml:"hooks,optional"`
|
||||||
}
|
}
|
||||||
|
|
||||||
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"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SnapshottingManual struct {
|
type SnapshottingManual struct {
|
||||||
Type string `yaml:"type"`
|
Type string `yaml:"type"`
|
||||||
}
|
}
|
||||||
@@ -408,7 +348,6 @@ type PruneKeepNotReplicated struct {
|
|||||||
type PruneKeepLastN struct {
|
type PruneKeepLastN struct {
|
||||||
Type string `yaml:"type"`
|
Type string `yaml:"type"`
|
||||||
Count int `yaml:"count"`
|
Count int `yaml:"count"`
|
||||||
Regex string `yaml:"regex,optional"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type PruneKeepRegex struct { // FIXME rename to KeepRegex
|
type PruneKeepRegex struct { // FIXME rename to KeepRegex
|
||||||
@@ -589,7 +528,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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,90 +0,0 @@
|
|||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"github.com/zrepl/yaml-config"
|
|
||||||
)
|
|
||||||
|
|
||||||
type A struct {
|
|
||||||
B *B `yaml:"b,optional,fromdefaults"`
|
|
||||||
A1 string `yaml:"a1,optional"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type B struct {
|
|
||||||
C *C `yaml:"c,optional,fromdefaults"`
|
|
||||||
D string `yaml:"d,default=ddd"`
|
|
||||||
E string `yaml:"e,optional"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type C struct {
|
|
||||||
Q string `yaml:"q,optional"`
|
|
||||||
R string `yaml:"r,default=r"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDepFromDefaults(t *testing.T) {
|
|
||||||
|
|
||||||
type testcase struct {
|
|
||||||
name string
|
|
||||||
yaml string
|
|
||||||
expect *A
|
|
||||||
}
|
|
||||||
|
|
||||||
tcs := []testcase{
|
|
||||||
{
|
|
||||||
name: "empty",
|
|
||||||
yaml: `{}`,
|
|
||||||
expect: &A{
|
|
||||||
B: &B{
|
|
||||||
C: &C{
|
|
||||||
R: "r",
|
|
||||||
},
|
|
||||||
D: "ddd",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "a1 set",
|
|
||||||
yaml: `{"a1":"blah"}`,
|
|
||||||
expect: &A{
|
|
||||||
A1: "blah",
|
|
||||||
B: &B{
|
|
||||||
C: &C{
|
|
||||||
R: "r",
|
|
||||||
},
|
|
||||||
D: "ddd",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "D set",
|
|
||||||
yaml: `
|
|
||||||
b:
|
|
||||||
d: 4d
|
|
||||||
`,
|
|
||||||
expect: &A{
|
|
||||||
B: &B{
|
|
||||||
D: "4d",
|
|
||||||
C: &C{
|
|
||||||
R: "r",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for tci := range tcs {
|
|
||||||
t.Run(fmt.Sprintf("%d-%s", tci, tcs[tci].name), func(t *testing.T) {
|
|
||||||
tc := tcs[tci]
|
|
||||||
|
|
||||||
var a A
|
|
||||||
err := yaml.UnmarshalStrict([]byte(tc.yaml), &a)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
require.Equal(t, tc.expect, &a)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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) {
|
||||||
|
|||||||
+15
-17
@@ -5,9 +5,6 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
|
|
||||||
zfsprop "github.com/zrepl/zrepl/zfs/property"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestRecvOptions(t *testing.T) {
|
func TestRecvOptions(t *testing.T) {
|
||||||
@@ -80,54 +77,55 @@ jobs:
|
|||||||
`
|
`
|
||||||
|
|
||||||
fill := func(s string) string { return fmt.Sprintf(tmpl, s) }
|
fill := func(s string) string { return fmt.Sprintf(tmpl, s) }
|
||||||
|
var c *Config
|
||||||
|
|
||||||
t.Run("recv_inherit_empty", func(t *testing.T) {
|
t.Run("recv_inherit_empty", func(t *testing.T) {
|
||||||
c := testValidConfig(t, fill(recv_inherit_empty))
|
c, err := testConfig(t, fill(recv_inherit_empty))
|
||||||
|
assert.NoError(t, err)
|
||||||
assert.NotNil(t, c)
|
assert.NotNil(t, c)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("recv_inherit", func(t *testing.T) {
|
t.Run("recv_inherit", func(t *testing.T) {
|
||||||
c := testValidConfig(t, fill(recv_inherit))
|
c = testValidConfig(t, fill(recv_inherit))
|
||||||
inherit := c.Jobs[0].Ret.(*PullJob).Recv.Properties.Inherit
|
inherit := c.Jobs[0].Ret.(*PullJob).Recv.Properties.Inherit
|
||||||
assert.NotEmpty(t, inherit)
|
assert.NotEmpty(t, inherit)
|
||||||
assert.Contains(t, inherit, zfsprop.Property("testprop"))
|
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("recv_override_empty", func(t *testing.T) {
|
t.Run("recv_override_empty", func(t *testing.T) {
|
||||||
c := testValidConfig(t, fill(recv_override_empty))
|
c, err := testConfig(t, fill(recv_override_empty))
|
||||||
|
assert.NoError(t, err)
|
||||||
assert.NotNil(t, c)
|
assert.NotNil(t, c)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("recv_override", func(t *testing.T) {
|
t.Run("recv_override", func(t *testing.T) {
|
||||||
c := testValidConfig(t, fill(recv_override))
|
c = testValidConfig(t, fill(recv_override))
|
||||||
override := c.Jobs[0].Ret.(*PullJob).Recv.Properties.Override
|
override := c.Jobs[0].Ret.(*PullJob).Recv.Properties.Override
|
||||||
require.Len(t, override, 1)
|
assert.NotEmpty(t, override)
|
||||||
require.Equal(t, "test123", override["testprop2"])
|
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("recv_override_and_inherit", func(t *testing.T) {
|
t.Run("recv_override_and_inherit", func(t *testing.T) {
|
||||||
c := testValidConfig(t, fill(recv_override_and_inherit))
|
c = testValidConfig(t, fill(recv_override_and_inherit))
|
||||||
inherit := c.Jobs[0].Ret.(*PullJob).Recv.Properties.Inherit
|
inherit := c.Jobs[0].Ret.(*PullJob).Recv.Properties.Inherit
|
||||||
override := c.Jobs[0].Ret.(*PullJob).Recv.Properties.Override
|
override := c.Jobs[0].Ret.(*PullJob).Recv.Properties.Override
|
||||||
assert.NotEmpty(t, inherit)
|
assert.NotEmpty(t, inherit)
|
||||||
assert.Contains(t, inherit, zfsprop.Property("testprop"))
|
|
||||||
assert.NotEmpty(t, override)
|
assert.NotEmpty(t, override)
|
||||||
assert.Equal(t, "test123", override["testprop2"])
|
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("recv_properties_empty", func(t *testing.T) {
|
t.Run("recv_properties_empty", func(t *testing.T) {
|
||||||
c := testValidConfig(t, fill(recv_properties_empty))
|
c, err := testConfig(t, fill(recv_properties_empty))
|
||||||
|
assert.NoError(t, err)
|
||||||
assert.NotNil(t, c)
|
assert.NotNil(t, c)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("recv_empty", func(t *testing.T) {
|
t.Run("recv_empty", func(t *testing.T) {
|
||||||
c := testValidConfig(t, fill(recv_empty))
|
c, err := testConfig(t, fill(recv_empty))
|
||||||
|
assert.NoError(t, err)
|
||||||
assert.NotNil(t, c)
|
assert.NotNil(t, c)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("send_not_specified", func(t *testing.T) {
|
t.Run("send_not_specified", func(t *testing.T) {
|
||||||
c := testValidConfig(t, fill(recv_not_specified))
|
c, err := testConfig(t, fill(recv_not_specified))
|
||||||
|
assert.NoError(t, err)
|
||||||
assert.NotNil(t, c)
|
assert.NotNil(t, c)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -80,14 +80,15 @@ jobs:
|
|||||||
`
|
`
|
||||||
|
|
||||||
fill := func(s string) string { return fmt.Sprintf(tmpl, s) }
|
fill := func(s string) string { return fmt.Sprintf(tmpl, s) }
|
||||||
|
var c *Config
|
||||||
|
|
||||||
t.Run("encrypted_false", func(t *testing.T) {
|
t.Run("encrypted_false", func(t *testing.T) {
|
||||||
c := testValidConfig(t, fill(encrypted_false))
|
c = testValidConfig(t, fill(encrypted_false))
|
||||||
encrypted := c.Jobs[0].Ret.(*PushJob).Send.Encrypted
|
encrypted := c.Jobs[0].Ret.(*PushJob).Send.Encrypted
|
||||||
assert.Equal(t, false, encrypted)
|
assert.Equal(t, false, encrypted)
|
||||||
})
|
})
|
||||||
t.Run("encrypted_true", func(t *testing.T) {
|
t.Run("encrypted_true", func(t *testing.T) {
|
||||||
c := testValidConfig(t, fill(encrypted_true))
|
c = testValidConfig(t, fill(encrypted_true))
|
||||||
encrypted := c.Jobs[0].Ret.(*PushJob).Send.Encrypted
|
encrypted := c.Jobs[0].Ret.(*PushJob).Send.Encrypted
|
||||||
assert.Equal(t, true, encrypted)
|
assert.Equal(t, true, encrypted)
|
||||||
})
|
})
|
||||||
@@ -139,7 +140,8 @@ jobs:
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("send_not_specified", func(t *testing.T) {
|
t.Run("send_not_specified", func(t *testing.T) {
|
||||||
c := testValidConfig(t, fill(send_not_specified))
|
c, err := testConfig(t, fill(send_not_specified))
|
||||||
|
assert.NoError(t, err)
|
||||||
assert.NotNil(t, c)
|
assert.NotNil(t, c)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
+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)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ func (t *RetentionIntervalList) UnmarshalYAML(u func(interface{}, bool) error) (
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
intervals, err := ParseRetentionIntervalSpec(in)
|
intervals, err := parseRetentionGridIntervalsString(in)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -102,7 +102,7 @@ func parseRetentionGridIntervalString(e string) (intervals []RetentionInterval,
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func ParseRetentionIntervalSpec(s string) (intervals []RetentionInterval, err error) {
|
func parseRetentionGridIntervalsString(s string) (intervals []RetentionInterval, err error) {
|
||||||
|
|
||||||
ges := strings.Split(s, "|")
|
ges := strings.Split(s, "|")
|
||||||
intervals = make([]RetentionInterval, 0, 7*len(ges))
|
intervals = make([]RetentionInterval, 0, 7*len(ges))
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -52,15 +52,12 @@ jobs:
|
|||||||
}
|
}
|
||||||
send:
|
send:
|
||||||
encrypted: true
|
encrypted: true
|
||||||
replication:
|
# disable incremental step holds so that
|
||||||
protection:
|
# - we can yank out the backup drive during replication
|
||||||
initial: guarantee_resumability
|
# - thereby sacrificing resumability
|
||||||
# Downgrade protection to guarantee_incremental which uses zfs bookmarks instead of zfs holds.
|
# - in exchange for the replicating snapshot not sticking around until we reconnect the backup drive
|
||||||
# Thus, when we yank out the backup drive during replication
|
step_holds:
|
||||||
# - we might not be able to resume the interrupted replication step because the partially received `to` snapshot of a `from`->`to` step may be pruned any time
|
disable_incremental: true
|
||||||
# - but in exchange we get back the disk space allocated by `to` when we prune it
|
|
||||||
# - and because we still have the bookmarks created by `guarantee_incremental`, we can still do incremental replication of `from`->`to2` in the future
|
|
||||||
incremental: guarantee_incremental
|
|
||||||
snapshotting:
|
snapshotting:
|
||||||
type: manual
|
type: manual
|
||||||
pruning:
|
pruning:
|
||||||
|
|||||||
@@ -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
|
|
||||||
+4
-6
@@ -8,7 +8,6 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
@@ -125,9 +124,8 @@ 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
|
||||||
}})
|
}})
|
||||||
@@ -158,8 +156,8 @@ func (j *controlJob) Run(ctx context.Context) {
|
|||||||
server := http.Server{
|
server := http.Server{
|
||||||
Handler: mux,
|
Handler: mux,
|
||||||
// control socket is local, 1s timeout should be more than sufficient, even on a loaded system
|
// control socket is local, 1s timeout should be more than sufficient, even on a loaded system
|
||||||
WriteTimeout: 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:
|
||||||
|
|||||||
+3
-12
@@ -3,7 +3,6 @@ package daemon
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/rand"
|
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -13,7 +12,6 @@ import (
|
|||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"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/endpoint"
|
"github.com/zrepl/zrepl/endpoint"
|
||||||
"github.com/zrepl/zrepl/util/envconst"
|
"github.com/zrepl/zrepl/util/envconst"
|
||||||
@@ -39,19 +37,13 @@ func Run(ctx context.Context, conf *config.Config) error {
|
|||||||
cancel()
|
cancel()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// The math/rand package is used presently for generating trace IDs, we
|
|
||||||
// seed it with the current time and pid so that the IDs are mostly
|
|
||||||
// unique.
|
|
||||||
rand.Seed(time.Now().UnixNano())
|
|
||||||
rand.Seed(int64(os.Getpid()))
|
|
||||||
|
|
||||||
outlets, err := logging.OutletsFromConfig(*conf.Global.Logging)
|
outlets, err := logging.OutletsFromConfig(*conf.Global.Logging)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "cannot build logging from config")
|
return errors.Wrap(err, "cannot build logging from config")
|
||||||
}
|
}
|
||||||
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")
|
||||||
}
|
}
|
||||||
@@ -160,9 +152,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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import (
|
|||||||
"text/template"
|
"text/template"
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/daemon/logging/trace"
|
"github.com/zrepl/zrepl/daemon/logging/trace"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/config"
|
"github.com/zrepl/zrepl/config"
|
||||||
@@ -161,7 +160,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 +184,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 +233,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 +266,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 +294,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{
|
||||||
|
|||||||
+42
-88
@@ -9,11 +9,10 @@ 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/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/config"
|
"github.com/zrepl/zrepl/config"
|
||||||
|
"github.com/zrepl/zrepl/daemon/filters"
|
||||||
"github.com/zrepl/zrepl/daemon/job/reset"
|
"github.com/zrepl/zrepl/daemon/job/reset"
|
||||||
"github.com/zrepl/zrepl/daemon/job/wakeup"
|
"github.com/zrepl/zrepl/daemon/job/wakeup"
|
||||||
"github.com/zrepl/zrepl/daemon/pruner"
|
"github.com/zrepl/zrepl/daemon/pruner"
|
||||||
@@ -34,15 +33,11 @@ type ActiveSide struct {
|
|||||||
name endpoint.JobID
|
name endpoint.JobID
|
||||||
connecter transport.Connecter
|
connecter transport.Connecter
|
||||||
|
|
||||||
replicationDriverConfig driver.Config
|
|
||||||
|
|
||||||
prunerFactory *pruner.PrunerFactory
|
prunerFactory *pruner.PrunerFactory
|
||||||
|
|
||||||
promRepStateSecs *prometheus.HistogramVec // labels: state
|
promRepStateSecs *prometheus.HistogramVec // labels: state
|
||||||
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
|
|
||||||
promLastSuccessful prometheus.Gauge
|
|
||||||
|
|
||||||
tasksMtx sync.Mutex
|
tasksMtx sync.Mutex
|
||||||
tasks activeSideTasks
|
tasks activeSideTasks
|
||||||
@@ -101,7 +96,7 @@ type modePush struct {
|
|||||||
receiver *rpc.Client
|
receiver *rpc.Client
|
||||||
senderConfig *endpoint.SenderConfig
|
senderConfig *endpoint.SenderConfig
|
||||||
plannerPolicy *logic.PlannerPolicy
|
plannerPolicy *logic.PlannerPolicy
|
||||||
snapper snapper.Snapper
|
snapper *snapper.PeriodicOrManual
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *modePush) ConnectEndpoints(ctx context.Context, connecter transport.Connecter) {
|
func (m *modePush) ConnectEndpoints(ctx context.Context, connecter transport.Connecter) {
|
||||||
@@ -137,8 +132,7 @@ func (m *modePush) RunPeriodic(ctx context.Context, wakeUpCommon chan<- struct{}
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *modePush) SnapperReport() *snapper.Report {
|
func (m *modePush) SnapperReport() *snapper.Report {
|
||||||
r := m.snapper.Report()
|
return m.snapper.Report()
|
||||||
return &r
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *modePush) ResetConnectBackoff() {
|
func (m *modePush) ResetConnectBackoff() {
|
||||||
@@ -151,33 +145,30 @@ func (m *modePush) ResetConnectBackoff() {
|
|||||||
|
|
||||||
func modePushFromConfig(g *config.Global, in *config.PushJob, jobID endpoint.JobID) (*modePush, error) {
|
func modePushFromConfig(g *config.Global, in *config.PushJob, jobID endpoint.JobID) (*modePush, error) {
|
||||||
m := &modePush{}
|
m := &modePush{}
|
||||||
var err error
|
|
||||||
|
|
||||||
m.senderConfig, err = buildSenderConfig(in, jobID)
|
fsf, err := filters.DatasetMapFilterFromConfig(in.Filesystems)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "sender config")
|
return nil, errors.Wrap(err, "cannot build filesystem filter")
|
||||||
}
|
}
|
||||||
|
|
||||||
replicationConfig, err := logic.ReplicationConfigFromConfig(in.Replication)
|
m.senderConfig = &endpoint.SenderConfig{
|
||||||
if err != nil {
|
FSF: fsf,
|
||||||
return nil, errors.Wrap(err, "field `replication`")
|
JobID: jobID,
|
||||||
}
|
DisableIncrementalStepHolds: in.Send.StepHolds.DisableIncremental,
|
||||||
|
|
||||||
conflictResolution, err := logic.ConflictResolutionFromConfig(in.ConflictResolution)
|
Encrypt: &zfs.NilBool{B: in.Send.Encrypted},
|
||||||
if err != nil {
|
SendRaw: in.Send.Raw,
|
||||||
return nil, errors.Wrap(err, "field `conflict_resolution`")
|
SendProperties: in.Send.SendProperties,
|
||||||
|
SendBackupProperties: in.Send.BackupProperties,
|
||||||
|
SendLargeBlocks: in.Send.LargeBlocks,
|
||||||
|
SendCompressed: in.Send.Compressed,
|
||||||
|
SendEmbeddedData: in.Send.EmbeddedData,
|
||||||
}
|
}
|
||||||
|
|
||||||
m.plannerPolicy = &logic.PlannerPolicy{
|
m.plannerPolicy = &logic.PlannerPolicy{
|
||||||
ConflictResolution: conflictResolution,
|
EncryptedSend: logic.TriFromBool(in.Send.Encrypted),
|
||||||
ReplicationConfig: replicationConfig,
|
|
||||||
SizeEstimationConcurrency: in.Replication.Concurrency.SizeEstimates,
|
|
||||||
}
|
|
||||||
if err := m.plannerPolicy.Validate(); err != nil {
|
|
||||||
return nil, errors.Wrap(err, "cannot build planner policy")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if m.snapper, err = snapper.FromConfig(g, m.senderConfig.FSF, in.Snapshotting); err != nil {
|
if m.snapper, err = snapper.FromConfig(g, fsf, in.Snapshotting); err != nil {
|
||||||
return nil, errors.Wrap(err, "cannot build snapper")
|
return nil, errors.Wrap(err, "cannot build snapper")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,6 +180,7 @@ type modePull struct {
|
|||||||
receiver *endpoint.Receiver
|
receiver *endpoint.Receiver
|
||||||
receiverConfig endpoint.ReceiverConfig
|
receiverConfig endpoint.ReceiverConfig
|
||||||
sender *rpc.Client
|
sender *rpc.Client
|
||||||
|
rootFS *zfs.DatasetPath
|
||||||
plannerPolicy *logic.PlannerPolicy
|
plannerPolicy *logic.PlannerPolicy
|
||||||
interval config.PositiveDurationOrManual
|
interval config.PositiveDurationOrManual
|
||||||
}
|
}
|
||||||
@@ -262,44 +254,35 @@ func modePullFromConfig(g *config.Global, in *config.PullJob, jobID endpoint.Job
|
|||||||
m = &modePull{}
|
m = &modePull{}
|
||||||
m.interval = in.Interval
|
m.interval = in.Interval
|
||||||
|
|
||||||
replicationConfig, err := logic.ReplicationConfigFromConfig(in.Replication)
|
m.rootFS, err = zfs.NewDatasetPath(in.RootFS)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "field `replication`")
|
return nil, errors.New("RootFS is not a valid zfs filesystem path")
|
||||||
}
|
}
|
||||||
|
if m.rootFS.Length() <= 0 {
|
||||||
conflictResolution, err := logic.ConflictResolutionFromConfig(in.ConflictResolution)
|
return nil, errors.New("RootFS must not be empty") // duplicates error check of receiver
|
||||||
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,
|
|
||||||
SizeEstimationConcurrency: in.Replication.Concurrency.SizeEstimates,
|
|
||||||
}
|
|
||||||
if err := m.plannerPolicy.Validate(); err != nil {
|
|
||||||
return nil, errors.Wrap(err, "cannot build planner policy")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
m.receiverConfig, err = buildReceiverConfig(in, jobID)
|
m.receiverConfig = endpoint.ReceiverConfig{
|
||||||
if err != nil {
|
JobID: jobID,
|
||||||
return nil, err
|
RootWithoutClientComponent: m.rootFS,
|
||||||
|
AppendClientIdentity: false, // !
|
||||||
|
UpdateLastReceivedHold: true,
|
||||||
|
|
||||||
|
InheritProperties: in.Recv.Properties.Inherit,
|
||||||
|
OverrideProperties: in.Recv.Properties.Override,
|
||||||
|
}
|
||||||
|
if err := m.receiverConfig.Validate(); err != nil {
|
||||||
|
return nil, errors.Wrap(err, "cannot build receiver config")
|
||||||
}
|
}
|
||||||
|
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func replicationDriverConfigFromConfig(in *config.Replication) (c driver.Config, err error) {
|
func activeSide(g *config.Global, in *config.ActiveJob, configJob interface{}) (j *ActiveSide, err error) {
|
||||||
c = driver.Config{
|
|
||||||
StepQueueConcurrency: in.Concurrency.Steps,
|
|
||||||
MaxAttempts: envconst.Int("ZREPL_REPLICATION_MAX_ATTEMPTS", 3),
|
|
||||||
ReconnectHardFailTimeout: envconst.Duration("ZREPL_REPLICATION_RECONNECT_HARD_FAIL_TIMEOUT", 10*time.Minute),
|
|
||||||
}
|
|
||||||
err = c.Validate()
|
|
||||||
return c, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func activeSide(g *config.Global, in *config.ActiveJob, configJob interface{}, parseFlags config.ParseFlags) (j *ActiveSide, err error) {
|
|
||||||
|
|
||||||
j = &ActiveSide{}
|
j = &ActiveSide{}
|
||||||
j.name, err = endpoint.MakeJobID(in.Name)
|
j.name, err = endpoint.MakeJobID(in.Name)
|
||||||
@@ -333,22 +316,8 @@ 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{
|
|
||||||
Namespace: "zrepl",
|
|
||||||
Subsystem: "replication",
|
|
||||||
Name: "filesystem_errors",
|
|
||||||
Help: "number of filesystems that failed replication in the latest replication attempt, or -1 if the job failed before enumerating the filesystems",
|
|
||||||
ConstLabels: prometheus.Labels{"zrepl_job": j.name.String()},
|
|
||||||
})
|
|
||||||
j.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")
|
||||||
}
|
}
|
||||||
@@ -365,11 +334,6 @@ func activeSide(g *config.Global, in *config.ActiveJob, configJob interface{}, p
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
j.replicationDriverConfig, err = replicationDriverConfigFromConfig(in.Replication)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "cannot build replication driver config")
|
|
||||||
}
|
|
||||||
|
|
||||||
return j, nil
|
return j, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -377,8 +341,6 @@ func (j *ActiveSide) RegisterMetrics(registerer prometheus.Registerer) {
|
|||||||
registerer.MustRegister(j.promRepStateSecs)
|
registerer.MustRegister(j.promRepStateSecs)
|
||||||
registerer.MustRegister(j.promPruneSecs)
|
registerer.MustRegister(j.promPruneSecs)
|
||||||
registerer.MustRegister(j.promBytesReplicated)
|
registerer.MustRegister(j.promBytesReplicated)
|
||||||
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() }
|
||||||
@@ -413,7 +375,7 @@ func (j *ActiveSide) OwnedDatasetSubtreeRoot() (rfs *zfs.DatasetPath, ok bool) {
|
|||||||
_ = j.mode.(*modePush) // make sure we didn't introduce a new job type
|
_ = j.mode.(*modePush) // make sure we didn't introduce a new job type
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
return pull.receiverConfig.RootWithoutClientComponent.Copy(), true
|
return pull.rootFS.Copy(), true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *ActiveSide) SenderConfig() *endpoint.SenderConfig {
|
func (j *ActiveSide) SenderConfig() *endpoint.SenderConfig {
|
||||||
@@ -504,21 +466,13 @@ func (j *ActiveSide) do(ctx context.Context) {
|
|||||||
*tasks = activeSideTasks{}
|
*tasks = activeSideTasks{}
|
||||||
tasks.replicationCancel = func() { repCancel(); endSpan() }
|
tasks.replicationCancel = func() { repCancel(); endSpan() }
|
||||||
tasks.replicationReport, repWait = replication.Do(
|
tasks.replicationReport, repWait = replication.Do(
|
||||||
ctx, j.replicationDriverConfig, logic.NewPlanner(j.promRepStateSecs, j.promBytesReplicated, sender, receiver, j.mode.PlannerPolicy()),
|
ctx, logic.NewPlanner(j.promRepStateSecs, j.promBytesReplicated, sender, receiver, j.mode.PlannerPolicy()),
|
||||||
)
|
)
|
||||||
tasks.state = ActiveSideReplicating
|
tasks.state = ActiveSideReplicating
|
||||||
})
|
})
|
||||||
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()
|
|
||||||
var numErrors = replicationReport.GetFailedFilesystemsCountInLatestAttempt()
|
|
||||||
j.promReplicationErrors.Set(float64(numErrors))
|
|
||||||
if numErrors == 0 {
|
|
||||||
j.promLastSuccessful.SetToCurrentTime()
|
|
||||||
}
|
|
||||||
|
|
||||||
endSpan()
|
endSpan()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/endpoint"
|
"github.com/zrepl/zrepl/endpoint"
|
||||||
"github.com/zrepl/zrepl/transport"
|
"github.com/zrepl/zrepl/transport"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,102 +0,0 @@
|
|||||||
package job
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/config"
|
|
||||||
"github.com/zrepl/zrepl/daemon/filters"
|
|
||||||
"github.com/zrepl/zrepl/endpoint"
|
|
||||||
"github.com/zrepl/zrepl/util/nodefault"
|
|
||||||
"github.com/zrepl/zrepl/zfs"
|
|
||||||
)
|
|
||||||
|
|
||||||
type SendingJobConfig interface {
|
|
||||||
GetFilesystems() config.FilesystemsFilter
|
|
||||||
GetSendOptions() *config.SendOptions // must not be nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildSenderConfig(in SendingJobConfig, jobID endpoint.JobID) (*endpoint.SenderConfig, error) {
|
|
||||||
|
|
||||||
fsf, err := filters.DatasetMapFilterFromConfig(in.GetFilesystems())
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "cannot build filesystem filter")
|
|
||||||
}
|
|
||||||
sendOpts := in.GetSendOptions()
|
|
||||||
bwlim, err := buildBandwidthLimitConfig(sendOpts.BandwidthLimit)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "cannot build bandwith limit config")
|
|
||||||
}
|
|
||||||
|
|
||||||
sc := &endpoint.SenderConfig{
|
|
||||||
FSF: fsf,
|
|
||||||
JobID: jobID,
|
|
||||||
|
|
||||||
Encrypt: &nodefault.Bool{B: sendOpts.Encrypted},
|
|
||||||
SendRaw: sendOpts.Raw,
|
|
||||||
SendProperties: sendOpts.SendProperties,
|
|
||||||
SendBackupProperties: sendOpts.BackupProperties,
|
|
||||||
SendLargeBlocks: sendOpts.LargeBlocks,
|
|
||||||
SendCompressed: sendOpts.Compressed,
|
|
||||||
SendEmbeddedData: sendOpts.EmbeddedData,
|
|
||||||
SendSaved: sendOpts.Saved,
|
|
||||||
|
|
||||||
BandwidthLimit: bwlim,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := sc.Validate(); err != nil {
|
|
||||||
return nil, errors.Wrap(err, "cannot build sender config")
|
|
||||||
}
|
|
||||||
|
|
||||||
return sc, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type ReceivingJobConfig interface {
|
|
||||||
GetRootFS() string
|
|
||||||
GetAppendClientIdentity() bool
|
|
||||||
GetRecvOptions() *config.RecvOptions
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildReceiverConfig(in ReceivingJobConfig, jobID endpoint.JobID) (rc endpoint.ReceiverConfig, err error) {
|
|
||||||
rootFs, err := zfs.NewDatasetPath(in.GetRootFS())
|
|
||||||
if err != nil {
|
|
||||||
return rc, errors.New("root_fs is not a valid zfs filesystem path")
|
|
||||||
}
|
|
||||||
if rootFs.Length() <= 0 {
|
|
||||||
return rc, errors.New("root_fs must not be empty") // duplicates error check of receiver
|
|
||||||
}
|
|
||||||
|
|
||||||
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{
|
|
||||||
JobID: jobID,
|
|
||||||
RootWithoutClientComponent: rootFs,
|
|
||||||
AppendClientIdentity: in.GetAppendClientIdentity(),
|
|
||||||
|
|
||||||
InheritProperties: recvOpts.Properties.Inherit,
|
|
||||||
OverrideProperties: recvOpts.Properties.Override,
|
|
||||||
|
|
||||||
BandwidthLimit: bwlim,
|
|
||||||
|
|
||||||
PlaceholderEncryption: placeholderEncryption,
|
|
||||||
}
|
|
||||||
if err := rc.Validate(); err != nil {
|
|
||||||
return rc, errors.Wrap(err, "cannot build receiver config")
|
|
||||||
}
|
|
||||||
|
|
||||||
return rc, nil
|
|
||||||
}
|
|
||||||
@@ -2,11 +2,8 @@ package job
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"path"
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/kr/pretty"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
@@ -95,7 +92,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)
|
||||||
@@ -111,208 +108,3 @@ jobs:
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSampleConfigsAreBuiltWithoutErrors(t *testing.T) {
|
|
||||||
paths, err := filepath.Glob("../../config/samples/*")
|
|
||||||
if err != nil {
|
|
||||||
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 {
|
|
||||||
|
|
||||||
if path.Ext(p) != ".yml" {
|
|
||||||
t.Logf("skipping file %s", p)
|
|
||||||
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) {
|
|
||||||
c, err := config.ParseConfig(p)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("error parsing %s:\n%+v", p, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Logf("file: %s", p)
|
|
||||||
t.Log(pretty.Sprint(c))
|
|
||||||
|
|
||||||
jobs, err := JobsFromConfig(c, config.ParseFlagsNoCertCheck)
|
|
||||||
t.Logf("jobs: %#v", jobs)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
if additionalCheck != nil {
|
|
||||||
additionalCheck.test(t, jobs)
|
|
||||||
additionalCheck.state = 2
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
for basename, c := range additionalChecks {
|
|
||||||
if c.state == 0 {
|
|
||||||
panic("univisited additional check " + basename)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func testSampleConfig_BandwidthLimit(t *testing.T, jobs []Job) {
|
|
||||||
require.Len(t, jobs, 3)
|
|
||||||
|
|
||||||
{
|
|
||||||
limitedSink, ok := jobs[0].(*PassiveSide)
|
|
||||||
require.True(t, ok, "%T", jobs[0])
|
|
||||||
limitedSinkMode, ok := limitedSink.mode.(*modeSink)
|
|
||||||
require.True(t, ok, "%T", limitedSink)
|
|
||||||
|
|
||||||
assert.Equal(t, int64(12345), limitedSinkMode.receiverConfig.BandwidthLimit.Max)
|
|
||||||
assert.Equal(t, int64(1<<17), limitedSinkMode.receiverConfig.BandwidthLimit.BucketCapacity)
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
limitedPush, ok := jobs[1].(*ActiveSide)
|
|
||||||
require.True(t, ok, "%T", jobs[1])
|
|
||||||
limitedPushMode, ok := limitedPush.mode.(*modePush)
|
|
||||||
require.True(t, ok, "%T", limitedPush)
|
|
||||||
|
|
||||||
assert.Equal(t, int64(54321), limitedPushMode.senderConfig.BandwidthLimit.Max)
|
|
||||||
assert.Equal(t, int64(1024), limitedPushMode.senderConfig.BandwidthLimit.BucketCapacity)
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
unlimitedSink, ok := jobs[2].(*PassiveSide)
|
|
||||||
require.True(t, ok, "%T", jobs[2])
|
|
||||||
unlimitedSinkMode, ok := unlimitedSink.mode.(*modeSink)
|
|
||||||
require.True(t, ok, "%T", unlimitedSink)
|
|
||||||
|
|
||||||
max := unlimitedSinkMode.receiverConfig.BandwidthLimit.Max
|
|
||||||
assert.Less(t, max, int64(0), max, "unlimited mode <=> negative value for .Max, see bandwidthlimit.Config")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestReplicationOptions(t *testing.T) {
|
|
||||||
tmpl := `
|
|
||||||
jobs:
|
|
||||||
- name: foo
|
|
||||||
type: push
|
|
||||||
connect:
|
|
||||||
type: local
|
|
||||||
listener_name: foo
|
|
||||||
client_identity: bar
|
|
||||||
filesystems: {"<": true}
|
|
||||||
%s
|
|
||||||
snapshotting:
|
|
||||||
type: manual
|
|
||||||
pruning:
|
|
||||||
keep_sender:
|
|
||||||
- type: last_n
|
|
||||||
count: 10
|
|
||||||
keep_receiver:
|
|
||||||
- type: last_n
|
|
||||||
count: 10
|
|
||||||
`
|
|
||||||
|
|
||||||
type Test struct {
|
|
||||||
name string
|
|
||||||
input string
|
|
||||||
expectOk func(t *testing.T, a *ActiveSide, m *modePush)
|
|
||||||
expectError bool
|
|
||||||
}
|
|
||||||
|
|
||||||
tests := []Test{
|
|
||||||
{
|
|
||||||
name: "defaults",
|
|
||||||
input: `
|
|
||||||
replication: {}
|
|
||||||
`,
|
|
||||||
expectOk: func(t *testing.T, a *ActiveSide, m *modePush) {},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "steps_zero",
|
|
||||||
input: `
|
|
||||||
replication:
|
|
||||||
concurrency:
|
|
||||||
steps: 0
|
|
||||||
`,
|
|
||||||
expectError: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "size_estimates_zero",
|
|
||||||
input: `
|
|
||||||
replication:
|
|
||||||
concurrency:
|
|
||||||
size_estimates: 0
|
|
||||||
`,
|
|
||||||
expectError: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "custom_values",
|
|
||||||
input: `
|
|
||||||
replication:
|
|
||||||
concurrency:
|
|
||||||
steps: 23
|
|
||||||
size_estimates: 42
|
|
||||||
`,
|
|
||||||
expectOk: func(t *testing.T, a *ActiveSide, m *modePush) {
|
|
||||||
assert.Equal(t, 23, a.replicationDriverConfig.StepQueueConcurrency)
|
|
||||||
assert.Equal(t, 42, m.plannerPolicy.SizeEstimationConcurrency)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "negative_values_forbidden",
|
|
||||||
input: `
|
|
||||||
replication:
|
|
||||||
concurrency:
|
|
||||||
steps: -23
|
|
||||||
size_estimates: -42
|
|
||||||
`,
|
|
||||||
expectError: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
fill := func(s string) string { return fmt.Sprintf(tmpl, s) }
|
|
||||||
|
|
||||||
for _, ts := range tests {
|
|
||||||
t.Run(ts.name, func(t *testing.T) {
|
|
||||||
assert.True(t, (ts.expectError) != (ts.expectOk != nil))
|
|
||||||
|
|
||||||
cstr := fill(ts.input)
|
|
||||||
t.Logf("testing config:\n%s", cstr)
|
|
||||||
c, err := config.ParseConfigBytes([]byte(cstr))
|
|
||||||
require.NoError(t, err)
|
|
||||||
jobs, err := JobsFromConfig(c, config.ParseFlagsNone)
|
|
||||||
if ts.expectOk != nil {
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NotNil(t, c)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Len(t, jobs, 1)
|
|
||||||
a := jobs[0].(*ActiveSide)
|
|
||||||
m := a.mode.(*modePush)
|
|
||||||
ts.expectOk(t, a, m)
|
|
||||||
} else if ts.expectError {
|
|
||||||
require.Error(t, err)
|
|
||||||
} else {
|
|
||||||
t.Fatalf("test must define expectOk or expectError")
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|||||||
+36
-12
@@ -6,10 +6,10 @@ import (
|
|||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"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/config"
|
"github.com/zrepl/zrepl/config"
|
||||||
|
"github.com/zrepl/zrepl/daemon/filters"
|
||||||
"github.com/zrepl/zrepl/daemon/logging"
|
"github.com/zrepl/zrepl/daemon/logging"
|
||||||
"github.com/zrepl/zrepl/daemon/snapper"
|
"github.com/zrepl/zrepl/daemon/snapper"
|
||||||
"github.com/zrepl/zrepl/endpoint"
|
"github.com/zrepl/zrepl/endpoint"
|
||||||
@@ -48,9 +48,22 @@ func (m *modeSink) SnapperReport() *snapper.Report { return nil }
|
|||||||
func modeSinkFromConfig(g *config.Global, in *config.SinkJob, jobID endpoint.JobID) (m *modeSink, err error) {
|
func modeSinkFromConfig(g *config.Global, in *config.SinkJob, jobID endpoint.JobID) (m *modeSink, err error) {
|
||||||
m = &modeSink{}
|
m = &modeSink{}
|
||||||
|
|
||||||
m.receiverConfig, err = buildReceiverConfig(in, jobID)
|
rootDataset, err := zfs.NewDatasetPath(in.RootFS)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, errors.New("root dataset is not a valid zfs filesystem path")
|
||||||
|
}
|
||||||
|
|
||||||
|
m.receiverConfig = endpoint.ReceiverConfig{
|
||||||
|
JobID: jobID,
|
||||||
|
RootWithoutClientComponent: rootDataset,
|
||||||
|
AppendClientIdentity: true, // !
|
||||||
|
UpdateLastReceivedHold: true,
|
||||||
|
|
||||||
|
InheritProperties: in.Recv.Properties.Inherit,
|
||||||
|
OverrideProperties: in.Recv.Properties.Override,
|
||||||
|
}
|
||||||
|
if err := m.receiverConfig.Validate(); err != nil {
|
||||||
|
return nil, errors.Wrap(err, "cannot build receiver config")
|
||||||
}
|
}
|
||||||
|
|
||||||
return m, nil
|
return m, nil
|
||||||
@@ -58,19 +71,31 @@ 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) {
|
||||||
// FIXME exact dedup of modePush
|
// FIXME exact dedup of modePush
|
||||||
m = &modeSource{}
|
m = &modeSource{}
|
||||||
|
fsf, err := filters.DatasetMapFilterFromConfig(in.Filesystems)
|
||||||
m.senderConfig, err = buildSenderConfig(in, jobID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "send options")
|
return nil, errors.Wrap(err, "cannot build filesystem filter")
|
||||||
|
}
|
||||||
|
m.senderConfig = &endpoint.SenderConfig{
|
||||||
|
FSF: fsf,
|
||||||
|
JobID: jobID,
|
||||||
|
DisableIncrementalStepHolds: in.Send.StepHolds.DisableIncremental,
|
||||||
|
|
||||||
|
Encrypt: &zfs.NilBool{B: in.Send.Encrypted},
|
||||||
|
SendRaw: in.Send.Raw,
|
||||||
|
SendProperties: in.Send.SendProperties,
|
||||||
|
SendBackupProperties: in.Send.BackupProperties,
|
||||||
|
SendLargeBlocks: in.Send.LargeBlocks,
|
||||||
|
SendCompressed: in.Send.Compressed,
|
||||||
|
SendEmbeddedData: in.Send.EmbeddedData,
|
||||||
}
|
}
|
||||||
|
|
||||||
if m.snapper, err = snapper.FromConfig(g, m.senderConfig.FSF, in.Snapshotting); err != nil {
|
if m.snapper, err = snapper.FromConfig(g, fsf, in.Snapshotting); err != nil {
|
||||||
return nil, errors.Wrap(err, "cannot build snapper")
|
return nil, errors.Wrap(err, "cannot build snapper")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,11 +113,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 +135,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")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+9
-20
@@ -4,14 +4,10 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sort"
|
"sort"
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"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/config"
|
"github.com/zrepl/zrepl/config"
|
||||||
"github.com/zrepl/zrepl/daemon/filters"
|
"github.com/zrepl/zrepl/daemon/filters"
|
||||||
@@ -26,14 +22,13 @@ 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
|
||||||
|
|
||||||
promPruneSecs *prometheus.HistogramVec // labels: prune_side
|
promPruneSecs *prometheus.HistogramVec // labels: prune_side
|
||||||
|
|
||||||
prunerMtx sync.Mutex
|
pruner *pruner.Pruner
|
||||||
pruner *pruner.Pruner
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *SnapJob) Name() string { return j.name.String() }
|
func (j *SnapJob) Name() string { return j.name.String() }
|
||||||
@@ -81,13 +76,10 @@ type SnapJobStatus struct {
|
|||||||
func (j *SnapJob) Status() *Status {
|
func (j *SnapJob) Status() *Status {
|
||||||
s := &SnapJobStatus{}
|
s := &SnapJobStatus{}
|
||||||
t := j.Type()
|
t := j.Type()
|
||||||
j.prunerMtx.Lock()
|
|
||||||
if j.pruner != nil {
|
if j.pruner != nil {
|
||||||
s.Pruning = j.pruner.Report()
|
s.Pruning = j.pruner.Report()
|
||||||
}
|
}
|
||||||
j.prunerMtx.Unlock()
|
s.Snapshotting = j.snapper.Report()
|
||||||
r := j.snapper.Report()
|
|
||||||
s.Snapshotting = &r
|
|
||||||
return &Status{Type: t, JobSpecific: s}
|
return &Status{Type: t, JobSpecific: s}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,7 +130,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 +140,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,15 +173,12 @@ 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: &zfs.NilBool{B: true},
|
||||||
// However, the implementation requires them to be set.
|
// FIXME DisableIncrementalStepHolds setting is irrelevant for SnapJob because the endpoint is only used as pruner.Target
|
||||||
Encrypt: &nodefault.Bool{B: true},
|
DisableIncrementalStepHolds: false,
|
||||||
BandwidthLimit: bandwidthlimit.NoLimitConfig(),
|
|
||||||
})
|
})
|
||||||
j.prunerMtx.Lock()
|
|
||||||
j.pruner = j.prunerFactory.BuildLocalPruner(ctx, sender, alwaysUpToDateReplicationCursorHistory{sender})
|
j.pruner = j.prunerFactory.BuildLocalPruner(ctx, sender, alwaysUpToDateReplicationCursorHistory{sender})
|
||||||
j.prunerMtx.Unlock()
|
|
||||||
log.Info("start pruning")
|
log.Info("start pruning")
|
||||||
j.pruner.Prune()
|
j.pruner.Prune()
|
||||||
log.Info("finished pruning")
|
log.Info("finished pruning")
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
|
|
||||||
"github.com/mattn/go-isatty"
|
"github.com/mattn/go-isatty"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/config"
|
"github.com/zrepl/zrepl/config"
|
||||||
"github.com/zrepl/zrepl/daemon/logging/trace"
|
"github.com/zrepl/zrepl/daemon/logging/trace"
|
||||||
"github.com/zrepl/zrepl/logger"
|
"github.com/zrepl/zrepl/logger"
|
||||||
|
|||||||
@@ -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 .
|
||||||
@@ -90,15 +93,11 @@ package trace
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
runtimedebug "runtime/debug"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/kr/pretty"
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/util/chainlock"
|
"github.com/zrepl/zrepl/util/chainlock"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -121,19 +120,15 @@ func RegisterMetrics(r prometheus.Registerer) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type traceNode struct {
|
type traceNode struct {
|
||||||
// NOTE: members with prefix debug are only valid if the package variable debugEnabled is true.
|
id string
|
||||||
|
annotation string
|
||||||
id string
|
parentTask *traceNode
|
||||||
annotation string
|
|
||||||
parentTask *traceNode
|
|
||||||
debugCreationStack string // debug: stack trace of when the traceNode was created
|
|
||||||
|
|
||||||
mtx chainlock.L
|
mtx chainlock.L
|
||||||
|
|
||||||
activeChildTasks int32 // only for task nodes, insignificant for span nodes
|
activeChildTasks int32 // only for task nodes, insignificant for span nodes
|
||||||
debugActiveChildTasks map[*traceNode]bool // debug: set of active child tasks
|
parentSpan *traceNode
|
||||||
parentSpan *traceNode
|
activeChildSpan *traceNode // nil if task or span doesn't have an active child span
|
||||||
activeChildSpan *traceNode // nil if task or span doesn't have an active child span
|
|
||||||
|
|
||||||
startedAt time.Time
|
startedAt time.Time
|
||||||
endedAt time.Time
|
endedAt time.Time
|
||||||
@@ -142,14 +137,6 @@ type traceNode struct {
|
|||||||
func (s *traceNode) StartedAt() time.Time { return s.startedAt }
|
func (s *traceNode) StartedAt() time.Time { return s.startedAt }
|
||||||
func (s *traceNode) EndedAt() time.Time { return s.endedAt }
|
func (s *traceNode) EndedAt() time.Time { return s.endedAt }
|
||||||
|
|
||||||
// caller must hold mtx
|
|
||||||
func (s *traceNode) debugString() string {
|
|
||||||
if !debugEnabled {
|
|
||||||
return "<debugEnabled=false>"
|
|
||||||
}
|
|
||||||
return pretty.Sprint(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returned from WithTask or WithSpan.
|
// Returned from WithTask or WithSpan.
|
||||||
// Must be called once the task or span ends.
|
// Must be called once the task or span ends.
|
||||||
// See package-level docs for nesting rules.
|
// See package-level docs for nesting rules.
|
||||||
@@ -209,16 +196,8 @@ func WithTask(ctx context.Context, taskName string) (context.Context, DoneFunc)
|
|||||||
endedAt: time.Time{},
|
endedAt: time.Time{},
|
||||||
}
|
}
|
||||||
|
|
||||||
if debugEnabled {
|
|
||||||
this.debugCreationStack = string(runtimedebug.Stack())
|
|
||||||
this.debugActiveChildTasks = map[*traceNode]bool{}
|
|
||||||
}
|
|
||||||
|
|
||||||
if parentTask != nil {
|
if parentTask != nil {
|
||||||
this.parentTask.activeChildTasks++
|
this.parentTask.activeChildTasks++
|
||||||
if debugEnabled {
|
|
||||||
this.parentTask.debugActiveChildTasks[this] = true
|
|
||||||
}
|
|
||||||
parentTask.mtx.Unlock()
|
parentTask.mtx.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,11 +218,7 @@ func WithTask(ctx context.Context, taskName string) (context.Context, DoneFunc)
|
|||||||
defer this.mtx.Lock().Unlock()
|
defer this.mtx.Lock().Unlock()
|
||||||
|
|
||||||
if this.activeChildTasks != 0 {
|
if this.activeChildTasks != 0 {
|
||||||
if debugEnabled {
|
panic(errors.Wrapf(ErrTaskStillHasActiveChildTasks, "end task: %v active child tasks", this.activeChildTasks))
|
||||||
// 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())
|
|
||||||
}
|
|
||||||
panic(errors.WithMessagef(ErrTaskStillHasActiveChildTasks, "end task: %v active child tasks (run daemon with env var %s=1 for more details)\n", this.activeChildTasks, debugEnabledEnvVar))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// support idempotent task ends
|
// support idempotent task ends
|
||||||
@@ -254,15 +229,8 @@ func WithTask(ctx context.Context, taskName string) (context.Context, DoneFunc)
|
|||||||
|
|
||||||
if this.parentTask != nil {
|
if this.parentTask != nil {
|
||||||
this.parentTask.activeChildTasks--
|
this.parentTask.activeChildTasks--
|
||||||
if debugEnabled {
|
|
||||||
delete(this.parentTask.debugActiveChildTasks, this)
|
|
||||||
}
|
|
||||||
if this.parentTask.activeChildTasks < 0 {
|
if this.parentTask.activeChildTasks < 0 {
|
||||||
if debugEnabled {
|
panic("impl error: parent task with negative activeChildTasks count")
|
||||||
// the debugString can be quite long and panic won't print it completely
|
|
||||||
fmt.Fprintf(os.Stderr, "going to panic due to activeChildTasks < 0:\n%s\n", this.parentTask.debugString())
|
|
||||||
}
|
|
||||||
panic(fmt.Sprintf("impl error: parent task with negative activeChildTasks count: %v", this.parentTask.activeChildTasks))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
@@ -311,10 +279,6 @@ func WithSpan(ctx context.Context, annotation string) (context.Context, DoneFunc
|
|||||||
endedAt: time.Time{},
|
endedAt: time.Time{},
|
||||||
}
|
}
|
||||||
|
|
||||||
if debugEnabled {
|
|
||||||
this.debugCreationStack = string(runtimedebug.Stack())
|
|
||||||
}
|
|
||||||
|
|
||||||
parentSpan.mtx.HoldWhile(func() {
|
parentSpan.mtx.HoldWhile(func() {
|
||||||
if parentSpan.activeChildSpan != nil {
|
if parentSpan.activeChildSpan != nil {
|
||||||
panic(ErrAlreadyActiveChildSpan)
|
panic(ErrAlreadyActiveChildSpan)
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -41,19 +43,15 @@ func WithTaskAndSpan(ctx context.Context, task string, span string) (context.Con
|
|||||||
}
|
}
|
||||||
|
|
||||||
// create a span during which several child tasks are spawned using the `add` function
|
// create a span during which several child tasks are spawned using the `add` function
|
||||||
//
|
|
||||||
// IMPORTANT FOR USERS: Caller must ensure that the capturing behavior is correct, the Go linter doesn't catch this.
|
|
||||||
func WithTaskGroup(ctx context.Context, taskGroup string) (_ context.Context, add func(f func(context.Context)), waitEnd DoneFunc) {
|
func WithTaskGroup(ctx context.Context, taskGroup string) (_ context.Context, add func(f func(context.Context)), waitEnd DoneFunc) {
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
ctx, endSpan := WithSpan(ctx, taskGroup)
|
ctx, endSpan := WithSpan(ctx, taskGroup)
|
||||||
add = func(f func(context.Context)) {
|
add = func(f func(context.Context)) {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
defer wg.Done()
|
||||||
defer wg.Done()
|
ctx, endTask := WithTask(ctx, taskGroup)
|
||||||
ctx, endTask := WithTask(ctx, taskGroup)
|
defer endTask()
|
||||||
defer endTask()
|
f(ctx)
|
||||||
f(ctx)
|
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
waitEnd = func() {
|
waitEnd = func() {
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
package trace
|
package trace
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"sync/atomic"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
@@ -17,46 +14,3 @@ func TestGetCallerOrPanic(t *testing.T) {
|
|||||||
// zrepl prefix is stripped
|
// zrepl prefix is stripped
|
||||||
assert.Equal(t, "daemon/logging/trace.TestGetCallerOrPanic", ret)
|
assert.Equal(t, "daemon/logging/trace.TestGetCallerOrPanic", ret)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestWithTaskGroupRunTasksConcurrently(t *testing.T) {
|
|
||||||
|
|
||||||
// spawn a task group where each task waits for the other to start
|
|
||||||
// => without concurrency, they would hang
|
|
||||||
|
|
||||||
rootCtx, endRoot := WithTaskFromStack(context.Background())
|
|
||||||
defer endRoot()
|
|
||||||
|
|
||||||
_, add, waitEnd := WithTaskGroup(rootCtx, "test-task-group")
|
|
||||||
|
|
||||||
schedulerTimeout := 2 * time.Second
|
|
||||||
timeout := time.After(schedulerTimeout)
|
|
||||||
var hadTimeout uint32
|
|
||||||
started0, started1 := make(chan struct{}), make(chan struct{})
|
|
||||||
for i := 0; i < 2; i++ {
|
|
||||||
i := i // capture by copy
|
|
||||||
add(func(ctx context.Context) {
|
|
||||||
switch i {
|
|
||||||
case 0:
|
|
||||||
close(started0)
|
|
||||||
select {
|
|
||||||
case <-started1:
|
|
||||||
case <-timeout:
|
|
||||||
atomic.AddUint32(&hadTimeout, 1)
|
|
||||||
}
|
|
||||||
case 1:
|
|
||||||
close(started1)
|
|
||||||
select {
|
|
||||||
case <-started0:
|
|
||||||
case <-timeout:
|
|
||||||
atomic.AddUint32(&hadTimeout, 1)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
panic("unreachable")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
waitEnd()
|
|
||||||
assert.Zero(t, hadTimeout, "either bad impl or scheduler timeout (which is %v)", schedulerTimeout)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -3,13 +3,9 @@ package trace
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/util/envconst"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const debugEnabledEnvVar = "ZREPL_TRACE_DEBUG_ENABLED"
|
const debugEnabled = false
|
||||||
|
|
||||||
var debugEnabled = envconst.Bool(debugEnabledEnvVar, false)
|
|
||||||
|
|
||||||
func debug(format string, args ...interface{}) {
|
func debug(format string, args ...interface{}) {
|
||||||
if !debugEnabled {
|
if !debugEnabled {
|
||||||
|
|||||||
@@ -3,11 +3,20 @@ package trace
|
|||||||
import (
|
import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/util/envconst"
|
"github.com/zrepl/zrepl/util/envconst"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var genIdPRNG = rand.New(rand.NewSource(1))
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
genIdPRNG.Seed(time.Now().UnixNano())
|
||||||
|
genIdPRNG.Seed(int64(os.Getpid()))
|
||||||
|
}
|
||||||
|
|
||||||
var genIdNumBytes = envconst.Int("ZREPL_TRACE_ID_NUM_BYTES", 3)
|
var genIdNumBytes = envconst.Int("ZREPL_TRACE_ID_NUM_BYTES", 3)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -21,7 +30,7 @@ func genID() string {
|
|||||||
enc := base64.NewEncoder(base64.RawStdEncoding, &out)
|
enc := base64.NewEncoder(base64.RawStdEncoding, &out)
|
||||||
buf := make([]byte, genIdNumBytes)
|
buf := make([]byte, genIdNumBytes)
|
||||||
for i := 0; i < len(buf); {
|
for i := 0; i < len(buf); {
|
||||||
n, err := rand.Read(buf[i:])
|
n, err := genIdPRNG.Read(buf[i:])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-20
@@ -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
|
||||||
@@ -351,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
|
||||||
@@ -418,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
|
||||||
@@ -464,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,172 +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/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,
|
|
||||||
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{}) {
|
|
||||||
|
|
||||||
t := time.NewTimer(0)
|
|
||||||
defer func() {
|
|
||||||
if !t.Stop() {
|
|
||||||
select {
|
|
||||||
case <-t.C:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
for {
|
|
||||||
now := time.Now()
|
|
||||||
s.mtx.Lock()
|
|
||||||
s.wakeupTime = s.config.Cron.Schedule.Next(now)
|
|
||||||
s.mtx.Unlock()
|
|
||||||
|
|
||||||
// Re-arm the timer.
|
|
||||||
// Need to Stop before Reset, see docs.
|
|
||||||
if !t.Stop() {
|
|
||||||
// Use non-blocking read from timer channel
|
|
||||||
// because, except for the first loop iteration,
|
|
||||||
// the channel is already drained
|
|
||||||
select {
|
|
||||||
case <-t.C:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
t.Reset(s.wakeupTime.Sub(now))
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return
|
|
||||||
case <-t.C:
|
|
||||||
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,251 +0,0 @@
|
|||||||
package snapper
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"sort"
|
|
||||||
"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
|
|
||||||
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) 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 := time.Now().In(time.UTC).Format("20060102_150405_000")
|
|
||||||
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,399 +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/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 <= 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,
|
|
||||||
fsf: fsf,
|
|
||||||
planArgs: planArgs{
|
|
||||||
prefix: in.Prefix,
|
|
||||||
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
|
|
||||||
})
|
|
||||||
t := time.NewTimer(time.Until(syncPoint))
|
|
||||||
defer t.Stop()
|
|
||||||
select {
|
|
||||||
case <-t.C:
|
|
||||||
return u(func(s *Periodic) {
|
|
||||||
s.state = Planning
|
|
||||||
}).sf()
|
|
||||||
case <-a.ctx.Done():
|
|
||||||
return onMainCtxDone(a.ctx, u)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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")
|
|
||||||
})
|
|
||||||
|
|
||||||
t := time.NewTimer(time.Until(sleepUntil))
|
|
||||||
defer t.Stop()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-t.C:
|
|
||||||
return u(func(snapper *Periodic) {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
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}
|
|
||||||
}
|
|
||||||
+479
-22
@@ -3,40 +3,497 @@ 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/filters"
|
||||||
|
"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 *filters.DatasetMapFilter
|
||||||
|
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 *filters.DatasetMapFilter, 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 <-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 <-a.ctx.Done():
|
||||||
|
return onMainCtxDone(a.ctx, u)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func listFSes(ctx context.Context, mf *filters.DatasetMapFilter) (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/daemon/filters"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FIXME: properly abstract snapshotting:
|
||||||
|
// - split up things that trigger snapshotting from the mechanism
|
||||||
|
// - timer-based trigger (periodic)
|
||||||
|
// - call from control socket (manual)
|
||||||
|
// - mixed modes?
|
||||||
|
// - support a `zrepl snapshot JOBNAME` subcommand for config.SnapshottingManual
|
||||||
|
type PeriodicOrManual struct {
|
||||||
|
s *Snapper
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PeriodicOrManual) Run(ctx context.Context, wakeUpCommon chan<- struct{}) {
|
||||||
|
if s.s != nil {
|
||||||
|
s.s.Run(ctx, wakeUpCommon)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns nil if manual
|
||||||
|
func (s *PeriodicOrManual) Report() *Report {
|
||||||
|
if s.s != nil {
|
||||||
|
return s.s.Report()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func FromConfig(g *config.Global, fsf *filters.DatasetMapFilter, 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 ""
|
|
||||||
}
|
|
||||||
+15
-175
@@ -69,139 +69,6 @@
|
|||||||
"title": "Panel Title",
|
"title": "Panel Title",
|
||||||
"type": "text"
|
"type": "text"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"cacheTimeout": null,
|
|
||||||
"colorBackground": true,
|
|
||||||
"colorPostfix": false,
|
|
||||||
"colorPrefix": false,
|
|
||||||
"colorValue": false,
|
|
||||||
"colors": [
|
|
||||||
"#bf1b00",
|
|
||||||
"#508642",
|
|
||||||
"#bf1b00"
|
|
||||||
],
|
|
||||||
"datasource": "${DS_PROMETHEUS}",
|
|
||||||
"description": "Number of filesystems that failed replications",
|
|
||||||
"format": "none",
|
|
||||||
"gauge": {
|
|
||||||
"maxValue": 100,
|
|
||||||
"minValue": 0,
|
|
||||||
"show": false,
|
|
||||||
"thresholdLabels": false,
|
|
||||||
"thresholdMarkers": true
|
|
||||||
},
|
|
||||||
"gridPos": {
|
|
||||||
"h": 3,
|
|
||||||
"w": 24,
|
|
||||||
"x": 0,
|
|
||||||
"y": 10
|
|
||||||
},
|
|
||||||
"id": 50,
|
|
||||||
"interval": null,
|
|
||||||
"links": [],
|
|
||||||
"mappingType": 1,
|
|
||||||
"mappingTypes": [
|
|
||||||
{
|
|
||||||
"name": "value to text",
|
|
||||||
"value": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "range to text",
|
|
||||||
"value": 2
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"maxDataPoints": 100,
|
|
||||||
"nullPointMode": "connected",
|
|
||||||
"nullText": null,
|
|
||||||
"postfix": "",
|
|
||||||
"postfixFontSize": "50%",
|
|
||||||
"prefix": "",
|
|
||||||
"prefixFontSize": "50%",
|
|
||||||
"rangeMaps": [
|
|
||||||
{
|
|
||||||
"from": "",
|
|
||||||
"text": "",
|
|
||||||
"to": ""
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"repeat": "zrepl_job_name",
|
|
||||||
"repeatDirection": "h",
|
|
||||||
"scopedVars": {
|
|
||||||
"zrepl_job_name": {
|
|
||||||
"selected": false,
|
|
||||||
"text": "desktop_to_homesrv",
|
|
||||||
"value": "desktop_to_homesrv"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"sparkline": {
|
|
||||||
"fillColor": "rgba(31, 118, 189, 0.18)",
|
|
||||||
"full": true,
|
|
||||||
"lineColor": "rgb(31, 120, 193)",
|
|
||||||
"show": true
|
|
||||||
},
|
|
||||||
"tableColumn": "__name__",
|
|
||||||
"targets": [
|
|
||||||
{
|
|
||||||
"expr": "zrepl_replication_filesystem_errors{job=\"$prom_job_name\",zrepl_job=\"$zrepl_job_name\"}",
|
|
||||||
"format": "time_series",
|
|
||||||
"groupBy": [
|
|
||||||
{
|
|
||||||
"params": [
|
|
||||||
"$__interval"
|
|
||||||
],
|
|
||||||
"type": "time"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"params": [
|
|
||||||
"null"
|
|
||||||
],
|
|
||||||
"type": "fill"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"instant": true,
|
|
||||||
"interval": "",
|
|
||||||
"intervalFactor": 1,
|
|
||||||
"legendFormat": "",
|
|
||||||
"orderByTime": "ASC",
|
|
||||||
"policy": "default",
|
|
||||||
"refId": "A",
|
|
||||||
"resultFormat": "time_series",
|
|
||||||
"select": [
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"params": [
|
|
||||||
"value"
|
|
||||||
],
|
|
||||||
"type": "field"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"params": [],
|
|
||||||
"type": "mean"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
],
|
|
||||||
"tags": []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"thresholds": "0,1",
|
|
||||||
"title": "Failed replications $zrepl_job_name",
|
|
||||||
"transparent": false,
|
|
||||||
"type": "singlestat",
|
|
||||||
"valueFontSize": "80%",
|
|
||||||
"valueMaps": [
|
|
||||||
{
|
|
||||||
"op": "=",
|
|
||||||
"text": "All failed",
|
|
||||||
"value": "-1"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"op": "=",
|
|
||||||
"text": "All OK",
|
|
||||||
"value": "0"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"valueName": "avg"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"aliasColors": {},
|
"aliasColors": {},
|
||||||
"bars": false,
|
"bars": false,
|
||||||
@@ -220,7 +87,7 @@
|
|||||||
"h": 4,
|
"h": 4,
|
||||||
"w": 12,
|
"w": 12,
|
||||||
"x": 0,
|
"x": 0,
|
||||||
"y": 13
|
"y": 10
|
||||||
},
|
},
|
||||||
"hiddenSeries": false,
|
"hiddenSeries": false,
|
||||||
"id": 48,
|
"id": 48,
|
||||||
@@ -249,7 +116,7 @@
|
|||||||
"stack": true,
|
"stack": true,
|
||||||
"steppedLine": false,
|
"steppedLine": false,
|
||||||
"targets": [{
|
"targets": [{
|
||||||
"expr": "sgn(zrepl_start_time{job='$prom_job_name'})",
|
"expr": "zrepl_version_daemon{job='$prom_job_name'}",
|
||||||
"format": "time_series",
|
"format": "time_series",
|
||||||
"interval": "",
|
"interval": "",
|
||||||
"intervalFactor": 1,
|
"intervalFactor": 1,
|
||||||
@@ -314,7 +181,7 @@
|
|||||||
"h": 5,
|
"h": 5,
|
||||||
"w": 12,
|
"w": 12,
|
||||||
"x": 12,
|
"x": 12,
|
||||||
"y": 13
|
"y": 10
|
||||||
},
|
},
|
||||||
"hiddenSeries": false,
|
"hiddenSeries": false,
|
||||||
"id": 44,
|
"id": 44,
|
||||||
@@ -406,7 +273,7 @@
|
|||||||
"h": 5,
|
"h": 5,
|
||||||
"w": 12,
|
"w": 12,
|
||||||
"x": 0,
|
"x": 0,
|
||||||
"y": 17
|
"y": 14
|
||||||
},
|
},
|
||||||
"hiddenSeries": false,
|
"hiddenSeries": false,
|
||||||
"id": 42,
|
"id": 42,
|
||||||
@@ -506,7 +373,7 @@
|
|||||||
"h": 4,
|
"h": 4,
|
||||||
"w": 12,
|
"w": 12,
|
||||||
"x": 12,
|
"x": 12,
|
||||||
"y": 18
|
"y": 15
|
||||||
},
|
},
|
||||||
"hiddenSeries": false,
|
"hiddenSeries": false,
|
||||||
"id": 22,
|
"id": 22,
|
||||||
@@ -598,7 +465,7 @@
|
|||||||
"h": 5,
|
"h": 5,
|
||||||
"w": 12,
|
"w": 12,
|
||||||
"x": 0,
|
"x": 0,
|
||||||
"y": 22
|
"y": 19
|
||||||
},
|
},
|
||||||
"hiddenSeries": false,
|
"hiddenSeries": false,
|
||||||
"id": 33,
|
"id": 33,
|
||||||
@@ -706,7 +573,7 @@
|
|||||||
"h": 5,
|
"h": 5,
|
||||||
"w": 12,
|
"w": 12,
|
||||||
"x": 12,
|
"x": 12,
|
||||||
"y": 22
|
"y": 19
|
||||||
},
|
},
|
||||||
"hiddenSeries": false,
|
"hiddenSeries": false,
|
||||||
"id": 23,
|
"id": 23,
|
||||||
@@ -798,7 +665,7 @@
|
|||||||
"h": 5,
|
"h": 5,
|
||||||
"w": 12,
|
"w": 12,
|
||||||
"x": 0,
|
"x": 0,
|
||||||
"y": 27
|
"y": 24
|
||||||
},
|
},
|
||||||
"hiddenSeries": false,
|
"hiddenSeries": false,
|
||||||
"id": 41,
|
"id": 41,
|
||||||
@@ -891,7 +758,7 @@
|
|||||||
"h": 5,
|
"h": 5,
|
||||||
"w": 12,
|
"w": 12,
|
||||||
"x": 12,
|
"x": 12,
|
||||||
"y": 27
|
"y": 24
|
||||||
},
|
},
|
||||||
"hiddenSeries": false,
|
"hiddenSeries": false,
|
||||||
"id": 47,
|
"id": 47,
|
||||||
@@ -920,7 +787,7 @@
|
|||||||
"stack": false,
|
"stack": false,
|
||||||
"steppedLine": false,
|
"steppedLine": false,
|
||||||
"targets": [{
|
"targets": [{
|
||||||
"expr": "zrepl_endpoint_abstractions_cache_entry_count",
|
"expr": "zrepl_endpoint_send_abstractions_cache_entry_count",
|
||||||
"format": "time_series",
|
"format": "time_series",
|
||||||
"intervalFactor": 1,
|
"intervalFactor": 1,
|
||||||
"refId": "A"
|
"refId": "A"
|
||||||
@@ -929,7 +796,7 @@
|
|||||||
"timeFrom": null,
|
"timeFrom": null,
|
||||||
"timeRegions": [],
|
"timeRegions": [],
|
||||||
"timeShift": null,
|
"timeShift": null,
|
||||||
"title": "zfs abstractions cache entry count (should not be zero and not grow unboundedly)",
|
"title": "send abstractions cache entry count (should not be zero and not grow unboundedly)",
|
||||||
"tooltip": {
|
"tooltip": {
|
||||||
"shared": true,
|
"shared": true,
|
||||||
"sort": 0,
|
"sort": 0,
|
||||||
@@ -983,7 +850,7 @@
|
|||||||
"h": 5,
|
"h": 5,
|
||||||
"w": 12,
|
"w": 12,
|
||||||
"x": 0,
|
"x": 0,
|
||||||
"y": 32
|
"y": 29
|
||||||
},
|
},
|
||||||
"hiddenSeries": false,
|
"hiddenSeries": false,
|
||||||
"id": 17,
|
"id": 17,
|
||||||
@@ -1076,7 +943,7 @@
|
|||||||
"h": 5,
|
"h": 5,
|
||||||
"w": 12,
|
"w": 12,
|
||||||
"x": 0,
|
"x": 0,
|
||||||
"y": 37
|
"y": 34
|
||||||
},
|
},
|
||||||
"hiddenSeries": false,
|
"hiddenSeries": false,
|
||||||
"id": 19,
|
"id": 19,
|
||||||
@@ -1177,33 +1044,6 @@
|
|||||||
"tagsQuery": "",
|
"tagsQuery": "",
|
||||||
"type": "query",
|
"type": "query",
|
||||||
"useTags": false
|
"useTags": false
|
||||||
},
|
|
||||||
{
|
|
||||||
"allValue": null,
|
|
||||||
"current": {
|
|
||||||
"text": "All",
|
|
||||||
"value": [
|
|
||||||
"$__all"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"datasource": "${DS_PROMETHEUS}",
|
|
||||||
"definition": "label_values(zrepl_replication_filesystem_errors{job=\"$prom_job_name\"}, zrepl_job)",
|
|
||||||
"hide": 2,
|
|
||||||
"includeAll": true,
|
|
||||||
"label": "Zrepl Job Name",
|
|
||||||
"multi": true,
|
|
||||||
"name": "zrepl_job_name",
|
|
||||||
"options": [],
|
|
||||||
"query": "label_values(zrepl_replication_filesystem_errors{job=\"$prom_job_name\"}, zrepl_job)",
|
|
||||||
"refresh": 1,
|
|
||||||
"regex": "",
|
|
||||||
"skipUrlSync": false,
|
|
||||||
"sort": 1,
|
|
||||||
"tagValuesQuery": "",
|
|
||||||
"tags": [],
|
|
||||||
"tagsQuery": "",
|
|
||||||
"type": "query",
|
|
||||||
"useTags": false
|
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
"time": {
|
"time": {
|
||||||
@@ -1236,6 +1076,6 @@
|
|||||||
},
|
},
|
||||||
"timezone": "",
|
"timezone": "",
|
||||||
"title": "zrepl 0.3",
|
"title": "zrepl 0.3",
|
||||||
"uid": "etQuvBnGz",
|
"uid": "etJuvBmGz",
|
||||||
"version": 7
|
"version": 6
|
||||||
}
|
}
|
||||||
+26
-131
@@ -6,7 +6,6 @@
|
|||||||
.. |docs| replace:: [DOCS]
|
.. |docs| replace:: [DOCS]
|
||||||
.. |feature| replace:: [FEATURE]
|
.. |feature| replace:: [FEATURE]
|
||||||
.. |mig| replace:: **[MIGRATION]**
|
.. |mig| replace:: **[MIGRATION]**
|
||||||
.. |maint| replace:: [MAINT]
|
|
||||||
|
|
||||||
.. _changelog:
|
.. _changelog:
|
||||||
|
|
||||||
@@ -16,116 +15,17 @@ 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.
|
||||||
|
|
||||||
0.6 (Unreleased)
|
We use the following annotations for classifying changes:
|
||||||
----------------
|
|
||||||
|
|
||||||
* `Feature Wishlist on GitHub <https://github.com/zrepl/zrepl/discussions/547>`_
|
* |break_config| Change that breaks the config.
|
||||||
|
As a package maintainer, make sure to warn your users about config breakage somehow.
|
||||||
* |feature| :ref:`Schedule-based snapshotting<job-snapshotting--cron>` using ``cron`` syntax instead of an interval.
|
* |break| Change that breaks interoperability or persistent state representation with previous releases.
|
||||||
* |feature| Add ``ZREPL_DESTROY_MAX_BATCH_SIZE`` env var (default 0=unlimited).
|
As a package maintainer, make sure to warn your users about config breakage somehow.
|
||||||
* |bugfix| Fix resuming from interrupted replications that use ``send.raw`` on unencrypted datasets.
|
Note that even updating the package on both sides might not be sufficient, e.g. if persistent state needs to be migrated to a new format.
|
||||||
|
* |mig| Migration that must be run by the user.
|
||||||
* The send options introduced in zrepl 0.4 allow users to specify additional zfs send flags for zrepl to use.
|
* |feature| Change that introduces new functionality.
|
||||||
Before this fix, when setting ``send.raw=true`` on a job that replicates unencrypted datasets,
|
* |bugfix| Change that fixes a bug, no regressions or incompatibilities expected.
|
||||||
zrepl would not allow an interrupted replication to resume.
|
* |docs| Change to the documentation.
|
||||||
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| |feature| 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`.
|
|
||||||
|
|
||||||
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!
|
|
||||||
|
|
||||||
.. NOTE::
|
|
||||||
| zrepl is a spare-time project primarily developed by `Christian Schwarz <https://cschwarz.com>`_.
|
|
||||||
| You can support maintenance and feature development through one of the following services:
|
|
||||||
| |Donate via Patreon| |Donate via GitHub Sponsors| |Donate via Liberapay| |Donate via PayPal|
|
|
||||||
| Note that PayPal processing fees are relatively high for small donations.
|
|
||||||
| For SEPA wire transfer and **commercial support**, please `contact Christian directly <https://cschwarz.com>`_.
|
|
||||||
|
|
||||||
|
|
||||||
0.4.0
|
|
||||||
-----
|
|
||||||
|
|
||||||
* |feature| support setting zfs send / recv flags in the config (send: ``-wLcepbS`` , recv: ``-ox`` ).
|
|
||||||
Config docs :ref:`here <job-send-options>` and :ref:`here <job-recv-options>` .
|
|
||||||
* |feature| parallel replication is now configurable (disabled by default, :ref:`config docs here <replication-option-concurrency>` ).
|
|
||||||
* |feature| New ``zrepl status`` UI:
|
|
||||||
|
|
||||||
* Interactive job selection.
|
|
||||||
* Interactively ``zrepl signal`` jobs.
|
|
||||||
* Filter filesystems in the job view by name.
|
|
||||||
* An approximation of the old UI is still included as `--mode legacy` but will be removed in a future release of zrepl.
|
|
||||||
|
|
||||||
* |bugfix| Actually use concurrency when listing zrepl abstractions & doing size estimation.
|
|
||||||
These operations were accidentally made sequential in zrepl 0.3.
|
|
||||||
* |bugfix| Job hang-up during second replication attempt.
|
|
||||||
* |bugfix| Data races conditions in the dataconn rpc stack.
|
|
||||||
* |maint| Update to protobuf v1.25 and grpc 1.35.
|
|
||||||
|
|
||||||
For users who skipped the 0.3.1 update: please make sure your pruning grid config is correct.
|
|
||||||
The following bugfix in 0.3.1 :issue:`caused problems for some users <400>`:
|
|
||||||
|
|
||||||
* |bugfix| pruning: ``grid``: add all snapshots that do not match the regex to the rule's destroy list.
|
|
||||||
|
|
||||||
0.3.1
|
|
||||||
-----
|
|
||||||
|
|
||||||
Mostly a bugfix release for :ref:`zrepl 0.3 <release-0.3>`.
|
|
||||||
|
|
||||||
* |feature| pruning: add optional ``regex`` field to ``last_n`` rule
|
|
||||||
* |docs| pruning: ``grid`` : improve documentation and add an example
|
|
||||||
* |bugfix| pruning: ``grid``: add all snapshots that do not match the regex to the rule's destroy list.
|
|
||||||
This brings the implementation in line with the docs.
|
|
||||||
* |bugfix| ``easyrsa`` script in docs
|
|
||||||
* |bugfix| platformtest: fix skipping encryption-only tests on systems that don't support encryption
|
|
||||||
* |bugfix| replication: report AttemptDone if no filesystems are replicated
|
|
||||||
* |feature| status + replication: warning if replication succeeeded without any filesystem being replicated
|
|
||||||
* |docs| update multi-job & multi-host setup section
|
|
||||||
* RPM Packaging
|
|
||||||
* CI infrastructure rework
|
|
||||||
* Continuous deployment of that new `stable` branch to zrepl.github.io.
|
|
||||||
|
|
||||||
.. _release-0.3:
|
|
||||||
|
|
||||||
0.3
|
0.3
|
||||||
---
|
---
|
||||||
@@ -134,38 +34,27 @@ This is a big one! Headlining features:
|
|||||||
|
|
||||||
* **Resumable Send & Recv Support**
|
* **Resumable Send & Recv Support**
|
||||||
No knobs required, automatically used where supported.
|
No knobs required, automatically used where supported.
|
||||||
|
* **Hold-Protected Send & Recv**
|
||||||
|
Automatic ZFS holds to ensure that we can always use resumable send&recv for a replication step.
|
||||||
* **Encrypted Send & Recv Support** for OpenZFS native encryption,
|
* **Encrypted Send & Recv Support** for OpenZFS native encryption,
|
||||||
:ref:`configurable <job-send-options>` at the job level, i.e., for all filesystems a job is responsible for.
|
:ref:`configurable <job-send-options>` at the job level, i.e., for all filesystems a job is responsible for.
|
||||||
* **Replication Guarantees**
|
* **Receive-side hold on last received dataset**
|
||||||
Automatic use of ZFS holds and bookmarks to protect a replicated filesystem from losing synchronization between sender and receiver.
|
The counterpart to the replication cursor bookmark on the send-side.
|
||||||
By default, zrepl guarantees that incremental replication will always be possible and interrupted steps will always be resumable.
|
Ensures that incremental replication will always be possible between a sender and receiver.
|
||||||
|
|
||||||
.. TIP::
|
.. TIP::
|
||||||
|
|
||||||
We highly recommend studying the updated :ref:`overview section of the configuration chapter <overview-how-replication-works>` to understand how replication works.
|
We highly recommend studying the :ref:`overview section of the configuration chapter <overview-how-replication-works>` to understand how replication works.
|
||||||
|
|
||||||
.. TIP::
|
|
||||||
|
|
||||||
Go 1.15 changed the default TLS validation policy to **require Subject Alternative Names (SAN) in certificates**.
|
|
||||||
The openssl commands we provided in the quick-start guides up to and including the zrepl 0.3 docs seem not to work properly.
|
|
||||||
If you encounter certificate validation errors regarding SAN and wish to continue to use your old certificates, start the zrepl daemon with env var ``GODEBUG=x509ignoreCN=0``.
|
|
||||||
Alternatively, generate new certificates with SANs (see :ref:`both options int the TLS transport docs <transport-tcp+tlsclientauth-certgen>` ).
|
|
||||||
|
|
||||||
Quick-start guides:
|
|
||||||
|
|
||||||
* We have added :ref:`another quick-start guide for a typical workstation use case for zrepl <quickstart-backup-to-external-disk>`.
|
|
||||||
Check it out to learn how you can use zrepl to back up your workstation's OpenZFS natively-encrypted root filesystem to an external disk.
|
|
||||||
|
|
||||||
Additional changelog:
|
Additional changelog:
|
||||||
|
|
||||||
* |break| Go 1.15 TLS changes mentioned above.
|
|
||||||
* |break| |break_config| **more restrictive job names than in prior zrepl versions**
|
* |break| |break_config| **more restrictive job names than in prior zrepl versions**
|
||||||
Starting with this version, job names are going to be embedded into ZFS holds and bookmark names (see :ref:`this section for details <zrepl-zfs-abstractions>`).
|
Starting with this version, job names are going to be embedded into ZFS holds and bookmark names (see :ref:`here<replication-cursor-and-last-received-hold>` and :ref:`here<step-holds-and-bookmarks>`).
|
||||||
Therefore you might need to adjust your job names.
|
Therefore you might need to adjust your job names.
|
||||||
**Note that jobs** cannot be renamed easily **once you start using zrepl 0.3.**
|
**Note that jobs** :issue:`cannot be renamed easily` **once you start using zrepl 0.3.**
|
||||||
* |break| |mig| replication cursor representation changed
|
* |break| |mig| replication cursor representation changed
|
||||||
|
|
||||||
* zrepl now manages the :ref:`replication cursor bookmark <zrepl-zfs-abstractions>` per job-filesystem tuple instead of a single replication cursor per filesystem.
|
* zrepl now manages the :ref:`replication cursor bookmark <replication-cursor-and-last-received-hold>` per job-filesystem tuple instead of a single replication cursor per filesystem.
|
||||||
In the future, this will permit multiple sending jobs to send from the same filesystems.
|
In the future, this will permit multiple sending jobs to send from the same filesystems.
|
||||||
* ZFS does not allow bookmark renaming, thus we cannot migrate the old replication cursors.
|
* ZFS does not allow bookmark renaming, thus we cannot migrate the old replication cursors.
|
||||||
* zrepl 0.3 will automatically create cursors in the new format for new replications, and warn if it still finds ones in the old format.
|
* zrepl 0.3 will automatically create cursors in the new format for new replications, and warn if it still finds ones in the old format.
|
||||||
@@ -173,7 +62,6 @@ Additional changelog:
|
|||||||
The migration will ensure that only those old-format cursors are destroyed that have been superseeded by new-format cursors.
|
The migration will ensure that only those old-format cursors are destroyed that have been superseeded by new-format cursors.
|
||||||
|
|
||||||
* |feature| New option ``listen_freebind`` (tcp, tls, prometheus listener)
|
* |feature| New option ``listen_freebind`` (tcp, tls, prometheus listener)
|
||||||
* |feature| :issue:`341` Prometheus metric for failing replications + corresponding Grafana panel
|
|
||||||
* |feature| :issue:`265` transport/tcp: support for CIDR masks in client IP whitelist
|
* |feature| :issue:`265` transport/tcp: support for CIDR masks in client IP whitelist
|
||||||
* |feature| documented subcommand to generate ``bash`` and ``zsh`` completions
|
* |feature| documented subcommand to generate ``bash`` and ``zsh`` completions
|
||||||
* |feature| :issue:`307` ``chrome://trace`` -compatible activity tracing of zrepl daemon activity
|
* |feature| :issue:`307` ``chrome://trace`` -compatible activity tracing of zrepl daemon activity
|
||||||
@@ -189,6 +77,13 @@ Additional changelog:
|
|||||||
* **[MAINTAINER NOTICE]** New platform tests in this version, please make sure you run them for your distro!
|
* **[MAINTAINER NOTICE]** New platform tests in this version, please make sure you run them for your distro!
|
||||||
* **[MAINTAINER NOTICE]** Please add the shell completions to the zrepl packages.
|
* **[MAINTAINER NOTICE]** Please add the shell completions to the zrepl packages.
|
||||||
|
|
||||||
|
.. NOTE::
|
||||||
|
| zrepl is a spare-time project primarily developed by `Christian Schwarz <https://cschwarz.com>`_.
|
||||||
|
| You can support maintenance and feature development through one of the following services:
|
||||||
|
| |Donate via Patreon| |Donate via GitHub Sponsors| |Donate via Liberapay| |Donate via PayPal|
|
||||||
|
| Note that PayPal processing fees are relatively high for small donations.
|
||||||
|
| For SEPA wire transfer and **commercial support**, please `contact Christian directly <https://cschwarz.com>`_.
|
||||||
|
|
||||||
0.2.1
|
0.2.1
|
||||||
-----
|
-----
|
||||||
|
|
||||||
|
|||||||
@@ -12,8 +12,6 @@ Configuration
|
|||||||
configuration/transports
|
configuration/transports
|
||||||
configuration/filter_syntax
|
configuration/filter_syntax
|
||||||
configuration/sendrecvoptions
|
configuration/sendrecvoptions
|
||||||
configuration/replication
|
|
||||||
configuration/conflict_resolution
|
|
||||||
configuration/snapshotting
|
configuration/snapshotting
|
||||||
configuration/prune
|
configuration/prune
|
||||||
configuration/logging
|
configuration/logging
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
.. include:: ../global.rst.inc
|
|
||||||
|
|
||||||
|
|
||||||
Conflict Resolution Options
|
|
||||||
===========================
|
|
||||||
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
- type: push
|
|
||||||
filesystems: ...
|
|
||||||
conflict_resolution:
|
|
||||||
initial_replication: most_recent | all | fail # default: most_recent
|
|
||||||
|
|
||||||
...
|
|
||||||
|
|
||||||
.. _conflict_resolution-initial_replication-option-send_all_snapshots:
|
|
||||||
|
|
||||||
|
|
||||||
``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``.
|
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
.. _miscellaneous:
|
|
||||||
|
|
||||||
Miscellaneous
|
Miscellaneous
|
||||||
=============
|
=============
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+86
-143
@@ -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.
|
||||||
|
|
||||||
@@ -58,8 +59,8 @@ Note that snapshot-creation denoted by "(snap)" is orthogonal to whether a job i
|
|||||||
| Pull mode | ``pull`` | ``source`` | * Central backup-server for many nodes |
|
| Pull mode | ``pull`` | ``source`` | * Central backup-server for many nodes |
|
||||||
| | | (snap) | * Remote server to NAS behind NAT |
|
| | | (snap) | * Remote server to NAS behind NAT |
|
||||||
+-----------------------+--------------+----------------------------------+------------------------------------------------------------------------------------+
|
+-----------------------+--------------+----------------------------------+------------------------------------------------------------------------------------+
|
||||||
| Local replication | | ``push`` + ``sink`` in one config | * Backup to :ref:`locally attached disk <quickstart-backup-to-external-disk>` |
|
| Local replication | | ``push`` + ``sink`` in one config | * Backup FreeBSD boot pool |
|
||||||
| | | with :ref:`local transport <transport-local>` | * Backup FreeBSD boot pool |
|
| | | with :ref:`local transport <transport-local>` | |
|
||||||
+-----------------------+--------------+----------------------------------+------------------------------------------------------------------------------------+
|
+-----------------------+--------------+----------------------------------+------------------------------------------------------------------------------------+
|
||||||
| Snap & prune-only | ``snap`` | N/A | * | Snapshots & pruning but no replication |
|
| Snap & prune-only | ``snap`` | N/A | * | Snapshots & pruning but no replication |
|
||||||
| | (snap) | | | required |
|
| | (snap) | | | required |
|
||||||
@@ -71,29 +72,27 @@ 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:
|
|
||||||
|
|
||||||
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*.
|
Each transport listener provides a client's identity to the passive side job.
|
||||||
The passive side job uses this *client identity* as follows:
|
It uses the client identity for access control:
|
||||||
|
|
||||||
* 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`` job has a whitelist of client identities that are allowed pull access.
|
||||||
|
|
||||||
.. 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 +103,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:
|
||||||
|
|
||||||
@@ -133,10 +132,23 @@ The following high-level steps take place during replication and can be monitore
|
|||||||
|
|
||||||
The idea behind the execution order of replication steps is that if the sender snapshots all filesystems simultaneously at fixed intervals, the receiver will have all filesystems snapshotted at time ``T1`` before the first snapshot at ``T2 = T1 + $interval`` is replicated.
|
The idea behind the execution order of replication steps is that if the sender snapshots all filesystems simultaneously at fixed intervals, the receiver will have all filesystems snapshotted at time ``T1`` before the first snapshot at ``T2 = T1 + $interval`` is replicated.
|
||||||
|
|
||||||
|
Placeholder Filesystems
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
.. _replication-placeholder-property:
|
||||||
|
|
||||||
|
**Placeholder filesystems** on the receiving side are regular ZFS filesystems with the placeholder property ``zrepl:placeholder=on``.
|
||||||
|
Placeholders allow the receiving side to mirror the sender's ZFS dataset hierarchy without replicating every filesystem at every intermediary dataset path component.
|
||||||
|
Consider the following example: ``S/H/J`` shall be replicated to ``R/sink/job/S/H/J``, but neither ``S/H`` nor ``S`` shall be replicated.
|
||||||
|
ZFS requires the existence of ``R/sink/job/S`` and ``R/sink/job/S/H`` in order to receive into ``R/sink/job/S/H/J``.
|
||||||
|
Thus, zrepl creates the parent filesystems as placeholders on the receiving side.
|
||||||
|
If at some point ``S/H`` and ``S`` shall be replicated, the receiving side invalidates the placeholder flag automatically.
|
||||||
|
The ``zrepl test placeholder`` command can be used to check whether a filesystem is a placeholder.
|
||||||
|
|
||||||
ZFS Background Knowledge
|
ZFS Background Knowledge
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
This section gives some background knowledge about ZFS features that zrepl uses to provide guarantees for a replication filesystem.
|
|
||||||
Specifically, zrepl guarantees by default that **incremental replication is always possible and that started replication steps can always be resumed if they are interrupted.**
|
This section gives some background knowledge about ZFS features that zrepl uses to guarantee that
|
||||||
|
**incremental replication is always possible and that started replication steps can always be resumed if they are interrupted.**
|
||||||
|
|
||||||
**ZFS Send Modes & Bookmarks**
|
**ZFS Send Modes & Bookmarks**
|
||||||
ZFS supports full sends (``zfs send fs@to``) and incremental sends (``zfs send -i @from fs@to``).
|
ZFS supports full sends (``zfs send fs@to``) and incremental sends (``zfs send -i @from fs@to``).
|
||||||
@@ -146,22 +158,6 @@ Incremental sends require that ``@from`` be present on the receiving side when r
|
|||||||
Incremental sends can also use a ZFS bookmark as *from* on the sending side (``zfs send -i #bm_from fs@to``), where ``#bm_from`` was created using ``zfs bookmark fs@from fs#bm_from``.
|
Incremental sends can also use a ZFS bookmark as *from* on the sending side (``zfs send -i #bm_from fs@to``), where ``#bm_from`` was created using ``zfs bookmark fs@from fs#bm_from``.
|
||||||
The receiving side must always have the actual snapshot ``@from``, regardless of whether the sending side uses ``@from`` or a bookmark of it.
|
The receiving side must always have the actual snapshot ``@from``, regardless of whether the sending side uses ``@from`` or a bookmark of it.
|
||||||
|
|
||||||
.. _zfs-background-knowledge-plain-vs-raw-sends:
|
|
||||||
|
|
||||||
**Plain and raw sends**
|
|
||||||
By default, ``zfs send`` sends the most generic, backwards-compatible data stream format (so-called 'plain send').
|
|
||||||
If the sent uses newer features, e.g. compression or encryption, ``zfs send`` has to un-do these operations on the fly to produce the plain send stream.
|
|
||||||
If the receiver uses newer features (e.g. compression or encryption inherited from the parent FS), it applies the necessary transformations again on the fly during ``zfs recv``.
|
|
||||||
|
|
||||||
Flags such as ``-e``, ``-c`` and ``-L`` tell ZFS to produce a send stream that is closer to how the data is stored on disk.
|
|
||||||
Sending with those flags removes computational overhead from sender and receiver.
|
|
||||||
However, the receiver will not apply certain transformations, e.g., it will not compress with the receive-side ``compression`` algorithm.
|
|
||||||
|
|
||||||
The ``-w`` (``--raw``) flag produces a send stream that is as *raw* as possible.
|
|
||||||
For unencrypted datasets, its current effect is the same as ``-Lce``.
|
|
||||||
|
|
||||||
Encrypted datasets can only be sent plain (unencrypted) or raw (encrypted) using the ``-w`` flag.
|
|
||||||
|
|
||||||
**Resumable Send & Recv**
|
**Resumable Send & Recv**
|
||||||
The ``-s`` flag for ``zfs recv`` tells zfs to save the partially received send stream in case it is interrupted.
|
The ``-s`` flag for ``zfs recv`` tells zfs to save the partially received send stream in case it is interrupted.
|
||||||
To resume the replication, the receiving side filesystem's ``receive_resume_token`` must be passed to a new ``zfs send -t <value> | zfs recv`` command.
|
To resume the replication, the receiving side filesystem's ``receive_resume_token`` must be passed to a new ``zfs send -t <value> | zfs recv`` command.
|
||||||
@@ -170,56 +166,41 @@ An incremental send can only be resumed if ``@to`` still exists *and* either ``@
|
|||||||
|
|
||||||
**ZFS Holds**
|
**ZFS Holds**
|
||||||
ZFS holds prevent a snapshot from being deleted through ``zfs destroy``, letting the destroy fail with a ``datset is busy`` error.
|
ZFS holds prevent a snapshot from being deleted through ``zfs destroy``, letting the destroy fail with a ``datset is busy`` error.
|
||||||
Holds are created and referred to by a *tag*. They can be thought of as a named, persistent lock on the snapshot.
|
Holds are created and referred to by a user-defined *tag*. They can be thought of as a named, persistent lock on the snapshot.
|
||||||
|
|
||||||
|
|
||||||
.. _zrepl-zfs-abstractions:
|
|
||||||
|
|
||||||
ZFS Abstractions Managed By zrepl
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
With the background knowledge from the previous paragraph, we now summarize the different on-disk ZFS objects that zrepl manages to provide its functionality.
|
|
||||||
|
|
||||||
.. _replication-placeholder-property:
|
|
||||||
|
|
||||||
**Placeholder filesystems** on the receiving side are regular ZFS filesystems with the ZFS property ``zrepl:placeholder=on``.
|
|
||||||
Placeholders allow the receiving side to mirror the sender's ZFS dataset hierarchy without replicating every filesystem at every intermediary dataset path component.
|
|
||||||
Consider the following example: ``S/H/J`` shall be replicated to ``R/sink/job/S/H/J``, but neither ``S/H`` nor ``S`` shall be replicated.
|
|
||||||
ZFS requires the existence of ``R/sink/job/S`` and ``R/sink/job/S/H`` in order to receive into ``R/sink/job/S/H/J``.
|
|
||||||
Thus, zrepl creates the parent filesystems as placeholders on the receiving side.
|
|
||||||
If at some point ``S/H`` and ``S`` shall be replicated, the receiving side invalidates the placeholder flag automatically.
|
|
||||||
The ``zrepl test placeholder`` command can be used to check whether a filesystem is a placeholder.
|
|
||||||
|
|
||||||
.. _replication-cursor-and-last-received-hold:
|
.. _replication-cursor-and-last-received-hold:
|
||||||
|
|
||||||
The **replication cursor** bookmark and **last-received-hold** are managed by zrepl to ensure that future replications can always be done incrementally.
|
Guaranteeing That Incremental Sends Are Always Possible
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
**Replication cursor** bookmark and **last-received-hold** are managed by zrepl to ensure that future replications can always be done incrementally.
|
||||||
The replication cursor is a send-side bookmark of the most recent successfully replicated snapshot,
|
The replication cursor is a send-side bookmark of the most recent successfully replicated snapshot,
|
||||||
and the last-received-hold is a hold of that snapshot on the receiving side.
|
and the last-received-hold is a hold of that snapshot on the receiving side.
|
||||||
Both are moved atomically after the receiving side has confirmed that a replication step is complete.
|
Both are moved aomically after the receiving side has confirmed that a replication step is complete.
|
||||||
|
|
||||||
The replication cursor has the format ``#zrepl_CUSOR_G_<GUID>_J_<JOBNAME>``.
|
The replication cursor has the format ``#zrepl_CUSOR_G_<GUID>_J_<JOBNAME>``.
|
||||||
The last-received-hold tag has the format ``zrepl_last_received_J_<JOBNAME>``.
|
The last-received-hold tag has the format ``zrepl_last_received_J_<JOBNAME>``.
|
||||||
Encoding the job name in the names ensures that multiple sending jobs can replicate the same filesystem to different receivers without interference.
|
Encoding the job name in the names ensures that multiple sending jobs can replicate the same filesystem to different receivers without interference.
|
||||||
|
|
||||||
.. _tentative-replication-cursor-bookmarks:
|
|
||||||
|
|
||||||
**Tentative replication cursor bookmarks** are short-lived bookmarks that protect the atomic moving-forward of the replication cursor and last-received-hold (see :issue:`this issue <340>`).
|
|
||||||
They are only necessary if step holds are not used as per the :ref:`replication.protection <replication-option-protection>` setting.
|
|
||||||
The tentative replication cursor has the format ``#zrepl_CUSORTENTATIVE_G_<GUID>_J_<JOBNAME>``.
|
|
||||||
The ``zrepl zfs-abstraction list`` command provides a listing of all bookmarks and holds managed by zrepl.
|
The ``zrepl zfs-abstraction list`` command provides a listing of all bookmarks and holds managed by zrepl.
|
||||||
|
|
||||||
.. _step-holds:
|
.. _step-holds-and-bookmarks:
|
||||||
|
|
||||||
|
Guaranteeing That Sends Are Always Resumable
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
**Step holds** are zfs holds managed by zrepl to ensure that a replication step can always be resumed if it is interrupted, e.g., due to network outage.
|
**Step holds** are zfs holds managed by zrepl to ensure that a replication step can always be resumed if it is interrupted, e.g., due to network outage.
|
||||||
zrepl creates step holds before it attempts a replication step and releases them after the receiver confirms that the replication step is complete.
|
zrepl creates step holds before it attempts a replication step and releases them after the receiver confirms that the replication step is complete.
|
||||||
For an initial replication ``full @initial_snap``, zrepl puts a zfs hold on ``@initial_snap``.
|
For an initial replication ``full @initial_snap``, zrepl puts a zfs hold on ``@initial_snap``.
|
||||||
For an incremental send ``@from -> @to``, zrepl puts a zfs hold on both ``@from`` and ``@to``.
|
For an incremental send ``@from -> @to``, zrepl puts a zfs hold on both ``@from`` and ``@to``.
|
||||||
Note that ``@from`` is not strictly necessary for resumability -- a bookmark on the sending side would be sufficient --, but size-estimation in currently used OpenZFS versions only works if ``@from`` is a snapshot.
|
Note that ``@from`` is not strictly necessary for resumability -- a bookmark on the sending side would be sufficient --, but size-estimation in currently used OpenZFS versions only works if ``@from`` is a snapshot.
|
||||||
|
|
||||||
The hold tag has the format ``zrepl_STEP_J_<JOBNAME>``.
|
The hold tag has the format ``zrepl_STEP_J_<JOBNAME>``.
|
||||||
A job only ever has one active send per filesystem.
|
A job only ever has one active send per filesystem.
|
||||||
Thus, there are never more than two step holds for a given pair of ``(job,filesystem)``.
|
Thus, there are never more than two step holds for a given pair of ``(job,filesystem)``.
|
||||||
|
|
||||||
**Step bookmarks** are zrepl's equivalent for holds on bookmarks (ZFS does not support putting holds on bookmarks).
|
**Step bookmarks** are zrepl's equivalent for holds on bookmarks (ZFS does not support putting holds on bookmarks).
|
||||||
They are intended for a situation where a replication step uses a bookmark ``#bm`` as incremental ``from`` where ``#bm`` is not managed by zrepl.
|
They are intended for a situation where a replication step uses a bookmark ``#bm`` as incremental ``from`` that is not managed by zrepl.
|
||||||
To ensure resumability, zrepl copies ``#bm`` to step bookmark ``#zrepl_STEP_G_<GUID>_J_<JOBNAME>``.
|
To ensure resumability, zrepl copies ``#bm`` to step bookmark ``#zrepl_STEP_G_<GUID>_J_<JOBNAME>``.
|
||||||
If the replication is interrupted and ``#bm`` is deleted by the user, the step bookmark remains as an incremental source for the resumable send.
|
If the replication is interrupted and ``#bm`` is deleted by the user, the step bookmark remains as an incremental source for the resumable send.
|
||||||
Note that zrepl does not yet support creating step bookmarks because the `corresponding ZFS feature for copying bookmarks <https://github.com/openzfs/zfs/pull/9571>`_ is not yet widely available .
|
Note that zrepl does not yet support creating step bookmarks because the `corresponding ZFS feature for copying bookmarks <https://github.com/openzfs/zfs/pull/9571>`_ is not yet widely available .
|
||||||
@@ -231,92 +212,54 @@ 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)
|
^^^^^^^^^^^
|
||||||
----------------------------------------------------------
|
|
||||||
|
|
||||||
Most users are served well with a single sender and a single receiver job.
|
|
||||||
This section documents considerations for more complex setups.
|
|
||||||
|
|
||||||
.. ATTENTION::
|
.. ATTENTION::
|
||||||
|
|
||||||
Before you continue, make sure you have a working understanding of :ref:`how zrepl works <overview-how-replication-works>`
|
Currently, zrepl does not replicate filesystem properties.
|
||||||
and :ref:`what zrepl does to ensure <zrepl-zfs-abstractions>` that replication between sender and receiver is always
|
When receiving a filesystem, it is never mounted (`-u` flag) and `mountpoint=none` is set.
|
||||||
possible without conflicts.
|
This is temporary and being worked on :issue:`24`.
|
||||||
This will help you understand why certain kinds of multi-machine setups do not (yet) work.
|
|
||||||
|
|
||||||
.. NOTE::
|
|
||||||
|
|
||||||
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
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
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.
|
|
||||||
|
|
||||||
On your setup, ensure that
|
|
||||||
|
|
||||||
* all ``filesystems`` filter specifications are disjoint
|
|
||||||
* no ``root_fs`` is a prefix or equal to another ``root_fs``
|
|
||||||
* no ``filesystems`` filter matches any ``root_fs``
|
|
||||||
|
|
||||||
**Exceptions to the rule**:
|
|
||||||
|
|
||||||
* A ``snap`` and ``push`` job on the same machine can match the same ``filesystems``.
|
|
||||||
To avoid interference, only one of the jobs should be pruning snapshots on the sender, the other one should keep all snapshots.
|
|
||||||
Since the jobs won't coordinate, errors in the log are to be expected, but :ref:`zrepl's ZFS abstractions <zrepl-zfs-abstractions>` ensure that ``push`` and ``sink`` can always replicate incrementally.
|
|
||||||
This scenario is detailed in one of the :ref:`quick-start guides <quickstart-backup-to-external-disk>`.
|
|
||||||
|
|
||||||
|
|
||||||
Two Or More Machines
|
.. _jobs-multiple-jobs:
|
||||||
^^^^^^^^^^^^^^^^^^^^
|
|
||||||
|
|
||||||
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).
|
Multiple Jobs & More than 2 Machines
|
||||||
|
------------------------------------
|
||||||
|
|
||||||
**Working setups**:
|
.. ATTENTION::
|
||||||
|
|
||||||
* **Fan-in: N servers replicated to one receiver, disjoint dataset trees.**
|
When using multiple jobs across single or multiple machines, the following rules are critical to avoid race conditions & data loss:
|
||||||
|
|
||||||
* This is the common use case of a centralized backup server.
|
1. The sets of ZFS filesystems matched by the ``filesystems`` filter fields must be disjoint across all jobs configured on a machine.
|
||||||
|
2. The ZFS filesystem subtrees of jobs with ``root_fs`` must be disjoint.
|
||||||
|
3. Across all zrepl instances on all machines in the replication domain, there must be a 1:1 correspondence between active and passive jobs.
|
||||||
|
|
||||||
* Implementation:
|
Explanations & exceptions to above rules are detailed below.
|
||||||
|
|
||||||
* 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>`)
|
If you would like to see improvements to multi-job setups, please `open an issue on GitHub <https://github.com/zrepl/zrepl/issues/new>`_.
|
||||||
* 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}``.
|
No Overlapping
|
||||||
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.
|
Jobs run independently of each other.
|
||||||
|
If two jobs match the same filesystem with their ``filesystems`` filter, they will operate on that filesystem independently and potentially in parallel.
|
||||||
|
For example, if job A prunes snapshots that job B is planning to replicate, the replication will fail because B assumed the snapshot to still be present.
|
||||||
|
However, the next replication attempt will re-examine the situation from scratch and should work.
|
||||||
|
|
||||||
.. _fan-out-replication:
|
N push jobs to 1 sink
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
* **Fan-out: 1 server replicated to N receivers**
|
The :ref:`sink job <job-sink>` namespaces by client identity.
|
||||||
|
It is thus safe to push to one sink job with different client identities.
|
||||||
|
If the push jobs have the same client identity, the filesystems matched by the push jobs must be disjoint to avoid races.
|
||||||
|
|
||||||
* Can be implemented either in a pull or push fashion.
|
N pull jobs from 1 source
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
* **pull setup**: 1 ``pull`` job on each receiver server, each with a corresponding **unique** ``source`` job on the sender server.
|
Multiple pull jobs pulling from the same source have potential for race conditions during pruning:
|
||||||
* **push setup**: 1 ``sink`` job on each receiver server, each with a corresponding **unique** ``push`` job on the sender server.
|
each pull job prunes the source side independently, causing replication-prune and prune-prune races.
|
||||||
|
|
||||||
* It is critical that we have one sending-side job (``source``, ``push``) per receiver.
|
There is currently no way for a pull job to filter which snapshots it should attempt to replicate.
|
||||||
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`).
|
Thus, it is not possible to just manually assert that the prune rules of all pull jobs are disjoint to avoid replication-prune and prune-prune races.
|
||||||
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**:
|
|
||||||
|
|
||||||
* N ``pull`` identities, 1 ``source`` job. Tracking :issue:`380`.
|
|
||||||
|
|
||||||
|
|||||||
@@ -46,8 +46,6 @@ Example Configuration:
|
|||||||
grid: 1x1h(keep=all) | 24x1h | 35x1d | 6x30d
|
grid: 1x1h(keep=all) | 24x1h | 35x1d | 6x30d
|
||||||
regex: "^zrepl_.*"
|
regex: "^zrepl_.*"
|
||||||
# manually created snapshots will be kept forever on receiver
|
# manually created snapshots will be kept forever on receiver
|
||||||
- type: regex
|
|
||||||
regex: "^manual_.*"
|
|
||||||
|
|
||||||
.. DANGER::
|
.. DANGER::
|
||||||
You might have **existing snapshots** of filesystems affected by pruning which you want to keep, i.e. not be destroyed by zrepl.
|
You might have **existing snapshots** of filesystems affected by pruning which you want to keep, i.e. not be destroyed by zrepl.
|
||||||
@@ -67,9 +65,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:
|
||||||
|
|
||||||
@@ -85,91 +82,35 @@ Policy ``grid``
|
|||||||
- type: grid
|
- type: grid
|
||||||
regex: "^zrepl_.*"
|
regex: "^zrepl_.*"
|
||||||
grid: 1x1h(keep=all) | 24x1h | 35x1d | 6x30d
|
grid: 1x1h(keep=all) | 24x1h | 35x1d | 6x30d
|
||||||
│ │ │
|
│ │
|
||||||
└─ 1 repetition of a one-hour interval with keep=all
|
└─ one hour interval
|
||||||
│ │
|
│
|
||||||
└─ 24 repetitions of a one-hour interval with keep=1
|
└─ 24 adjacent one-hour intervals
|
||||||
│
|
|
||||||
└─ 6 repetitions of a 30-day interval with keep=1
|
|
||||||
...
|
...
|
||||||
|
|
||||||
The retention grid can be thought of as a time-based sieve that thins out snapshots as they get older.
|
The retention grid can be thought of as a time-based sieve:
|
||||||
|
The ``grid`` field specifies a list of adjacent time intervals:
|
||||||
|
the left edge of the leftmost (first) interval is the ``creation`` date of the youngest snapshot.
|
||||||
|
All intervals to its right describe time intervals further in the past.
|
||||||
|
|
||||||
|
Each interval carries a maximum number of snapshots to keep.
|
||||||
|
It is specified via ``(keep=N)``, where ``N`` is either ``all`` (all snapshots are kept) or a positive integer.
|
||||||
|
The default value is **keep=1**.
|
||||||
|
|
||||||
The ``grid`` field specifies a list of adjacent time intervals.
|
|
||||||
Each interval is a bucket with a maximum capacity of ``keep`` snapshots.
|
|
||||||
The following procedure happens during pruning:
|
The following procedure happens during pruning:
|
||||||
|
|
||||||
#. The list of snapshots is filtered by the regular expression in ``regex``.
|
#. The list of snapshots is filtered by the regular expression in ``regex``.
|
||||||
Only snapshots names that match the regex are considered for this rule, all others will be pruned unless another rule keeps them.
|
Only snapshots names that match the regex are considered for this rule, all others are not affected.
|
||||||
#. The snapshots that match ``regex`` are placed onto a time axis according to their ``creation`` date.
|
#. The filtered list of snapshots is sorted by ``creation``
|
||||||
The youngest snapshot is on the left, the oldest on the right.
|
#. The left edge of the first interval is aligned to the ``creation`` date of the youngest snapshot
|
||||||
#. The first buckets are placed "under" that axis so that the ``grid`` spec's first bucket's left edge aligns with youngest snapshot.
|
#. A list of buckets is created, one for each interval
|
||||||
#. All subsequent buckets are placed adjacent to their predecessor bucket.
|
#. The list of snapshots is split up into the buckets.
|
||||||
#. Now each snapshot on the axis either falls into one bucket or it is older than our rightmost bucket.
|
#. For each 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'.
|
|
||||||
#. Snapshots older than the rightmost bucket are **not kept** by the grid specification.
|
|
||||||
#. For each bucket, we only keep the ``keep`` oldest snapshots.
|
|
||||||
|
|
||||||
The syntax to describe the bucket list is as follows:
|
#. the contained snapshot list is sorted by creation.
|
||||||
|
#. snapshots from the list, oldest first, are destroyed until the specified ``keep`` count is reached.
|
||||||
|
#. all remaining snapshots on the list are kept.
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
Repeat x Duration (keep=all)
|
|
||||||
|
|
||||||
* The **duration** specifies the length of the interval.
|
|
||||||
* The **keep** count specifies the number of snapshots that fit into the bucket.
|
|
||||||
It can be either a positive integer or ``all`` (all snapshots are kept).
|
|
||||||
* The **repeat** count repeats the bucket definition for the specified number of times.
|
|
||||||
|
|
||||||
**Example**:
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
Assume the following grid specification:
|
|
||||||
|
|
||||||
grid: 1x1h(keep=all) | 2x2h | 1x3h
|
|
||||||
|
|
||||||
This grid specification produces the following constellation of buckets:
|
|
||||||
|
|
||||||
0h 1h 2h 3h 4h 5h 6h 7h 8h 9h
|
|
||||||
| | | | | | | | | |
|
|
||||||
|-Bucket1-|-----Bucket2-------|------Bucket3------|-----------Bucket4-----------|
|
|
||||||
| keep=all| keep=1 | keep=1 | keep=1 |
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Now assume that we have a set of snapshots @a, @b, ..., @D.
|
|
||||||
Snapshot @a is the most recent snapshot.
|
|
||||||
Snapshot @D is the oldest snapshot, it is almost 9 hours older than snapshot @a.
|
|
||||||
We place the snapshots on the same timeline as the buckets:
|
|
||||||
|
|
||||||
|
|
||||||
0h 1h 2h 3h 4h 5h 6h 7h 8h 9h
|
|
||||||
| | | | | | | | | |
|
|
||||||
|-Bucket1-|-----Bucket2-------|------Bucket3------|-----------Bucket4-----------|
|
|
||||||
| keep=all| keep=1 | keep=1 | keep=1 |
|
|
||||||
| | | | |
|
|
||||||
| a b c | d e f g h i j k l m n o p |q r s t u v w x y z |A B C D
|
|
||||||
|
|
||||||
We obtain the following mapping of snapshots to buckets:
|
|
||||||
|
|
||||||
Bucket1: a,b,c
|
|
||||||
Bucket2: d,e,f,g,h,i
|
|
||||||
Bucket3: j,k,l,m,n,o,p
|
|
||||||
Bucket4: q,r,s,t,u,v,w,x,y,z
|
|
||||||
No bucket: A,B,C,D
|
|
||||||
|
|
||||||
For each bucket, we now prune snapshots until it only contains `keep` snapshots.
|
|
||||||
Newer snapshots are destroyed first.
|
|
||||||
Snapshots that do not fall into a bucket are always destroyed.
|
|
||||||
|
|
||||||
Result after pruning:
|
|
||||||
|
|
||||||
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:
|
||||||
|
|
||||||
@@ -184,11 +125,9 @@ Policy ``last_n``
|
|||||||
keep_receiver:
|
keep_receiver:
|
||||||
- type: last_n
|
- type: last_n
|
||||||
count: 10
|
count: 10
|
||||||
regex: ^zrepl_.*$ # optional
|
|
||||||
...
|
...
|
||||||
|
|
||||||
``last_n`` filters the snapshot list by ``regex``, then keeps the last ``count`` snapshots in that list (last = youngest = most recent creation date)
|
``last_n`` keeps the last ``count`` snapshots (last = youngest = most recent creation date).
|
||||||
All snapshots that don't match ``regex`` or exceed ``count`` in the filtered list are destroyed unless matched by other rules.
|
|
||||||
|
|
||||||
.. _prune-keep-regex:
|
.. _prune-keep-regex:
|
||||||
|
|
||||||
|
|||||||
@@ -1,74 +0,0 @@
|
|||||||
.. include:: ../global.rst.inc
|
|
||||||
|
|
||||||
|
|
||||||
Replication Options
|
|
||||||
===================
|
|
||||||
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
- type: push
|
|
||||||
filesystems: ...
|
|
||||||
replication:
|
|
||||||
protection:
|
|
||||||
initial: guarantee_resumability # guarantee_{resumability,incremental,nothing}
|
|
||||||
incremental: guarantee_resumability # guarantee_{resumability,incremental,nothing}
|
|
||||||
concurrency:
|
|
||||||
size_estimates: 4
|
|
||||||
steps: 1
|
|
||||||
|
|
||||||
...
|
|
||||||
|
|
||||||
.. _replication-option-protection:
|
|
||||||
|
|
||||||
``protection`` option
|
|
||||||
--------------------------
|
|
||||||
|
|
||||||
The ``protection`` variable controls the degree to which a replicated filesystem is protected from getting out of sync through a zrepl pruner or external tools that destroy snapshots.
|
|
||||||
zrepl can guarantee :ref:`resumability <step-holds>` or just :ref:`incremental replication <replication-cursor-and-last-received-hold>`.
|
|
||||||
|
|
||||||
``guarantee_resumability`` is the **default** value and guarantees that a replication step is always resumable and that incremental replication will always be possible.
|
|
||||||
The implementation uses replication cursors, last-received-hold and step holds.
|
|
||||||
|
|
||||||
``guarantee_incremental`` only guarantees that incremental replication will always be possible.
|
|
||||||
If a step ``from -> to`` is interrupted and its `to` snapshot is destroyed, zrepl will remove the half-received ``to``'s resume state and start a new step ``from -> to2``.
|
|
||||||
The implementation uses replication cursors, tentative replication cursors and last-received-hold.
|
|
||||||
|
|
||||||
``guarantee_nothing`` does not make any guarantees with regards to keeping sending and receiving side in sync.
|
|
||||||
No bookmarks or holds are created to protect sender and receiver from diverging.
|
|
||||||
|
|
||||||
**Tradeoffs**
|
|
||||||
|
|
||||||
Using ``guarantee_incremental`` instead of ``guarantee_resumability`` obviously removes the resumability guarantee.
|
|
||||||
This means that replication progress is no longer monotonic which might lead to a replication setup that never makes progress if mid-step interruptions are too frequent (e.g. frequent network outages).
|
|
||||||
However, the advantage and :issue:`reason for existence <288>` of the ``incremental`` mode is that it allows the pruner to delete snapshots of interrupted replication steps
|
|
||||||
which is useful if replication happens so rarely (or fails so frequently) that the amount of disk space exclusively referenced by the step's snapshots becomes intolerable.
|
|
||||||
|
|
||||||
.. NOTE::
|
|
||||||
|
|
||||||
When changing this flag, obsoleted zrepl-managed bookmarks and holds will be destroyed on the next replication step that is attempted for each filesystem.
|
|
||||||
|
|
||||||
|
|
||||||
.. _replication-option-concurrency:
|
|
||||||
|
|
||||||
``concurrency`` option
|
|
||||||
----------------------
|
|
||||||
|
|
||||||
The ``concurrency`` options control the maximum amount of concurrency during replication.
|
|
||||||
The default values allow some concurrency during size estimation but no parallelism for the actual replication.
|
|
||||||
|
|
||||||
* ``concurrency.steps`` (default = 1) controls the maximum number of concurrently executed :ref:`replication steps <overview-how-replication-works>`.
|
|
||||||
The planning step for each file system is counted as a single step.
|
|
||||||
* ``concurrency.size_estimates`` (default = 4) controls the maximum number of concurrent step size estimations done by the job.
|
|
||||||
|
|
||||||
Note that initial replication cannot start replicating child filesystems before the parent filesystem's initial replication step has completed.
|
|
||||||
|
|
||||||
Some notes on tuning these values:
|
|
||||||
|
|
||||||
* Disk: Size estimation is less I/O intensive than step execution because it does not need to access the data blocks.
|
|
||||||
* CPU: Size estimation is usually a dense CPU burst whereas step execution CPU utilization is stretched out over time because of disk IO.
|
|
||||||
Faster disks, sending a compressed dataset in :ref:`plain mode <zfs-background-knowledge-plain-vs-raw-sends>` and the zrepl transport mode all contribute to higher CPU requirements.
|
|
||||||
* Network bandwidth: Size estimation does not consume meaningful amounts of bandwidth, step execution does.
|
|
||||||
* :ref:`zrepl ZFS abstractions <zrepl-zfs-abstractions>`: for each replication step zrepl needs to update its ZFS abstractions through the ``zfs`` command which often waits multiple seconds for the zpool to sync.
|
|
||||||
Thus, if the actual send & recv time of a step is small compared to the time spent on zrepl ZFS abstractions then increasing step execution concurrency will result in a lower overall turnaround time.
|
|
||||||
@@ -9,65 +9,24 @@ Send & Recv Options
|
|||||||
Send Options
|
Send Options
|
||||||
~~~~~~~~~~~~
|
~~~~~~~~~~~~
|
||||||
|
|
||||||
:ref:`Source<job-source>` and :ref:`push<job-push>` jobs have an optional ``send`` configuration section.
|
|
||||||
|
|
||||||
::
|
::
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
- type: push
|
- type: push
|
||||||
filesystems: ...
|
filesystems: ...
|
||||||
send:
|
send:
|
||||||
# flags from the table below go here
|
encrypted: true
|
||||||
|
step_holds:
|
||||||
|
disable_incremental: false
|
||||||
...
|
...
|
||||||
|
|
||||||
The following table specifies the list of (boolean) options.
|
:ref:`Source<job-source>` and :ref:`push<job-push>` jobs have an optional ``send`` configuration section.
|
||||||
Flags with an entry in the ``zfs send`` column map directly to the zfs send CLI flags.
|
|
||||||
zrepl does not perform feature checks for these flags.
|
|
||||||
If you enable a flag that is not supported by the installed version of ZFS, the zfs error will show up at runtime in the logs and zrepl status.
|
|
||||||
See the `upstream man page <https://openzfs.github.io/openzfs-docs/man/8/zfs-send.8.html>`_ (``man zfs-send``) for their semantics.
|
|
||||||
|
|
||||||
.. list-table::
|
``encryption`` option
|
||||||
:widths: 20 10 70
|
---------------------
|
||||||
:header-rows: 1
|
|
||||||
|
|
||||||
* - ``send.``
|
The ``encryption`` variable controls whether the matched filesystems are sent as `OpenZFS native encryption <http://open-zfs.org/wiki/ZFS-Native_Encryption>`_ raw sends.
|
||||||
- ``zfs send``
|
More specifically, if ``encryption=true``, zrepl
|
||||||
- Comment
|
|
||||||
* - ``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``
|
|
||||||
- ``-w``
|
|
||||||
- Use ``encrypted`` to only allow encrypted sends. Mixed sends are not supported.
|
|
||||||
* - ``send_properties``
|
|
||||||
- ``-p``
|
|
||||||
- **Be careful**, read the :ref:`note on property replication below <job-note-property-replication>`.
|
|
||||||
* - ``backup_properties``
|
|
||||||
- ``-b``
|
|
||||||
- **Be careful**, read the :ref:`note on property replication below <job-note-property-replication>`.
|
|
||||||
* - ``large_blocks``
|
|
||||||
- ``-L``
|
|
||||||
- **Potential data loss on OpenZFS < 2.0**, see :ref:`warning below <job-send-options-large-blocks>`.
|
|
||||||
* - ``compressed``
|
|
||||||
- ``-c``
|
|
||||||
-
|
|
||||||
* - ``embedded_data``
|
|
||||||
- ``-e``
|
|
||||||
-
|
|
||||||
* - ``saved``
|
|
||||||
- ``-S``
|
|
||||||
-
|
|
||||||
|
|
||||||
.. _job-send-options-encrypted:
|
|
||||||
|
|
||||||
``encrypted``
|
|
||||||
-------------
|
|
||||||
|
|
||||||
The ``encrypted`` option controls whether the matched filesystems are sent as `OpenZFS native encryption <http://open-zfs.org/wiki/ZFS-Native_Encryption>`_ raw sends.
|
|
||||||
More specifically, if ``encrypted=true``, zrepl
|
|
||||||
|
|
||||||
* checks for any of the filesystems matched by ``filesystems`` whether the ZFS ``encryption`` property indicates that the filesystem is actually encrypted with ZFS native encryption and
|
* checks for any of the filesystems matched by ``filesystems`` whether the ZFS ``encryption`` property indicates that the filesystem is actually encrypted with ZFS native encryption and
|
||||||
* invokes the ``zfs send`` subcommand with the ``-w`` option (raw sends) and
|
* invokes the ``zfs send`` subcommand with the ``-w`` option (raw sends) and
|
||||||
@@ -75,208 +34,32 @@ More specifically, if ``encrypted=true``, zrepl
|
|||||||
|
|
||||||
Filesystems matched by ``filesystems`` that are not encrypted are not sent and will cause error log messages.
|
Filesystems matched by ``filesystems`` that are not encrypted are not sent and will cause error log messages.
|
||||||
|
|
||||||
If ``encrypted=false``, zrepl expects that filesystems matching ``filesystems`` are not encrypted or have loaded encryption keys.
|
If ``encryption=false``, zrepl expects that filesystems matching ``filesystems`` are not encrypted or have loaded encryption keys.
|
||||||
|
|
||||||
|
.. _job-send-option-step-holds-disable-incremental:
|
||||||
|
|
||||||
|
``step_holds.disable_incremental`` option
|
||||||
|
-----------------------------------------
|
||||||
|
|
||||||
|
The ``step_holds.disable_incremental`` variable controls whether the creation of :ref:`step holds <step-holds-and-bookmarks>` should be disabled for incremental replication.
|
||||||
|
The default value is ``false``.
|
||||||
|
|
||||||
|
Disabling step holds has the disadvantage that steps :ref:`might not be resumable <step-holds-and-bookmarks>` if interrupted.
|
||||||
|
Non-resumability means that replication progress is no longer monotonic which might result in a replication setup that never makes progress if mid-step interruptions are too frequent (e.g. frequent network outages).
|
||||||
|
|
||||||
|
However, the advantage and :issue:`reason for existence <288>` of this flag is that it allows the pruner to delete snapshots of interrupted replication steps
|
||||||
|
which is useful if replication happens so rarely (or fails so frequently) that the amount of disk space exclusively referenced by the step's snapshots becomes intolerable.
|
||||||
|
|
||||||
.. NOTE::
|
.. NOTE::
|
||||||
|
|
||||||
Use ``encrypted`` instead of ``raw`` to make your intent clear that zrepl must only replicate filesystems that are actually encrypted by OpenZFS native encryption.
|
When setting this flag to ``true``, existing step holds for the job will be destroyed on the next replication attempt.
|
||||||
It is meant as a safeguard to prevent unintended sends of unencrypted filesystems in raw mode.
|
|
||||||
|
|
||||||
.. _job-send-options-properties:
|
|
||||||
|
|
||||||
``properties``
|
|
||||||
--------------
|
|
||||||
Sends the dataset properties along with snapshots.
|
|
||||||
Please be careful with this option and read the :ref:`note on property replication below <job-note-property-replication>`.
|
|
||||||
|
|
||||||
.. _job-send-options-backup-properties:
|
|
||||||
|
|
||||||
``backup_properties``
|
|
||||||
---------------------
|
|
||||||
|
|
||||||
When properties are modified on a filesystem that was received from a send stream with ``send.properties=true``, ZFS archives the original received value internally.
|
|
||||||
This also applies to :ref:`inheriting or overriding properties during zfs receive <job-recv-options--inherit-and-override>`.
|
|
||||||
|
|
||||||
When sending those received filesystems another hop, the ``backup_properties`` flag instructs ZFS to send the original property values rather than the current locally set values.
|
|
||||||
|
|
||||||
This is useful for replicating properties across multiple levels of backup machines.
|
|
||||||
**Example:**
|
|
||||||
Suppose we want to flow snapshots from Machine A to B, then from B to C.
|
|
||||||
A will enable the :ref:`properties send option <job-send-options-properties>`.
|
|
||||||
B will want to override :ref:`critical properties such as mountpoint or canmount <job-note-property-replication>`.
|
|
||||||
But the job that replicates from B to C should be sending the original property values received from A.
|
|
||||||
Thus, B sets the ``backup_properties`` option.
|
|
||||||
|
|
||||||
Please be careful with this option and read the :ref:`note on property replication below <job-note-property-replication>`.
|
|
||||||
|
|
||||||
.. _job-send-options-large-blocks:
|
|
||||||
|
|
||||||
``large_blocks``
|
|
||||||
----------------
|
|
||||||
|
|
||||||
This flag should not be changed after initial replication.
|
|
||||||
Prior to `OpenZFS commit 7bcb7f08 <https://github.com/openzfs/zfs/pull/10383/files#diff-4c1e47568f46fb63546e984943b09a3e6b051e2242649523f7835bbdfe2a9110R337-R342>`_
|
|
||||||
it was possible to change this setting which resulted in **data loss on the receiver**.
|
|
||||||
The commit in question is included in OpenZFS 2.0 and works around the problem by prohibiting receives of incremental streams with a flipped setting.
|
|
||||||
|
|
||||||
.. WARNING::
|
|
||||||
|
|
||||||
This bug has **not been fixed in the OpenZFS 0.8 releases** which means that changing this flag after initial replication might cause **data loss** on the receiver.
|
|
||||||
|
|
||||||
.. _job-recv-options:
|
.. _job-recv-options:
|
||||||
|
|
||||||
Recv Options
|
Recv Options
|
||||||
~~~~~~~~~~~~
|
~~~~~~~~~~~~
|
||||||
|
|
||||||
:ref:`Sink<job-sink>` and :ref:`pull<job-pull>` jobs have an optional ``recv`` configuration section:
|
:ref:`Sink<job-sink>` and :ref:`pull<job-pull>` jobs have an optional ``recv`` configuration section.
|
||||||
|
However, there are currently no variables to configure there.
|
||||||
::
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
- type: pull
|
|
||||||
recv:
|
|
||||||
properties:
|
|
||||||
inherit:
|
|
||||||
- "mountpoint"
|
|
||||||
override: {
|
|
||||||
"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:
|
|
||||||
|
|
||||||
``properties``
|
|
||||||
--------------
|
|
||||||
|
|
||||||
``override`` maps directly to the `zfs recv -o flag <https://openzfs.github.io/openzfs-docs/man/8/zfs-recv.8.html>`_.
|
|
||||||
Property name-value pairs specified in this map will apply to all received filesystems, regardless of whether the send stream contains properties or not.
|
|
||||||
|
|
||||||
``inherit`` maps directly to the `zfs recv -x flag <https://openzfs.github.io/openzfs-docs/man/8/zfs-recv.8.html>`_.
|
|
||||||
Property names specified in this list will be inherited from the receiving side's parent filesystem (e.g. ``root_fs``).
|
|
||||||
|
|
||||||
With both options, the sending side's property value is still stored on the receiver, but the local override or inherit is the one that takes effect.
|
|
||||||
You can send the original properties from the first receiver to another receiver using :ref:`send.backup_properties<job-send-options-backup-properties>`.
|
|
||||||
|
|
||||||
|
|
||||||
.. _job-note-property-replication:
|
|
||||||
|
|
||||||
A Note on Property Replication
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
If a send stream contains properties, as per ``send.properties`` or ``send.backup_properties``,
|
|
||||||
the default ZFS behavior is to use those properties on the receiving side, verbatim.
|
|
||||||
|
|
||||||
In many use cases for zrepl, this can have devastating consequences.
|
|
||||||
For example, when backing up a filesystem that has ``mountpoint=/`` to a storage server,
|
|
||||||
that storage server's root filesystem will be shadowed by the received file system on some platforms.
|
|
||||||
Also, many scripts and tools use ZFS user properties for configuration and do not check the property source (``local`` vs. ``received``).
|
|
||||||
If they are installed on the receiving side as well as the sending side, property replication could have unintended effects.
|
|
||||||
|
|
||||||
**zrepl currently does not provide any automatic safe-guards for property replication:**
|
|
||||||
|
|
||||||
* Make sure to read the entire man page on zfs recv (`man zfs recv <https://openzfs.github.io/openzfs-docs/man/8/zfs-recv.8.html>`_) before enabling this feature.
|
|
||||||
* Use ``recv.properties.override`` whenever possible, e.g. for ``mountpoint=none`` or ``canmount=off``.
|
|
||||||
* Use ``recv.properties.inherit`` if that makes more sense to you.
|
|
||||||
|
|
||||||
Below is an **non-exhaustive list of problematic properties**.
|
|
||||||
Please open a pull request if you find a property that is missing from this list.
|
|
||||||
(Both with regards to core ZFS tools and other software in the broader ecosystem.)
|
|
||||||
|
|
||||||
Mount behaviour
|
|
||||||
---------------
|
|
||||||
|
|
||||||
* ``mountpoint``
|
|
||||||
* ``canmount``
|
|
||||||
* ``overlay``
|
|
||||||
|
|
||||||
Note: inheriting or overriding the ``mountpoint`` property on ZVOLs fails in ``zfs recv``.
|
|
||||||
This is an `issue in OpenZFS <https://github.com/openzfs/zfs/issues/11416>`_ .
|
|
||||||
As a workaround, consider creating separate zrepl jobs for your ZVOL and filesystem datasets.
|
|
||||||
Please comment at zrepl :issue:`430` if you encounter this issue and/or would like zrepl to automatically work around it.
|
|
||||||
|
|
||||||
|
|
||||||
Systemd
|
|
||||||
-------
|
|
||||||
|
|
||||||
With systemd, you should also consider the properties processed by the `zfs-mount-generator <https://manpages.debian.org/buster-backports/zfsutils-linux/zfs-mount-generator.8.en.html>`_ .
|
|
||||||
|
|
||||||
Most notably:
|
|
||||||
|
|
||||||
* ``org.openzfs.systemd:ignore``
|
|
||||||
* ``org.openzfs.systemd:wanted-by``
|
|
||||||
* ``org.openzfs.systemd:required-by``
|
|
||||||
|
|
||||||
Encryption
|
|
||||||
----------
|
|
||||||
|
|
||||||
If the sender filesystems are encrypted but the sender does :ref:`plain sends <zfs-background-knowledge-plain-vs-raw-sends>`
|
|
||||||
and property replication is enabled, the receiver must :ref:`inherit the following properties<job-recv-options--inherit-and-override>`:
|
|
||||||
|
|
||||||
* ``keylocation``
|
|
||||||
* ``keyformat``
|
|
||||||
* ``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,117 +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
|
|
||||||
hooks: ...
|
|
||||||
pruning: ...
|
|
||||||
|
|
||||||
The ``periodic`` snapshotter ensures that snapshots are taken in the specified ``interval``.
|
|
||||||
If you use zrepl for backup, this translates into your recovery point objective (RPO).
|
|
||||||
To meet your RPO, you still need to monitor that replication, which happens asynchronously to snapshotting, actually works.
|
|
||||||
|
|
||||||
It is desirable to get all ``filesystems`` snapshotted simultaneously because it results in a more consistent backup.
|
|
||||||
To accomplish this while still maintaining the ``interval``, the ``periodic`` snapshotter attempts to get the snapshotting rhythms in sync.
|
|
||||||
To find that sync point, the most recent snapshot, created by the snapshotter, in any of the matched ``filesystems`` is used.
|
|
||||||
A filesystem that does not have snapshots by the snapshotter has lower priority than filesystem that do, and thus might not be snapshotted (and replicated) until it is snapshotted at the next sync point.
|
A filesystem that does not have snapshots by the snapshotter has lower priority than filesystem that do, and thus might not be snapshotted (and replicated) until it is snapshotted at the next sync point.
|
||||||
The snapshotter uses the ``prefix`` to identify which snapshots it created.
|
|
||||||
|
|
||||||
.. _job-snapshotting--cron:
|
For ``push`` jobs, replication is automatically triggered after all filesystems have been snapshotted.
|
||||||
|
|
||||||
|
Note that the ``zrepl signal wakeup JOB`` subcommand does not trigger snapshotting.
|
||||||
|
|
||||||
``cron`` Snapshotting
|
|
||||||
---------------------
|
|
||||||
|
|
||||||
::
|
::
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
- type: snap
|
- type: push
|
||||||
filesystems: { ... }
|
filesystems: {
|
||||||
snapshotting:
|
"<": true,
|
||||||
type: cron
|
"tmp": false
|
||||||
prefix: zrepl_
|
}
|
||||||
# (second, optional) minute hour day-of-month month day-of-week
|
snapshotting:
|
||||||
# This example takes snapshots daily at 3:00.
|
type: periodic
|
||||||
cron: "0 3 * * *"
|
prefix: zrepl_
|
||||||
pruning: ...
|
interval: 10m
|
||||||
|
hooks: ...
|
||||||
|
...
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
``manual`` Snapshotting
|
* Existing infrastructure for automatic snapshots: you only want to use this zrepl job for replication.
|
||||||
-----------------------
|
* Handling snapshotting through a separate ``snap`` job.
|
||||||
|
|
||||||
|
Note that you will have to trigger replication manually using the ``zrepl signal wakeup JOB`` subcommand in that case.
|
||||||
|
|
||||||
::
|
::
|
||||||
|
|
||||||
jobs:
|
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
|
||||||
@@ -195,7 +131,7 @@ The following environment variables are set:
|
|||||||
* ``ZREPL_SNAPNAME``: the zrepl-generated snapshot name (e.g. ``zrepl_20380119_031407_000``)
|
* ``ZREPL_SNAPNAME``: the zrepl-generated snapshot name (e.g. ``zrepl_20380119_031407_000``)
|
||||||
* ``ZREPL_DRYRUN``: set to ``"true"`` if a dry run is in progress so scripts can print, but not run, their commands
|
* ``ZREPL_DRYRUN``: set to ``"true"`` if a dry run is in progress so scripts can print, but not run, their commands
|
||||||
|
|
||||||
An empty template hook can be found in :sampleconf:`/hooks/template.sh`.
|
An empty template hook can be found in :sampleconf:`hooks/template.sh`.
|
||||||
|
|
||||||
.. _job-hook-type-postgres-checkpoint:
|
.. _job-hook-type-postgres-checkpoint:
|
||||||
|
|
||||||
|
|||||||
@@ -91,8 +91,7 @@ The ``tls`` transport uses TCP + TLS with client authentication using client cer
|
|||||||
The client identity is the common name (CN) presented in the client certificate.
|
The client identity is the common name (CN) presented in the client certificate.
|
||||||
|
|
||||||
It is recommended to set up a dedicated CA infrastructure for this transport, e.g. using OpenVPN's `EasyRSA <https://github.com/OpenVPN/easy-rsa>`_.
|
It is recommended to set up a dedicated CA infrastructure for this transport, e.g. using OpenVPN's `EasyRSA <https://github.com/OpenVPN/easy-rsa>`_.
|
||||||
For a simple 2-machine setup, mutual TLS might also be sufficient.
|
For a simple 2-machine setup, see the :ref:`instructions below<transport-tcp+tlsclientauth-2machineopenssl>`.
|
||||||
We provide :ref:`copy-pastable instructions to generate the certificates below <transport-tcp+tlsclientauth-certgen>`.
|
|
||||||
|
|
||||||
The implementation uses `Go's TLS library <https://golang.org/pkg/crypto/tls/>`_.
|
The implementation uses `Go's TLS library <https://golang.org/pkg/crypto/tls/>`_.
|
||||||
Since Go binaries are statically linked, you or your distribution need to recompile zrepl when vulnerabilities in that library are disclosed.
|
Since Go binaries are statically linked, you or your distribution need to recompile zrepl when vulnerabilities in that library are disclosed.
|
||||||
@@ -104,16 +103,6 @@ If intermediate CAs are used, the **full chain** must be present in either in th
|
|||||||
Regardless, the client's certificate must be first in the ``cert`` file, with each following certificate directly certifying the one preceding it (see `TLS's specification <https://tools.ietf.org/html/rfc5246#section-7.4.2>`_).
|
Regardless, the client's certificate must be first in the ``cert`` file, with each following certificate directly certifying the one preceding it (see `TLS's specification <https://tools.ietf.org/html/rfc5246#section-7.4.2>`_).
|
||||||
This is the common default when using a CA management tool.
|
This is the common default when using a CA management tool.
|
||||||
|
|
||||||
.. NOTE::
|
|
||||||
|
|
||||||
As of Go 1.15 (zrepl 0.3.0 and newer), the Go TLS / x509 library **requrires Subject Alternative Names**
|
|
||||||
be present in certificates. You might need to re-generate your certificates using one of the :ref:`two alternatives
|
|
||||||
provided below<transport-tcp+tlsclientauth-certgen>`.
|
|
||||||
|
|
||||||
Note further that zrepl continues to use the CommonName field to assign client identities.
|
|
||||||
Hence, we recommend to keep the Subject Alternative Name and the CommonName in sync.
|
|
||||||
|
|
||||||
|
|
||||||
Serve
|
Serve
|
||||||
~~~~~
|
~~~~~
|
||||||
|
|
||||||
@@ -158,25 +147,45 @@ The ``server_cn`` specifies the expected common name (CN) of the server's certif
|
|||||||
It overrides the hostname specified in ``address``.
|
It overrides the hostname specified in ``address``.
|
||||||
The connection fails if either do not match.
|
The connection fails if either do not match.
|
||||||
|
|
||||||
.. _transport-tcp+tlsclientauth-certgen:
|
|
||||||
|
|
||||||
.. _transport-tcp+tlsclientauth-2machineopenssl:
|
.. _transport-tcp+tlsclientauth-2machineopenssl:
|
||||||
|
|
||||||
Mutual-TLS between Two Machines
|
Self-Signed Certificates
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Tools like `EasyRSA <https://github.com/OpenVPN/easy-rsa>`_ make it easy to manage CA infrastructure for multiple clients, e.g. a central zrepl backup server (in sink mode).
|
||||||
However, for a two-machine setup, self-signed certificates distributed using an out-of-band mechanism will also work just fine:
|
However, for a two-machine setup, self-signed certificates distributed using an out-of-band mechanism will also work just fine:
|
||||||
|
|
||||||
Suppose you have a push-mode setup, with `backups.example.com` running the :ref:`sink job <job-sink>`, and `prod.example.com` running the :ref:`push job <job-push>`.
|
Suppose you have a push-mode setup, with `backups.example.com` running the :ref:`sink job <job-sink>`, and `prod.example.com` running the :ref:`push job <job-push>`.
|
||||||
Run the following OpenSSL commands on each host, substituting HOSTNAME in both filenames and the interactive input prompt by OpenSSL:
|
Run the following OpenSSL commands on each host, substituting HOSTNAME in both filenames and the interactive input prompt by OpenSSL:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
:emphasize-lines: 1-5,24
|
||||||
|
|
||||||
(name=HOSTNAME; openssl req -x509 -sha256 -nodes \
|
openssl req -x509 -sha256 -nodes \
|
||||||
-newkey rsa:4096 \
|
-newkey rsa:4096 \
|
||||||
-days 365 \
|
-days 365 \
|
||||||
-keyout $name.key \
|
-keyout HOSTNAME.key \
|
||||||
-out $name.crt -addext "subjectAltName = DNS:$name" -subj "/CN=$name")
|
-out HOSTNAME.crt
|
||||||
|
|
||||||
|
#Generating a 4096 bit RSA private key
|
||||||
|
#................++++
|
||||||
|
#.++++
|
||||||
|
#writing new private key to 'backups.key'
|
||||||
|
#-----
|
||||||
|
#You are about to be asked to enter information that will be incorporated
|
||||||
|
#into your certificate request.
|
||||||
|
#What you are about to enter is what is called a Distinguished Name or a DN.
|
||||||
|
#There are quite a few fields but you can leave some blank
|
||||||
|
#For some fields there will be a default value,
|
||||||
|
#If you enter '.', the field will be left blank.
|
||||||
|
#-----
|
||||||
|
#Country Name (2 letter code) [XX]:
|
||||||
|
#State or Province Name (full name) []:
|
||||||
|
#Locality Name (eg, city) [Default City]:
|
||||||
|
#Organization Name (eg, company) [Default Company Ltd]:
|
||||||
|
#Organizational Unit Name (eg, section) []:
|
||||||
|
#Common Name (eg, your name or your server's hostname) []:HOSTNAME
|
||||||
|
#Email Address []:
|
||||||
|
|
||||||
Now copy each machine's ``HOSTNAME.crt`` to the other machine's ``/etc/zrepl/HOSTNAME.crt``, for example using `scp`.
|
Now copy each machine's ``HOSTNAME.crt`` to the other machine's ``/etc/zrepl/HOSTNAME.crt``, for example using `scp`.
|
||||||
The serve & connect configuration will thus look like the following:
|
The serve & connect configuration will thus look like the following:
|
||||||
@@ -207,36 +216,6 @@ The serve & connect configuration will thus look like the following:
|
|||||||
...
|
...
|
||||||
|
|
||||||
|
|
||||||
Certificate Authority using EasyRSA
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
For more than two machines, it might make sense to set up a CA infrastructure.
|
|
||||||
Tools like `EasyRSA <https://github.com/OpenVPN/easy-rsa>`_ make this very easy:
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
HOSTS=(backupserver prod1 prod2 prod3)
|
|
||||||
|
|
||||||
curl -L https://github.com/OpenVPN/easy-rsa/releases/download/v3.0.7/EasyRSA-3.0.7.tgz > EasyRSA-3.0.7.tgz
|
|
||||||
echo "157d2e8c115c3ad070c1b2641a4c9191e06a32a8e50971847a718251eeb510a8 EasyRSA-3.0.7.tgz" | sha256sum -c
|
|
||||||
rm -rf EasyRSA-3.0.7
|
|
||||||
tar -xf EasyRSA-3.0.7.tgz
|
|
||||||
cd EasyRSA-3.0.7
|
|
||||||
./easyrsa
|
|
||||||
./easyrsa init-pki
|
|
||||||
./easyrsa build-ca nopass
|
|
||||||
|
|
||||||
for host in "${HOSTS[@]}"; do
|
|
||||||
./easyrsa build-serverClient-full $host nopass
|
|
||||||
echo cert for host $host available at pki/issued/$host.crt
|
|
||||||
echo key for host $host available at pki/private/$host.key
|
|
||||||
done
|
|
||||||
echo ca cert available at pki/ca.crt
|
|
||||||
|
|
||||||
|
|
||||||
.. _transport-ssh+stdinserver:
|
.. _transport-ssh+stdinserver:
|
||||||
|
|
||||||
``ssh+stdinserver`` Transport
|
``ssh+stdinserver`` Transport
|
||||||
|
|||||||
@@ -58,11 +58,13 @@ for latest_patch in latest_by_major_minor:
|
|||||||
cmdline.append("--whitelist-tags")
|
cmdline.append("--whitelist-tags")
|
||||||
cmdline.append(f"^{re.escape(latest_patch.orig)}$")
|
cmdline.append(f"^{re.escape(latest_patch.orig)}$")
|
||||||
|
|
||||||
# we want flexibility to update docs for the latest stable release
|
# we want to render the latest non-rc version as the default page
|
||||||
# => we have a branch for that, called `stable` which we move manually
|
# (latest_by_major_minor is already sorted)
|
||||||
# TODO: in the future, have f"stable-{latest_by_major_minor[-1]}"
|
default_version = latest_by_major_minor[-1]
|
||||||
default_version = "stable"
|
for tag in reversed(latest_by_major_minor):
|
||||||
cmdline.extend(["--whitelist-branches", default_version])
|
if tag.rc == 0:
|
||||||
|
default_version = tag
|
||||||
|
break
|
||||||
|
|
||||||
cmdline.extend(["--root-ref", f"{default_version}"])
|
cmdline.extend(["--root-ref", f"{default_version}"])
|
||||||
cmdline.extend(["--banner-main-ref", f"{default_version}"])
|
cmdline.extend(["--banner-main-ref", f"{default_version}"])
|
||||||
|
|||||||
+3
-4
@@ -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,14 +13,13 @@
|
|||||||
: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/endpoint.svg?url=https%3A%2F%2Fshieldsio-patreon.herokuapp.com%2Fzrepl%2Fpledges&style=flat&color=yellow
|
||||||
: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>`
|
||||||
|
|||||||
+4
-10
@@ -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)
|
||||||
@@ -61,12 +61,7 @@ Main Features
|
|||||||
* [x] Automatic ZFS holds during send & receive
|
* [x] Automatic ZFS holds during send & receive
|
||||||
* [x] Automatic bookmark \& hold management for guaranteed incremental send & recv
|
* [x] Automatic bookmark \& hold management for guaranteed incremental send & recv
|
||||||
* [x] Encrypted raw send & receive to untrusted receivers (OpenZFS native encryption)
|
* [x] Encrypted raw send & receive to untrusted receivers (OpenZFS native encryption)
|
||||||
* [x] Properties send & receive
|
* [ ] Compressed send & receive
|
||||||
* [x] Compressed send & receive
|
|
||||||
* [x] Large blocks send & receive
|
|
||||||
* [x] Embedded data send & receive
|
|
||||||
* [x] Resume state send & receive
|
|
||||||
* [x] Bandwidth limiting
|
|
||||||
|
|
||||||
* **Automatic snapshot management**
|
* **Automatic snapshot management**
|
||||||
|
|
||||||
@@ -137,6 +132,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
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ Installation
|
|||||||
installation/user-privileges
|
installation/user-privileges
|
||||||
installation/packages
|
installation/packages
|
||||||
installation/apt-repos
|
installation/apt-repos
|
||||||
installation/rpm-repos
|
|
||||||
installation/compile-from-source
|
installation/compile-from-source
|
||||||
installation/freebsd-jail-with-iocage
|
installation/freebsd-jail-with-iocage
|
||||||
installation/what-next
|
installation/what-next
|
||||||
|
|||||||
@@ -7,34 +7,23 @@ Debian / Ubuntu APT repositories
|
|||||||
We maintain APT repositories for Debian, Ubuntu and derivatives.
|
We maintain APT repositories for Debian, Ubuntu and derivatives.
|
||||||
The fingerprint of the signing key is ``E101 418F D3D6 FBCB 9D65 A62D 7086 99FC 5F2E BF16``.
|
The fingerprint of the signing key is ``E101 418F D3D6 FBCB 9D65 A62D 7086 99FC 5F2E BF16``.
|
||||||
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 the packaging repository <https://github.com/zrepl/debian-binary-packaging>`_ 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::
|
||||||
|
|
||||||
Until zrepl reaches 1.0, the repositories will be updated to the latest zrepl release immediately.
|
Until zrepl reaches 1.0, all APT repositories will be updated to the latest zrepl release immediately.
|
||||||
This includes breaking changes between zrepl versions.
|
This includes breaking changes between zrepl versions.
|
||||||
Use ``apt-mark hold zrepl`` to prevent upgrades of zrepl.
|
Use ``apt-mark hold zrepl`` to prevent upgrades of zrepl.
|
||||||
|
|||||||
@@ -9,35 +9,37 @@ Producing a release requires **Go 1.11** or newer and **Python 3** + **pip3** +
|
|||||||
A tutorial to install Go is available over at `golang.org <https://golang.org/doc/install>`_.
|
A tutorial to install Go is available over at `golang.org <https://golang.org/doc/install>`_.
|
||||||
Python and pip3 should probably be installed via your distro's package manager.
|
Python and pip3 should probably be installed via your distro's package manager.
|
||||||
|
|
||||||
::
|
|
||||||
cd to/your/zrepl/checkout
|
|
||||||
python3 -m venv3
|
|
||||||
source venv3/bin/activate
|
|
||||||
./lazy.sh devsetup
|
|
||||||
make release
|
|
||||||
# build artifacts are available in ./artifacts/release
|
|
||||||
|
|
||||||
The Python venv is used for the documentation build dependencies.
|
|
||||||
If you just want to build the zrepl binary, leave it out and use `./lazy.sh godep` instead.
|
|
||||||
|
|
||||||
Alternatively, you can use the Docker build process:
|
Alternatively, you can use the Docker build process:
|
||||||
it is used to produce the official zrepl `binary releases`_
|
it is used to produce the official zrepl `binary releases`_
|
||||||
and serves as a reference for build dependencies and procedure:
|
and serves as a reference for build dependencies and procedure:
|
||||||
|
|
||||||
::
|
::
|
||||||
|
|
||||||
cd to/your/zrepl/checkout
|
git clone https://github.com/zrepl/zrepl.git && \
|
||||||
# make sure your user has access to the docker socket
|
cd zrepl && \
|
||||||
make release-docker
|
sudo docker build -t zrepl_build -f build.Dockerfile . && \
|
||||||
# if you want .deb or .rpm packages, invoke the follwoing
|
sudo docker run -it --rm \
|
||||||
# targets _after_ you invoked release-docker
|
-v "${PWD}:/src" \
|
||||||
make deb-docker
|
--user "$(id -u):$(id -g)" \
|
||||||
make rpm-docker
|
zrepl_build make release
|
||||||
# build artifacts are available in ./artifacts/release
|
|
||||||
# packages are available in ./artifacts
|
|
||||||
|
|
||||||
|
Alternatively, you can install build dependencies on your local system and then build in your ``$GOPATH``:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
mkdir -p "${GOPATH}/src/github.com/zrepl/zrepl"
|
||||||
|
git clone https://github.com/zrepl/zrepl.git "${GOPATH}/src/github.com/zrepl/zrepl"
|
||||||
|
cd "${GOPATH}/src/github.com/zrepl/zrepl"
|
||||||
|
python3 -m venv3
|
||||||
|
source venv3/bin/activate
|
||||||
|
./lazy.sh devsetup
|
||||||
|
make release
|
||||||
|
|
||||||
|
The Python venv is used for the documentation build dependencies.
|
||||||
|
If you just want to build the zrepl binary, leave it out and use `./lazy.sh godep` instead.
|
||||||
|
Either way, all build results are located in the ``artifacts/`` directory.
|
||||||
|
|
||||||
.. NOTE::
|
.. NOTE::
|
||||||
|
|
||||||
It is your job to install the built binary in the zrepl users's ``$PATH``, e.g. ``/usr/local/bin/zrepl``.
|
It is your job to install the appropriate binary in the zrepl users's ``$PATH``, e.g. ``/usr/local/bin/zrepl``.
|
||||||
Otherwise, the examples in the :ref:`quick-start guides <quickstart-toc>` may need to be adjusted.
|
Otherwise, the examples in the :ref:`quick-start guides <quickstart-toc>` may need to be adjusted.
|
||||||
|
|||||||
@@ -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.
|
|
||||||
|
|||||||
@@ -30,12 +30,15 @@ The following list may be incomplete, feel free to submit a PR with an update:
|
|||||||
* - Arch Linux
|
* - Arch Linux
|
||||||
- ``yay install zrepl``
|
- ``yay install zrepl``
|
||||||
- Available on `AUR <https://aur.archlinux.org/packages/zrepl>`_
|
- Available on `AUR <https://aur.archlinux.org/packages/zrepl>`_
|
||||||
* - Fedora, CentOS, RHEL, OpenSUSE
|
* - Fedora
|
||||||
- ``dnf install zrepl``
|
- ``dnf install zrepl``
|
||||||
- :ref:`RPM repository config <installation-rpm-repos>`
|
- Available on `COPR <https://copr.fedorainfracloud.org/coprs/poettlerric/zrepl/>`_
|
||||||
|
* - CentOS/RHEL
|
||||||
|
- ``yum install zrepl``
|
||||||
|
- Available on `COPR <https://copr.fedorainfracloud.org/coprs/poettlerric/zrepl/>`_
|
||||||
* - Debian + Ubuntu
|
* - Debian + Ubuntu
|
||||||
- ``apt install zrepl``
|
- ``apt install zrepl``
|
||||||
- :ref:`APT repository config <installation-apt-repos>`
|
- APT repository config :ref:`see below <installation-apt-repos>`
|
||||||
* - OmniOS
|
* - OmniOS
|
||||||
- ``pkg install zrepl``
|
- ``pkg install zrepl``
|
||||||
- Available since `r151030 <https://pkg.omniosce.org/r151030/extra/en/search.shtml?token=zrepl&action=Search>`_
|
- Available since `r151030 <https://pkg.omniosce.org/r151030/extra/en/search.shtml?token=zrepl&action=Search>`_
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
.. _installation-rpm-repos:
|
|
||||||
|
|
||||||
RPM repositories
|
|
||||||
~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
We provide a single RPM repository for all RPM-based Linux distros.
|
|
||||||
The zrepl binary in the repo is the same as the one published to GitHub.
|
|
||||||
Since Go binaries are statically linked, the RPM should work about everywhere.
|
|
||||||
|
|
||||||
The fingerprint of the signing key is ``F6F6 E8EA 6F2F 1462 2878 B5DE 50E3 4417 826E 2CE6``.
|
|
||||||
It is available at `<https://zrepl.cschwarz.com/rpm/rpm-key.asc>`_ .
|
|
||||||
Please open an issue on GitHub if you encounter any issues with the repository.
|
|
||||||
|
|
||||||
Copy-paste the following snippet into your shell to set up the zrepl repository.
|
|
||||||
Then ``dnf install zrepl`` and make sure to confirm that the signing key matches the one shown above.
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
cat > /etc/yum.repos.d/zrepl.repo <<EOF
|
|
||||||
[zrepl]
|
|
||||||
name = zrepl
|
|
||||||
baseurl = https://zrepl.cschwarz.com/rpm/repo
|
|
||||||
gpgkey = https://zrepl.cschwarz.com/rpm/rpm-key.asc
|
|
||||||
EOF
|
|
||||||
|
|
||||||
.. NOTE::
|
|
||||||
|
|
||||||
Until zrepl reaches 1.0, the repository will be updated to the latest zrepl release immediately.
|
|
||||||
This includes breaking changes between zrepl versions.
|
|
||||||
If that bothers you, use the `dnf versionlock plugin <https://dnf-plugins-core.readthedocs.io/en/latest/versionlock.html>`_ to pin the version of zrepl on your system.
|
|
||||||
+14
-42
@@ -1,22 +1,5 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -euo pipefail
|
set -eo pipefail
|
||||||
|
|
||||||
NON_INTERACTIVE=false
|
|
||||||
DO_CLONE=false
|
|
||||||
while getopts "ca" arg; do
|
|
||||||
case "$arg" in
|
|
||||||
"a")
|
|
||||||
NON_INTERACTIVE=true
|
|
||||||
;;
|
|
||||||
"c")
|
|
||||||
DO_CLONE=true
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "invalid option '-$arg'"
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
GHPAGESREPO="git@github.com:zrepl/zrepl.github.io.git"
|
GHPAGESREPO="git@github.com:zrepl/zrepl.github.io.git"
|
||||||
SCRIPTDIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
|
SCRIPTDIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
|
||||||
@@ -24,8 +7,15 @@ PUBLICDIR="${SCRIPTDIR}/public_git"
|
|||||||
|
|
||||||
checkout_repo_msg() {
|
checkout_repo_msg() {
|
||||||
echo "clone ${GHPAGESREPO} to ${PUBLICDIR}:"
|
echo "clone ${GHPAGESREPO} to ${PUBLICDIR}:"
|
||||||
|
echo " git clone ${GHPAGESREPO} ${PUBLICDIR}"
|
||||||
|
git clone "${GHPAGESREPO}" "${PUBLICDIR}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exit_msg() {
|
||||||
|
echo "error, exiting..."
|
||||||
|
}
|
||||||
|
trap exit_msg EXIT
|
||||||
|
|
||||||
if ! type sphinx-versioning >/dev/null; then
|
if ! type sphinx-versioning >/dev/null; then
|
||||||
echo "install sphinx-versioning and come back"
|
echo "install sphinx-versioning and come back"
|
||||||
exit 1
|
exit 1
|
||||||
@@ -35,19 +25,11 @@ cd "$SCRIPTDIR"
|
|||||||
|
|
||||||
if [ ! -d "$PUBLICDIR" ]; then
|
if [ ! -d "$PUBLICDIR" ]; then
|
||||||
checkout_repo_msg
|
checkout_repo_msg
|
||||||
if $DO_CLONE; then
|
exit 1
|
||||||
git clone "${GHPAGESREPO}" "${PUBLICDIR}"
|
|
||||||
else
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if $NON_INTERACTIVE; then
|
echo -n "PRESS ENTER to confirm you commited and pushed docs changes and tags to the zrepl repo"
|
||||||
echo "non-interactive mode"
|
read
|
||||||
else
|
|
||||||
echo -n "PRESS ENTER to confirm you commited and pushed docs changes and tags to the zrepl repo"
|
|
||||||
read -r
|
|
||||||
fi
|
|
||||||
|
|
||||||
pushd "$PUBLICDIR"
|
pushd "$PUBLICDIR"
|
||||||
|
|
||||||
@@ -65,18 +47,14 @@ 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"
|
||||||
|
|
||||||
flags="$(python3 gen-sphinx-versioning-flags.py)"
|
|
||||||
set -e
|
set -e
|
||||||
sphinx-versioning build \
|
sphinx-versioning build \
|
||||||
$flags \
|
$(python3 gen-sphinx-versioning-flags.py) \
|
||||||
docs ./public_git \
|
docs ./public_git \
|
||||||
-- -c sphinxconf # older conf.py throw errors because they used
|
-- -c sphinxconf # older conf.py throw errors because they used
|
||||||
# version = subprocess.show_output(["git", "describe"])
|
# version = subprocess.show_output(["git", "describe"])
|
||||||
@@ -88,18 +66,12 @@ 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="sphinx-versioning 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"
|
|
||||||
else
|
|
||||||
echo "nothing to commit"
|
|
||||||
fi
|
|
||||||
echo "pushing to GitHub pages repo"
|
|
||||||
git push origin master
|
git push origin master
|
||||||
|
|
||||||
|
|||||||
@@ -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,19 +51,6 @@ 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
|
||||||
=============
|
=============
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ A few additional requirements:
|
|||||||
* We want to be able to put off the backups for more than three weeks, i.e., longer than the lifetime of the automatically created snapshots on our workstation.
|
* We want to be able to put off the backups for more than three weeks, i.e., longer than the lifetime of the automatically created snapshots on our workstation.
|
||||||
**zrepl should use bookmarks and holds to achieve this goal**.
|
**zrepl should use bookmarks and holds to achieve this goal**.
|
||||||
* When we yank out the drive during replication and go on a long vacation, we do *not* want the partially replicated snapshot to stick around as it would hold on to too much disk space over time.
|
* When we yank out the drive during replication and go on a long vacation, we do *not* want the partially replicated snapshot to stick around as it would hold on to too much disk space over time.
|
||||||
Therefore, we want zrepl to deviate from its :ref:`default behavior <replication-option-protection>` and sacrifice resumability, but nonetheless retain the ability to do incremental replication once we return from our vacation.
|
Therefore, we want zrepl to deviate from its :ref:`default step-hold behavior <step-holds-and-bookmarks>` and sacrifice resumability, but nonetheless retain the ability to do incremental replication once we return from our vacation.
|
||||||
**zrepl should provide an easy config knob to disable step holds for incremental replication**.
|
**zrepl should provide an easy config knob to disable step holds for incremental replication**.
|
||||||
|
|
||||||
The following config snippet implements the setup described above.
|
The following config snippet implements the setup described above.
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|
||||||
@@ -51,18 +51,21 @@ To get things going quickly, we skip setting up a CA and generate two self-signe
|
|||||||
For convenience, we generate the key pairs on our local machine and distribute them using ssh:
|
For convenience, we generate the key pairs on our local machine and distribute them using ssh:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
:emphasize-lines: 6,13
|
||||||
|
|
||||||
(name=backups; openssl req -x509 -sha256 -nodes \
|
openssl req -x509 -sha256 -nodes \
|
||||||
-newkey rsa:4096 \
|
-newkey rsa:4096 \
|
||||||
-days 365 \
|
-days 365 \
|
||||||
-keyout $name.key \
|
-keyout backups.key \
|
||||||
-out $name.crt -addext "subjectAltName = DNS:$name" -subj "/CN=$name")
|
-out backups.crt
|
||||||
|
# ... and use "backups" as Common Name (CN)
|
||||||
|
|
||||||
(name=prod; openssl req -x509 -sha256 -nodes \
|
openssl req -x509 -sha256 -nodes \
|
||||||
-newkey rsa:4096 \
|
-newkey rsa:4096 \
|
||||||
-days 365 \
|
-days 365 \
|
||||||
-keyout $name.key \
|
-keyout prod.key \
|
||||||
-out $name.crt -addext "subjectAltName = DNS:$name" -subj "/CN=$name")
|
-out prod.crt
|
||||||
|
# ... and use "prod" as Common Name (CN)
|
||||||
|
|
||||||
ssh root@backups "mkdir /etc/zrepl"
|
ssh root@backups "mkdir /etc/zrepl"
|
||||||
scp backups.key backups.crt prod.crt root@backups:/etc/zrepl
|
scp backups.key backups.crt prod.crt root@backups:/etc/zrepl
|
||||||
|
|||||||
@@ -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.
|
|
||||||
@@ -25,6 +25,6 @@ sphinxcontrib-htmlhelp==1.0.2
|
|||||||
sphinxcontrib-jsmath==1.0.1
|
sphinxcontrib-jsmath==1.0.1
|
||||||
sphinxcontrib-qthelp==1.0.2
|
sphinxcontrib-qthelp==1.0.2
|
||||||
sphinxcontrib-serializinghtml==1.1.3
|
sphinxcontrib-serializinghtml==1.1.3
|
||||||
git+https://github.com/rwblair/sphinxcontrib-versioning.git@7e3885a389a809e17ea55261316b7b0e98dbf98f#egg=sphinxcontrib-versioning
|
-e git://github.com/rwblair/sphinxcontrib-versioning.git@7e3885a389a809e17ea55261316b7b0e98dbf98f#egg=sphinxcontrib-versioning
|
||||||
sphinxcontrib-websupport==1.1.2
|
sphinxcontrib-websupport==1.1.2
|
||||||
urllib3==1.25.3
|
urllib3==1.25.3
|
||||||
|
|||||||
+1
-8
@@ -32,13 +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-gold| Prominic.NET, Inc.
|
|
||||||
* |supporter-std| Torsten Blum
|
|
||||||
* |supporter-gold| Cyberiada GmbH
|
|
||||||
* |supporter-std| `Gordon Schulz <https://github.com/azmodude>`_
|
|
||||||
* |supporter-std| `@jwittlincohen <https://github.com/jwittlincohen>`_
|
|
||||||
* |supporter-std| `Michael D. Schmitt <https://waterbendingscroll.dancingdragons.org>`_
|
|
||||||
* |supporter-std| `Hans Schulz <https://github.com/schulzh>`_
|
|
||||||
* |supporter-std| Henning Kessler
|
* |supporter-std| Henning Kessler
|
||||||
* |supporter-std| `John Ramsden <https://github.com/johnramsden>`_
|
* |supporter-std| `John Ramsden <https://github.com/johnramsden>`_
|
||||||
* |supporter-std| `DrLuke <https://github.com/drluke>`_
|
* |supporter-std| `DrLuke <https://github.com/drluke>`_
|
||||||
@@ -50,7 +43,7 @@ We would like to thank the following people and organizations for supporting zre
|
|||||||
* |supporter-gold| `MNX.io <https://mnx.io>`_
|
* |supporter-gold| `MNX.io <https://mnx.io>`_
|
||||||
* |supporter-std| `Marshall Clyburn <https://github.com/mdclyburn>`_
|
* |supporter-std| `Marshall Clyburn <https://github.com/mdclyburn>`_
|
||||||
* |supporter-code| `Ross Williams <https://github.com/overhacked>`_
|
* |supporter-code| `Ross Williams <https://github.com/overhacked>`_
|
||||||
* |supporter-gold| Mike T.
|
* |supporter-std| Mike T.
|
||||||
* |supporter-code| `Justin Scholz <https://github.com/JMoVS>`_
|
* |supporter-code| `Justin Scholz <https://github.com/JMoVS>`_
|
||||||
* |supporter-code| `InsanePrawn <https://github.com/InsanePrawn>`_
|
* |supporter-code| `InsanePrawn <https://github.com/InsanePrawn>`_
|
||||||
* |supporter-code| `Ben Woods <https://www.freshports.org/sysutils/zrepl/>`_
|
* |supporter-code| `Ben Woods <https://www.freshports.org/sysutils/zrepl/>`_
|
||||||
|
|||||||
+1
-71
@@ -26,7 +26,7 @@ CLI Overview
|
|||||||
* - ``zrepl daemon``
|
* - ``zrepl daemon``
|
||||||
- run the daemon, required for all zrepl functionality
|
- run the daemon, required for all zrepl functionality
|
||||||
* - ``zrepl status``
|
* - ``zrepl status``
|
||||||
- show job activity, or with ``--mode raw`` for JSON output
|
- show job activity, or with ``--raw`` for JSON output
|
||||||
* - ``zrepl stdinserver``
|
* - ``zrepl stdinserver``
|
||||||
- see :ref:`transport-ssh+stdinserver`
|
- see :ref:`transport-ssh+stdinserver`
|
||||||
* - ``zrepl signal wakeup JOB``
|
* - ``zrepl signal wakeup JOB``
|
||||||
@@ -78,73 +78,3 @@ Systemd Unit File
|
|||||||
A systemd service definition template is available in :repomasterlink:`dist/systemd`.
|
A systemd service definition template is available in :repomasterlink:`dist/systemd`.
|
||||||
Note that some of the options only work on recent versions of systemd.
|
Note that some of the options only work on recent versions of systemd.
|
||||||
Any help & improvements are very welcome, see :issue:`145`.
|
Any help & improvements are very welcome, see :issue:`145`.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
============
|
|
||||||
Ops Runbooks
|
|
||||||
============
|
|
||||||
|
|
||||||
|
|
||||||
.. toctree::
|
|
||||||
|
|
||||||
usage/runbooks/migrating_sending_side_to_new_zpool.rst
|
|
||||||
|
|
||||||
|
|
||||||
.. _usage-platform-tests:
|
|
||||||
|
|
||||||
==============
|
|
||||||
Platform Tests
|
|
||||||
==============
|
|
||||||
|
|
||||||
Along with the main ``zrepl`` binary, we release the ``platformtest`` binaries.
|
|
||||||
The zrepl platform tests are an integration test suite that is complementary to the pure Go unit tests.
|
|
||||||
Any test that needs to interact with ZFS is a platform test.
|
|
||||||
|
|
||||||
The platform need to run as root.
|
|
||||||
For each test, we create a fresh dummy zpool backed by a file-based vdev.
|
|
||||||
The file path, and a root mountpoint for the dummy zpool, must be specified on the command line:
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
mkdir -p /tmp/zreplplatformtest
|
|
||||||
./platformtest \
|
|
||||||
-poolname 'zreplplatformtest' \ # <- name must contain zreplplatformtest
|
|
||||||
-imagepath /tmp/zreplplatformtest.img \ # <- zrepl will create the file
|
|
||||||
-mountpoint /tmp/zreplplatformtest # <- must exist
|
|
||||||
|
|
||||||
|
|
||||||
.. WARNING::
|
|
||||||
|
|
||||||
``platformtest`` will unconditionally overwrite the file at `imagepath`
|
|
||||||
and unconditionally ``zpool destroy $poolname``.
|
|
||||||
So, don't use a production poolname, and consider running the test in a VM.
|
|
||||||
It'll be a lot faster as well because the underlying operations, ``zfs list`` in particular, will be faster.
|
|
||||||
|
|
||||||
|
|
||||||
While the platformtests are running, there will be a log of log output.
|
|
||||||
After all tests have run, it prints a summary with a list of tests, grouped by result type (success, failure, skipped):
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
PASSING TESTS:
|
|
||||||
github.com/zrepl/zrepl/platformtest/tests.BatchDestroy
|
|
||||||
github.com/zrepl/zrepl/platformtest/tests.CreateReplicationCursor
|
|
||||||
github.com/zrepl/zrepl/platformtest/tests.GetNonexistent
|
|
||||||
github.com/zrepl/zrepl/platformtest/tests.HoldsWork
|
|
||||||
...
|
|
||||||
github.com/zrepl/zrepl/platformtest/tests.SendStreamNonEOFReadErrorHandling
|
|
||||||
github.com/zrepl/zrepl/platformtest/tests.UndestroyableSnapshotParsing
|
|
||||||
SKIPPED TESTS:
|
|
||||||
github.com/zrepl/zrepl/platformtest/tests.SendArgsValidationEncryptedSendOfUnencryptedDatasetForbidden__EncryptionSupported_false
|
|
||||||
FAILED TESTS: []
|
|
||||||
|
|
||||||
|
|
||||||
If there is a failure, or a skipped test that you believe should be passing, re-run the test suite, capture stderr & stdout to a text file, and create an issue on GitHub.
|
|
||||||
|
|
||||||
To run a specific test case, or a subset of tests matched by regex, use the ``-run REGEX`` command line flag.
|
|
||||||
|
|
||||||
To stop test execution at the first failing test, and prevent cleanup of the dummy zpool, use the ``-failure.stop-and-keep-pool`` flag.
|
|
||||||
|
|
||||||
To build the platformtests yourself, use ``make test-platform-bin``.
|
|
||||||
There's also the ``make test-platform`` target to run the platform tests with a default command line.
|
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
|
|
||||||
Migrating Sending Side
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
**Objective**:
|
|
||||||
Move sending-side zpool to new hardware.
|
|
||||||
Make the move fully transparent to the sending-side jobs.
|
|
||||||
After the move is done, all sending-side zrepl jobs should continue to work as if the move had not happened.
|
|
||||||
In particular, incremental replication should be able to pick up where it left before the move.
|
|
||||||
|
|
||||||
Suppose we want to migrate all data from one zpool ``oldpool`` to another zpool ``newpool``.
|
|
||||||
A possible reason might be that we want to change RAID levels, ``ashift``, or just migrate over to next-gen hardware.
|
|
||||||
|
|
||||||
If the pool names are different, zrepl's matching between sender and receiver dataset will break becase the receive-side dataset names contain ``oldpool``.
|
|
||||||
To avoid this, we will need the name of the new pool to match that of the old pool.
|
|
||||||
The following steps will accomplish this:
|
|
||||||
|
|
||||||
1. Stop zrepl.
|
|
||||||
2. Create the new pool: ``zpool create newpool ...``
|
|
||||||
3. Take a snapshot of the old pool so that you have something that you can ``zfs send``.
|
|
||||||
For example, run ``zfs snapshot -r oldpool@migration_oldpool_newpool``.
|
|
||||||
4. Send all of the oldpool's datasets to the new pool:
|
|
||||||
``zfs send -R oldpool@migration_oldpool_newpool | zfs recv -F newpool``
|
|
||||||
5. Export the old pool: ``zpool export oldpool``
|
|
||||||
6. Export the new pool: ``zpool export newpool``
|
|
||||||
7. (Optional) Change the name of the old pool to something that does not conflict with the new pool.
|
|
||||||
We are going to use the name ``oldoldpool`` in this example.
|
|
||||||
Use ``zpool import`` with no arguments to see the pool id.
|
|
||||||
Then ``zpool import <id> oldoldpool && zpool export oldoldpool``.
|
|
||||||
8. Import the new pool, while changing the name to match the old pool: ``zpool import newpool oldpool``
|
|
||||||
9. Start zrepl again and wake up the relevant jobs.
|
|
||||||
10. Use ``zrepl status`` or you monitoring to ensure that replication works.
|
|
||||||
The best test is an end-to-end test where you write some junk data on a sender dataset and wait until a snapshot with that data appears on the receiving side.
|
|
||||||
11. Once you are confident that replication is working, you may dispose of the old pool.
|
|
||||||
|
|
||||||
Note that, depending on pruning rules, it will not be possible to switch back to the old pool seamlessly, i.e., without a full re-replication.
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user